初探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指定此着色器应用的平台。支持的选项有glesgles3glcored3d11d3d11_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

使用HLSLPROGRAMENDHLSL定义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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 2D纹理,通过SAMPLE_TEXTURE2D、SAMPLE_TEXTURE2D_LOD采样
TEXTURE2D(textureName);
SAMPLER(sampler_textureName);
// 2D数组纹理,通过SAMPLE_TEXTURE_2D_ARRAY、SAMPLE_TEXTURE_2D_ARRAY_LOD采样
TEXTURE2D_ARRAY(textureName);
SAMPLER(sampler_textureName);
// 3D纹理,通过SAMPLE_TEXTURE3D和对应LOD版本采样
TEXTURE3D(textureName);
SAMPLER(sampler_textureName);
// Cube纹理,通过SAMPLER_TEXTURE_CUBE和对应LOD版本采样
TEXTURECUBE(textureName);
SAMPLER(sampler_textureName);
// Cube数组纹理,通过SAMPLER_TEXTURE_CUBE_ARRAY和对应LOD版本采样
TEXTURECUBE_ARRAY(textureName);
SAMPLER(sampler_textureName);

需要注意的是,使用宏定义纹理和采样器时,应当定义在CBUFFER外。而CBUFFER内使用float4定义纹理名_ST变量。

  • 可定义任意类型的数组,并通过循环进行索引访问。但Unity仅能从C#脚本设置float4和float类型数组。

数组无法包含在CBUFFER块中,这意味着SRP Batcher对于内部定义了数组变量的Shader不太好起作用。一般,我们更多地将数组变量通过Shader.SetGlobalVectorShader.SetGlobalFloat等方法设置为全局变量。

  • 可使用StructedBuffer<结构体类型>定义缓冲对象。

缓冲是数组的替代方案,仅能在支持#pragma target 4.5的部分平台上使用。通过SystemInfo.supportsComputeShader检查平台是否支持该特性。

缓冲对象可以通过索引访问。StructedBuffer是只读的,但RWStructedBuffer是可读可写的。

1
2
3
4
5
6
7
8
struct Example {
float3 A;
float B;
};
StructuredBuffer<Example> _BufferExample;
void GetBufferValue(float Index, out float3 Out) {
Out = _BufferExample[Index].A;
}

在C#脚本中,需要定义与Shader中缓冲泛型类型相同的结构体类型,保证内部成员数据类型一致,并借助ComputeBuffer类的SetData方法和material.SetBuffer方法进行数据传递。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using UnityEngine;
[ExecuteAlways]
public class BufferTest : MonoBehaviour {
private ComputeBuffer buffer;
private struct Test {
public Vector3 A;
public float B;
}
private void OnEnable() {
Test test = new Test {
A = new Vector3(0, 0.5f, 0.5f),
B = 0.1f,
};
Test test2 = new Test {
A = new Vector3(0.5f, 0.5f, 0),
B = 0.1f,
};
Test[] data = new Test[] { test, test2 };
buffer = new ComputeBuffer(data.Length, sizeof(float) * 4);
buffer.SetData(data);
GetComponent<MeshRenderer>().sharedMaterial.SetBuffer("_BufferExample", buffer);
}
private void OnDisable() {
buffer.Dispose(); // 注意缓冲区的释放
}
}

CBUFFER

UnityPerMaterial CBUFFER应当包含在HLSLINCLUDE块内,用于确保所有Pass使用相同的CBUFFER。

CBUFFER必须包含纹理以外的所有在Properties块中声明过的变量,此外,还必须包含纹理变量的_ST和_TexelSize变量。它不应当包含未在Properties块中声明的变量。

CBUFFER块从CBUFFER_START(UnityPerMateiral)开始,CBUFFER_END结束。

结构体

URP中,VS的输入结构体类型应当命名为Attributes。其中,涉及到位置的,应当在变量名标注其所在坐标空间,如positionWS。Attributes中的常用于语义包括POSITIONCOLORTEXCOORD0-7NORMALTANGENT等。

SV_VertexID是一类特殊的用于Attributes的语义,它可以获取每个顶点的标识符,常与ComputeBuffer一同使用。

FS的输入结构体类型应当被命名为Varyings。其中必须包含用SV_POSITION语义标记的positionCS。对于Varyings,COLORNORMALTANGENT语义尽量少用,尽量多用TEXCOORD进行数据传递。

一个有趣的语义是VFACE,用于标记一个float变量。若此片段为正面,则为正数,否则为负数。

FS输出

一般情况下,使用half4作为FS输出。然而,在某些情况下,我们需要用到MRT。如果使用UniversalGBufferLightMode,则默认开启MRT,否则,需要在C#脚本中使用CommandBuffer.SetRenderTarget中使用RenderTargetIdentifier[]数组。GLES2等平台不支持MRT

若要使用MRT,我们这样撰写FS:

1
2
3
4
5
6
7
8
9
10
struct FragOut {
half4 color : SV_Target0;
half4 color2 : SV_Target1;
};
FragOut UnlitPassFragment(Varyings input) {
FragOut output;
output.color = color;
output.color2 = color2;
return output;
}

VS

一个典型的VS如下:

1
2
3
4
5
6
7
8
9
10
Varyings UnlitPassVertex(Attributes IN) {
Varyings OUT;
VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
OUT.positionCS = positionInputs.positionCS;

OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);

OUT.color = IN.color;
return OUT;
}

可以看到,我们并没有用TransformObjectToHClip函数进行顶点变换,而是用GetVertexPositionInputs(float3 positionOS)获取了VertexPositionInputs类型的positionInputs变量。这个函数包含在ShaderVariablesFunctions.hlsl内(也包含在Core.hlsl内)。

VertexPositionInputs结构体包含了positionWSpositionVSpositionCSpositionNDC四个变量,简化了计算。对于未使用的变量,编译器会自动将它们去除,所以不会有多余的计算。

通过下列代码,我们可以获取法线、切线和副切线向量的世界空间位置,用于构建TBN矩阵:

1
2
3
4
VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.normalWS = normalInputs.normalWS;
OUT.tangentWS = normalInputs.tangentWS;
OUT.bitangentWS = normalInputs.bitangentWS;

FS

一个典型的最简FS如下:

1
2
3
4
5
half4 UnlitPassFragment(Varyings IN) : SV_Target {
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);

return baseMap * _BaseColor * IN.color;
}

在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等控制语句来进行条件编译。条件编译有逻辑运算符号,如andor&&||等。

常见的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 SettingsQuality Settings进行剔除。

如果某个关键字仅用于VS或FS,可以在multi_compile后面加_vertex_fragment,这有助于减少变体数量。

关键字有全局和局部之分。一个项目最多有256个全局关键字;一个Shader最多有64个局部关键字。局部的关键字通过在multi_compile后加_local定义。局部关键字同样支持指定vertexfragment

如果multi compile后面跟了一个下划线+一个空格(#pragma multi_compile _ _A _B),意思是生成一个禁用这两个关键字的变体。

Shader Feature

Shader Feature与Multi Compile类似,但有以下不同:

  • 自动生成禁用后附关键字的额外变体
  • 代码中未使用的关键字将不会包含在最终Build。这可以大幅缩短构建时间或运行时着色器编译时间。

示例:

1
#pragma shader_featrue _A _B

光照

URP不支持Surface Shader。

Lighting.hlsl中包含许多光照计算的帮助函数。包含Lighting.hlsl前,必须确保定义下列关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE

#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ _SCREEN_SPACE_OCCLUSION

#pragma multi_compile_fog
#pragma multi_compile_instancing

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

通过下列代码传入结构体:

1
half4 color = UniversalFragmentPBR(inputData, surfaceData);

快速开始

通过UniversalFragmentPBRUniversalFragmentBlinnPhong函数,可以快速为Shader附加光照。为了使用这两个函数,我们需要设置InputDataSurfaceData结构。

SurfaceData的结构定义包含在SurfaceData.hlsl中。如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct SurfaceData {
half3 albedo;
half3 specular;
half metallic;
half smoothness;
half3 normalTS; // 切线空间法线
half3 emission;
half occlusion;
half alpha;
half clearCoatMask;
half clearCoatSmoothness;
};

InputData的结构定义同样包含在SurfaceData.hlsl中。如下:

1
2
3
4
5
6
7
8
9
10
11
struct InputData {
float3 positionWS;
half3 normalWS;
half3 viewDirectionWS;
float4 shadowCoord;
half fogCoord;
half3 vertexLighting;
half3 bakedGI;
float2 normalizedScreenSpaceUV;
half4 shadowMask;
};

初始化InputData

对于InputData,PBR和简单光照都是相同的。我们这样进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData) {
inputData = (InputData)0; // 用于避免不完全的初始化
inputData.positionWS = input.positionWS; // 世界空间顶点坐标
// 若存在法线贴图
#ifdef _NORMALMAP
half3 viewDirWS = half3(input.normalWS.w, input.tangentWS.w, input.bitangentWS.w); // 似乎是为了减少计算,直接把ViewDir存放在TBN矩阵中
inputData.normalWS = TransformTangentToWorld(normalTS,half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz)); // 构建TBN矩阵并将法线转换到世界空间
#else
half3 viewDirWS = GetWorldSpaceNormalizeViewDir(inputData.positionWS);
inputData.normalWS = input.normalWS;
#endif

inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS); // 逐像素归一化法线
viewDirWS = SafeNormalize(viewDirWS); // 归一化世界空间ViewDir

inputData.viewDirectionWS = viewDirWS;

// 阴影相关
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) // 顶点阴影坐标插值
inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS) // 计算阴影坐标
inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#else
inputData.shadowCoord = float4(0, 0, 0, 0);
#endif

// 雾效相关
/*#ifdef _ADDITIONAL_LIGHTS_VERTEX // 若有额外光源
inputData.fogCoord = input.fogFactorAndVertexLight.x;
inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
#else
inputData.fogCoord = input.fogFactor;
inputData.vertexLighting = half3(0, 0, 0);
#endif */

// 新版本URP用这个
#ifdef _ADDITIONAL_LIGHTS_VERTEX
inputData.fogCoord = InitializeInputDataFog(float4(inputData.positionWS, 1.0), input.fogFactorAndVertexLight.x);
inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
#else
inputData.fogCoord = InitializeInputDataFog(float4(inputData.positionWS, 1.0), input.fogFactor);
inputData.vertexLighting = half3(0, 0, 0);
#endif

inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS); // 仅用于对SSAO纹理采样
inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV);
}

简单光照

Lighting.hlsl头文件中包含了LightLambertLightingSpecular函数,用于计算Blinn-Phong模型中的漫反射和镜面反射项。UniversalFragmentBlinnPhong函数内部调用了这两个函数。

在使用Blinn-Phong光照模型时,我们这样初始化SurfaceData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

TEXTURE2D(_SpecGlossMap);
SAMPLER(sampler_SpecGlossMap);

half4 SampleSpecularSmoothness(float2 uv, half alpha, half4 specColor, TEXTURE2D_PARAM(specMap, sampler_specMap)) {
half4 specularSmoothness = half4(0.0h, 0.0h, 0.0h, 1.0h);
#ifdef _SPECGLOSSMAP
specularSmoothness = SAMPLE_TEXTURE2D(specMap, sampler_specMap, uv) * specColor;
#elif defined(_SPECULAR_COLOR)
specularSmoothness = specColor;
#endif

#ifdef _GLOSSINESS_FROM_BASE_ALPHA
// 若重载gloss值
specularSmoothness.a = exp2(10 * alpha + 1);
#else
// 若从SPECGLOSS的alpha通道获取gloss值
specularSmoothness.a = exp2(10 * specularSmoothness.a + 1);
#endif
return specularSmoothness;
}

void InitializeSurfaceData(Varyings IN, out SurfaceData surfaceData){
surfaceData = (SurfaceData)0;

half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);

// 若开启透明度测试
#ifdef _ALPHATEST_ON
clip(baseMap.a - _Cutoff);
#endif

half4 diffuse = baseMap * _BaseColor * IN.color;
surfaceData.albedo = diffuse.rgb;
surfaceData.normalTS = SampleNormal(IN.uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
// 此处TEXTURE2D_ARGS宏无实际意义
surfaceData.emission = SampleEmission(IN.uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));

half4 specular = SampleSpecularSmoothness(IN.uv, diffuse.a, _SpecColor, TEXTURE2D_ARGS(_SpecGlossMap, sampler_SpecGlossMap));
surfaceData.specular = specular.rgb;
surfaceData.smoothness = specular.a * _Smoothness;
}

然后,我们便可以这样调用UniversalFragmentBlinnPhong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
half4 LitPassFragment(Varyings IN) : SV_Target {
SurfaceData surfaceData;
InitializeSurfaceData(IN, surfaceData);

InputData inputData;
InitializeInputData(IN, surfaceData.normalTS, inputData);

half4 color = UniversalFragmentBlinnPhong(inputData, surfaceData.albedo, half4(surfaceData.specular, 1),
surfaceData.smoothness, surfaceData.emission, surfaceData.alpha);

color.rgb = MixFog(color.rgb, inputData.fogCoord);
return color;
}

PBR光照

对于PBR光照,我们如此配置SurfaceData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

TEXTURE2D(_MetallicSpecGlossMap); SAMPLER(sampler_MetallicSpecGlossMap);
TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap);

half4 SampleMetallicSpecGloss(float2 uv, half albedoAlpha) {
half4 specGloss;
#ifdef _METALLICSPECGLOSSMAP
specGloss = SAMPLE_TEXTURE2D(_MetallicSpecGlossMap, sampler_MetallicSpecGlossMap, uv)
#ifdef _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
specGloss.a = albedoAlpha * _Smoothness;
#else
specGloss.a *= _Smoothness;
#endif
#else // _METALLICSPECGLOSSMAP
#if _SPECULAR_SETUP
specGloss.rgb = _SpecColor.rgb;
#else
specGloss.rgb = _Metallic.rrr;
#endif

#ifdef _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
specGloss.a = albedoAlpha * _Smoothness;
#else
specGloss.a = _Smoothness;
#endif
#endif
return specGloss;
}

half SampleOcclusion(float2 uv) {
#ifdef _OCCLUSIONMAP
#if defined(SHADER_API_GLES)
return SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
#else
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
return LerpWhiteTo(occ, _OcclusionStrength);
#endif
#else
return 1.0;
#endif
}

void InitializeSurfaceData(Varyings IN, out SurfaceData surfaceData){
surfaceData = (SurfaceData)0;

half4 albedoAlpha = SampleAlbedoAlpha(IN.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
surfaceData.alpha = Alpha(albedoAlpha.a, _BaseColor, _Cutoff);
surfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb * IN.color.rgb;

surfaceData.normalTS = SampleNormal(IN.uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
surfaceData.emission = SampleEmission(IN.uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));
surfaceData.occlusion = SampleOcclusion(IN.uv);

half4 specGloss = SampleMetallicSpecGloss(IN.uv, albedoAlpha.a);
#if _SPECULAR_SETUP
surfaceData.metallic = 1.0h;
surfaceData.specular = specGloss.rgb;
#else
surfaceData.metallic = specGloss.r;
surfaceData.specular = half3(0.0h, 0.0h, 0.0h);
#endif
surfaceData.smoothness = specGloss.a;
}

在FS中:

1
2
3
4
5
6
7
8
9
10
11
12
half4 LitPassFragment(Varyings IN) : SV_Target {
SurfaceData surfaceData;
InitializeSurfaceData(IN, surfaceData);

InputData inputData;
InitializeInputData(IN, surfaceData.normalTS, inputData);

half4 color = UniversalFragmentPBR(inputData, surfaceData);

color.rgb = MixFog(color.rgb, inputData.fogCoord);
return color;
}

其他Pass

ShadowCaster

LightModeShadowCaster的Pass用于投射阴影。在Built-in中,我们使用FallbackUsePass来启用阴影投射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.hlslDepthNormalsPass.hlsl完成操作。

类似地,若应用了顶点动画,则需要指定新VS。

Meta

烘焙GI时需要使用Meta Pass。我们可以使用UnlitMetaPass.hlslLitMetaPass.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().distanceAttenuationGetMainLight().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)

初探URP(二) - URP SL特性、HLSL与光照基础
http://example.com/2025/02/11/初探URP(二)-URP-SL特性、HLSL与光照基础/
作者
Yoi
发布于
2025年2月11日
许可协议