跳过正文

庄懂的技术美术入门课 - 光照贴图

2201 字·11 分钟

22. Unity 光照烘焙系统
#

烘焙前设置
#

天空盒
#

设置天空球材质

路径: Windows → Rendering → Lighting Settings → Skybox Material

L22_LightingSetting

L22_Skybox

静态
#

为了让场景响应烘焙, 我们需要将场景物体设置成静态, 并启用 Contribute GI 和 Reflection Probe Static 这两个Flags

各静态选项说明:

组件功能
Contribute GI响应全局光照
Occluder/Occludee Static响应遮挡剔除 (OccCulling)
Batching Static合批优化
Navigation Static/Off Mesh Link Generation导航相关
Reflection Probe Static决定物体是否出现在反射探头记录中

L22_Static

LightingMode
#

路径: Windows → Rendering → Lighting Settings → MixedLighting → LightingMode

LightingMode 选项Directional Light Mode 选项
Baked IndirectRealTime
SubtractiveMixed
ShadowMaskBaked

L22_LightingMode

L22_Mode

注:ShadowMask Mode 需在 ProjectSetting → Quality → Shadow → Shadowmask Mode 开启.ShadoeMask Mode 额外生成一张 Shadowmask 贴图

组合列表
#

LightingMode 选项Directional Light Mode 选项光照公式投影处理
Baked IndirectRealTimeLM = GI = EmitLighting + SkyLighting实时投影
MixedLM = GI = EmitLighting + SkyLighting + LightsGI实时投影
BakedLM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting静态物:投影写入LM 动态物:无
SubtractiveRealTimeLM = GI = EmitLighting + SkyLighting实时投影; RealTime Shadow Color设置失效
MixedLM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting静态物:投影写入LM 动态物:实时; RealTime Shadow Color设置有效
BakedLM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting静态物:投影写入LM 动态物:无; RealTime Shadow Color设置失效
ShadowMaskRealTimeLM-light = GI = EmitLighting + SkyLighting
LM-shadowmask = null
实时投影
MixedLM-light = GI = EmitLighting + SkyLighting + LightsGI
LM-shadowmask = LightsShadow
静态物:投影写入LM 动态物:实时投影
BakedLM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting
LM-shadowmask = null
静态物:投影写入LM 动态物:无
Lighting说明
EmitLighting发光材质造成的光照
SkyLighting天空球造成的光照
LightsGI主光造成的反弹光
LightsLighting主光造成的光照
LMLightmap
DLDirectLighting

注:Subtractive 和 ShadowMask 对动态物体的实时投影的处理不同,ShadowMask 对动静态物的投影混合更自然

其他设置
#

自发光材质 Global Illumination 设为 Baked,Color 设为 HDR

L22_GIBake

  • Lightmapper: Progressive CPU
  • Lightmap Resolution: 40
  • Directional Mode: Non-Directional(一般不推荐,效果不明显,能耗翻倍)

L22_OtherSetting

常见方案
#

  • 全实时光照:不需要 Lightmap
  • 全实时直接光照 + Baked Indirect
  • 静态物体烘焙,动态物体实时 (Subtractive/ShadowMask)
  • 辅助光只用于烘焙 (Baked/Mixed)Mixed 决定是否影响动态物体
  • 特效 RealTime

合批策略
#

定义
#

把渲染过程相同的几个批次的渲染合并起来,节省资源,增加速度.一般情况下,共用 Material, 参数相同才能合批

常见策略
#

  • Unity static 设置
  • Unity SRPBatching (同 Shader 即可合批)
  • GPU Instancing
  • 手动合批 (DDC 软件内合并模型)

FrameDebugger
#

Windows → Analisis → FrameDebugger

L22_FrameDebugger

FrameDebugger 可查看合批与渲染过程

L22_FrameDebug

23. 外部烘焙光照贴图
#

外部烘焙流程
#

使用 3dsMax/MAYA 烘焙 LightingMap,需为模型创建 UV2

贴图:

  • AO Map 代替 SkyLighting
  • EmitLighting
  • LightsShadow
  • LightGI + LightsLighting
  • Alpha

用 SD 的 Blur HQ 节点消除噪点,合并贴图节省资源

L23_Lightingmap

光照构成
#

