# 前言

前几天跟同事讨论 Unity Lightmap ,在 Shader (自定义) 中是怎么取的,然后我说 Unity 在 Shader 中直接会给数据,调用 Unity 提弄函数直接采样就行了。

同事表示说,这个光照贴图 UV 呢?哪里取。

当时也是回答说是 Unity 直接提供的,虽说实际也差不多,不过后面下来又想了想,细节方面也已经记不清了。

再想到过去研究光照这块,早已经是多年前的事了,于是重新实际试了试,直接把光照烘焙这块都重新了解了下,简单做个记录。

主要使用工具有:

  • Unity3D 2021.3.6f1
  • 默认渲染管线
  • Forward RenderPath
  • Standard Shader

# LightMapping 数据

首先回到上面那个问题,光照烘焙后,会为场景生成对应光照贴图,贴图数据本身位于场景同名同级的子目录中,Lighting 设置中 LightMaps 会自动被设置上去,此时直接在同场景对应静态物体上也能预览:

默认情况下, Lightmap Index、TilingX、TilingY、OffsetX、OffsetY 这类参数虽然看起来是直接记录在场景对象上的,实际并不是,最简单的测试方法就是将该对象存储为 Prefab (或者复制一份),就会发现 Prefab 上的 LightMap 信息已经丢失 —— 再放回去也是丢失状态 (可能是为了避免污染 Prefab)。
当然此时也可以通过记录 Render.lightmapIndex 之类的,通过脚本给还原。

动态加载 Lightmap 一般都该是通过这种方式进行:先给 LightmapSettings.lightmaps 赋值对应的 Lightmap 数据、贴图,然后给场景静态物体 Render 设置上对应的 lightmapIndex 等数据,Shader 中就能通过 Unity 提供的函数识别然后采样。

注 1:烘焙光照贴图 Shader 中通过 TEXCOORD1 采样:

Unity stores baked lightmap UVs in its mesh in the Mesh.uv2 channel. This channel maps to the TEXCOORD1 shader semantic, and is commonly called “UV1”.

注 2:实时光照贴图 Shader 中通过 TEXCOORD2 采样(勾选 Lighting->Realtime Lighting 生效):

Unity can use data in the Mesh.uv3 channel as input for the real-time lightmap UV calculations. Mesh.uv3 maps to the TEXCOORD2 shader semantic, and is commonly called “UV2”.

# 光源与阴影

每一个光源,若光源是实时的,对于动态物体至少是双倍消耗,对于静态物体也一样。

消耗点主要在于动态阴影上,在 Forwardbase Render Path 中,绘制阴影分为两部分:自己接收阴影以及投射阴影,MeshRender 上也可以单独在 Lighting 选项设置开关。

  • 是否有阴影跟三个条件有关:
    • (1) Shader 中的 接收投射阴影 Pass
    • (2) Render 上的接收投射开关
    • (3) 光源是否设置阴影

投射阴影的 Pass 是特有的一个,它会将自身到光源的方向渲染至一张深度图,投射及采样的大概步骤如下:

  • Shader 中阴影投射 Pass 必须:

    • 打 Tags {"LightMode":"ShadowCaster"},标记该 Pass 专用于阴影投射
    • 增加预编译指令 #pragma multi_compile_shadowcaster
    • 在顶点、片段中调用 Unity 提供的预定义进行处理:V2F_SHADOW_CASTER、TRANSFER_SHADOW_CASTER_NORMALOFFSET、SHADOW_CASTER_FRAGMENT
      • V2F_SHADOW_CASTER:顶点到片段数据变量,直接在 v2f 定义即可
      • TRANSFER_SHADOW_CASTER_NORMALOFFSET:在顶点 Shader 函数中计算了 物体顶点世界坐标 -(减) 光源坐标,即当前顶点到光源向量
      • SHADOW_CASTER_FRAGMENT:在片段 Shader 函数中将顶点到光源向量转长度,计算深度 (光源设置的 shadowbias 就在这里用的:UnityEncodeCubeShadowDepth ((长度 + unity_Light_ShadowBias.x)*_LightPositionRange.w))
  • Shader 中阴影接收 Pass 必须:

    • 打 Tags {"LightMode":"ForwardBase"},标记该 Pass 为向前渲染
    • 增加预编译指令 #pragma multi_compile_fwdbase
    • 在顶点、片段中调用 Unity 提供的预定义进行处理:
      • SHADOWCOORDS (ID):v2f 结构体使用,ID 是个数值,表示使用的 TEXCOORD+ID,保存顶点函数通过 ComputeScreenPos (o.pos) 计算的 showCoord 以传入片元着色函数 (定义的 ID 要避免与其它 TEXCOORD 产生冲突)
      • TRANFER_SHADOW (o):通过顶点 clip 裁剪空间坐标计算屏幕空间阴影采样坐标内部调用的是:ComputeScreenPos (o.pos)
      • SHADOW_ATTENUATION (i):根据 showCoord 采样阴影颜色,返回叠加的阴影强度值。

