一、着色器应用 Lightmaps
对于烘焙了 lightmaps 的场景,使用了自己的着色器可能得不到正确的结果
1.1 关键字 LIGHTMAP_ON
当着色器应用 lightmaps 时,内置关键字 LIGHTMAP_ON 会起作用,同时不会再包含顶点光照,也就是对应的 VERTEXLIGHT_ON 关键字必然会失效,因此我们可以添加如下的 #pragma 指令:
#pragma multi_compile __ LIGHTMAP_ON VERTEXLIGHT_ON
//or #pragma multi_compile __ LIGHTMAP_ON
用于采样 lightmaps 的纹理坐标会在第二套 uv 坐标中:
struct appdata
{
#if defined(LIGHTMAP_ON)
float2 lightMapUV: TEXCOORD1; //光照贴图uv坐标
float2 dynLightMapUV: TEXCOORD2; //动态光照贴图uv坐标
#endif
}
//……
struct v2f
{
#if defined(LIGHTMAP_ON)
float2 lightMapUV: TEXCOORD1; //光照贴图uv坐标
float2 dynLightMapUV: TEXCOORD2; //动态光照贴图uv坐标
#endif
}
在顶点着色器中,必须要对 lightmaps 的 uv 坐标进行偏移和变换,并且这个偏移&变换对于每个对象都是不同的,而不是对于材质,想想看一张光照贴图里面存储了多少物体的光照信息
除此之外,我们不能直接使用 TRANSFORM_TEX 宏进行纹理的偏移,因为这个宏假定 lightmaps 的偏移&变换数据存储在 unity_Lightmap_ST 中,而实际上,这个数据是存储在 unity_LightmapST 上的
#if defined(LIGHTMAP_ON)
o.lightmapUV = v.lightmapUV * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
1.2 lightmaps 采样
到这一步并没有结束,紧接着是片段着色器:
#if defined(LIGHTMAP_ON)
float3 indirectDiffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
#else
float3 indirectDiffuse = max(0, ShadeSH9(float4(normal, 1)));;
#endif
片段着色器很简单,保存后就可以看到正确的效果(当然前提条件是你的着色器本身可以得到一个还可以的效果)
其中的 indirectDiffuse 为间接光漫反射,在一般情况下 shader 中最终得到光照结果为:直接光漫反射 + 直接光镜面反射 + 间接光漫反射 + 间接光镜面反射
对于上面的代码:
- lightmaps 数据被当作是间接光
- 应用 lightmaps 的往往是场景中的静态物体,因此一般情况下只需要考虑 lightmaps,不需要也不会考虑顶点光照以及球谐光照这些
- 如果你的物体是场景中的动态物体,不应用 lightmaps,那么就需要采样周边光照探针来获得间接漫反射光照,这对应着上面的 ShadeSH9 方法
Untiy 在定义 lightmaps 采样器时使用了宏 UNITY_DECLARE_TEX2D,这考虑了平台差异,因此我们在采样时也要用对应的 UNITY_SAMPLE_TEX2D 宏,除此之外,由于光照贴图的数据并非直观的颜色数据(例如 RGBM,M 为强度),需要解码才可以得到正确的结果,所以我们在采样后还需调用 Unity 的 DecodeLightmap 方法
二、透明应用
由于 lightmap 的计算本质上不是实时渲染,所以在烘焙的过程中不会使用我们之前的着色器来完成工作,这个时候我们需要让光照烘焙系统知道哪些物体是透明的,就需要用一些约定
《UnityGI1:光照烘培》这一章的 4.2 节给出了一个方法,不过透明不止会影响环境光,还会影响阴影:

