22. Unity 光照烘焙系统#
烘焙前设置#
天空盒#
设置天空球材质
路径: Windows → Rendering → Lighting Settings → Skybox Material
静态#
为了让场景响应烘焙, 我们需要将场景物体设置成静态, 并启用 Contribute GI 和 Reflection Probe Static 这两个Flags
各静态选项说明:
组件 | 功能 |
---|---|
Contribute GI | 响应全局光照 |
Occluder/Occludee Static | 响应遮挡剔除 (OccCulling) |
Batching Static | 合批优化 |
Navigation Static/Off Mesh Link Generation | 导航相关 |
Reflection Probe Static | 决定物体是否出现在反射探头记录中 |
LightingMode#
路径: Windows → Rendering → Lighting Settings → MixedLighting → LightingMode
LightingMode 选项 | Directional Light Mode 选项 |
---|---|
Baked Indirect | RealTime |
Subtractive | Mixed |
ShadowMask | Baked |
注:ShadowMask Mode 需在 ProjectSetting → Quality → Shadow → Shadowmask Mode 开启.ShadoeMask Mode 额外生成一张 Shadowmask 贴图
组合列表#
LightingMode 选项 | Directional Light Mode 选项 | 光照公式 | 投影处理 |
---|---|---|---|
Baked Indirect | RealTime | LM = GI = EmitLighting + SkyLighting | 实时投影 |
Mixed | LM = GI = EmitLighting + SkyLighting + LightsGI | 实时投影 | |
Baked | LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting | 静态物:投影写入LM 动态物:无 | |
Subtractive | RealTime | LM = GI = EmitLighting + SkyLighting | 实时投影; RealTime Shadow Color设置失效 |
Mixed | LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting | 静态物:投影写入LM 动态物:实时; RealTime Shadow Color设置有效 | |
Baked | LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting | 静态物:投影写入LM 动态物:无; RealTime Shadow Color设置失效 | |
ShadowMask | RealTime | LM-light = GI = EmitLighting + SkyLighting LM-shadowmask = null | 实时投影 |
Mixed | LM-light = GI = EmitLighting + SkyLighting + LightsGI LM-shadowmask = LightsShadow | 静态物:投影写入LM 动态物:实时投影 | |
Baked | LM = GI + DL = (EmitLighting + SkyLighting + LightsGI) + LightsLighting LM-shadowmask = null | 静态物:投影写入LM 动态物:无 | |
Lighting | 说明 | ||
EmitLighting | 发光材质造成的光照 | ||
SkyLighting | 天空球造成的光照 | ||
LightsGI | 主光造成的反弹光 | ||
LightsLighting | 主光造成的光照 | ||
LM | Lightmap | ||
DL | DirectLighting |
注:Subtractive 和 ShadowMask 对动态物体的实时投影的处理不同,ShadowMask 对动静态物的投影混合更自然
其他设置#
自发光材质 Global Illumination 设为 Baked,Color 设为 HDR
- Lightmapper: Progressive CPU
- Lightmap Resolution: 40
- Directional Mode: Non-Directional(一般不推荐,效果不明显,能耗翻倍)
常见方案#
- 全实时光照:不需要 Lightmap
- 全实时直接光照 + Baked Indirect
- 静态物体烘焙,动态物体实时 (Subtractive/ShadowMask)
- 辅助光只用于烘焙 (Baked/Mixed)Mixed 决定是否影响动态物体
- 特效 RealTime
合批策略#
定义#
把渲染过程相同的几个批次的渲染合并起来,节省资源,增加速度.一般情况下,共用 Material, 参数相同才能合批
常见策略#
- Unity static 设置
- Unity SRPBatching (同 Shader 即可合批)
- GPU Instancing
- 手动合批 (DDC 软件内合并模型)
FrameDebugger#
Windows → Analisis → FrameDebugger
FrameDebugger 可查看合批与渲染过程
23. 外部烘焙光照贴图#
外部烘焙流程#
使用 3dsMax/MAYA 烘焙 LightingMap,需为模型创建 UV2
贴图:
- AO Map 代替 SkyLighting
- EmitLighting
- LightsShadow
- LightGI + LightsLighting
- Alpha
用 SD 的 Blur HQ 节点消除噪点,合并贴图节省资源
光照构成#
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.烘焙完成后,可以分别预览每一张贴图的独立效果,并可通过全局参数灵活调节最终画面表现.
主光
天光
自发光
文件结构#
文件 | 说明 |
---|---|
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 | 自发光颜色 |
烘焙流程#
- 收集场景烘焙信息:包括贴图数量、路径、纹理对象
- 创建缓存:为主光、天光、自发光 GI 和最终合成的 Lightmap 分别准备缓存.
- 烘焙:依次烘焙主光、天光、自发光 GI,每步结果都存入缓存.
- 合成 Lightmap:把三步烘焙的结果合成一张最终 Lightmap.
- 替换旧 Lightmap:用新合成的 Lightmap 覆盖场景原有的光照贴图.
- 重置场景光照环境:修改场景设置.
- 更新全局参数:同步参数到所有相关材质.
- 烘焙反射探头:让反射效果也能正确显示.
核心代码#
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) | 比如"载具"是父类,“小轿车"是子类,子类自动拥有父类的基本功能 |
MonoBehaviour | Unity 所有能挂在物体上的脚本都要继承它,才能参与游戏生命周期 |
[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/方法 | 说明 |
---|---|
EncodeToEXR | Unity 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 的实现思路和关键代码.
编辑器类#
类/方法 | 说明 |
---|---|
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).
继承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 中手动从输入数据计算并赋值