《Unity Shader入门精要》读书笔记08

铁名_IronName Lv2

第 9 章 更复杂的光照

这一部分跟 Unity 强相关,并不是通用的知识,所以应该速通,硬看也记不住。

Unity 的渲染路径

渲染路径(Rendering Path) 决定了光照是如何应用到 Unity Shader 中的。
大多数情况下,一个项目只用一种渲染路径,默认是前向渲染路径。但也可以为每个摄像机使用不同的渲染路径。
Pass 使用的渲染路径通过 LightMode 标签设置。设置了正确的渲染路径,才访问到正确的内置光照变量。

1
2
3
Pass {
Tags { "LightMode" = "ForwardBase" }
}

前向渲染路径

原理

1
2
3
4
5
6
7
8
9
10
11
12
Pass {
for (模型中的 每一个 图元){
for (这个图元覆盖的 每一个 片元){
if (没有通过深度测试)
discard;舍弃
else{
如果片元可见,就进行光照计算
更新帧缓冲
}
}
}
}

Unity 中的前向渲染

Unity 中,前向渲染路径处理光照的方式有 3 种:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics,SH)处理。同时,光源的渲染模式有 Auto/Important/Not Important,可以在光源的 Light 组件中设置。Unity 会根据重要程度来选择处理每个光源的方式。

“前面提到过,前向渲染有两种 Pass:Base Pass 和 Additional Pass。”
我之前笔记里没有,PDF 文件里也搜不到…

  • Base Pass 中可实现光照纹理、环境光、自发光、平行光的阴影。光照计算有 一个逐像素的平行光以及所以逐顶点和SH光源。渲染设置:
1
2
Tags {"LightMode"="ForwardBase"}
#pragma multi_compile_fwdbase
  • Addtional Pass 中默认情况下不支持阴影,但是可以通过使用#pragma multi_compile_fwdadd_fullshadows编译指令来开启阴影。光照计算有 其他影响该物体的逐像素光源,每个光源执行一次Pass。渲染设置:
1
2
Tags {"LightMode"="ForwardAdd"}
#pragma multi_compile_fwdadd
  • #pragma multi_compile_fwdbase 这种是编译指令,保证Unity可以为相应类型的Pass生成所有需要的Shader变种。这些变种会处理不同条件下的渲染逻辑,比如是否使用那个 lightmap、当前使用哪种光源类型等。
  • 内置的光照变量和函数等

顶点照明渲染路径

全部使用逐顶点处理,在一个Pass中就可以完成对物体的渲染。
属于前向渲染路径的一个子集。

延迟渲染路径

使用额外的缓冲区,G 缓冲(G-buffer),G 是 Geometry 的缩写。
使用 2 个 Pass。第一个 Pass 中,计算片元的可见性,存入 G 缓冲区。第二个 Pass 中,利用 G 缓冲区的各个片元信息,进行真正的光照计算。改善在场景中包含大量实时光源时,前向渲染性能会急速下降的问题。

此处提到的 Pass 似乎和 SubShader 中的 Pass 有出入。

  • 不支持真正的抗锯齿 (anti-aliasing)
  • 不能处理半透明物体
  • 对显卡有要求,必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

Unity 的光源类型

三种光源

1.平行光

太阳光,几何定义最简单,没有衰减。

2.点光源

照亮空间有限,由空间中的一个球体定义,有衰减。

3.聚光灯

照亮空间有限,由空间中的一个锥形定义,有衰减。

实践:处理不同的光源类型

实践

使用前向渲染,处理不同的光源类型

  • #pragma multi_compile_fwdbase可以保证在Shader中使用光照衰减等光照变量可以被正确赋值。#pragma是编译指令。
  • 在 Additional Pass 中使用 Blend 命令开启了混合模式,因为我们希望这个 Pass 中的光照结果可以叠加(多个光源嘛)
  • 因为衰减计算的计算量大,Unity使用一张纹理作为查找表(Lookup Table,LUT)。先转换到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值。

#include "AutoLight.cginc"可以消除 unity_WorldToLight 引起的报错。

衰减值atten不影响环境光!也就是说 ambient + (specular + diffuse) * atten

实验

Base Pass 和 Additional Pass 的调用。更直观地理解 Unity 自动决定哪些光源是逐像素光,而哪些光源是逐顶点或SH光。使用帧调试器查看。

  • 被标记为 Not Important 的光源不会执行 Additional Pass。

Unity 的光照衰减

用于光照衰减的纹理

1
2
float3 lightCoord = mul(_LightMatrix0, float4(i.worlPosition, 1)).xyz;
fixed atten = tex3D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

注:需要#include "AutoLight.cginc"

不使用距离值采样,是为了避免开方操作。

使用数学公式计算衰减

线性衰减:

1
2
float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
atten = 1.0 / distance; // 线性衰减

是的,上面这个叫线性衰减。 1distance2\frac{1}{distance^2}叫二次衰减。现代实践多用 11+distance2\frac{1}{1+distance^2}这样的混合模型,和distance2distance^2相加的 11 还可以调整为其他值。

目前 Unity Shader 中无法通过内置变量直接访问光源的范围、聚光灯的朝向等信息,要用的话得用脚本将光源信息传给Shader。灵活性低。

