自定义通道
HDRP 自定义通道允许您在渲染循环中的某些点注入着色器和 C#,从而让您能够绘制对象、执行全屏通道和读取某些摄像机缓冲区(例如深度、颜色或法线)。
以下是使用自定义通道可以实现的示例:
含体积的工作流程
自定义通道已通过一个体积系统实现,但请注意,由于设计原因和用法不同,这个体积系统与 HDRP 中的内置体积系统有所不同。因此,您可能注意到以下主要差异:
- 自定义通道体积不能像标准体积那样混合,只能淡入淡出
- 如果多个体积在同一个注入点重叠,则将执行最小的体积(以包围体积计),所有其他体积将被忽略
- 自定义通道的数据本身保存在体积游戏对象中,而不是保存在项目的资源中
与在体积中一样,自定义通道体积有两种模式:Local
和 Global
。Local
模式使用附加在游戏对象上的碰撞体,这种模式下,自定义通道用于定义将要执行效果的区域。Global
体积将在场景中的任何地方执行。
当场景中有多个共享同一注入点的自定义通道时,通过优先级来确定执行顺序。
还可以使用 fade
系统来平滑正常渲染和自定义通道之间的过渡。通过自定义通道体积组件 UI 中的 Fade Radius 字段可以控制淡化距离,半径以米为单位显示,并且不会随对象变换而缩放。
因为我们完全控制了可在自定义通道中执行的操作,所以必须手动将淡化包含在效果中。为了向您提供帮助,在着色器中有一个内置变量 _FadeValue
,在 C# 中有一个 CustomPass.fadeValue
,且相应的值介于 0 到 1 之间,表示摄像机距碰撞体包围体积有多远。如需了解有关在脚本中淡化的更多详细信息,可以跳转到脚本 API 标签。
下面提供了一个采用盒型碰撞体(透明实心盒体)的自定义通道的示例,淡化半径由线框立方体表示。
注意:您可以堆叠多个自定义通道体积,但每个注入点只能执行一个,要执行的自定义通道体积是当前重叠的最小碰撞体范围(无淡化半径)。这意味着全局体积的优先级低于局部体积。
注入点
可以在 6 个不同位置注入自定义通道。请注意,这些注入点并不会告诉您通道的确切执行位置,我们唯一保证的是,某些缓冲区可用于读取或写入,并会包含您的通道之前渲染的某个对象子集。与通用渲染管线不同,我们不保证您的通道将在任何 HDRP 内部操作之前或之后执行(原因还在于我们的异步渲染)。但是,我们保证将在一个帧中按以下顺序自上而下触发注入点:
名称 | 可用缓冲区 | 描述 |
---|---|---|
BeforeRendering | 深度(写入) | 清除深度后可以立即写入深度缓冲区,因此不会渲染经过 Z 测试的不透明对象。遮蔽一部分渲染很有用。在此处还可以清除分配的缓冲区,即 Custom Buffer 。 |
AfterOpaqueDepthAndNormal | Depth (Read | Write), Normal + roughness (Read | Write) | 缓冲区将包含所有不透明的对象。在此处可以修改法线、粗糙度和深度缓冲区,在光照和深度棱锥中将考虑这一点。注意,法线和粗糙度在同一缓冲区内,您可以使用 DecodeFromNormalBuffer 和 EncodeIntoNormalBuffer 函数读取/写入法线和粗糙度数据。 |
BeforePreRefraction | Color (no pyramid | Read | Write), Depth (Read | Write), Normal + roughness (Read) | 缓冲区将包含所有不透明的对象以及天空。使用此注入点可以渲染要在折射中使用的任何透明对象(这些对象将最终成为绘制透明对象时用于折射的颜色棱锥)。 |
BeforeTransparent | Color (Pyramid | Read | Write), Depth (Read | Write), Normal + roughness (Read) | 在此处可以采样我们用于透明折射的颜色棱锥。实施一些模糊效果很有用,但请注意,在此注入点渲染的所有对象都不会位于颜色棱锥中。您还可以在此处绘制一些需要折射场景的透明对象(例如,水)。 |
BeforePostProcess | Color (Pyramid | Read | Write), Depth (Read | Write), Normal + roughness (Read) | 缓冲区包含 HDR 帧中的所有对象。 |
AfterPostProcess | Color(Read | Write), Depth (Read) | 缓冲区位于后期处理模式之后,这意味着深度将会抖动(因此,您无法绘制没有瑕疵的经过深度测试的对象)。 |
您可以在下图上看到在 HDRP 帧内注入了自定义通道的情况。
自定义通道列表
Custom Pass Volume 组件的主要部分是 Custom Passes 可重排序列表,允许您添加新的自定义通道效果并进行配置。HDRP 内置了两个自定义通道,即 FullScreen 和 DrawRenderers 自定义通道。 在此截屏中,您可以看到 FullScreen 渲染通道:
默认情况下,每个自定义通道上都有一些设置,它们的含义如下:
名称 | 描述 |
---|---|
名称 | 通道的名称,它将用作性能分析标记的名称以进行调试 |
Target color buffer | 要写入颜色的目标缓冲区 |
Target depth buffer | 要写入和测试深度和模板的目标缓冲区 |
Clear flags | 当上面的渲染目标被绑定进行渲染时,您希望如何清除这些目标 |
注意:默认情况下,目标缓冲区设置为摄像机缓冲区,但您也可以选择自定义缓冲区。它是 HDRP 分配的另一个缓冲区,您可以在其中放置所需的所有内容,然后可以在自定义通道着色器中对其进行采样。可以在 HDRP 资源设置的 Rendering 部分下选择该自定义缓冲区的格式。
在 HDRP 资源中,您还可以禁用自定义通道,这也将禁用自定义缓冲区分配。此外,在帧设置中,您可以选择是否渲染自定义通道,请注意,这不会影响自定义通道分配。
FullScreen 自定义通道
添加 FullScreen 通道时,将使用 UI 字段中提供的全屏材质 (FullScreen Material) 渲染该通道。要创建与 FullScreen 通道兼容的材质,首先需要使用菜单 Create/Shader/HDRP/Custom FullScreen Pass 创建一个着色器,然后在该着色器上单击右键并创建一个新材质。
然后,在将材质添加到 Inspector 中的 FullScreen Material
字段中时,您会看到一个名为 Pass Name
的下拉选单,可通过该下拉选单选择使用哪个着色器通道来绘制全屏四边形;如果您想对一种效果使用多种变体并在它们之间切换,这会特别有用。
现在已经设置了自定义通道,接下来可以开始编辑全屏材质的着色器。默认情况下,在模板中具有 FullScreenPass
函数,可在其中添加代码:
float4 FullScreenPass(Varyings varyings) : SV_Target
{
float depth = LoadCameraDepth(varyings.positionCS.xy);
PositionInputs posInput = GetPositionInput(varyings.positionCS.xy, _ScreenSize.zw, depth, UNITY_MATRIX_I_VP, UNITY_MATRIX_V);
float3 viewDirection = GetWorldSpaceNormalizeViewDir(posInput.positionWS);
float4 color = float4(0.0, 0.0, 0.0, 0.0);
// 如果我们不在渲染注入点之前,则将摄像机颜色缓冲区加载到 Mip 0
if (_CustomPassInjectionPoint != CUSTOMPASSINJECTIONPOINT_BEFORE_RENDERING)
color = float4(CustomPassSampleCameraColor(posInput.positionNDC.xy, 0), 1);
// 在此处添加自定义通道代码
// 淡化值可让您在摄像机接近自定义通道体积的同时增强效果的强度
float f = 1 - abs(_FadeValue * 2 - 1);
return float4(color.rgb + f, color.a);
}
在此代码段中,我们可获取许多有用的输入数据,例如深度、视图方向、世界空间中的位置等,您的效果中可能需要这些输入数据。默认情况下,我们还使用 _FadeValue
变量,如果在自定义通道体积上设置了淡化半径,则此变量会影响效果。
⚠️ 警告:编写全屏着色器 (FullScreen Shader) 时需要注意一些陷阱 |
---|
不能读写同一个渲染目标。即,无法对摄像机颜色进行采样,对其进行修改,然后将结果写回到摄像机颜色缓冲区,您必须借助一个辅助缓冲区在两个通道中执行此过程。 |
深度缓冲区可能未包含所需的内容。深度缓冲区绝不会包含写入深度的透明对象,并且在启用 TAA 时始终会抖动(这意味着渲染需要深度的后期处理后对象将导致摆动) |
仅在后期处理后和后期处理前通道中才能通过 LOD 对摄像机颜色进行采样。在渲染前调用 CustomPassSampleCameraColor 将仅返回黑色。 |
DrawRenderers 通道与 FullScreen 通道链接在一起:在多通道设置中,您在摄像机颜色缓冲区中绘制对象,然后从全屏自定义通道中读取缓冲区,您将看不到在全屏通道之前的通道中绘制的对象(除非您处于“透明前”)。 |
MSAA:在处理 MSAA 时,必须确保正确设置了 Fetch color buffer 布尔值,因为该设置将确定您是否可以通过此通道来获取颜色缓冲区。 |
DrawRenderers 自定义通道
此通道让您可以绘制摄像机视图中的对象子集(摄像机剔除的结果)。 DrawRenderers 通道的 Inspector 如下所示:
使用过滤器 (Filters) 可以选择要渲染的对象,通过队列选择要渲染哪种材质(透明、不透明等),还可以设置作为游戏对象层的层。
默认情况下,对象与对象的材质一起显示,您可以通过在 Material
字段中分配材质来覆盖此自定义通道中所有的材质。这里有很多选项,无光照 ShaderGraph 和无光照 HDRP 着色器均有效,此外还有一个自定义的无光照着色器(您可以使用 Create/Shader/HDRP/Custom Renderers Pass 菜单来创建)。
⚠️ 请注意,并非每个注入点都支持所有类型的材质。以下是兼容性表:
注入点 | 材质类型 |
---|---|
渲染前 | 无光照前向,但不会写入摄像机颜色 |
不透明深度和法线后 | 无光照前向 |
预折射前 | 仅限无光照 + 光照前向 |
透明前 | 仅限无光照 + 光照前向(包含折射) |
后期处理前 | 仅限无光照 + 光照前向(包含折射) |
后期处理后 | 仅限无光照 + 光照前向(包含折射) |
如果尝试在不受支持的配置中渲染材质,将导致不明的行为。例如,在“不透明深度和法线后”(After Opaque Depth And Normal
) 期间渲染光照对象将产生意外的结果。
还会使用通道名称来选择要渲染的着色器通道,这在 ShaderGraph 或 HDRP 无光照材质上很有用,因为默认通道为 SceneSelectionPass
,而用于渲染对象的通道为 ForwardOnly
。如果您只想渲染对象的深度,则可能还需要使用 DepthForwardOnly
通道。
要创建高级效果,可以使用自定义渲染器通道 (Custom Renderers Pass) 着色器,该着色器将创建无光照的单通道 HDRP 着色器,在 GetSurfaceAndBuiltinData
函数内部,您可以添加片元着色器代码:
// 在此函数中添加用于在自定义通道中渲染对象的代码
void GetSurfaceAndBuiltinData(FragInputs fragInputs, float3 viewDirection, inout PositionInputs posInput, out SurfaceData surfaceData, out BuiltinData builtinData)
{
float2 colorMapUv = TRANSFORM_TEX(fragInputs.texCoord0.xy, _ColorMap);
float4 result = SAMPLE_TEXTURE2D(_ColorMap, s_trilinear_clamp_sampler, colorMapUv) * _Color;
float opacity = result.a;
float3 color = result.rgb;
# ifdef _ALPHATEST_ON
DoAlphaTest(opacity, _AlphaCutoff);
# endif
// 将数据写回到输出结构
ZERO_INITIALIZE(BuiltinData, builtinData); // 不调用 InitBuiltinData,因为没有任何光照
builtinData.opacity = opacity;
builtinData.emissiveColor = float3(0, 0, 0);
surfaceData.color = color;
}
如果需要修改顶点着色器,则可以取消对 `` 函数上方的 ApplyVertexModification() 代码块的注释:
# define HAVE_MESH_MODIFICATION
AttributesMesh ApplyMeshModification(AttributesMesh input, float3 timeParameters)
{
input.positionOS += input.normalOS * 0.0001; // 使网格膨胀一点以避免深度冲突
return input;
}
AttributesMesh 结构的定义如下(供参考):
struct AttributesMesh
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT; // 将符号存储到 w 中
float2 uv0 : TEXCOORD0;
float2 uv1 : TEXCOORD1;
float2 uv2 : TEXCOORD2;
float2 uv3 : TEXCOORD3;
float4 color : COLOR;
};
请注意,此函数中的所有变换都是在对象空间中进行的
务必要注意 ATTRIBUTES_NEED
和 VARYINGS_NEED
系统,有一个定义列表,控制将哪些数据发送到顶点和片元着色器。ATTRIBUTES_NEED
用于顶点数据,VARYINGS_NEED用于片元,因此,例如,如果要在片元着色器中采样 uv,则需要同时定义
ATTRIBUTES_NEED_TEXCOORD0和
VARYINGS_NEED_TEXCOORD0`。请注意,默认情况下您可以访问 UV 0 和法线。
以下是您可以启用的所有定义的列表
# define ATTRIBUTES_NEED_NORMAL
# define ATTRIBUTES_NEED_TANGENT
# define ATTRIBUTES_NEED_TEXCOORD0
# define ATTRIBUTES_NEED_TEXCOORD1
# define ATTRIBUTES_NEED_TEXCOORD2
# define ATTRIBUTES_NEED_TEXCOORD3
# define ATTRIBUTES_NEED_COLOR
# define VARYINGS_NEED_POSITION_WS
# define VARYINGS_NEED_TANGENT_TO_WORLD
# define VARYINGS_NEED_TEXCOORD0
# define VARYINGS_NEED_TEXCOORD1
# define VARYINGS_NEED_TEXCOORD2
# define VARYINGS_NEED_TEXCOORD3
# define VARYINGS_NEED_COLOR
# define VARYINGS_NEED_CULLFACE
请注意,您还可以覆盖通道中对象的深度状态。当渲染不在摄像机剔除遮罩中的对象时(这些对象仅在自定义通道中渲染),此功能特别有用。因为在这些对象中,不透明的对象将在 Depth Equal
测试中渲染,而只有这些对象已经在深度缓冲区中时,该测试才起作用。在这种情况下,您可能需要将深度测试覆盖为 Less Equal
。
⚠️ 如果您使用延迟渲染,则在渲染不透明对象时要小心:在自定义通道内渲染的所有对象都将前向渲染。这意味着您需要将 HDRP 设置中的 Lit Shader Mode 选项设置为“Both”,以防在构建发布时遇到问题。
脚本 API
为了创建可能需要多个缓冲区甚至 Compute Shaders
的更复杂效果,您可以使用脚本 API 来扩展 CustomPass
类。
单击自定义通道列表中的 +
按钮时,将列出从 CustomPass
继承的每个非抽象类。
C# 模板
要创建新的 C# 自定义通道,请选择 Create/Rendering/C# Custom Pass,这将生成一个 C# 文件,其中包含如下所示的类:
class #SCRIPTNAME# : CustomPass
{
protected override void Setup(ScriptableRenderContext renderContext, CommandBuffer cmd) {}
protected override void Execute(ScriptableRenderContext renderContext, CommandBuffer cmd, HDCamera camera, CullingResults cullingResult) {}
protected override void Cleanup() {}
}
要编写自定义通道,您需要三个入口点:
Setup
,用于分配渲染通道所需的所有资源(RTHandle/渲染纹理、材质、计算缓冲区等)。Cleanup
,用于释放在Setup
函数中分配的所有资源。注意不要忘记一个资源,否则会泄漏(尤其是 RTHandle,这种资源的内存消耗量可能很大)。Execute
,在通道中用于写入需要渲染的所有内容。
在 Setup
和 Execute
函数中,允许您访问 ScriptableRenderContext 和 CommandBuffer,这两个类包含渲染几乎所有内容所需的所有函数,但是这里我们将重点介绍 ScriptableRenderContext.DrawRenderers 和 CommandBuffer.DrawProcedural 两个函数。
重要信息:如果在任何场景中从未引用某个着色器,则不会构建该着色器,并且在编辑器外部运行游戏时该效果将不起作用。将该着色器添加到 Resources 文件夹,或放入
Edit > Project Settings > Graphics
中的 Always Included Shaders 列表。特别是在使用Shader.Find()
加载着色器的情况下需要注意这一点,否则将导致黑屏。专业提示:
- 要分配适用于各种情况(VR、摄像机调整大小等)的渲染目标缓冲区,请使用 RTHandles 系统,如下所示:
CSharp RTHandle myBuffer = RTHandles.Alloc(Vector2.one, TextureXR.slices, dimension: TextureXR.dimension, colorFormat: GraphicsFormat.R8G8B8A8_SRGB, useDynamicScale: true, name: "My Buffer");
- 不要忘了随后使用
myBuffer.Release();
释放缓冲区- 要创建材质,您可以使用
CoreUtils.CreateEngineMaterial
函数,并使用CoreUtils.Destroy
将其销毁。- 在编写通道的脚本时,目标渲染目标将设置为 UI 中定义的目标。这意味着如果仅使用一个渲染目标,则无需设置渲染目标。
- MSAA:如果启用了 MSAA,然后想要将对象渲染到主摄像机的颜色缓冲区,并在第二个通道中对该缓冲区进行采样,则需要首先对缓冲区进行解析。为此,您需要
CustomPass.ResolveMSAAColorBuffer
函数,该函数会将 MSAA 摄像机颜色缓冲区解析为标准的摄像机颜色缓冲区。- 如果您不希望在 Scene 视图中执行您的效果,则可以将受保护的
executeInSceneView
布尔值覆盖为 false。
现在已经分配了资源,接下来可以开始在 Execute
函数中编写代码了。
在 C# 中调用 FullScreen 通道
为使用材质执行 FullScreen 通道,我们使用 CoreUtils.DrawFullScreen
,这会在幕后通过参数针对命令缓冲区调用 DrawProcedural
。因此,执行 FullScreen 通道时,代码如下所示:
SetCameraRenderTarget(cmd); // 将摄像机颜色缓冲区与深度绑定,不清除缓冲区。
// 或者使用 CoreUtils.SetRenderTarget() 设置自定义渲染目标
CoreUtils.DrawFullScreen(cmd, material, shaderPassId: 0);
其中,cmd
是命令缓冲区,shaderPassId 等效于 UI 中的 Pass Name
,但使用的是索引。请注意,在此示例中,SetCameraRenderTarget
是可选的,因为调用 Execute
函数时绑定的渲染目标是 UI 中通过 Target Color Buffer
和 Target Depth Buffer
字段设置的目标。
在 C# 中调用 DrawRenderers
在 ScriptableRenderContext 上调用 DrawRenderers 函数需要大量样板代码,而为了对此进行简化,HDRP 提供了一个更简单的接口:
var result = new RendererListDesc(shaderTags, cullingResult, hdCamera.camera)
{
rendererConfiguration = PerObjectData.None,
renderQueueRange = RenderQueueRange.all,
sortingCriteria = SortingCriteria.BackToFront,
excludeObjectMotionVectors = false,
layerMask = layer,
};
HDUtils.DrawRendererList(renderContext, cmd, RendererList.Create(result));
这里棘手的一件事情是 shaderTags
,这是一个过滤器,用于过滤将要渲染的对象(有点类似于层过滤器),但这个过滤器基于当前渲染的着色器中的通道名称。这意味着,如果视图中的某个材质有一个着色器在这些通道名称中包含上述其中一个标签,则将通过测试,否则将不会渲染。
对于 renderQueueRange
,您可以在 CustomPass
类中使用 GetRenderQueueRange
函数,该函数可将 CustomPass.RenderQueueType
(在 UI 中用于配置渲染队列)转换成您可以在 RendererListDesc
中使用的 RenderQueueRange
。
⚠️ 警告:注意覆盖材质通道索引:使用覆盖材质调用 DrawRenderers 时,您需要选择要使用覆盖材质通道索引来渲染的通道。但是在构建过程中,此索引在以下操作之后可能会被更改:着色器剥离器从着色器(如每个 HDRP 着色器)中移除了通道,这样会移动着色器中的通道索引,因而导致您的索引变为无效。为了防止出现此问题,我们建议存储通道的名称,然后在发出绘制时使用
Material.FindPass
。
编写体积组件的脚本
您可以使用 GetComponent 在脚本中获取 CustomPassVolume
,并可访问通过 UI 提供的大部分内容,例如 isGlobal
、fadeRadius
和 injectionPoint
。
也可以通过修改 customPasses
列表来动态更改执行的自定义通道的列表。
编写自定义通道 UI 的脚本
要定制自定义通道的 UI,我们使用与 MonoBehaviour 编辑器相似的模式,但是属性不同,例如,以下是 FullScreen 自定义通道绘制器 (FullScreen Custom Pass Drawer) 的一部分:
[CustomPassDrawerAttribute(typeof(FullScreenCustomPass))]
public class FullScreenCustomPassDrawer : CustomPassDrawer
{
protected override void Initialize(SerializedProperty customPass)
{
// 初始化将在通道中使用的本地 SerializedProperty。
}
protected override void DoPassGUI(SerializedProperty customPass, Rect rect)
{
// 使用 `EditorGUI` 调用来绘制自定义 GUI。请注意,Layout 函数在此处无效
}
protected override float GetPassHeight(SerializedProperty customPass)
{
// 返回在 DoPassGUI 中使用的垂直高度(以像素为单位)。
// 可以是动态的。
return (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing) * X;
}
}
创建自定义通道绘制器时,即使 DoPassGUI 为空,您也将获得默认 UI,拥有通道的所有通用设置(名称、目标缓冲区、清除标志)。如果这不是想要的结果,则可以覆盖 commonPassUIFlags
属性以移除其中的一些设置。例如,此处我们只保留名称和目标缓冲区枚举:
protected override PassUIFlag commonPassUIFlags => PassUIFlag.Name | PassUIFlag.TargetColorBuffer;
其他 API 函数
有时,您只想在自定义通道中渲染对象,而不在摄像机中渲染对象。为此,您可以在摄像机剔除遮罩中禁用您的对象层,但这也意味着您在 Execute
函数中收到的 cullingResult 将不会包含此对象(因为默认情况下,此 cullingResult 是摄像机 cullingResult)。要解决此问题,您可以在 CustomPass 类中覆盖此函数:
protected virtual void AggregateCullingParameters(ref ScriptableCullingParameters cullingParameters, HDCamera camera) {}
这样,便可以向您在 Execute
函数中收到的 cullingResult 中添加更多的层/自定义剔除选项。
⚠️ 警告:如果不透明对象仅在自定义通道中渲染,则可能不可见,因为我们认定这些对象已经在深度预通道中,所以我们将
Depth Test
设置为Depth Equal
。因此,您可能需要使用 RenderStateBlock 的depthState
属性将Depth Test
覆盖为Less Equal
。
故障排除
缩放问题:两个摄像机使用不同分辨率时(在 Game 视图和 Scene 视图中很常见),可能会出现这种问题,可能由以下原因引起:
- 调用 CommandBuffer.SetRenderTarget(),而不是 CoreUtils.SetRenderTarget().请注意,CoreUtils 的函数也会设置视口。
- 在着色器中,当对 RTHandle 缓冲区进行采样时,UV 缺少
_RTHandleScale.xy
的乘积。
Shuriken 粒子系统:渲染仅在自定义通道中可见的粒子系统时,粒子会朝向错误的方向,这可能是因为您没有覆盖 AggregateCullingParameters
。Shuriken 中粒子的方向是在剔除过程中计算得出的,因此,如果没有正确设置,则无法正确渲染。
示例:毛刺效果(无代码)
要在对象顶部应用毛刺效果,我们可以将 ShaderGraph 与自定义通道一起使用:
首先,我们创建一个无光照 HDRP ShaderGraph,然后添加一些节点并以随机的 x 偏移量来对场景颜色采样,从而创建类似扭曲的效果。我们还有一个外部参数可以控制效果的强度,称为 offset
。
不要忘了将 ShaderGraph 置于透明模式,否则高清场景颜色节点 (HD Scene Color Node) 将返回黑色
完成此着色器后,我们在 Project 视图中选择着色器,右键单击并选择 Create/Material 便可从着色器创建材质。
然后,我们可以创建一个新的游戏对象并添加一个自定义通道体积组件。选择 Before Post Process
注入点,然后添加一个新的 DrawRenderers
自定义通道,其配置如下所示:
注意,我们要使用 ShaderGraph 的 ForwardOnly
通道,因为我们要渲染对象的颜色。
此处的 Selection
层将用于渲染有毛刺的对象。
然后,只需将游戏对象放入 Selection
层,就可以了!
脚本示例:Outline 通道
首先,我们创建一个名为 Outline 的自定义通道 C# 脚本:
using UnityEngine;
using UnityEngine.Rendering.HighDefinition;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
class Outline : CustomPass
{
public LayerMask outlineLayer = 0;
[ColorUsage(false, true)]
public Color outlineColor = Color.black;
public float threshold = 1;
// 为了确保着色器最终出现在构建中,我们将着色器的引用保留在自定义通道中
[SerializeField, HideInInspector]
Shader outlineShader;
Material fullscreenOutline;
MaterialPropertyBlock outlineProperties;
ShaderTagId[] shaderTags;
RTHandle outlineBuffer;
protected override void Setup(ScriptableRenderContext renderContext, CommandBuffer cmd)
{
outlineShader = Shader.Find("Hidden/Outline");
fullscreenOutline = CoreUtils.CreateEngineMaterial(outlineShader);
outlineProperties = new MaterialPropertyBlock();
// 列出帧中将要替换的所有材质
shaderTags = new ShaderTagId[3]
{
new ShaderTagId("Forward"),
new ShaderTagId("ForwardOnly"),
new ShaderTagId("SRPDefaultUnlit"),
};
outlineBuffer = RTHandles.Alloc(
Vector2.one, TextureXR.slices, dimension: TextureXR.dimension,
colorFormat: GraphicsFormat.B10G11R11_UFloatPack32,
useDynamicScale: true, name: "Outline Buffer"
);
}
void DrawOutlineMeshes(ScriptableRenderContext renderContext, CommandBuffer cmd, HDCamera hdCamera, CullingResults cullingResult)
{
var result = new RendererListDesc(shaderTags, cullingResult, hdCamera.camera)
{
// 我们需要光照渲染配置以支持渲染光照对象
rendererConfiguration = PerObjectData.LightProbe | PerObjectData.LightProbeProxyVolume | PerObjectData.Lightmaps,
renderQueueRange = RenderQueueRange.all,
sortingCriteria = SortingCriteria.BackToFront,
excludeObjectMotionVectors = false,
layerMask = outlineLayer,
};
CoreUtils.SetRenderTarget(cmd, outlineBuffer, ClearFlag.Color);
HDUtils.DrawRendererList(renderContext, cmd, RendererList.Create(result));
}
protected override void Execute(ScriptableRenderContext renderContext, CommandBuffer cmd, HDCamera camera, CullingResults cullingResult)
{
DrawOutlineMeshes(renderContext, cmd, camera, cullingResult);
SetCameraRenderTarget(cmd);
outlineProperties.SetColor("_OutlineColor", outlineColor);
outlineProperties.SetTexture("_OutlineBuffer", outlineBuffer);
outlineProperties.SetFloat("_Threshold", threshold);
CoreUtils.DrawFullScreen(cmd, fullscreenOutline, outlineProperties, shaderPassId: 0);
}
protected override void Cleanup()
{
CoreUtils.Destroy(fullscreenOutline);
outlineBuffer.Release();
}
}
在 Setup 函数中,我们分配一个缓冲区来渲染 outlineLayer
层中的对象。
Execute 函数非常简单,我们渲染对象,然后执行全屏通道以绘制轮廓。绘制对象时,我们在此处不使用覆盖材质,因为我们希望该效果与 Alpha 裁剪和透明度一起使用。 对于着色器,这是绘制轮廓的经典方法:对轮廓缓冲区中的颜色进行采样,如果颜色低于阈值,则意味着我们可能在轮廓中。要检查是否属于这种情况,我们执行邻居搜索,如果发现有像素高于阈值,则为这个像素绘制轮廓。
为了提高效率,我们将透明全屏通道与混合模式结合使用,这将替换需要绘制轮廓的像素。
Shader "Hidden/Outline"
{
HLSLINCLUDE
#pragma vertex Vert
#pragma target 4.5
#pragma only_renderers d3d11 ps4 xboxone vulkan metal switch
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/RenderPass/CustomPass/CustomPassCommon.hlsl"
TEXTURE2D_X(_OutlineBuffer);
float4 _OutlineColor;
float _Threshold;
#define v2 1.41421
#define c45 0.707107
#define c225 0.9238795
#define s225 0.3826834
#define MAXSAMPLES 8
// 邻近像素位置
static float2 samplingPositions[MAXSAMPLES] =
{
float2( 1, 1),
float2( 0, 1),
float2(-1, 1),
float2(-1, 0),
float2(-1, -1),
float2( 0, -1),
float2( 1, -1),
float2( 1, 0),
};
float4 FullScreenPass(Varyings varyings) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(varyings);
float depth = LoadCameraDepth(varyings.positionCS.xy);
PositionInputs posInput = GetPositionInput(varyings.positionCS.xy, _ScreenSize.zw, depth, UNITY_MATRIX_I_VP, UNITY_MATRIX_V);
float4 color = float4(0.0, 0.0, 0.0, 0.0);
float luminanceThreshold = max(0.000001, _Threshold * 0.01);
// 如果我们不在渲染注入点之前,则将摄像机颜色缓冲区加载到 Mip 0
if (_CustomPassInjectionPoint != CUSTOMPASSINJECTIONPOINT_BEFORE_RENDERING)
color = float4(CustomPassSampleCameraColor(posInput.positionNDC.xy, 0), 1);
// 采样 RTHandle 纹理时,请始终先使用 _RTHandleScale.xy 来缩放 UV。
float2 uv = posInput.positionNDC.xy * _RTHandleScale.xy;
float4 outline = SAMPLE_TEXTURE2D_X_LOD(_OutlineBuffer, s_linear_clamp_sampler, uv, 0);
outline.a = 0;
if (Luminance(outline.rgb) < luminanceThreshold)
{
for (int i = 0; i < MAXSAMPLES; i++)
{
float2 uvN = uv + _ScreenSize.zw * _RTHandleScale.xy * samplingPositions[i];
float4 neighbour = SAMPLE_TEXTURE2D_X_LOD(_OutlineBuffer, s_linear_clamp_sampler, uvN, 0);
if (Luminance(neighbour) > luminanceThreshold)
{
outline.rgb = _OutlineColor.rgb;
outline.a = 1;
break;
}
}
}
return outline;
}
ENDHLSL
SubShader
{
Pass
{
Name "Custom Pass 0"
ZWrite Off
ZTest Always
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
HLSLPROGRAM
#pragma fragment FullScreenPass
ENDHLSL
}
}
Fallback Off
}