首页 >Unity3D频道 >【Unity3D研究院之游戏开发】 > Unity3D研究院之Lightmap支持GPU Instancing(一百零七)
2019
08-23

Unity3D研究院之Lightmap支持GPU Instancing(一百零七)

Unity2017是明确不支持GPU Instancing的,如果在材质上勾选了enable GPU Instancing后,如下图所示,在Frame Debugger中,我们能清楚的看到所有的游戏对象没有合批,提示Objects are lightmapped。

QQ20190823-215340

所以为了让Unity2017支持GPU Instancing我们就不能使用游戏对象的模式,必须采用Graphics.DrawMeshInstanced接口来绘制。具体使用方法可以看我之前的文章Unity3D研究院GPU Instancing实战(九十七)

并且参与烘焙的材质我们需要手动修改,不能使用默认的standard着色器,原因就是我们要把每个物体烘焙过的LightmapScaleOffset传进去。如果你对LightmapIndex、LightmapScaleOffset、UV2之间的关系还不太了解,可以看我之前的文章。Unity3D研究院之LightmapIndex、LightmapScaleOffset、UV2的关系(一百零六)

首先需要改造standard着色器,在ps结构体里声明float2 uv1 : TEXCOORD1。

        struct v2f
        {
            float2 uv0 : TEXCOORD0;
            float2 uv1 : TEXCOORD1;
            float4 pos : SV_POSITION;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

接着需要在vs里返回和LightmapScaleOffset计算后的uv2,也就是上面定义的float2 uv1 : TEXCOORD1。

        v2f vert(appdata IN)
        {
            v2f o;
            UNITY_SETUP_INSTANCE_ID(IN);
            UNITY_TRANSFER_INSTANCE_ID(IN, o); 
            o.uv0 = TRANSFORM_TEX(IN.uv0, _MainTex);
            o.pos = UnityObjectToClipPos(IN.pos);
            
            //IN.uv1.xy表示每个mesh的uv2
            //unity_LightmapST.xy表示LightmapScaleOffset.xy也就是Tiling x Tiling y
            //unity_LightmapST.zw表示LightmapScaleOffset.zw Offset x Offset y
            o.uv1 = IN.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
            return o;
        }

最后在PS里就可以采样lightmap的颜色了。

        fixed4 frag(v2f IN) : SV_Target
        {
            UNITY_SETUP_INSTANCE_ID(IN);
            fixed4 col;
            col.rgb = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.uv1.xy));
            return col;
        }

以上代码修改完毕,我们就可以开始烘焙场景了,如下图所示,右侧我们能看到现在还游戏对象模式。

QQ20190823-221332

接着我们要使用Graphics.DrawMeshInstanced来进行绘制,按照上面我们修改的shader其实只需要将unity_LightmapST传入即可,但是很遗憾的说我们不能直接传入。如下图所示,首先指定Custom材质,然后在指定一张特定的烘焙贴图,因为实际环境中可能会有多张烘焙贴图的情况。

QQ20190823-222525

Custom.shader,请大家注意//—add—中的内容。

Shader "Mobile/Custom" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        //---add---
        _LightmapST("_LightmapST",Vector)=(0,0,0,0)
        //---add---
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
            #include "UnityCG.cginc"
            
            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct appdata
            {
                float3 pos : POSITION;
                float3 uv0 : TEXCOORD0;
                float3 uv1 : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float2 uv0 : TEXCOORD0;
                //---add---
                float2 uv1 : TEXCOORD1;
                //---add---
                float4 pos : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
            
            //将MeshRenderer中的LightmapScaleOffset分别传入_LightmapST中
            UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(fixed4, _LightmapST)
            UNITY_INSTANCING_BUFFER_END(Props)
            
            v2f vert(appdata IN)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_TRANSFER_INSTANCE_ID(IN, o); 
                
                o.uv0 = TRANSFORM_TEX(IN.uv0, _MainTex);
                 
                //---add---
                //取出每一个物体的LightmapScaleOffset重新算UV2
                fixed4 l =  UNITY_ACCESS_INSTANCED_PROP(Props, _LightmapST);
                o.uv1 = IN.uv1.xy * l.xy + l.zw;
                //---add---
                o.pos = UnityObjectToClipPos(IN.pos);
                return o;
            }
            fixed4 frag(v2f IN) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(IN);
             
                fixed4 col;
                //在这里采样lightmap
                col.rgb = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.uv1.xy));
                return col;
            }

            ENDCG
        }
    }
}

最后就是Custom.cs

using System.Collections.Generic;
using UnityEngine;
public class Custom : MonoBehaviour
{
   
    public Material material;
    public Texture2D lightmapTexture;

    MeshFilter meshfilter;
    List<Matrix4x4> materix = new List<Matrix4x4>();
    List<Vector4> lightmapOffset = new List<Vector4>();
    MaterialPropertyBlock block;