Unity 的阴影

阴影的实现

传统的 Shadow Map

把摄像机放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。
使用 LightMode 标签被设置为 ShadowCaster 的 Pass,这个 Pass 的渲染目标不是帧缓存,而是阴影映射纹理/深度纹理。Shader中包含ShadowCaster的物体才能向其他物体投射阴影。

平形光的级联

级联阴影贴图(Cascaded Shadow Map) 是针对平行光阴影的专用技术,用来解决一个根本矛盾:用一张固定分辨率的Shadow Map,既想看清脚边的清晰阴影,又想覆盖远山的轮廓,是不可能的。

  • 问题:平行光照射范围极广(如整个游戏世界)。如果只用一张Shadow Map覆盖整个范围,那么每个纹素对应的世界空间面积会很大,导致近处物体的阴影边缘严重锯齿化,像打了马赛克。

  • 解决方案:CSM将相机视锥体按深度由近到远划分为多个区域(级联)。然后,为每一个级联区域单独生成一张高精度的Shadow Map。

    • 第一级联:覆盖最近、最需要细节的区域(如角色周围),使用最高分辨率。

    • 后续级联:覆盖更远的区域,分辨率可以相同或依次降低。

点光源的阴影纹理

对于点光源(omnidirectional light),单一的“光源相机”视角无法捕捉其周围全部6个方向。解决方案是使用 立方体贴图阴影

  • 数据结构立方体贴图是一张特殊纹理,它包含6个独立的2D正方形面,分别对应三维空间的 +X, -X, +Y, -Y, +Z, -Z 六个方向,可以理解为包裹在点光源外面的一个盒子内壁的六张照片

  • 生成过程:为了生成点光源的Shadow Map,GPU会以点光源为中心,沿这6个轴向分别渲染一次场景(执行6次 ShadowCaster Pass),将深度分别记录到立方体贴图的6个面上。这个过程你可以想象为,在点光源的位置放置一个特殊的6面全景相机,一次拍摄生成完整的周围深度信息。

  • 采样比较:当判断一个片元是否在点光源的阴影中时,需要:

    1. 计算从点光源到该片元的方向向量
    2. 用这个方向向量作为坐标,去采样那张立方体贴图深度纹理,获取“从光源到最近物体的距离”。
    3. 再与“从光源到当前片元的实际距离”进行比较,判断阴影。

屏幕空间的阴影映射技术(Screenspace Shadow Map)

原本是延迟渲染中产生阴影的方法。要求显卡支持MRT。
先通过 ShadowCaster 的 Pass 得到阴影映射纹理;然后根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。物体接收阴影时,把表面坐标变换到屏幕空间中,然后使用这个坐标对阴影图采样即可。

物体接收阴影和向其他物体投射阴影是两个过程。接收阴影是物体对阴影映射纹理采样;而投射阴影是执行 ShadowCaster,把该物体加入阴影隐射纹理的计算中,这样其他物体在对阴影纹理采样时就会得到和该物体有关的信息。

实践:不透明物体的阴影

实践

在 Unity 中尝试 Light 组件的Shadow Type,Mesh Renderer 组件中的 Cast Shadows 和 Receive Shadows。

  • 虽然我们写的 Shader 中没有 ShadowCaster,但是Fallback的“Specular”的Fallback的“VertexLit”中有 LightMode 为 ShadowCaster 的 Pass。
  • 默认情况下,计算光源的阴影映射纹理时,会剔除物体的背面。

实践

在 Chapter9-ForwardRendering.shader 基础上添加接收阴影的效果。

  • 计算阴影会用到的三个内置宏:SHADOW_COORDS、TRANSFER_SHADOW 和 SHADOW_ATTENUATION。使用这些宏需要保证:a2v 结构体中顶点坐标变量名称是 vertex;v2f 结构体中的顶点位置变量名称是 pos。

shadow不影响环境光!也就是说 ambient + (specular + diffuse) * atten * shadow

实践

使用帧调试器查看阴影绘制过程

  • UpdateDepthTexture 更新摄像机的深度纹理
  • RenderShadowmap 渲染平行光的阴影映射纹理
  • CollectShadows 根据深度纹理和阴影映射纹理得到屏幕空间的阴影图

统一管理光照衰减和阴影

由于光照衰减和阴影效果都是对 散射和高光部分 相乘,所以我们想找到一个方法,同时计算两个信息。

实践

通过内置的 UNITY_LIGHT_ATTENUATION 实现统一管理光照衰减和阴影。

1
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

不需要提前声明 atten,这个宏会声明这个变量。

  • 使用内置宏可以简化代码。

实践:透明物体的阴影

实践

使用透明度测试和阴影,渲染一个正方体。

  • 由于透明度混合需要关闭深度写入,让阴影处理变得非常复杂。所以 Unity 内置的半透明 Shader 是不会产生任何阴影效果的。
  • 标题: 《Unity Shader入门精要》读书笔记08
  • 作者: 铁名_IronName
  • 创建于 : 2026-01-20 13:25:02
  • 更新于 : 2026-01-23 14:35:17
  • 链接: https://blog.ironname.top/2026/01/20/《Unity-Shader入门精要》读书笔记08/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论