graph LR
    subgraph identifier1[" "]
        direction LR
        光照
        结果
        subgraph identifier2[" "]
            direction LR
            简单光源
            主平行光
            漫反射Lambert
            镜面反射Phong 
            top1[遮挡LM]
        end
        subgraph identifier3[" "]
            direction LR
            复杂光源
            subgraph identifier4[" "]
                direction LR
                漫反射Cubemap
                漫反射1Col
                漫反射
                top2[遮挡LM]
                top3[遮挡LM]
                忽略
                subgraph identifier5[" "]
                    direction LR
                    自发光
                    其他环境光
                    天光
                end
                subgraph identifier6[" "]
                    direction LR
                    遮挡SurfaceOcc
                    镜面反射Cubemap
                    衰减Fresnel
                end
            end
        end
    end
    光照 ==> 简单光源
    简单光源 ==> 主平行光
    主平行光 ==> 漫反射Lambert
    主平行光 ==> 镜面反射Phong 
    漫反射Lambert ==> top1[遮挡LM]
    镜面反射Phong ==> top1[遮挡LM]
    top1[遮挡LM] ==> 结果
    光照 ==> 复杂光源
    复杂光源 ==> 天光
    复杂光源 ==> 自发光
    复杂光源 ==> 其他环境光
    天光 ==> 漫反射Cubemap
    漫反射Cubemap ==> top2[遮挡LM]
    自发光 ==> 漫反射1Col
    漫反射1Col ==> top3[遮挡LM]
    其他环境光 ==> 漫反射
    top2[遮挡LM] ==> 结果
    top3[遮挡LM] ==> 结果
    漫反射 ==> 忽略
    identifier5[" "] ==> 镜面反射Cubemap
    镜面反射Cubemap ==> 遮挡SurfaceOcc
    镜面反射Cubemap ==> 衰减Fresnel
    遮挡SurfaceOcc ==> 结果
    衰减Fresnel ==> 结果

    style identifier1 stroke:none
    style identifier1 fill:transparent
    style identifier2 stroke:none
    style identifier2 fill:transparent
    style identifier3 stroke:none
    style identifier3 fill:transparent
    style identifier4 stroke:none
    style identifier4 fill:transparent
    style identifier5 fill:transparent
    style identifier6 stroke:none
    style identifier6 fill:transparent

Shader 核心结构
#