这时就还需要告诉光照烘焙系统每一个物体的透明度:一个约定是通过着色器 _Color 属性的 alpha 通道以及主纹理 _MainTex 的 alpha 通道
Properties
{
_Color("Tint", Color) = (1, 1, 1, 1)
_MainTex("Albedo", 2D) = "white" {}
}
//……
float GetAlpha(v2f i)
{
float alpha = tex2D(_MainTex, i.uv.xy).a * _Color.a;
return alpha;
}
2.1 自定义 ShaderGUI
如果可以的话,可以一个 Shader 同时负责渲染透明和不透明的物体
Properties
{
_Color("Tint", Color) = (1, 1, 1, 1)
_MainTex("Albedo", 2D) = "white" {}
//……
[Enum(Off, 0, Cutoff, 1, blend, 2)] _AlphaMode("AlphaMode", float) = 0
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", float) = 2
[HideInInspector] _Mode("Blend Mode", float) = 0
[HideInInspector][Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend("Src Blend", float) = 1
[HideInInspector][Enum(UnityEngine.Rendering.BlendMode)] _DstBlend("Dst Blend", float) = 0
[Toggle] _ZWrite("ZWrite", float) = 0
[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest("ZTest", float) = 4
}
//……
Pass
{
Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]
ZTest [_ZTest]
Cull [_Cull]
CGPROGRAM
#pragma multi_compile __ LIGHTMAP_ON VERTEXLIGHT_ON
#pragma shader_feature __ _ALPHAMODE_CUTOUT _ALPHAMODE_BLEND
//……
CGEND
}
这些基本参数全部暴露到材质面板中就可以,如果觉得材质面板比较乱,又或者发现没法动态调整 RenderType 的 Tag,也可以自己写一个 ShaderGUI:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
//允许多个对象同时编辑
[CanEditMultipleObjects]
public class PBRGUI: ShaderGUI
{
public enum AlphaMode
{
Opaque = 0,
AlphaTest = 1,
Transparent = 2
}
MaterialProperty color = null;
MaterialProperty specularColor = null;
MaterialProperty mainTex = null;
MaterialProperty metallic = null;
MaterialProperty glossiness = null;
MaterialProperty LUT = null;
MaterialProperty alphaMode = null;
MaterialProperty cull = null;
MaterialProperty srcBlend = null;
MaterialProperty dstBlend = null;
MaterialProperty zwrite = null;
MaterialProperty ztest = null;
MaterialProperty cutoff = null;
MaterialEditor m_MaterialEditor = null;
Material m_Material = null;
public void FindProperties(MaterialProperty[] props)
{
color = FindProperty("_Color", props);
mainTex = FindProperty("_MainTex", props);
specularColor = FindProperty("_SpecularColor", props);
metallic = FindProperty("_Metallic", props);
glossiness = FindProperty("_Glossiness", props);
LUT = FindProperty("_LUT", props);
cull = FindProperty("_Cull", props);
srcBlend = FindProperty("_SrcBlend", props);
dstBlend = FindProperty("_DstBlend", props);
zwrite = FindProperty("_ZWrite", props);
ztest = FindProperty("_ZTest", props);
alphaMode = FindProperty("_AlphaMode", props);
cutoff = FindProperty("_Cutoff", props);
}
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
FindProperties(props);
m_MaterialEditor = materialEditor;
m_Material = materialEditor.target as Material;
//设置颜色框和纹理框为默认规则大小
m_MaterialEditor.SetDefaultGUIWidths();
m_MaterialEditor.TexturePropertySingleLine(
new GUIContent("Albedo(RGB)"), mainTex, FindProperty("_Color", props)
);
m_MaterialEditor.ShaderProperty(metallic, "金属");
m_MaterialEditor.ShaderProperty(glossiness, "光滑");
m_MaterialEditor.ShaderProperty(specularColor, "镜面色");
m_MaterialEditor.TexturePropertySingleLine(
new GUIContent("LUT"), mainTex
);
EditorGUILayout.Space();
AlphaControl();
EditorGUILayout.Space();
CullAndTest();
EditorGUILayout.Space();
Instancing();
DoubleGI();
QueueControl();
}
void AlphaControl()
{
EditorGUI.BeginChangeCheck();
var mode = (AlphaMode)EditorGUILayout.EnumPopup("透明模式", (AlphaMode)alphaMode.floatValue);
//和BeginChangeCheck对应,当用户对材质的透明模式进行修改后,会自动执行方法里的代码
if (EditorGUI.EndChangeCheck())
{
m_MaterialEditor.RegisterPropertyChangeUndo("Alpha Mode");
alphaMode.floatValue = (float)mode;
if (alphaMode.floatValue < 2)
{
srcBlend.floatValue = (int)UnityEngine.Rendering.BlendMode.One;
dstBlend.floatValue = (int)UnityEngine.Rendering.BlendMode.Zero;
zwrite.floatValue = 1;
if (alphaMode.floatValue == 1)
{
//透明剔除
m_Material.EnableKeyword("_ALPHAMODE_CUTOUT");
m_Material.DisableKeyword("_ALPHAMODE_BLEND");
m_Material.SetOverrideTag("RenderType", "TransparentCutout");
m_Material.renderQueue = Mathf.Min(2500, m_Material.renderQueue);
}
else
{
//不透明
m_Material.DisableKeyword("_ALPHAMODE_CUTOUT");
m_Material.DisableKeyword("_ALPHAMODE_BLEND");
m_Material.SetOverrideTag("RenderType", "Opaque");
m_Material.renderQueue = Mathf.Min(2400, m_Material.renderQueue);
}
}
else
{
srcBlend.floatValue = (int)UnityEngine.Rendering.BlendMode.SrcAlpha;
dstBlend.floatValue = (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha;
zwrite.floatValue = 0;
//半透明
m_Material.DisableKeyword("_ALPHAMODE_CUTOUT");
m_Material.EnableKeyword("_ALPHAMODE_BLEND");
m_Material.SetOverrideTag("RenderType", "Transparent");
m_Material.renderQueue = Mathf.Max(3000, m_Material.renderQueue);
}
EditorUtility.SetDirty(m_Material);
}
//透明度测试
if (alphaMode.floatValue == 1)
{
//缩进
EditorGUI.indentLevel += 2;
m_MaterialEditor.ShaderProperty(cutoff, "透明剔除阈值");
EditorGUI.indentLevel -= 2;
}
///透明度混合
if (alphaMode.floatValue == 2)
{
}
}
void CullAndTest()
{
m_MaterialEditor.ShaderProperty(cull, "剔除");
m_MaterialEditor.ShaderProperty(ztest, "深度测试");
m_MaterialEditor.ShaderProperty(zwrite, "深度写入");
}
void Instancing()
{
m_MaterialEditor.EnableInstancingField();
}
void DoubleGI()
{
m_MaterialEditor.LightmapEmissionProperty();
m_MaterialEditor.DoubleSidedGIField();
}
void QueueControl()
{
m_MaterialEditor.RenderQueueField();
}
}
想要使用自定义的材质面板,只需要在 Shader 的最下面声明一下就OK:
FallBack "Specular"
CustomEditor "PBRGUI"
搞定
2.2 透明度测试
透明度测试也有一样的问题,对于透明度的透明剔除阈值需要命名为 _Cutoff,需要注意的是 lightmaps 对开启了透明度测试的物体的剔除模式默认为不剔除
三、MetaPass
到此为止,可能会发现使用了自己的着色器的物体确实得到了一个正确的效果,但是并没有体现出间接光:

这是少了 MetaPass 的原因,如果你是使用的表面着色器,又或者是自带的 UnityStandard,内部都自带 MetaPass
所以这还需要自己写一个 MetaPass
3.1 MetaPass 和 Enlighten 烘焙系统
Unity5 后的版本将烘焙系统由 Beast 换成了 Enlighten,Enlighten 需要 Unity 提供材质的反射率(Albdeo)、高光(SpecularColor)和自发光(emissive)纹理用来计算间接光照,而这两个贴图都是 Unity 自己在 GPU 上渲染得到的,这就需要提供一个相应的 Pass 来让 Unity 用来进行这种渲染,就是 MetaPass 的作用,它专门负责给光照映射和动态全局光照提取表面信息
2021.1 之后,Enlighten 也被 Unity 废弃,在此之后 Unity 主推 Progressive CPU/GPU Lightmapper 烘焙技术,该烘焙技术基于路径追踪
Pass
{
Tags { "LightMode" = "Meta" }
Cull Off
CGPROGRAM
#pragma vertex vert_meta
#pragma fragment frag_meta
#include "CGINC/SceneMeta.cginc"
ENDCG
}
代码量不会小,所以这里可以将主体部分写在单独的 cginc 文件中,不过需要注意一点,由于子 Pass 设置了 LightMode 为 Meta,那么就需要确保 SubShader 不要去设置 LightMode,这是一个坑
MetaPass.cginc 完整代码如下:
#ifndef SCENE_META_INCLUDE
#define SCENE_META_INCLUDE
#define UNITY_PASS_META 1
#include "UnityCG.cginc"
#include "UnityMetaPass.cginc"
#include "UnityStandardUtils.cginc"
float4 _Color;
//float4 _SpecularColor;
float4 _EmissionColor;
float _Glossiness;
float _Metallic;
float _Cutoff;
//float _Ior;
//sampler2D _LUT;
sampler2D _MainTex;
sampler2D _EmissionX;
float4 _MainTex_ST;
float4 _EmissionX_ST;
struct appdata_meta
{
float4 vertex: POSITION;
float2 uv: TEXCOORD0;
float2 uv1: TEXCOORD1;
float2 uv2: TEXCOORD2;
};
struct v2f_meta
{
float4 uv: TEXCOORD0;
float4 pos: SV_POSITION;
};
v2f_meta vert_meta(appdata_meta v)
{
v2f_meta o;
UNITY_INITIALIZE_OUTPUT(v2f_meta, o);
o.pos = UnityMetaVertexPosition(v.vertex, v.uv1, v.uv2, unity_LightmapST, unity_DynamicLightmapST);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float3 GetAlbedo(v2f_meta i)
{
float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
return albedo;
}
float3 GetEmission(v2f_meta i)
{
float3 emission = tex2D(_EmissionX, i.uv.xy).rgb + _EmissionColor.rgb;
return min(emission, 1);
}
//光照贴图的反照率应该就是漫反射颜色,但是粗糙的金属仍然会在周围散射相当多的光(尽管它的漫反射颜色是黑色的),所以这里最好也要考虑这一点
half3 GetLightmappingAlbedo(half3 diffuse, half3 specular, half smoothness)
{
half roughness = 1 - smoothness;
roughness *= roughness;
half3 res = diffuse;
res += specular * roughness * 0.5;
return res;
}
float4 frag_meta(v2f_meta i): SV_Target
{
half3 albedo = GetAlbedo(i);
half oneMinusReflectivity;
half3 specColor;
half3 diffColor = DiffuseAndSpecularFromMetallic(albedo, _Metallic, /*out*/specColor, /*out*/oneMinusReflectivity);
UnityMetaInput o;
UNITY_INITIALIZE_OUTPUT(UnityMetaInput, o);
o.Albedo = GetLightmappingAlbedo(diffColor, specColor, _Glossiness);
o.SpecularColor = specColor;
o.Emission = GetEmission(i);
return UnityMetaFragment(o);
}
#endif
3.2 MetaPass 顶点着色器与片段着色器
Meta 只有一个目的,那就是计算出反射率(Albdeo)、高光(SpecularColor)和自发光(emissive)三个参数,将它们传入内置的 UnityMetaInput 结构中由片段着色器返回
片段着色器中用了一个内置方法 DiffuseAndSpecularFromMetallic,我们只需要传入输入的反射率和金属度,就可以同时得到漫反射光和高光,毕竟在主 Pass 中肯定已经写过一遍了,这里就可以做简化。不过光照贴图的反照率是漫反射颜色,但是粗糙的金属仍然会在周围散射相当多的光(尽管它的漫反射颜色是黑色的),所以这里最好也要考虑这一点
//光照贴图的反照率应该就是漫反射颜色,但是粗糙的金属仍然会在周围散射相当多的光(尽管它的漫反射颜色是黑色的),所以这里最好也要考虑这一点
half3 GetLightmappingAlbedo(half3 diffuse, half3 specular, half smoothness)
{
half roughness = 1 - smoothness;
roughness *= roughness;
half3 res = diffuse;
res += specular * roughness * 0.5;
return res;
}
float4 frag_meta(v2f_meta i): SV_Target
{
half3 albedo = GetAlbedo(i);
half oneMinusReflectivity;
half3 specColor;
half3 diffColor = DiffuseAndSpecularFromMetallic(albedo, _Metallic, /*out*/specColor, /*out*/oneMinusReflectivity);
UnityMetaInput o;
UNITY_INITIALIZE_OUTPUT(UnityMetaInput, o);
o.Albedo = GetLightmappingAlbedo(diffColor, specColor, _Glossiness);
o.SpecularColor = specColor;
o.Emission = GetEmission(i);
return UnityMetaFragment(o);
}
顶点着色器使用了 Unity 内置方法 UnityMetaVertexPosition 去计算世界坐标点,它的内部实现如下:
float4 UnityMetaVertexPosition(float4 vertex, float2 uv1, float2 uv2, float4 lightmapST, float4 dynlightmapST)
{
#if !defined(EDITOR_VISUALIZATION)
//使用静态光照
if (unity_MetaVertexControl.x)
{
vertex.xy = uv1 * lightmapST.xy + lightmapST.zw;
// OpenGL right now needs to actually use incoming vertex position,
// so use it in a very dummy way
vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
}
//使用动态光照
if (unity_MetaVertexControl.y)
{
vertex.xy = uv2 * dynlightmapST.xy + dynlightmapST.zw;
// OpenGL right now needs to actually use incoming vertex position,
// so use it in a very dummy way
vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
}
return mul(UNITY_MATRIX_VP, float4(vertex.xyz, 1.0));
#else
return UnityObjectToClipPos(vertex);
#endif
}
方法内部看上去不是很好理解,特别是顶点位置的映射。因为它是为了提供数据给光照烘焙系统,而不是摄像机,所以这里相当于是要将坐标映射到光照贴图上的位置
如果没问题的话,就可以完全用自己的 Shader 得到一个正确的带有间接光的场景了:
3.3 自发光物体
如果片段着色器中计算出的反射率(Albdeo)是有值的,那么就可以看到周围物体间接光的效果,但如果物体本身拥有较强的自发光(emissive),就有可能得不到正确的效果:自发光对间接光的贡献为0
这个问题出在 Shader 的 globalIlluminationFlags 属性上,它用于存储材质与光照贴图和光照探针的交互方式,需要在 ShaderGUI 中将这个属性暴露:
void DoubleGI(MaterialEditor materialEditor)
{
EditorGUI.BeginChangeCheck();
m_MaterialEditor.LightmapEmissionProperty();
if (EditorGUI.EndChangeCheck())
{
Material m_Material = materialEditor.target as Material;
m_Material.globalIlluminationFlags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack
}
m_MaterialEditor.DoubleSidedGIField();
}
可以看到它有三个选项:分别对应着官方文档中的这三个变量:
- None:发射光照完全不影响全局光照
- RealtimeEmissive:发射光照会影响实时全局光照,它将光照发射到实时光照贴图和实时光照探针中
- BakedEmissive:发射光照影响烘焙全局光照,它将光照发射到烘焙光照贴图和烘焙光照探针中
由于物体拥有自发光属性就意味着它是作为光源的,因此这里要设置为 BakedEmissive,但是还有问题,globalIlluminationFlags 还有第四个变量,也就是 EmissiveIsBlack,它为 true 意味着发射光照保证是黑色的,这样光照贴图系统可知道不必从材质中提取发射光照信息。可是它在材质界面并没有被暴露,所以还需要做一个小操作,将它在代码里置为 false,这样问题就解决了
四、Directional Lightmaps
由于场景中的静态物体不带直接光照数据,基本上辐射率都来源于采样 lightmaps,其次是镜面 IBL,因此对于有法线贴图的物体,可能无法得到正确的结果:你会发现法线贴图几乎不起作用。这很正常,光照烘焙系统也只使用几何体的顶点数据

在 Lightmapping 设置里面,有一个 Directional Mode 选项,之前的例子里默认的是 Non-Directional:
- Non-Directional:不会生成额外的 Directional Lightmaps
- Directional:Unity 会额外生成第二张 Lightmap 来存储入射光的主要方向,在一些很旧的平台上(例如 GLES 2.0)可能不支持
这次改成 Directional 重新烘焙,就可以看到额外的 lightmap 了:
接下来是 Shader:
对于 lightmap 有一个很关键的宏 LIGHTMAP_ON,而 Directional lighmap 一样有,那就是 DIRLIGHTMAP_COMBINED 和 DIRLIGHTMAP_OFF,如果觉得加一些 #pragma shader_feature 预编译指令比较麻烦,可以直接通过下面一行代码全部包括,它帮你考虑了前向渲染中所以可能用到的 keyword,不过这样可能会导致变体变多
//#pragma multi_compile __ LIGHTMAP_ON VERTEXLIGHT_ON //不再需要了
#pragma multi_compile_fwdbase
接下来就是采样部分:
#if defined(LIGHTMAP_ON)
float3 indirectDiffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
#if defined(DIRLIGHTMAP_COMBINED)
float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, i.lightmapUV);
indirectDiffuse = DecodeDirectionalLightmap(indirectDiffuse, lightmapDirection, normal);
#endif
#else
float3 indirectDiffuse = max(0, ShadeSH9(float4(normal, 1)));
#endif
这里使用了一个小技巧:那就是对于 unity_Lightmap 和 unity_LightmapInd(directional lighmap) 使用了同一个采样器,采样器的数量是有上限的,DX11为16个,对于大型项目而言,如果做不到采样器复用就有可能会超数量:
- UNITY_DECLARE_TEX2D(tex):定义纹理(Texture2D)和采样器(sampler)
- UNITY_DECLARE_TEX2D_NOSAMPLER(tex):仅定义纹理
- UNITY_SAMPLE_TEX2D(tex, coord):纹理采样
- UNITY_SAMPLE_TEX2D_SAMPLER(tex, samplertex, coord):复用其它纹理的采样器对当前纹理采样
使用相同环绕方式和纹理过滤方式的两张同维度纹理可以使用同一个纹理采样器
其实对于法线纹理和辐照度纹理,也可以使用同一个采样器,之前我们都是这样写的:
sampler2D _MainTex;
sampler2D _EmissionX;
sampler2D _NormalMap;
//纹理3,纹理4……
如果你的平台支持采样器和纹理分离的话,它就同等与下面:
Texture2D _MainTex; sampler2D _MainTex;
Texture2D _EmissionX; sampler2D _EmissionX;
Texture2D _EmissionX; sampler2D _NormalMap;
// 定义纹理与对应的采样器
#define UNITY_DECLARE_TEX2D(tex) Texture2D tex; SamplerState sampler##tex
// 只定义纹理
#define UNITY_DECLARE_TEX2D_NOSAMPLER(tex) Texture2D tex
// 普通的纹理采样
#define UNITY_SAMPLE_TEX2D(tex, coord) tex.Sample(sampler##tex, coord)
// 通过samplertex传入其他纹理的名称,可复用其他纹理的采样器进行采样
#define UNITY_SAMPLE_TEX2D_SAMPLER(tex, samplertex, coord) tex.Sample(sampler##samplertex, coord)
和 lightmap 一样,最后需要用 DecodeDirectionalLightmap 方法解码
参考文章: