Unity Shader学习笔记(三) - 光照、高级纹理与时间动画
本文最后更新于 2025年2月2日 晚上
不语,只是一味的学。
光照
渲染路径
渲染路径(Rendering Path)决定了光照如何应用到Shader。Unity支持前向渲染路径(Forward Rendering Path)和延迟渲染路径(Deferred Rendering Path)。通过Edit-Project Settings-Player-Other Settings-Rendering Path选择全局渲染路径,也可以在Camera组件的Inspector中修改。
编写Unity Shader时,需要使用“LightMode”标签指定该Pass使用的渲染路径。具体包含下列选项:
标签名 | 描述 |
---|---|
Always | 无论使用何种渲染路径,始终渲染该Pass |
ForwardBase | 前向渲染,计算环境光、主平行光、逐顶点/球谐光源和光照贴图 |
ForwardAdd | 前向渲染,计算额外的逐像素光源,每个Pass对应一个光源 |
Deferred | 延迟渲染 |
ShadowCaster | 将物体深度信息渲染到Shadow Map或一张深度纹理中 |
前向渲染
前向渲染的步骤如下:
- 对于每个对象的片段:
- 若未通过深度测试,则剔除
- 若可见,则基于材质属性、位置、法线、光照方向、视线方向等信息计算光照
- 更新帧缓冲
对于每个逐像素光源,我们都需要进行一次完整的光照计算流程。也就是说,在前向渲染中,光源数越多,执行的Pass数量越多,性能消耗越大。
在Unity中,前向渲染路径有三种光照处理方式:逐顶点、逐像素、球谐函数。使用哪种方式处理光源取决于光源Inspector面板中的Render Mode属性。它包含Auto、Important、Not Important。设置为Important的光源将会被作为逐像素光源。
Unity的判断规则如下:
- 场景中Intensity最大的平行光始终按逐像素处理
- Not Important的光源不会按逐像素处理,而是使用逐顶点或球谐
- Important的光源始终逐像素
- 若按上述规则判定完毕的逐像素光源小于Quality Settings中的逐像素光源数量,则更多光源按逐像素渲染,直到达到上限。
光照计算在Pass中进行。前面提到,前向渲染的Pass有Base和Additional两种。
前者通过ForwardBase
的LightMode
Tag指定,且需要加上#pragma multi_compile_fwdbase
指令。
后者通过ForwardAdd
的LightMode
Tag指定,且需要加上#pragma multi_compile_fwdadd_fullshadows
指令,同时需要使用Blend One One
指令,使得计算结果与颜色缓冲中的值相加。
带有
_fullshadows
的指令用于给Additional
Pass中渲染的光源附加阴影。如果不需要阴影,删除该后缀即可。带有后缀的Additional Pass会导致更多的Shader变体。
对千前向渲染来说, 一个Unity Shader通常会定义一个Base Pass (Base Pass也可以定义多次, 例如需要双面渲染等情况)以及一个Additional Pass。 一个Base Pass仅会执行一 次(定义了多个BasePass的情况除外), 而一个Additional Pass会根据影响该物体的其他 逐像素光源的数目被多次调用, 即每个逐像素光源会执行一次AdditionalPass。
延迟渲染
延迟渲染中,第一个Pass仅用于深度测试,通过测试的片段将会将其携带的材质信息、位置、法线等数据写入G-Buffer。第二个Pass采样G-Buffer进行光照计算。
延迟渲染的缺点有:
- 对MSAA支持不佳
- 无法处理半透明物体
- 显卡必须支持MRT等较新的特性
延迟渲染的优点有:
- 支持的光源数量大大提升
Unity中,默认G-Buffer包含下列RT:
- RT0:ARGB32,RGB存储漫反射颜色,A未使用
- RT1:ARGB32,RGB存储镜面反射颜色,A存储镜面反射指数
- RT2:ARGB2101010,RGB存储法线,A未使用
- RT3:ARGB32(非HDR)或ARGBHalf(HDR),存储自发光、光照贴图、反射探针
- 深度、模板缓冲
在第二个Pass计算光照时,仅能使用Unity内置标准光照模型。若要替换,请见延迟着色渲染路径 - Unity 手册。
光源类型
Unity中,包含平行光、点光源、聚光灯以及面光源(仅在烘焙时起效)。
光源的常用属性有位置、方向、颜色、强度、衰减。
- 对于平行光,它的位置属性没有意义。
- 对于点光源,其照明范围为一个球体,衰减值由一个函数定义。
- 对于聚光灯,其照明范围为一个四棱锥。锥体的半径由Range属性决定,锥体张开角度由Spot Angle定义。
对于我们自己编写的Shader,需要考虑场景中存在的任何种类的光源,并分别对它们加以处理。主光源在ForwardBase Tag Pass下计算光照值与衰减(没错,衰减值要手动计算);其他光源在ForwardAdd Tag Pass下计算。
可以使用
USING_DIRECTIONAL_LIGHT
宏进行条件编译,来判断不同的光源类型。平行光的
_WorldSpaceLightPos0
的w分量没有意义,xyz分量作为LightDir
。
光照衰减
计算光照衰减是个性能消耗很大的过程,因为其中包含开根号。为了避免这种情况,Unity使用内置变量_LightTexture0
作为衰减贴图,将片段在光源空间下的坐标的平方作为采样坐标,对其进行采样,作为衰减值。
1 |
|
阴影
传统Shadow Map
Shadow Map是最常见的阴影技术,它将光源作为相机,对周围进行一次仅深度写入的渲染,得到一张深度图,从而得到哪些片段应当被照亮。
用于生成Shadow Map的仅深度写入的Pass的LightMode为ShadowCaster。
标记LightMode为ShadowCaster后,此Pass的RT便自动更改到深度纹理。
一般我们无需手动编写ShadowCaster Pass,而是在Fallback语义块中指定Unity的默认Shadow Caster Pass。
Mesh Renderer的Cast Shadow属性本质上是该物体是否被Shadow Caster Pass渲染。
屏幕空间Shadow Map
屏幕空间Shadow Map通过光源的深度纹理和摄像机的深度纹理计算得到屏幕空间的阴影图。通过将片段坐标变换到屏幕空间,就能知道该片段是否位于阴影中。
接受阴影
步骤如下。
#include “AutoLight.cnginc”
- 在
v2f
结构体中添加内置宏SHADOW_COORDS(2)
,声明用于对阴影纹理采样的坐标。
注意,括号中的数字为插值寄存器序号,代表
TEXCOORDN
- 在VS中,返回颜色值前添加宏
TRANSFER_SHADOW(o)
- 在FS中使用宏
SHADOW_ATTENUATION(i)
计算阴影值 - 添加到最终的颜色值上
衰减与阴影的统一管理
光源衰减和阴影本质上都是最终颜色值的系数。使用UNITY_LIGHT_ATTENUATION
宏可以同时处理这两个信息。步骤如下:
#include “Lighting.cginc”
、#include “AutoLight.cginc”
v2f
添加SHADOW_COORDS(2)
- VS中调用
TRANSFER_SHADOW
宏 - FS中调用
UNITY_LIGHT_ATTENUATION(atten, v2f, worldPos)
得到out atten
值。
在使用UNITY_LIGHT_ATTENUATION
时,我们不再需要在Base Pass中单独处理阴影,也无需在Add Pass中判断光源类型来处理衰减。
透明物体阴影
步骤如下:
- 像之前一样,使用
UNITY_LIGHT_ATTENUATION
计算阴影与衰减 - 声明_Cutoff属性
- 使用`“Transparent/Cutout/VertexLit”作为Fallback
- 设置此半透明物体的
Mesh Renderer
的Cast Shadow
属性为Two Sided
注意:此设置仅是用与Alpha Test处理的透明物体。使用Alpha Blend
的物体始终无法实现现实中的半透光阴影,只能把Fallback
设置为Vertex Lit
,投射完整阴影。
高级纹理
CubeMap
CubeMap包含六张图像,对应立方体的六个面。CubeMap使用三维向量而非二维纹理坐标进行采样。
CubeMap最常应用于Skybox。创建CubeMap天空盒材质的步骤如下:
- 新建材质,将其Shader选择为Skybox/6_Sided
- 将六张纹理分别赋值给六个面(纹理的环绕模式需要设置为Clamp)
- 在Window-Lighting菜单中把材质赋值给Skybox属性。
- 设置Camera组件的Clear Flag为Skybox
除了创建材质并赋值外,也可以直接导入HDRI,将其Import Settings中的Texture Type设置为Cubemap即可。
也可以通过脚本创建CubeMap。Camera组件的RenderToCubeMap方法可以将相机所看到的图像渲染到指定的CubeMap资产中。用此方法时,需要注意创建的CubeMap资产的导入设置需要设置为Readable,且尺寸要足够大,否则分辨率会比较低。
除此之外,CubeMap也可用于镜面反射的环境映射。以反射材质为例:
- 首先,Properties语义块中需要定义类型为Cube的CubeMap属性。
- 在VS中计算反射方向
1 |
|
- 在FS中使用
texCUBE
函数对CubeMap采样
1 |
|
通过计算菲涅尔项并将其作为漫反射光和镜面反射的lerp系数,可以模拟出较好的环境反射效果。
RenderTexture
由于Built-in管线和URP在RenderTexture和GrabPass方面的应用差距过大,故此处略。
程序纹理
Texture2D
的SetPixel
方法可以精准地设置纹理中某个像素的颜色。由此,我们可以在Unity内部生成程序化纹理。
然而,在实际项目中,更多会使用Substance Designed进行程序化纹理设计。SD生成的材质以.sbsar
为后缀,可以直接拖入Unity。
时间与动画
时间内置变量
Unity Shader包含下列时间相关内置变量:
名称 | 类型 | 描述 |
---|---|---|
_Time | float4 | t为自场景开始所经过的时间,分量分别为(t/20, t, 2t, 3t) |
_SinTime | float4 | t为时间正弦值,分量分别为(t/8, t/4, t/2, t) |
_CosTime | float4 | t为时间余弦值,其余同上 |
unity_DeltaTime | float4 | dt为时间增量,份量分别为(dt, 1/dt, smoothDt, 1/smoothDt) |
纹理动画
序列帧
一张序列帧如图:
使用下列代码实现序列帧播放。
- 首先,Properties块:
1 |
|
- 标准的半透明Shader起手,即Tag中Queue、RenderType设置为Transparent, IgnoreProjector设置为True,启用Blend,禁用深度写入
- VS转换顶点坐标即可
- FS如下:
1 |
|
尽管这种方法是脱裤子放屁,但它的思想仍然值得学习。
滚动动画
由于我已经实现过了,所以略。
顶点动画
河流
思路很简单,用时间正弦值对Sprite的顶点进行偏移。
需要注意的是,**使用顶点动画时,需要标记Tag “DisableBatching”为”True”。**因为Unity在进行Batching时,会将若干模型进行合并,导致原本的模型空间坐标发生差错。为了避免这种情况,需要关闭此材质的批处理。
VS如下:
1 |
|
Billboard
让渲染的物体始终指向摄像机的技术。
同样地,设置为半透明起手式,并禁用批处理。
VS如下:
1 |
|
注意事项
使用了顶点动画的物体,若使用默认的VertexLit作为Fallback,则无法正确投射进行顶点动画后的阴影。我们需要自己写一个ShadowCaster Pass,其中v2f仅包含V2F_SHADOW_CASTER
宏,并在VS中对顶点做一次相同的变换,然后使用TRANSFER_SHADOW_CASTER_NORMALOFFSET
传入一个v2f结构体。在FS中,则直接调用SHADOW_CASTER_FRAGMENT
宏即可。