Shader "Zhuangdong/AP1/L15/L15_LightingMap" {
    Properties{
        // ... 贴图与参数声明 ...
    }
    SubShader{
        Tags { "RenderType" = "Opaque" }
        Pass {
            Name "FORWARD"
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            // ... 以 orge 案例为模板,顶点着色器移除 TRANSFER_VERTEX_TO_FRAGMENT(o) ...
             VertexOutput vert(VertexInput v) { 
                    ...
                    //案例需要 2 个 uv, 这里将 2 个 uv 合并到一个 float4, 节省 TEXCOORD
                    o.uvs = float4(v.uv0, v.uv1);
                    ...
            }
            float3 DecodeNormal (float2 maskXY) {
                //贴图只有 RG 两个通道,我们要补上 B 通道
                float2 nDirTSxy = maskXY * 2.0 - 1.0;
                //贴图值域在 (0,1),法线是向量,向量值域在 (-1,1)
                float nDirTSz = sqrt(1.0 - (nDirTSxy.x * nDirTSxy.x + nDirTSxy.y * nDirTSxy.y));
                //sqrt 取平方根,x 平方+y 平方+z 平方=1
                return float3(nDirTSxy, nDirTSz);
            }
            float4 frag(VertexOutput i) : COLOR {
                // ... 采样、向量、点乘 ...
                //提取表面信息
                float occlusion = var_MaskTex.r;
                float matMask = var_MaskTex.g;//金属贴图
                float3 diffCol = var_MainTex.rgb * lerp(1.0, _MetalDarken, pow(matMask, 5.0));
                //这里给金属叠加_MetalDarken,降低金属部分的 diff
                float specPow = max(1.0, lerp(_SpecParams.x, _SpecParams.z, matMask));
                float specInt = max(0.0, lerp(_SpecParams.y, _SpecParams.w, matMask));
                float reflectMip = clamp(lerp(_EnvReflectParams.x, _EnvReflectParams.z, matMask), 0.0, 7.0);
                //clamp 即 clamp(value,min,max) 会将 value 限制在 (min,max) 中
                float reflectInt = max(0.0, lerp(_EnvReflectParams.y, _EnvReflectParams.w, matMask));
                float fresnel = lerp(pow(1.0 - max(0.0, ndotv), _FresnelPow), 1.0, matMask);
                //区分金属和非金属的 Fresnel 强度
                //提取光照信息
                float skyLightOcc = var_LightMap.r;
                float emitLightingInt = var_LightMap.g;
                float mainLightGIInt = pow(var_LightMap.b, _GIpow);
                float mainLightShadow = var_LightMap.a;
                //采样纹理 Cube
                float3 var_SkyCube = texCUBElod(_SkyCube, float4(vrDirWS, 7.0)).rgb;
                float3 var_EnvCube = texCUBElod(_EnvCube, float4(vrDirWS, reflectMip)).rgb;
                //MainLight
                //Diff
                float3 halfShadowCol = lerp(_HalfShadowCol.rgb, _MainLightCol, mainLightShadow);
                //获取半影颜色, 先给阴影和主光赋予颜色
                float3 mainLightCol = lerp(_MainLightCol, halfShadowCol, _HalfShadowCol.a) * mainLightShadow;
                //再用投影 mask 去掉阴影颜色,留下的就是主光颜色和半影颜色,这里_HalfShadowCol.a 控制半影强度
                float3 mainLightDiff = diffCol * mainLightCol * max(0.0, nDotl);
                //DiffCol*Lambert*MainLight
                //Spec
                float3 mainLightSpec = mainLightCol * pow(max(0.0, vDotr), specPow) * specInt;//Phong
                //GI
                float3 mainLightGI = _GICol * occlusion * mainLightGIInt * _GIInt;
                //GI 也就是主光的反弹光,这里叠加 AO 增加变化
                //Mixed
                float3 mainLight = (mainLightDiff + mainLightSpec + mainLightGI * _MainLightGIOn) * _MainLightOn;
                //这里 GI 已经有环境 AO 的信息了,所以不用*LM

                //OtherLight
                float3 skyLightDiff = diffCol * var_SkyCube * _SkyCol * _SkyLightInt * skyLightOcc * occlusion;
                //这里是物体漫反射*天光漫反射*LM(LightingMap)
                float3 emitLightDiff = diffCol * _EmissionCol * emitLightingInt * occlusion;
                //同上,这部分不受环境 AO 影响,所以不用*_SkyLightOcc
                //OtherEnvSpec
                float3 envLightSpec = var_EnvCube * reflectInt * fresnel * occlusion;
                float3 OtherLight = skyLightDiff * _SkyLightOn + emitLightDiff * _EmitLightOn + envLightSpec * _EnvReflectOn;
                float3 finalRGB = mainLight + OtherLight;
                return float4(finalRGB, 1.0);
            }
            ENDCG
        }
    }
        FallBack "Diffuse"
}

关键参数
#

参数说明参数说明
_SpecParams高光参数_EnvReflectParams反射参数
x非金属高光次幂x非金属 Cube 采样 Mip 值
y非金属高光强度y非金属反射强度
z金属高光次幂z金属 Cube 采样 Mip 值
w金属高光强度w金属反射强度

要点总结
#

  • 法线贴图只有 RG,需手动补 B 通道
  • 半影染色
  • 金属/非金属参数分开控制高光、反射
  • 根据光照构成合成光照

24-26. 自定义烘焙器
#

简介
#

由于此案例代码内容过多,讲解以摘要为主,如需源代码请自行到教程处下载

本案例实现了一个自定义烘焙器,用于完成自定义烘焙、结果预览以及材质编辑等功能.烘焙过程中会生成三张贴图,分别对应主光、天光和自发光三种光源,并最终将这三张贴图合并,输出为最终的 LightingMap.烘焙完成后,可以分别预览每一张贴图的独立效果,并可通过全局参数灵活调节最终画面表现.

主光

L24_MainLight

天光

L24_SkyLight

自发光

L24_EmitLight

文件结构
#

文件说明
LightingController负责整个烘焙流程的主脚本.
LightingControllerGUI自定义烘焙面板 (UI),让操作更直观.
EmissionShaderGUI让材质球在 Inspector 界面可以设置自发光参与 GI.
Building/EmitLight/Sky分别对应建筑、发光、天空球的 Shader.

