Unity2017是明确不支持GPU Instancing的,如果在材质上勾选了enable GPU Instancing后,如下图所示,在Frame Debugger中,我们能清楚的看到所有的游戏对象没有合批,提示Objects are lightmapped。
所以为了让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;
}
以上代码修改完毕,我们就可以开始烘焙场景了,如下图所示,右侧我们能看到现在还游戏对象模式。
接着我们要使用Graphics.DrawMeshInstanced来进行绘制,按照上面我们修改的shader其实只需要将unity_LightmapST传入即可,但是很遗憾的说我们不能直接传入。如下图所示,首先指定Custom材质,然后在指定一张特定的烘焙贴图,因为实际环境中可能会有多张烘焙贴图的情况。
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 一次绘制完毕,并且设置了正确的烘焙贴图。
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已经渲染出来了。
如果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也是同样的套路。
- 本文固定链接: https://www.xuanyusong.com/archives/4640
- 转载请注明: 雨松MOMO于 雨松MOMO程序研究院 发表






使用不同光照贴图的对象,是不能使用Graphics.DrawMeshInstanced渲染的吗
我也想问这个问题,这样好像同对象,但在不同两张光照贴图上 只能使用分开渲染了
可以使用 Texture2DArray
block.SetTexture(“unity_Lightmap”, lightmapTexture); 中的lightmapTexture 怎么赋值
用了custom的shader 材质球直接黑了 是哪里有问题吗
我也是 用这个shader拖进材质球 变黑了
materiall.EnableKeyword(“SHADOWS_SHADOWMASK”);
block.SetTexture(“unity_ShadowMask”, LightmapSettings.lightmaps[renderer.lightmapIndex].shadowMask);
如果需要烘焙阴影的话,建议加上这两句
大佬 要是多张shadowmask怎么处理啊
material打ab包的情况下,使用Graphics.DrawMeshInstanced无法渲染,请问有遇到过这样的问题吗?
bake完后lightmapScaleOffset数据会被自动写入么
对的