初探URP(二) - URP SL特性、HLSL与光照基础
本文最后更新于 2025年2月12日 上午
## URP ShaderLab
此处主要介绍与Built-in不同的部分。
渲染管线
只有SubShader的RenderPipeline
Tag与Shader.globalRenderPipeline
相同时,该SubShader
才会调用。
URP Shader的Renderpipeline
Tag始终应当被设置为UniversalPipeline
。
Pass
每个Pass都应当包含LightMode
Tag,用于指定在何时使用此Pass。
Name关键字用于定义此Pass的唯一名称,便于在其他SubShader中使用UsePass。但不建议这么做,因为UsePass会破坏SRP Batcher。
为什么会破坏?
Shader中的Pass只有共用同一个Unity Per Material CBUFFER时,才能进行SRP Batcher。而UsePass会引用其他Shader中定义的CBUFFER。
通过#pragma target x.x
可以指定指定的着色器编译目标。版本越高,支持的功能越多。
通过#pragma exclude-renderers
、#pragma only_renderers
指定此着色器应用的平台。支持的选项有gles
、gles3
、glcore
、d3d11
、d3d11_9x
等。
LightMode
URP可使用下列LightMode选项:
选项 | 描述 |
---|---|
UniversalForward | 用于渲染前向渲染路径的对象 |
ShadowCaster | 投射阴影 |
DepthOnly | 若启用MSAA或平台不支持复制深度缓冲区,则使用此选项来创建_CameraDepthTexture |
DepthNormals | 若Render Feature需要,则使用Depth Normals PrePass 创建_CameraDepthTexture 和_CameraNormalsTexture |
Meta | 烘焙LightMap期间使用 |
Universal2D | 使用Universal2D渲染器时使用 |
SRPDefaultUnlit | LightMode默认值。用于在前向、延迟渲染中绘制额外Pass。 |
UniversalGBuffer | 用于延迟渲染管线 |
UniversalForwardOnly | 若Shader中存在不适用于延迟渲染的数据,且使用前向渲染,则选择该选项 |
多Pass
URP中,基于SRPDefaultUnlit
标签的多Pass会破坏SRP Batcher兼容性。
实现多Pass的推荐方法为使用Render Feature。
HLSL
使用HLSLPROGRAM
和ENDHLSL
定义HLSL代码块。每个HLSL代码块中都必须包含VS和FS。通过#pragma vertex xx
和#pragma fragment xx
链接FS、VS到指定函数。
变量
- 有如下标量类型:
类型 | 说明 |
---|---|
bool | |
float | 32位浮点数,用于世界空间位置、纹理坐标或设计复杂函数的计算 |
half | 16位浮点数,用于短矢量、方向、物体空间位置、颜色 |
double | 64位浮点数,无法用于输入/输出 |
real | 若需要函数同时支持half、float输入,则将此类型作为参数。默认为half,若指定#define PREFER_HALF 0 ,则使用float |
int | 32位有符号整数 |
uint | 32位无符号整数 |
URP不支持fixed。若遇到原本为fixed的变量,请修改为half。
- 有如下矢量类型:
类型 | 说明 |
---|---|
floatx | 包含x个float的矢量 |
halfx | |
intx |
支持Swizzle操作。
- 有如下矩阵类型:
类型 | 说明 |
---|---|
floatNxM | N行M列的浮点数矩阵 |
intNxM | |
halfNxM |
进行mul
操作时,参数1位矩阵,参数2为需要变换的矢量。
通过数组访问时,索引顺序为[N][M]。
- 纹理对象定义如下:
1 |
|
需要注意的是,使用宏定义纹理和采样器时,应当定义在CBUFFER外。而CBUFFER内使用float4定义纹理名_ST
变量。
- 可定义任意类型的数组,并通过循环进行索引访问。但Unity仅能从C#脚本设置float4和float类型数组。
数组无法包含在CBUFFER块中,这意味着SRP Batcher对于内部定义了数组变量的Shader不太好起作用。一般,我们更多地将数组变量通过
Shader.SetGlobalVector
和Shader.SetGlobalFloat
等方法设置为全局变量。
- 可使用
StructedBuffer<结构体类型>
定义缓冲对象。
缓冲是数组的替代方案,仅能在支持
#pragma target 4.5
的部分平台上使用。通过SystemInfo.supportsComputeShader
检查平台是否支持该特性。
缓冲对象可以通过索引访问。StructedBuffer
是只读的,但RWStructedBuffer
是可读可写的。
1 |
|
在C#脚本中,需要定义与Shader中缓冲泛型类型相同的结构体类型,保证内部成员数据类型一致,并借助ComputeBuffer
类的SetData
方法和material.SetBuffer
方法进行数据传递。如下:
1 |
|
CBUFFER
UnityPerMaterial CBUFFER
应当包含在HLSLINCLUDE块内,用于确保所有Pass使用相同的CBUFFER。
CBUFFER必须包含纹理以外的所有在Properties块中声明过的变量,此外,还必须包含纹理变量的_ST和_TexelSize变量。它不应当包含未在Properties块中声明的变量。
CBUFFER块从CBUFFER_START(UnityPerMateiral)
开始,CBUFFER_END
结束。
结构体
URP中,VS的输入结构体类型应当命名为Attributes
。其中,涉及到位置的,应当在变量名标注其所在坐标空间,如positionWS
。Attributes中的常用于语义包括POSITION
、COLOR
、TEXCOORD0-7
、NORMAL
、TANGENT
等。
SV_VertexID
是一类特殊的用于Attributes的语义,它可以获取每个顶点的标识符,常与ComputeBuffer一同使用。
FS的输入结构体类型应当被命名为Varyings
。其中必须包含用SV_POSITION
语义标记的positionCS
。对于Varyings,COLOR
、NORMAL
、TANGENT
语义尽量少用,尽量多用TEXCOORD
进行数据传递。
一个有趣的语义是
VFACE
,用于标记一个float变量。若此片段为正面,则为正数,否则为负数。
FS输出
一般情况下,使用half4
作为FS输出。然而,在某些情况下,我们需要用到MRT。如果使用UniversalGBuffer
的LightMode
,则默认开启MRT,否则,需要在C#脚本中使用CommandBuffer.SetRenderTarget
中使用RenderTargetIdentifier[]
数组。GLES2等平台不支持MRT。
若要使用MRT,我们这样撰写FS:
1 |
|
VS
一个典型的VS如下:
1 |
|
可以看到,我们并没有用TransformObjectToHClip
函数进行顶点变换,而是用GetVertexPositionInputs(float3 positionOS)
获取了VertexPositionInputs
类型的positionInputs
变量。这个函数包含在ShaderVariablesFunctions.hlsl
内(也包含在Core.hlsl内)。
VertexPositionInputs
结构体包含了positionWS
、positionVS
、positionCS
、positionNDC
四个变量,简化了计算。对于未使用的变量,编译器会自动将它们去除,所以不会有多余的计算。
通过下列代码,我们可以获取法线、切线和副切线向量的世界空间位置,用于构建TBN矩阵:
1 |
|
FS
一个典型的最简FS如下:
1 |
|
在FS中可以实现剔除操作。有两种方式:1. 控制语句+discard关键字 2.使用函数clip(float)
,当float<0则自动剔除该片段。
关键字与Shader变体
Multi Compile
URP中,可以通过#pragma multi_compile _A _B _C (...etc)
来进行条件编译。其中,_A、_B、_C是关键字。在Shader代码中,后续就可以通过#ifdef _A
、#if defined(_A)
、#elif defined(_A)
、#else
等控制语句来进行条件编译。条件编译有逻辑运算符号,如and
、or
、&&
、||
等。
常见的Multi Compile关键字有:
关键字 | 作用 |
---|---|
_ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS | 在顶点着色器中处理附加光源(性能优化选项) |
_ _MAIN_LIGHT_SHADOWS | 启用主光源的阴影计算 |
_ _MAIN_LIGHT_SHADOWS_CASCADE | 启用主光源的级联阴影(CSM) |
_ _ADDITIONAL_LIGHT_SHADOWS | 启用附加光源的阴影计算 |
_ _SHADOWS_SOFT | 启用软阴影(PCF滤波) |
_ LIGHTMAP_ON | 启用光照贴图 |
_ DIRLIGHTMAP_COMBINED | 使用方向性光照贴图(结合法线信息) |
_ LIGHTMAP_SHADOW_MIXING | 混合光照贴图和实时阴影 |
_ SHADOWS_SHADOWMASK | 使用阴影遮罩混合技术 |
_fog | 启用雾效 |
_instancing | 启用GPU实例化支持 |
_ DOTS_INSTANCING_ON | 启用面向数据技术的实例化 |
_ _SCREEN_SPACE_OCCLUSION | 启用屏幕空间环境光遮蔽(SSAO) |
使用#pragma multi_compile
,编译此Shader时会产生若干Shader变体,导致Shader编译时间延长。我们可以通过下列策略计算Shader变体数量:
- 对于单个
#pragma multi_compile
语句,后面跟了N个关键字,则生成N个变体 - 若存在多个语句,则生成的变体数量为每个语句跟的关键字数量的笛卡尔积。
- 例如,语句1有2个关键字,语句2有3个,则最终变体组合总数为2*3 = 6个。
可以通过Edit-Project Settings-Graphics-Shader Loading-Log Shader Compilation开启Shader编译日志,开启后就能在日志中找到Shader变体数量。
最终的变体数量可能会小于计算得到的变体数量,因为Unity会在构建时根据Player Settings
、Quality Settings
进行剔除。
如果某个关键字仅用于VS或FS,可以在multi_compile
后面加_vertex
或_fragment
,这有助于减少变体数量。
关键字有全局和局部之分。一个项目最多有256个全局关键字;一个Shader最多有64个局部关键字。局部的关键字通过在multi_compile
后加_local
定义。局部关键字同样支持指定vertex
和fragment
。
如果multi compile后面跟了一个下划线+一个空格(#pragma multi_compile _ _A _B
),意思是生成一个禁用这两个关键字的变体。
Shader Feature
Shader Feature与Multi Compile类似,但有以下不同:
- 自动生成禁用后附关键字的额外变体
- 代码中未使用的关键字将不会包含在最终Build。这可以大幅缩短构建时间或运行时着色器编译时间。
示例:
1 |
|
光照
URP不支持Surface Shader。
Lighting.hlsl
中包含许多光照计算的帮助函数。包含Lighting.hlsl
前,必须确保定义下列关键字:
1 |
|
通过下列代码传入结构体:
1 |
|
快速开始
通过UniversalFragmentPBR
和UniversalFragmentBlinnPhong
函数,可以快速为Shader附加光照。为了使用这两个函数,我们需要设置InputData
和SurfaceData
结构。
SurfaceData的结构定义包含在SurfaceData.hlsl
中。如下:
1 |
|
InputData的结构定义同样包含在SurfaceData.hlsl
中。如下:
1 |
|
初始化InputData
对于InputData,PBR和简单光照都是相同的。我们这样进行初始化:
1 |
|
简单光照
Lighting.hlsl
头文件中包含了LightLambert
和LightingSpecular
函数,用于计算Blinn-Phong模型中的漫反射和镜面反射项。UniversalFragmentBlinnPhong
函数内部调用了这两个函数。
在使用Blinn-Phong光照模型时,我们这样初始化SurfaceData:
1 |
|
然后,我们便可以这样调用UniversalFragmentBlinnPhong
:
1 |
|
PBR光照
对于PBR光照,我们如此配置SurfaceData:
1 |
|
在FS中:
1 |
|
其他Pass
ShadowCaster
LightMode
为ShadowCaster
的Pass用于投射阴影。在Built-in中,我们使用Fallback
或UsePass
来启用阴影投射Pass,但这会破坏SRP Batcher兼容性。通过善加利用Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl
,我们可以解决这个问题。
我们只需要在Pass中include这个hlsl文件即可,不需要添加其他任何代码。
有一点需要注意,在应用ShadowCaster时,如果此物体应用了顶点动画,则需要使用#pragma vertex xxx
指定一个应用了顶点变化的新VS。
DepthOnly/DepthNormals
与ShadowCaster
类似,我们可以通过直接include DepthOnlyPass.hlsl
和DepthNormalsPass.hlsl
完成操作。
类似地,若应用了顶点动画,则需要指定新VS。
Meta
烘焙GI时需要使用Meta Pass。我们可以使用UnlitMetaPass.hlsl
和LitMetaPass.hlsl
完成此Pass定义。如果这两个文件不能满足我们的要求,也可以通过MetaInput.hlsl
自定义。
Built-in URP对照表
常见内置函数对照
Built-in | URP Function | 描述 |
---|---|---|
float3 WorldSpaceViewDir(float4 v) |
float3 GetWorldSpaceNormalizedViewDir(float3 positionWS) |
计算顶点所在位置到摄像机在世界空间中的方向。 |
float3 ObjSpaceViewDir(float4 v) |
half3 GetObjectSpaceViewDir(float3 positionOS) |
计算顶点在物体空间中的视角方向。一般通过将摄像机位置转换到物体空间后传入作为参数 |
float3 WorldSpaceLightDir(float4 v) |
GetMainLight().direction 或使用_MainLightPosition 计算得到 |
获取主光源在世界空间中的方向。 |
float3 ObjSpaceLightDir(float4 v) |
将_MainLightPosition 通过I_M矩阵变换后计算得到 |
获取主光源在物体空间中的方向。 |
float3 UnityObjectToWorldNormal(float3 norm) |
float3 TransformObjectToWorldNormal(float3 norm) 或使用GetVertexNormalInputs() |
将法线从物体空间转换到世界空间。 |
float3 UnityObjectToWorldDir(float3 dir) |
float3 TransformObjectToWorldDir(float3 dir) |
将方向向量从物体空间转换到世界空间(不考虑平移分量)。 |
float3 UnityWorldToObjectDir(float3 dir) |
float3 TransformWorldToObjectDir(float3 dir) |
将方向向量从世界空间转换到世界空间。 |
float4 UnityObjectToClipPos(float4 v) |
float4 TransformObjectToHClip(float4 v) 或使用GetVertexPositionInputs() |
将物体空间中的顶点坐标转换到裁剪空间。常用于顶点变换 |
fixed3 UnpackNormal(fixed4 sampleValue) |
float3 UnpackNormalScale(float4 sampleValue) |
解包法线贴图中存储的法线数据。内部实现将采样值转换成 -1~1 范围的法线。 |
float4 ComputeScreenPos(float4 pos) |
float4 ComputeScreenPos(float4 positionOS) 或GetVertexPositionInputs().positionNDC 。后者更推荐。 |
计算模型空间顶点坐标在屏幕空间的坐标。 |
Linear01Depth(z) |
Linear01Depth(z, _ZBufferParams) |
将深度值转化为线性深度,且映射到[0,1]范围 |
LinearEyeDepth(z) |
LinearEyeDepth(z, _ZBufferParams) |
将深度值转化为线性深度 |
常见内置变量对照:
Built-in Variable | URP Variable | 描述 |
---|---|---|
_Time (float4) |
_Time (float4) |
包含时间信息(t/20, t, t2, t3),常用于动画、周期性效果等。 |
_SinTime (float4) |
_SinTime (float4) |
正弦函数形式的时间,用于周期性动画。 |
_CosTime (float4) |
_CosTime (float4) |
余弦函数形式的时间。 |
_WorldSpaceCameraPos (float3) |
_WorldSpaceCameraPos (float3) |
摄像机在世界空间中的位置。 |
_ProjectionParams (float4) |
_ProjectionParams (float4) |
包含投影相关参数(1 or -1, near, far, 1/far),其中第一个参数为投影翻转(-1为翻转),通常用于屏幕空间计算。 |
_ScreenParams (float4) |
_ScreenParams (float4) |
包含屏幕相关参数(width, height, 1+1.0/widfth, 1+1.0/height) |
_MainTex (sampler2D) |
_MainTex (sampler2D) |
主纹理采样器。 |
_LightColor0 (float4) |
_MainLightColor (float4) |
Built-in 中的主光源颜色;在 URP 中更名为 _MainLightColor 。 |
_WorldSpaceLightPos0 (float4) |
_MainLightPosition (float4) |
主光源的位置或方向(对方向光,w 为 0,对点光,w 为 1),URP 中常用名称为 _MainLightPosition 。 |
_WorldSpaceLightDir0 (float3) |
GetMainLight().direction 计算得到 |
主光源在世界空间中的方向; |
_LightMatrix0 |
_MainLightWorldToShadow |
光源的阴影矩阵 |
_ZBufferParams |
_ZBufferParams |
深度缓冲参数 |
常见内置宏对照:
Built-in Macro | URP 宏/函数及说明 | 描述 |
---|---|---|
UNITY_MATRIX_MVP |
UNITY_MATRIX_MVP (float4x4) |
模型-视图-投影矩阵,将顶点从模型空间转换到裁剪空间。 |
UNITY_MATRIX_V |
UNITY_MATRIX_V (float4x4) |
视图矩阵。 |
UNITY_MATRIX_P |
UNITY_MATRIX_P (float4x4) |
投影矩阵。 |
UNITY_MATRIX_I_V |
UNITY_MATRIX_I_V (float4x4) |
视图矩阵的逆矩阵。 |
UNITY_MATRIX_I_P |
UNITY_MATRIX_I_P (float4x4) |
投影矩阵的逆矩阵。 |
UNITY_LIGHT_ATTENUATION |
使用GetMainLight().distanceAttenuation 和GetMainLight().shadowAttenuation 得到。 |
用于计算光照衰减。 |
TRANSFORM_TEX |
TRANSFORM_TEX |
纹理坐标变换 |
UNITY_FOG_COORDS(n) |
float fogFactor : TEXCOORDn (即不需要额外处理) |
定义雾效坐标 |
UNITY_TRANSFER_FOG |
OUT.fogFactor = ComputeFogFactor(positionCS.z) |
计算雾效 |
UNITY_APPLY_FOG(fogCoord, color, fogColor) |
color.rgb = MixFog(color.rgb, fogCoord) |
应用雾效 |
UNITY_APPLY_FOG_COLOR(fogCoord, color) |
color.rgb = MixFogColor(color.rgb, fogColor.rgb, fogCoord) |
|
SHADOW_COORDS(n) |
float4 shadowCoord : TEXCOORD1 (即不需要额外处理) |
阴影坐标变量声明 |
TRANSFER_SHADOW(o) |
TransformWorldToShadowCoord(inputData.positionWS) |
|
SHADOW_ATTENUATION(i) |
GetMainLight(shadowCoord) |