全局参数
#

参数说明
MetalDarken金属部分的暗度 (影响金属表面反光强度)
MainLightCol主光颜色
SpecParams高光参数 (x/z 为高光次幂,y/w 为高光强度,分别对应金属/非金属)
SkyLightInt天光强度
ReflectParams反射参数 (x/z 为 CubeMap Mip,y/w 为反射强度)
FresnelPow菲涅尔现象的强度
EmissionCol自发光颜色

烘焙流程
#

  1. 收集场景烘焙信息:包括贴图数量、路径、纹理对象
  2. 创建缓存:为主光、天光、自发光 GI 和最终合成的 Lightmap 分别准备缓存.
  3. 烘焙:依次烘焙主光、天光、自发光 GI,每步结果都存入缓存.
  4. 合成 Lightmap:把三步烘焙的结果合成一张最终 Lightmap.
  5. 替换旧 Lightmap:用新合成的 Lightmap 覆盖场景原有的光照贴图.
  6. 重置场景光照环境:修改场景设置.
  7. 更新全局参数:同步参数到所有相关材质.
  8. 烘焙反射探头:让反射效果也能正确显示.

核心代码
#

public void MultiBake()
{
    var buffer = new LightmapsBuffer();
    Bake(BakeMode.BakeMainLight);
    var info = new LightmapsInfo(LightmapSettings.lightmaps);
    buffer.WriteBuffer(info, LightmapsBuffer.BufferType.MainLight);
    Bake(BakeMode.BakeSkyLight);
    buffer.WriteBuffer(info, LightmapsBuffer.BufferType.SkyLight);
    Bake(BakeMode.BakeEmissionGI);
    buffer.WriteBuffer(info, LightmapsBuffer.BufferType.EmissionGI);
    buffer.CreateLightmaps();
    buffer.OverrideLightmaps(info);
    buffer.Clear();
    ArrangeBakeScene(BakeMode.Default);
    UpdateGlobalProperties();
    BakeReflectProbe();
}

LightingController
#

类, 继承与生命周期
#

public class LightingController : MonoBehaviour
术语说明
class可以理解为"蓝图",比如 Shader 是蓝图,Material 是根据蓝图做出来的"物品"
继承(inheritance)比如"载具"是父类,“小轿车"是子类,子类自动拥有父类的基本功能
MonoBehaviourUnity 所有能挂在物体上的脚本都要继承它,才能参与游戏生命周期
[ExecuteInEditMode]一般情况下,脚本只会在 PlayMode 下运行,这个指令让脚本在预览窗口也会运行
private void OnEnable() {UpdateGlobalProperties();};
术语说明
void不同于之前案例的方法,void 类函数不返回值,它主要用于执行特定操作.
OnEnable意味着这个函数在游戏生命周期的 OnEnable 阶段开始运行

UpdateGlobalProperties() 的方法写在 OnEnable 后面,这在C#是允许的

LightmapsInfo
#

要了解以下概念,才能理解 LightmapsInfo 的作用

LightmapData, 结构体、构造函数、Dictionary(字典) 和 foreach 循环

LightmapData
#
  • LightmapData[] 是 Unity 的数据类型,它是当前场景所有光照贴图的集合
  • lightmapsData 是自定义名称,代表LightmapData[], 用于函数内部操作
  • LightmapsData[] 里面包含多个实例,每个实例代表每张 Lightingmap, 每个实例包含: lightmapColor, lightmapDir, shadowMask, occlusionMask
结构体
#

struct 可以理解为一个"容器”,里面可以装不同类型的数据.

语法: public/private 名称 (和 class/struct 同名) (类型,参数名)

构造函数
#

用来初始化结构体 (或类) 的特殊函数,名字和结构体同名.

示例
#
public struct LightmapsInfo {
    public readonly Dictionary<string, Texture2D> lightmapsInfo;
    public LightmapsInfo(LightmapData[] lightmapsData) {
        // 这里会自动初始化所有内容
    }
}
public class Person {
    public string Name;
    public int Age;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}
  • 如果没有构造函数,你需要手动一项项赋值,非常繁琐.
  • 有了构造函数,只需 new LightmapsInfo(参数),所有内容自动安排好.
构造函数的多种用法
#
  • 构造函数可以有多个,参数不同或方法不同即可.
  • 还可以在构造函数里做参数检查,防止出错.
示例
#
public LightmapsInfo(LightmapData[] lightmapsData){
    if(lightmapsData == null) {
        // 做一些容错处理
    }
    // 其他初始化内容
}
Dictionary(字典)
#
  • Dictionary 是一种"键值对"容器,可以通过"名字"快速找到对应的内容.
  • 例如:Dictionary<string, Texture2D>,可以用字符串名字查找对应的贴图路径和对象.
案例
#
Dictionary<string, Texture2D> myDict = new Dictionary<string, Texture2D>();
myDict["MainLight"] = mainLightTex;
Texture2D tex = myDict["MainLight"];
foreach 循环
#

foreach 用于遍历集合 (如数组、List、Dictionary 等)

示例
#
//data 代表实例
foreach (var data in lightmapsData) {
    var texture = data.lightmapColor;
    path = AssetDatabase.GetAssetPath(texture);
    lightmapsInfo.Add(path, texture);
}
int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int num in numbers){
    Debug.Log(num); // 输出:1, 2, 3, 4, 5
}

LightmapsBuffer
#

在 Unity 的光照烘焙流程中,我们需要对不同类型的光照贴图进行缓存、合成和保存.

缓存的作用与实现
#
缓存 (Buffer)
#

可以理解为"存储箱",用来临时保存不同类型的光照贴图,方便后续处理和传递.

枚举 (enum)
#

用来定义一组常量,让代码更清晰易懂.

public enum BufferType {
    MainLight,  // 主光光照:BufferA
    SkyLight,   // 天光光照:BufferB
    EmissionGI, // 自发光 GI: BufferC
    Lightmap    // 最终合成
}

// 分别为不同类型的光照创建缓存
private Texture2D[] _bufferA;   // 主光
private Texture2D[] _bufferB;   // 天光
private Texture2D[] _bufferC;   // 自发光
private Texture2D[] _lightmap;  // 最终合成

ClearBuffer&Clear
#

这两个方法会把对应或全部缓存清空

switch-case: 根据不同类型 (BufferType),执行不同的清理逻辑.

void(类型 参数名):

  • 如果是单个数据,参数名就是这个数据在函数内的临时名称
  • 如果是集合,参数名就是这个集合在函数内的临时名称
  • 如果是 enum, 参数名就是 enum 内元素的临时名称.

WriteBuffer
#

此方法根据模式写入指定缓存

  • 先判断类型,若是最终 Lightmap 则直接返回.
  • 清理对应类型的缓存.
  • 创建新缓存,并从 info 中复制纹理.
if (type == BufferType.Lightmap) return;
// 清理缓存
ClearBuffer(type);
// 创建缓存并从 info 复制纹理
var lightmapsCount = info.lightmapsCount;
var buffer = new Texture2D[lightmapsCount];
for (var i = 0; i < lightmapsCount; i++) {
    var lightmap = info.lightmapsInfo.Values.ElementAt(i);
    buffer[i] = new Texture2D(lightmap.width, lightmap.height, lightmap.format, false);
    Graphics.CopyTexture(lightmap, 0, 0, buffer[i], 0, 0);
}
  • info.lightmapsInfo.Values.ElementAt(i):从字典中按顺序取出第 i 个贴图.

  • Texture2D(纹理宽度,高度,格式(默认RGB24),是否生成mipmap)

  • Graphics.CopyTexture:Unity API,用于高效复制贴图数据.

CreateLightmaps
#

依次读取主光、天光、自发光三种缓存的像素,合成到一张新的 Lightmap 上.

for (var x = 0; x < width; x++) {
    for (var y = 0; y < height; y++) {
        var colA = _bufferA[i].GetPixel(x, y);
        var colB = _bufferB[i].GetPixel(x, y);
        var colC = _bufferC[i].GetPixel(x, y);
        var newCol = new Color(colA.r, colB.g, colC.b, 1.0f);
        lightmap.SetPixel(x, y, newCol.linear);
    }
}
  • 每个像素点都从三张缓存贴图中取出对应颜色,合成到最终 Lightmap.
  • 注意:缓存保存的是 Texture2D 对象,需要逐像素读取和写入.

OverrideLightmaps
#

  • 将合成好的 Lightmap 编码为 EXR 格式 (支持 HDR),并写入磁盘.
  • 刷新 Unity 资源数据库,让新文件被识别.
for (var i = 0; i < lightmapsCount; i++) {
    var bytes = _lightmap[i].EncodeToEXR(Texture2D.EXRFlags.CompressZIP);
    File.WriteAllBytes(lightmapsInfo.Keys.ElementAt(i), bytes);
    AssetDatabase.Refresh();
}
API/方法说明
EncodeToEXRUnity API,将贴图编码为 EXR 格式,EXR 是 Unity 光照系统专用的格式
Texture2D.EXRFlags.CompressZip压缩为 zip
File.WriteAllBytes.NET API,将字节数组写入文件
lightmapsInfo.Keys.ElementAt(i)从 dictionary lightmapsInfo 获取第 i 个 key,即路径
AssetDatabase.Refresh()刷新资源数据库 (仅编辑器模式下可用)

BakeMode&ArrangeBakeScene
#

BakeMode:用枚举定义不同的烘焙模式 (全部、主光、天光、自发光).

ArrangeBakeScene:根据不同模式设置场景参数,如环境光类型和强度.

RenderSettings.ambientMode = AmbientMode.Skybox;
RenderSettings.ambientIntensity = 1.0f;
API/方法说明
ambientMode决定环境光类型 (天空盒、渐变、单色).
ambientIntensity控制环境光强度 (烘焙后生效).

对应 lightingSetting → lighting → Scene → Environment Lighting → Source & Intensity Multiplier

设置主光为静态:

var staticFlags = StaticEditorFlags.ContributeGI | StaticEditorFlags.ReflectionProbeStatic;
GameObjectUtility.SetStaticEditorFlags(mainlight.gameObject, staticFlags);

Bake&BakeReflectProbe
#

Bake 直接调用 Unity API 进行光照烘焙.

public void Bake(BakeMode mode) {
    Lightmapping.Clear();
    Lightmapping.Bake();
}

BakeReflectProbe遍历场景所有反射探头,逐个烘焙并保存.

private void BakeReflectProbe() {
    var allProbe = FindObjectsOfType<ReflectionProbe>();
    foreach (var probe in allProbe) {
        var path = AssetDatabase.GetAssetPath(probe.texture);
        Lightmapping.BakeReflectionProbe(probe, path);
    }
    AssetDatabase.Refresh();
}
API/方法说明
FindObjectsOfType<>()查找场景中所有反射探头
AssetDatabase.GetAssetPath()获取这些反射探头的贴图和路径
Lightmapping.BakeReflectionProbe()Unity API,烘焙反射探头

如果不自定义存储路径的话,反射探头烘焙出来的 Cubemap 默认存在 LightingData 中

LightingControllerGUI
#

在了解了烘焙流程和方法后,我们还需要一个自定义的 UI 面板,让烘焙操作和参数调整更加直观.下面介绍 LightingControllerGUI 的实现思路和关键代码.

L24_LightingController_UI

编辑器类
#

类/方法说明
UnityEditor.Editor自定义编辑器类必须继承自 Editor,并实现 OnInspectorGUI() 方法
OnInspectorGUI()用于绘制 Inspector 面板的内容
public class LightingControllerGUI : Editor
{
    public override void OnInspectorGUI()
    {
        var controller = target as LightingController;
        if (controller == null) return;
        DrawFunctionButtons(controller);
        DrawGlobalProperties(controller);
    }
}
术语/方法说明
override当 class 中的方法被设定为 virtual 或 abstract, 只能用 override 引用并重写执行的内容
target as LightingController获取当前 Inspector 绑定的对象,必须是 MonoBehaviour 派生类
DrawFunctionButtons绘制烘焙相关的操作按钮
DrawGlobalProperties绘制全局参数的调节控件

功能的绘制
#

  • 使用 GUILayout.Button("按钮名称") 创建按钮,点击后执行对应方法.
  • EditorGUILayout.BeginHorizontal()EndHorizontal() 用于让多个按钮在同一行显示.
if (GUILayout.Button("禁术·多重烘培"))
    controller.MultiBake();

EditorGUILayout.BeginHorizontal();
// 可以在这里添加更多按钮
EditorGUILayout.EndHorizontal();