    void Start()
    {
        //获取节点下的所有MeshRenderer
        foreach (var item in GetComponentsInChildren<MeshRenderer>())
        {
            //隐藏它
            item.gameObject.SetActive(false);
            if (meshfilter == null)
            {
                //取出第一个mesh,因为我们是做例子可以肯定保证下面的mesh都是一样的
                meshfilter = item.GetComponent<MeshFilter>();
            }
            //保存每个物体的矩阵以及lightmapScaleOffset
            materix.Add(item.localToWorldMatrix);
            lightmapOffset.Add(item.lightmapScaleOffset);
           

        }
        //启动LIGHTMAP_ON宏
        material.EnableKeyword("LIGHTMAP_ON");
        //为了避免GC所以只new一次MaterialPropertyBlock
        block = new MaterialPropertyBlock();
        //设置具体用哪张烘焙贴图
        block.SetTexture("unity_Lightmap", lightmapTexture);
        //将每个物体的lightmapScaleOffset传入shader中
        block.SetVectorArray("_LightmapST", lightmapOffset.ToArray());

    }

    void Update()
    {
        //一次绘制
        Graphics.DrawMeshInstanced(meshfilter.sharedMesh, 0, material, materix, block);
    }

}

运行游戏,如下图所示,游戏对象都已经全部隐藏,通过Graphics.DrawMeshInstanced 一次绘制完毕,并且设置了正确的烘焙贴图。

QQ20190823-223627

OK,我们在来看看Unity2018和Unity2019,从Unity2018开始Lightmap已经支持GPUInstancing了,如果是游戏对象的方式烘焙完场景就直接显示lightmap了,但是为了极致的性能优化,Graphics.DrawMeshInstanced 的效率明显还是要高。

——————-以下方法在真机上不行但是编辑器上可行,所以目前来看只能用上面的方法//——————-

在Unity2019中我们打开了LWRP了,此时在去修改Shader就太麻烦了,不过还好我们可以直接传入unity_LightmapST,这样就不需要改代码了。

    void Start()
    {
        foreach (var item in GetComponentsInChildren<MeshRenderer>())
        {
            if(meshfilter == null)
            {
                meshfilter = item.GetComponent<MeshFilter>();
            }
            materix.Add(item.localToWorldMatrix);
            lightmapOffset.Add(item.lightmapScaleOffset);
            item.gameObject.SetActive(false);

        }
        material.EnableKeyword("LIGHTMAP_ON");
        block = new MaterialPropertyBlock();
        block.SetTexture("unity_Lightmap", lightmapTexture);
        //unity2019可以在这直接传入unity_LightmapST
        block.SetVectorArray("unity_LightmapST", lightmapOffset.ToArray());
    }

    void Update()
    {
        //一次渲染
        Graphics.DrawMeshInstanced(meshfilter.sharedMesh, 0, material, materix, block);
    }

如下图所示,在Unity2019中lightmap已经渲染出来了。

QQ20190823-224759

如果Unity2019没有用LWRP,unity会计算球谐光照,需要改下代码。主要是LightProbeUsage.CustomProvided,因为CustomProvider没有提供球谐,所以这样球谐就都没有值了,所以计算就对了。

        Graphics.DrawMeshInstanced(meshfilter.sharedMesh, 0, material, materix, block,ShadowCastingMode.Off,false,0,Camera.main,LightProbeUsage.CustomProvided);

总之现在项目都在用LWRP了,用上面的方法肯定就没错了。

最后在补充一下,SRP提供了SRPBatcher,它可以对不同Mesh相同材质进行“合并”,你也可以理解合批批次,它比GPUInstancing还要好,因为后者需要同时满足相同Mesh相同材质才能合批。

如果游戏对象情况下勾选了enable GPU insatncing会优先启动SRPBatcher。

非游戏对象情况,Graphics.DrawMeshInstanced绘制出来的,才会优先启用GPUInstancing。

还有ECS提供了无游戏对象的渲染,其实也存在Lightmap的问题,按照上面的做法也同样可以显示正确的Lightmap ,还有Hybird也是同样的套路。

最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

Unity3D研究院之Lightmap支持GPU Instancing(一百零七)》有 11 条评论

  1. gltfxe说:

    使用不同光照贴图的对象,是不能使用Graphics.DrawMeshInstanced渲染的吗

  2. alex说:

    block.SetTexture(“unity_Lightmap”, lightmapTexture); 中的lightmapTexture 怎么赋值

  3. 沈杰说:

    用了custom的shader 材质球直接黑了 是哪里有问题吗

  4. 搬砖战士说:

    materiall.EnableKeyword(“SHADOWS_SHADOWMASK”);

    block.SetTexture(“unity_ShadowMask”, LightmapSettings.lightmaps[renderer.lightmapIndex].shadowMask);
    如果需要烘焙阴影的话,建议加上这两句

  5. seven说:

    material打ab包的情况下,使用Graphics.DrawMeshInstanced无法渲染,请问有遇到过这样的问题吗?

  6. kaikai说:

    bake完后lightmapScaleOffset数据会被自动写入么

留下一个回复

你的email不会被公开。 必填项已用 * 标注。