其中投射 Pass 是必须的一个单独 Pass,导致了额外 DrawCall 消耗。接收 Pass 则可以与正常 Pass 一块处理对颜色进行叠加。

在 Standard Shader 中,开启实时阴影的情况下一个普通的 Sphere 对象甚至产生了 3 个 DrawCall:

1 个渲染深度
1 个渲染物体到光源向量
1 个渲染物体本身

理论上加上阴影的话,不是只会增加一个 ShadowCaster DrawCall 吗?为什么多渲染了 2 次?渲染深度是为了什么?

从上面 FrameDebugger 截图的信息中显示,额外的一个深度渲染依然是由 ShadowCaster 发起的,但是为什么呢?

其实,这是因为在 PC 平台的原因。

Unity Standard Shader 会判断对应平台,如果是手机平台,才会使用上述传统的 Shadowcaster 方式绘制阴影,而 PC 平台则会另外使用 『屏幕空间阴影』,因此才多额外一个 DrawCall 渲染物体的深度。

屏幕空间阴影原本应该是延迟渲染路径采用的方法,Unity 在默认渲染管线下,对支持的平台的前向渲染路径也采用相同方式,可以减少 OverDraw 但是会增加 DrawCall。

切换至 Adnroid 平台,再利用 FrameDebugger 查看:

1 个渲染物体到光源向量
1 个渲染物体本身

对上号了。

# 烘焙

烘焙只用于静态物体,可以让静态物体单独采样烘焙好的贴图,而非实时光源,减少计算。

烘焙后的效果 —— 离线渲染自然也会比实时渲染效果好。

Light Probe Group:根据官方文档及经过测试表明,该组件使得已烘焙的间接光照效果可以施加于动态物体上,更多是对内存消耗。(烘焙静态物体不使用,查看静态对象面板,烘焙后的 Light Probe 选项是置灰 Off 无法修改的)

Reflection Probe:对动态、静态对象都有影响,配合 Light Probe Group 效果更好。

对于烘焙的三个模式,官方文档解释有:

  • Baked Indirect 模式将实时直接光照与烘焙间接光照结合在一起,提供实时阴影。这种光照模式提供逼真的光照和合理的阴影保真度,适用于中档硬件。
  • Shadowmask 模式将实时直接光照与烘焙间接光照结合在一起,为远处的游戏对象启用烘焙阴影,并将烘焙阴影与实时阴影自动融合。这是最真实但也是最耗费资源的光照模式。您可以使用质量设置 (Quality Settings) 来配置其性能和视觉保真度。这种光照模式适用于高端或中档硬件。
  • Subtractive 模式提供烘焙的直射和间接光照,仅针对一个方向光渲染直接实时阴影。这种光照模式不能提供特别逼真的光照效果,适合于风格化的艺术效果或低端硬件。

# MixLighting

  • !混合灯光会大量 (相比 Baked 模式) 增加 Batches、顶点数量,它对静态物体也会产生 Baches ,如果不是一定需求阴影最好还是用纯 Baked

测试场景为:

  • 一个平行光 + 一个点光源
  • 10 个静态物体,2 个动态物体

# Backed Indirect

  • Backed Indirect:只烘焙间接光,灯光效果、阴影必须设置为 Backed 才会真的被烘焙,否则 (Mixed) 静态物体都会直接走实时计算。(消耗很高)

Shadow Distance:范围内的值使用实时阴影,超过此距离则不再渲染阴影。
注:Mixed 点光源在 Shadow Distance 内对所有物体产生阴影,因为是实时的。

如图所示,烘焙后开启灯光 + 隐藏灯光:

# Subtractive

  • Subtractive:Mixed 设置的灯光直接光和间接光都会被烘焙,被烘焙的静态物体在运行时不占用实时光计算,动态物体则单独走实时计算。

Shadow Distance:范围内的值动态对象使用实时阴影,超过此距离则不再渲染阴影。
注:Mixed 点光源对动态物体不产生阴影。

如图所示,烘焙后开启灯光 + 隐藏灯光:

从上图可以看出,点光源并未对物体形成阴影,只有平行光的阴影。

# Shadowmask

  • Shadowmask:根据设置决定显示,这个选项可以配合 Setting->Quality->Shadows 使用