参数的调节与监听
#

  • 使用 EditorGUILayout.BeginFoldoutHeaderGroup 创建可折叠的参数组.
  • EditorGUILayout.Slider 创建可拖拽的滑块,方便调整参数.
  • EditorGUI.BeginChangeCheck()EndChangeCheck() 监听参数变化,变化时自动更新.
EditorGUI.BeginChangeCheck();
_groupAToggle = EditorGUILayout.BeginFoldoutHeaderGroup(_groupAToggle, "材质属性");
if (_groupAToggle)
//等价于 if (groupAToggle == true)
{
    controller.metalDarken = EditorGUILayout.Slider(
        "金属压暗",
        controller.metalDarken,
        0.0f, 5.0f);
}
EditorGUILayout.EndFoldoutHeaderGroup();
if (EditorGUI.EndChangeCheck())
{
    controller.UpdateGlobalProperties();
    EditorUtility.SetDirty(controller);
}
  • _groupAToggle:用于控制折叠组的展开/收起,未赋值时默认为 false.
  • EditorUtility.SetDirty(controller):标记对象为"已修改",确保参数变动能被 Unity 记录.

EmissionShaderGUI
#

通过自定义 ShaderGUI,我们可以让材质球的 Inspector 面板出现"开启自发光 GI"的选项,方便控制自发光是否参与全局光照 (GI).

L24_EmitUI

继承ShaderGUI
#

自定义材质 Inspector 面板时,需要继承 ShaderGUI 类.

public class EmissionShaderGUI : ShaderGUI
{
    // 具体实现见下方
}

继承后,Unity 会自动获取使用该脚本的 Shader 和 Material 信息.

获取当前材质
#

通过 materialEditor.target as Material 获取当前正在编辑的材质实例.

var material = materialEditor.target as Material;

默认界面
#

如果不需要自定义复杂的 UI,可以直接调用基类的 OnGUI,这样会显示默认的属性面板.

base.OnGUI(materialEditor, properties);

自发光GI开关
#

使用 EditorGUILayout.Toggle 创建一个勾选框,控制自发光是否参与全局光照.

Toggle 有 4 个参数, label, value, style, option, 对应文本内容, 条件, 按键风格, 按键尺寸

var ifEmissionGIOn = EditorGUILayout.Toggle(
    "开启自发光 GI",
    material.globalIlluminationFlags == MaterialGlobalIlluminationFlags.AnyEmissive);

material.globalIlluminationFlags = ifEmissionGIOn
    ? MaterialGlobalIlluminationFlags.AnyEmissive
    : MaterialGlobalIlluminationFlags.EmissiveIsBlack;
  • EditorGUILayout.Toggle("界面文本", 条件):根据条件显示勾选状态.
  • 三元运算符 条件 ? 结果A : 结果B,等价于 if-else 语句:
if (ifEmissionGIOn)
    material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.AnyEmissive;
else
    material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack;

Shader部分讲解
#

在本案例中,Shader 与其他案例相比有以下几个关键差别:

  • Lightmap UV 的计算方式
  • MetaPass 的实现
  • 分支声明 (shader_feature)
  • ShadowCaster 的自定义

LightmapUV
#

在 Unity 中,Lightmap 的 UV 坐标通常这样计算:

float2 lmUV = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
  • uv1 是模型的第二套 UV(专门用于光照贴图).
  • unity_LightmapST 是 Unity 自动传入的缩放/偏移参数.

MetaPass
#

MetaPass 是 Unity 光照烘焙系统专用的 Shader 通道.只有带有 MetaPass 的 Shader 才能参与到光照烘焙.

