初探URP(一) - 认识ShaderLab与RenderFeature
本文最后更新于 2024年10月4日 下午
老本行。
使用Edit-Rendering-Materials-Convert Selected Built-in Materials to URP将Project面板中选中的Built-in材质转换为URP材质。
Shader结构
Properties块
用于暴露接口,定义Shader中的Uniform。
Shader属性来源包括Per-Instance、Material面板、全局属性。优先级从高到低排序。
Per-Instance属性可以使用
MaterialPropertyBlock
类定义。
1
2
3
4
5
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Intensity("Intensity", Range(0, 1)) = 0.5
}
1
2
3
4
MaterialPropertyBlock block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block);
block.SetFloat("_Intensity", 0);
renderer.SetPropertyBlock(block);
如果只需要对某个对象的渲染进行临时、局部的修改,而不想影响材质的共享状态,使用
MaterialPropertyBlock
是更好的选择。如果需要修改材质的属性并希望修改是持久的,或者材质本身不与其他对象共享,使用renderer.material.setFloat
会更简单。二者区别:
renderer.material.setFloat
:直接操作材质的material
属性时,会隐式地对共享材质进行实例化。如果多个对象共享同一个材质,调用renderer.material.setFloat
会让当前对象的材质与其他对象的材质分离,生成一个材质实例,导致多个对象不再共享同一个材质。这意味着这次修改只影响当前对象的材质,但会增加材质实例的内存开销。
renderer.getMaterialPropertyBlock
:使用MaterialPropertyBlock
可以避免实例化材质,修改的属性只会应用于当前的Renderer
渲染的结果,并不会影响材质的共享状态。也就是说,如果多个对象使用同一个材质,通过MaterialPropertyBlock
进行修改不会创建新的材质实例,而只是修改了该对象的渲染属性。
_MainTex(“Texture”,2D)=“white”{}
以该行为例。_MainTex
指该属性的变量名,Texture
指该属性在编辑器Inspector面板上的显示字样,2D
指该属性的类型,等号后面的部分指该属性的默认值。
SubShader
每个Shader都可以包含若干SubShader,用于在对性能要求不同的平台进行Shader的不同实现。
Tags
Tags用于告诉GPU何时、如何执行Shader代码。
LOD等级
提高LOD等级可以降低着色器的性能消耗。
Pass
每个Pass会对物体进行一次渲染。每个Pass都包含了一个完整的顶点着色器和片段着色器。
创建URP Shader
首先,按创建Built-in Shader的方式,右键Project面板-Create-Shader-Unlit Shader。
随后,删除CGPROGRAM
和ENDCG
以及它们中间的部分。
URP头文件库使用HLSL编写。因此,使用URP时,所有的Shader代码都必须使用HLSL编写。
告诉Shader编译器,该Shader使用HLSL的方式如下:
在Pass前加入HLSLINCLUDE
和ENDHLSL
关键字,并在二者中间加入HLSL头文件名。该方式允许加入的头文件在所有Pass中共享。
1 |
|
URP核心库的路径如下:
Packages/com.unity.renderer-pipelines.universal/ShaderLibrary/Core.hlsl
在HLSLINCLUDE
和ENDHLSL
关键字之间,使用CBUFFER_START
和CBUFFER_END
宏标记HLSL代码需要引用的属性所在的定义区域。
在
CBUFFER_START
关键字后加上(UnityPerMaterial)
,让区域内的所有属性都能共用于所有Pass。需要注意的是,纹理无需放在
CBUFFER
内。
1
2
3
4
5
6
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
CBUFFER_END
TEXTURE2D(_MainTex); //定义贴图时,要同时定义纹理实例和采样器
SAMPLER(sampler_MainTex); //注意变量命名
在HLSLINCLUDE
和ENDHLSL
关键字之间,需要定义顶点/偏远着色器输入结构体。
1 |
|
1 |
|
这里的
POSITION
和TEXCOORD0
被称为“语义”,用于告知编译器,这里的变量对应着什么属性,并自动处理数据输入。常用的语义还有:
Shader语义 作用和意义 示例 POSITION 定义了每个顶点的位置。它必须在顶点着色器的输出中使用。 float4 pos : POSITION
NORMAL 定义了每个顶点的法向量。法向量是垂直于表面的向量,用于计算光照和阴影效果。 float3 normal : NORMAL
COLOR 定义了每个顶点的颜色。可以用它来表示材质的颜色或在顶点着色器中计算出的其他颜色。 float4 color : COLOR
TEXCOORD0-3 定义了每个顶点在纹理图像中的位置,以便将纹理贴到表面上。 float2 uv : TEXCOORD0
TANGENT 定义了每个顶点的切线向量。切线向量是表面切线方向的向量,用于计算法线贴图。 float3 tangent : TANGENT
BINORMAL 定义了每个顶点的副切线向量。副切线向量是表面副切线方向的向量,用于计算法线贴图。 float3 binormal : BINORMAL
SV_POSITION 定义了像素在屏幕空间中的位置。用于像素着色器的输出。 float4 pos : SV_POSITION
UNITY_FOG_COORDS 定义了雾坐标。用于计算雾效果。 float fogCoord : UNITY_FOG_COORDS
UNITY_VERTEX_ID 定义了顶点的索引。用于访问顶点缓冲区中的顶点数据。 uint vertexID : UNITY_VERTEX_ID
UNITY_MATRIX_MVP 定义了世界空间到裁剪空间的变换矩阵。 float4x4 MVP : UNITY_MATRIX_MVP
UNITY_LIGHTMODEL_AMBIENT 定义了环境光颜色。用于计算全局光照。 float4 ambient : UNITY_LIGHTMODEL_AMBIENT
UNITY_LIGHTMODEL_DIRECTION 定义了灯光模型的方向。用于计算方向光照。 float3 direction : UNITY_LIGHTMODEL_DIRECTION
UNITY_LIGHT_ATTENUATION 定义了光照的衰减值。用于计算点光源和聚光灯的光照衰减。 float3 attenuation : UNITY_LIGHT_ATTENUATION
UNITY_LIGHT_COOKIE 定义了灯光的贴图。用于在灯光上添加纹理效果。 sampler2D cookie
:
在Pass内,通过HLSLPROGRAM
和ENDHLSL
关键字定义HLSL程序代码区域。
该代码段需要包含一个vert
函数和frag
函数。前者的返回类型为之前定义的VertexOutput
类型,接收一个VertexInput
参数;后者返回一个float4
作为最终颜色,接收一个VertexOutput
参数。需要注意的是,frag
函数需要用SV_TARGET
语义标记。
1 |
|
此外,还需要在HLSLPROGRAM
关键字下定义宏:
1 |
|
了解Render Feature
RenderFeature允许我们在渲染管线的若干特定阶段插入我们需要的渲染过程,实现管线的自定义。
在Project面板中右键-Create-Rendering-URP Render Feature创建新Render Feature。
Render Feature资产表现为一个.cs文件。其结构如下:
1 |
|
Create()
方法默认如下,一般会在其中进行Pass实例化与渲染时间指定。
1 |
|
AddRenderPasses
方法默认如下,一般用于将Pass实例入队:
1 |
|
CustomRenderPass
类定义是Render Feature的核心。其默认包含以下三个生命周期函数:
1 |
|
从相机RT获取Color纹理
Feature部分
在Create
方法中,我们已经创建了Pass实例并设置了渲染时机。
在AddRenderPasses
中,我们需要将Pass实例入队提交给上下文渲染。但这里有个问题:一般,我们撰写Pass的渲染逻辑应当作用于CameraType.Game
的相机,如果作用于其他类型的相机,如Reflection
等,可能会出错。所以,要做逻辑判断:
1 |
|
SetupRenderPass
用于在Pass实例正式起效前做一些预备工作,可以取代Pass内的OnCameraSetup
函数。一般,我们会给Pass类写一个Setup
方法,在里面进行Feature和Pass的实例传输(一般是各种Shader参数,RTHandle
等)
1 |
|
Dispose
是生命周期函数,在Feature不再生效时调用。一般,我们在这里调用Pass类内我们自定义的Dispose方法,用于释放资源。
1 |
|
Pass部分
Configure
方法包含了对相机RT的Descriptor的引用。我们可以通过该Descriptor为临时RT分配内存:
1 |
|
Execute
方法可以理解为渲染管线中的Update
,会频繁执行。
在该方法中,我们需要进行以下操作:
- 从命令缓冲池中获取cmd实例
- 将临时RT中的Render Texture取出,设置为全局Shader Texture变量,供Shader使用
- 使用
Blitter
工具类,将相机RT中的颜色纹理复制到临时RT - 执行cmd内命令
- 释放cmd实例
1 |
|
Dispose()
方法在Feature的Dispose()
函数中自动调用,释放临时RT资源:
1 |
|
Shader部分
1 |
|
效果
踩坑
- 资源不应在
OnCameraClear()
中释放
OnCameraClear()
会在每次Execute()
执行完毕后执行,而临时RT资源只会在Configure()
中分配一次。因此,在OnCameraClear()
中释放临时RT会导致不停地释放无效资源,导致报错。
若一定要这么做,需要在
OnCameraSetup()
中申请临时RT而非Configure()
中申请。谨记:
OnCameraSetup()
与OnCameraClear()
成对,每帧执行;Configure()
与Dispose()
成对,在每个Pass的生命周期中只应执行一次!
- 分配临时RT后,应当指定其为该Pass的渲染目标
使用ConfigureTarget(RTHandle)
指定临时RT为当前Pass的RT。否则Blit将失效。
- 使用
BlitCameraTexture
时,source
与destination
的RenderTextureDescriptor
参数应当一致。
必须尽可能严格一致,否则Blit
将无法生效。
- Shader全局属性无需在
Properties
块中定义
Properties
块中定义的属性始终由Inspector面板控制,且优先级始终高于全局Shader属性。