Shadow Distance:范围内的值使用实时阴影,超过此距离则使用烘焙阴影。
ShadowMask:静态物体使用烘焙阴影。
Shadow Cascades:阴影级联数量,大型场景渲染可以考虑至少 2 级,根据对象到观察者的距离提供不同分辨率的深度纹理来解决仅使用一张阴影贴图的质量问题
注:Mixed 点光源在 Shadow Distance 内对所有物体产生阴影,因为是实时的。

  • Mixed 设置的灯光必须存在场景中才能生效,烘焙后隐藏 Light 就 (对静态物体也) 无效了。具体表现取决于 Quality Shadowmask Mode 设置。
    两种设置模式中:
    • Shadow Distance:在范围内静态物体也是实时光,范围外烘焙光,当 Distance 设置为 0 时与 Shadowmask 消耗一致 (静态物体均使用烘焙阴影)—— 比 Shadowmask 消耗还低,因为... 没有动态物体的实时阴影了,但是这样不如直接用 Subtractive 消耗更低。
    • Shadowmask:静态物体始终使用烘焙阴影,动态物体使用实时阴影。

烘焙后相比 Backed Indirect 多了一张 Shadowmask 贴图,实际测试近距离下同一个场景效果及 Batches 与 Backed Indirect 基本一致(因为 Shadow Distance 都是实时阴影)

如图所示,烘焙后 ShadowDistance+ShadowMask 模式:

# Shader 使用 Lightmap

大致上分为四步:

  • 顶点着色器接收参数结构 定义 float2 texcoord1 : TEXCOORD1 以接收 Unity 传入的光照 uv
  • 顶点着色器为 uv 做一次转换: v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw
  • 片段着色器调用内置函数 DecodeLightmap 对 unity_Lightmap 采样颜色
  • 与物体像素本来颜色叠加混合

# 总结

目前测试下来,唯一的问题就是在 Forward RenderPath 下,烘焙光照中,仅平行光能被烘焙为静态对象的阴影,点光源只能用实时阴影。
表现情况是:点光源设置为 Baked 模式,仅光源颜色信息被烘焙至光照贴图,阴影却没有。

上述结论为错误操作导致!
Point Light 和 Directional Light 均有 『Baked Shadow Radius』和『Baked Shadow Angle』,合适的值可以让阴影被烘焙得更加柔和,但两者参数范围是完全是不同的,若光源设置过大,会导致直接丢失阴影 —— 可能在测试过程中不小心动到,导致了上述错误的结论。

经过测试,可以确定 Subtractive 消耗最低,Backed Indirect 和 Shadowmask 消耗都不少,按照消耗多少可以排序为:

  • Shadowmask(Shadow Distance Mode)->Backed Indirect->Shadowmask(Shadowmask Mode)->Subtractive

对比:

  • Shadowmask:烘焙间接光照、最多四个光源的 shadowmask(超出会直接将阴影及光照烘焙至光照贴图上,降级与 Subtractive 差不多),所有对象均为实时直接光照。(比另外两种多了一张 Shadowmask 贴图),动态与静态阴影可以存在融合。
    • Shadow Distance Mode:范围内静态、动态均为实时直接光照、阴影,范围外静态对象为烘焙阴影。
    • Shadowmask Mode:静态对象使用 实时直接光照、烘焙间接光照、阴影、范围内动态对象为实时阴影 (同上比另外两种多了一张 Shadowmask 贴图,最多可存 4 个灯光的 shadowmask,可与动态物体阴影混合)。
  • Backed Indirect:仅烘焙间接光,范围内静态、动态均为实时直接光照、阴影,范围外无阴影。
  • Subtractive:静态对象的所有直接光照、间接光照、阴影均烘焙、范围内动态对象为实时阴影,范围外无阴影。Mixed 点光源对动态物体无阴影 (这次应该没错了吧?)。静态物体无高光效果 (使用 Reflection Probe 会有改善)。

其它重要设置:

  • Shadow Distance:范围内采用实时阴影,实际情况合理设置,在 Shadow Resolution 阴影分辨率不变的情况下,Shadow Distance 越大,显示质量反而可能降低。
  • Shadow Cascades:只对方向光有效,可以将阴影的渲染划分成对应的几块区域,提升近处阴影的分辨率占用,减轻近处阴影的锯齿感,当然也会产生更多性能消耗。经过实测开启后阴影会变得更加清晰 (更硬)。
  • Shadow Projection:默认为 Stable Fit,整体阴影分辨率不变的情况下 Close Fit 可以进一步的提高近处物件的阴影分辨率,同时也有更高的开销
  • 光照贴图 UV 可以在模型设置上启用 Generate Lightmap UVs 自动生成。
  • Direction Mode:设置为 Direction 会多烘焙一张光源方向贴图,可用于模型法线计算。
  • Lightmap Resolution (像素 / 单位):光照图占用像素的全局值,设置越大越精细烘焙越久,当占用量超出 MaxLightMapSize 就会导致烘焙出多份光照贴图的情况。
  • Scale in Lightmap (物体对象上的设置):影响烘焙此物体在光照图上占用的精度。

参考文档:

  • LightingGiUvs
  • lighting-mode
  • mixed-lighting-lightmap-shader-in-unity
  • Unity 接收阴影
  • Unity 中的混合光照
  • Unity 移动平台下的光照烘焙及优化
  • Unity 为什么使用 screen space shadow