案例代码
#
//Building.Shader 的 MetaPass
Pass {
    Name "META"
    Tags { "LightMode" = "Meta" }
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"
    #include "UnityMetaPass.cginc"
    #pragma shader_feature __ _BAKE_MAINLIGHT _BAKE_SKYLIGHT _BAKE_EMISSIONGI

    struct VertexInput {
        float4 vertex   : POSITION;
        float2 uv0      : TEXCOORD0;
        float2 uv1      : TEXCOORD1;
        float2 uv2      : TEXCOORD2;
    };
    struct VertexOutput {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
    };
    VertexOutput vert (VertexInput v) {
        VertexOutput o = (VertexOutput)0;
        o.pos = UnityMetaVertexPosition(v.vertex, v.uv1, v.uv2, unity_LightmapST, unity_DynamicLightmapST);
        o.uv = v.uv0;
        return o;
    }
    float4 frag(VertexOutput i) : COLOR {
        UnityMetaInput metaIN;
        UNITY_INITIALIZE_OUTPUT(UnityMetaInput, metaIN);
        metaIN.Albedo = Luminance(tex2D(_MainTex, i.uv).rgb);
        metaIN.SpecularColor = 0.0f;
        metaIN.Emission = 0.0f;
        return UnityMetaFragment(metaIN);
    }
    ENDCG
}
要点说明
#
  • UnityStandardMeta.cgnic PBR 流程 META Pass 专用
  • UnityMetaPass.cginc 属于UnityStandardMeta.cgnic的一部分,由于案例不是 PBR 流程,出于优化的考虑,只引用该 cginc. 相比 UnityStandardMeta.cgnic需要自定义顶点着色器部分
  • UnityMetaVertexPosition:将顶点和 UV 转换到烘焙空间.
  • UnityMetaInput:包含 Albedo、SpecularColor、Emission 等烘焙所需数据的数组.
  • UNITY_INITIALIZE_OUTPUT初始化UnityMetaInput
  • Luminance:将颜色转为灰度,保证烘焙时只考虑明暗,不受高光和自发光影响.

分支控制
#

//LightingController.cs
...
case BakeMode.Default:
// 关闭主光
mainlight.enabled = true;
// 设置环境
RenderSettings.ambientMode = AmbientMode.Skybox;
RenderSettings.ambientIntensity = 1.0f;
// 设置 Shader 全局分支
Shader.DisableKeyword("_BAKE_MAINLIGHT");
Shader.DisableKeyword("_BAKE_SKYLIGHT");
Shader.DisableKeyword("_BAKE_EMISSIONGI");
break;
...
#pragma shader_feature __ _BAKE_MAINLIGHT _BAKE_SKYLIGHT _BAKE_EMISSIONGI
  • 需要在脚本和 Shader 设置全局分支
  • 通过分支声明,可以让 Shader 针对不同烘焙模式输出不同内容.
  • 在代码中用 #if defined(...) 进行分支判断:
#if defined (_BAKE_EMISSIONGI)
    metaIN.Emission = opacity;
#elif defined (_BAKE_MAINLIGHT) || defined (_BAKE_SKYLIGHT)
    metaIN.Emission = 0.0f;
#endif
Skybox 特殊说明
#
  • Skybox 的 Shader 不需要 MetaPass,Unity 内部会自动处理天空盒的光照烘焙.

ShadowCaster
#

由于本案例场景是非封闭盒状,且法线朝内,Unity 默认的 ShadowCaster 不能正确投影.需要自定义 ShadowCaster Pass.

案例代码
#
Pass {
    Name "ShadowCaster"
    Tags { "LightMode" = "ShadowCaster" }
    ZWrite On ZTest LEqual Cull off
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"

    struct v2f {
        V2F_SHADOW_CASTER;
    };

    v2f vert(appdata_base v) {
        v2f o;
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }

    float4 frag(v2f i) : SV_Target {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}
要点说明
#
  • appdata_base:Unity 预定义的顶点输入结构,包含 POSITION、NORMAL、TEXCOORD0.
  • V2F_SHADOW_CASTER:输出结构,包含投影所需的裁剪空间位置和辅助向量,等价于:
    • float4 pos : SV_POSITION;
    • float3 vec : TEXCOORD0;
  • TRANSFER_SHADOW_CASTER_NORMALOFFSET(o):计算投影偏移,保证投影正确.
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
    float4 clipPos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \
    o.pos = UnityApplyLinearShadowBias(clipPos); \
    o.vec = ComputeOffsetData(clipPos);
  • SHADOW_CASTER_FRAGMENT(i) Unity 内置宏,在片段着色器中计算阴影投射
  • Cull off:关闭剔除,保证内表面也能投影.

uv0: TEXCOORD0 在 VertexInput 里会自动识别为模型的第一套 UV, 在 VertexOutput, TEXCOORD0 就是通用插值寄存器,是开发者自定义的数据通道,需要在 vert 中手动从输入数据计算并赋值

相关文章

线性代数的本质 - 04
538 字·3 分钟
庄懂的技术美术入门课 - 特效着色器
3020 字·15 分钟
线性代数的本质 - 02
781 字·4 分钟