1. 项目概述为什么你需要关注HDRP Custom Passes如果你正在使用Unity的高清渲染管线HDRP开发项目并且已经不再满足于内置的后期处理效果或者需要实现一些“非主流”的渲染效果——比如为特定物体添加轮廓光、实现场景的局部扭曲、制作全屏的像素化风格滤镜甚至是实现一些复杂的自定义后处理链——那么HDRP的Custom Passes自定义渲染通道就是你工具箱里不可或缺的利器。简单来说Custom Passes允许你在HDRP的标准渲染流程中插入你自己定义的渲染步骤从而获得对最终画面的完全控制权。我最初接触Custom Passes是为了解决一个具体问题在一个科幻项目中需要为能量护盾实现一种“扫描线”效果这种效果需要基于物体的世界坐标和深度信息进行动态计算并且只影响特定层级的物体。尝试了多种Shader Graph和后期处理方案后发现要么性能开销太大要么效果不够精确。直到深入使用了Custom Passes才真正找到了一个既高效又灵活的解决方案。它不像编写一个完整的渲染器那样复杂但又比简单的材质球叠加强大得多是连接美术效果与程序逻辑的完美桥梁。对于Unity开发者而言掌握Custom Passes意味着你能够突破HDRP预设管线的限制实现那些让项目脱颖而出的独特视觉效果。无论是技术美术TA还是图形程序员这都是提升项目视觉品质和解决特定渲染难题的关键技能。接下来我将带你从零开始快速上手HDRP Custom Passes完成从环境配置到实现第一个自定义效果的完整流程。2. 环境准备与核心概念解析在开始编写代码之前确保你的开发环境已经就绪并理解几个核心概念这能让你在后续操作中少走很多弯路。2.1 项目环境配置首先你需要一个启用了HDRP的Unity项目。如果你是从头创建请确保在创建项目时选择“High Definition RP (HDRP)”模板。如果你是在现有项目中添加HDRP需要通过Package Manager安装“High Definition RP”包并按照向导创建HDRP资产和默认设置。一个常见的坑点是HDRP的版本兼容性。我建议使用相对稳定的LTS版本例如Unity 2021.3 LTS或2022.3 LTS并搭配对应版本的HDRP包。在Package Manager中你可以查看HDRP包的版本。确保你的项目设置Edit Project Settings Graphics中Scriptable Render Pipeline Settings字段已经分配了你创建的HDRP资产通常名为HDRenderPipelineAsset。这是Custom Passes能够正常工作的基础。注意如果你的项目是从Built-in Render Pipeline升级而来可能会遇到材质丢失或光照错误。务必在转换后使用HDRP的材质升级工具Window Rendering Render Pipeline Converter进行批量转换并手动检查关键材质的设置。2.2 理解Custom Pass的核心组件Custom Passes并非一个孤立的脚本而是一个由几个关键组件协同工作的系统。理解它们之间的关系至关重要Custom Pass Volume组件这是Custom Pass的“容器”和“触发器”。它被添加到一个GameObject上通常是一个空物体并定义了Custom Pass在何时Injection Point、何地Mode以及如何Fade被执行。你可以把它想象成一个特殊的“体积”当摄像机进入这个体积时其中包含的Custom Pass效果就会被激活。Custom Pass资产这是具体效果的实现载体。HDRP内置了几种类型最常用的是FullScreenCustomPass全屏后处理和DrawRenderersCustomPass绘制特定渲染器。你也可以通过编写C#脚本创建完全自定义的类型。自定义着色器Shader这是效果算法的核心。对于FullScreenCustomPass你需要一个全屏着色器对于DrawRenderersCustomPass你需要一个物体着色器。Unity提供了对应的Shader模板来创建它们。材质Material将着色器实例化并允许你调整其参数如颜色、强度等而无需修改代码。它们的工作流程是Custom Pass Volume在指定的注入点使用指定的材质其背后是自定义着色器将效果渲染到指定的目标缓冲区如摄像机颜色缓冲区或自定义缓冲区。理解这个数据流是调试和创作复杂效果的基础。2.3 注入点Injection Point详解注入点是Custom Passes中最关键的概念之一它决定了你的效果在渲染管线的哪个阶段被执行。选择错误的注入点效果可能完全看不见或者与其他效果如雾效、透明物体产生错误的叠加顺序。HDRP提供了多个注入点主要分为几大类在渲染不透明物体之后例如After Opaque Depth And Normal。这是最常用的注入点之一此时场景中所有不透明物体都已渲染完毕深度和法线信息可用。适合制作需要基于场景几何信息的后期效果如边缘检测、景深模拟虽然HDRP有更好的方案等。在渲染透明物体之前/之后例如Before Transparent和After Post Process。如果你希望效果被透明物体如玻璃、粒子遮挡就在透明物体之前注入如果你希望效果叠加在所有物体包括后期处理之上就在最后注入。在预折射/反射之前例如Before PreRefraction。这涉及到更底层的渲染阶段通常用于修改折射/反射所需的数据。对于初学者我的建议是先从After Opaque Depth And Normal开始尝试。这个阶段拥有完整的场景深度、法线和颜色信息是进行大多数自定义后处理最安全、最直观的起点。当你需要效果出现在UI之前、所有渲染之后时再考虑使用After Post Process。3. 实战创建你的第一个全屏Custom Pass灰度化效果理论说得再多不如动手实践。让我们来实现一个经典且简单的效果将屏幕实时转换为灰度图。这个例子将贯穿创建Custom Pass的完整流程。3.1 第一步创建Custom Pass Volume在Unity编辑器的Hierarchy窗口中右键点击选择Volume Custom Pass。这会在场景中创建一个带有Custom Pass Volume组件的GameObject。选中这个GameObject在Inspector面板中你会看到Custom Pass Volume组件。关键参数解析Mode选择Global让效果影响场景中的所有摄像机。Injection Point选择After Opaque Depth And Normal。这样我们就能在渲染完所有不透明物体后应用灰度效果。Fade Radius保持为0因为Mode是Global此参数不生效。Priority如果有多个Custom Pass Volume在同一个注入点这个数值决定执行顺序数字小的先执行。我们先保持默认0。3.2 第二步创建全屏Custom Pass与着色器在Custom Pass Volume组件的Custom Passes列表下方点击号从下拉菜单中选择FullScreen Custom Pass。这会创建一个新的全屏Pass条目。现在我们需要为这个Pass关联一个材质和着色器。在Project窗口中右键点击选择Create Shader HDRP Custom FullScreen Pass。将其命名为CustomGrayScale。右键点击刚创建的CustomGrayScale.shader文件选择Create Material。Unity会自动创建一个名为FullScreen_CustomGrayScale的材质。实操心得Unity的这个命名规则FullScreen_前缀有助于在材质列表中快速识别全屏Custom Pass材质建议保持这个习惯。3.3 第三步编写灰度着色器代码双击打开CustomGrayScale.shader文件。你会看到Unity已经生成了一个模板代码。我们主要关注FullScreenPass函数。找到以下代码块// 在此处添加自定义通道代码 // 淡化值可让你在摄像机接近自定义通道体积的同时增强效果的强度 float f 1 - abs(_FadeValue * 2 - 1); return float4(color.rgb f, color.a);将其替换为我们的灰度化算法。一个标准的灰度化公式是取RGB颜色的亮度值Gray 0.2126 * R 0.7152 * G 0.0722 * B。这个权重系数符合人眼对绿光最敏感、对蓝光最不敏感的特性。修改后的FullScreenPass函数核心部分如下float4 FullScreenPass(Varyings varyings) : SV_Target { // 1. 获取当前像素的深度和世界位置等信息模板已提供 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); // 2. 如果不是在渲染前注入点则加载摄像机颜色缓冲区的颜色 if (_CustomPassInjectionPoint ! CUSTOMPASSINJECTIONPOINT_BEFORE_RENDERING) color float4(CustomPassLoadCameraColor(varyings.positionCS.xy, 0), 1); // 3. 核心应用灰度化算法 // 计算亮度Luminance float luminance dot(color.rgb, float3(0.2126, 0.7152, 0.0722)); // 将RGB三个通道都设置为亮度值得到灰度颜色 float3 grayColor float3(luminance, luminance, luminance); // 4. 可以保留原始的淡化逻辑用于体积混合可选 // float f 1 - abs(_FadeValue * 2 - 1); // return float4(lerp(color.rgb, grayColor, f), color.a); // 5. 直接返回灰度颜色 return float4(grayColor, color.a); }代码解析CustomPassLoadCameraColor(varyings.positionCS.xy, 0)这是关键函数用于采样当前注入点下摄像机颜色缓冲区中该像素的颜色。第二个参数0表示Mipmap级别。dot(color.rgb, float3(0.2126, 0.7152, 0.0722))点乘运算是Shader中实现加权求和的常用且高效的方法。lerp函数线性插值。注释掉的代码展示了如何将灰度效果与原始颜色根据_FadeValue由Custom Pass Volume的Fade Radius控制进行混合这在制作局部体积效果时非常有用。3.4 第四步关联材质并测试回到之前创建的FullScreenCustomPass条目。点击FullScreen Material字段右侧的圆形图标在弹出的选择器中找到并选中我们刚才创建的FullScreen_CustomGrayScale材质。确保Target Color Buffer设置为Camera默认这样我们的输出才会覆盖到主摄像机的画面。运行游戏你应该能看到整个游戏画面变成了黑白灰度。恭喜你已经成功创建并运行了第一个HDRP Custom Pass。虽然效果简单但你已经打通了从创建Volume、编写Shader到最终渲染的完整链路。你可以尝试修改灰度公式的权重或者将luminance与原始color.rgb进行混合如lerp(color.rgb, grayColor, 0.5)来实现一个去饱和度的效果而不是完全黑白。4. 进阶使用Draw Renderers Custom Pass实现物体描边全屏效果很酷但有时我们只想影响特定的物体。DrawRenderersCustomPass就是为此而生。我们将用它实现一个经典的“物体外发光描边”效果。4.1 创建Draw Renderers Custom Pass在之前的Custom Pass Volume中或者新建一个点击号这次选择DrawRenderersCustomPass。我们需要配置几个关键属性Name命名为“OutlinePass”便于识别。Target Color/Depth Buffer都设置为Camera。Filters这是核心。我们将通过这里选择要渲染哪些物体。Queue选择Transparent。因为描边通常需要在不透明物体之后、透明物体之前渲染以避免被遮挡。Layer Mask点击下拉菜单选择一个或多个你希望添加描边的物体所在的层。例如你可以创建一个名为“Outline”的层并将需要描边的物体分配到这个层然后在这里只选择“Outline”层。OverridesMaterial我们需要创建一个新的着色器和材质。在Project窗口右键选择Create Shader HDRP Custom Renderers Pass命名为OutlineShader并为其创建材质OutlineMat。Pass Name当材质分配后这里会出现可选的Pass。对于描边我们通常使用ForwardOnly因为我们只需要正向渲染路径的颜色信息。4.2 编写物体描边着色器打开OutlineShader.shader。与全屏Pass不同这是一个物体着色器。我们需要在片段着色器Fragment Shader中计算描边。描边的常见实现方法之一是“法线外扩”在顶点着色器中沿着顶点法线方向将顶点位置稍微向外挤出并用纯色渲染这个“放大”的模型然后再正常渲染原模型。由于深度测试正常模型会覆盖掉放大模型的大部分区域只留下边缘部分可见形成描边。我们需要修改两个函数顶点着色器修改函数ApplyMeshModification和片段着色器函数GetSurfaceAndBuiltinData。首先启用顶点修改并实现外扩。找到被注释掉的ApplyMeshModification函数取消注释并修改#define HAVE_MESH_MODIFICATION // 确保这行取消注释或存在 AttributesMesh ApplyMeshModification(AttributesMesh input, float3 timeParameters) { // 沿着法线方向挤出顶点。_OutlineWidth是一个可在材质面板调节的属性。 input.positionOS input.normalOS * _OutlineWidth; return input; }然后在GetSurfaceAndBuiltinData函数中我们直接返回一个固定的描边颜色比如红色void GetSurfaceAndBuiltinData(FragInputs fragInputs, float3 viewDirection, inout PositionInputs posInput, out SurfaceData surfaceData, out BuiltinData builtinData) { // 完全不执行任何光照计算直接输出描边颜色 float3 outlineColor _OutlineColor.rgb; // _OutlineColor是一个Color属性 // 初始化SurfaceData ZERO_INITIALIZE(SurfaceData, surfaceData); surfaceData.color outlineColor; // 初始化BuiltinData ZERO_INITIALIZE(BuiltinData, builtinData); builtinData.emissiveColor outlineColor; // 将描边颜色作为自发光颜色输出确保它可见 builtinData.opacity 1.0; // 非常重要将alphaClipTreshold设置为一个大于1的值确保不会因为AlphaTest而被裁剪 builtinData.alphaClipTreshold 1.1; }最后在Shader文件的Properties块中定义我们需要的属性Properties { _OutlineWidth(Outline Width, Range(0, 0.1)) 0.03 _OutlineColor(Outline Color, Color) (1, 0, 0, 1) }4.3 配置与调试将创建好的OutlineMat材质拖拽到Draw Renderers Pass的Material字段。在场景中创建几个Cube或Sphere将它们分配到你在Pass的Layer Mask中选中的层例如“Outline”层。运行游戏你应该能看到这些物体有了红色的外描边。常见问题与排查描边看不见首先检查物体的Layer是否匹配。其次检查Queue设置如果物体是不透明的但Queue选了Transparent可能会被提前深度剔除。可以尝试将Queue改为Overlay或调整Depth Test为Always。描边内部有残留这是因为外扩的模型和原模型在边缘处的深度值非常接近可能产生Z-fighting。适当减小_OutlineWidth或在材质面板上调整Depth Test为Less默认是Less Equal让描边只在原模型后方渲染。性能考虑DrawRenderersCustomPass会导致指定物体被额外渲染一次。对于复杂模型或大量物体需谨慎使用。可以考虑通过脚本动态启用/禁用Custom Pass Volume来控制性能开销。5. 深入原理自定义缓冲区与多Pass协作当你掌握了基础用法后可能会遇到更复杂的需求例如一个效果需要另一个效果的输出作为输入。这时就需要使用自定义缓冲区Custom Buffer。5.1 创建与使用自定义缓冲区自定义缓冲区允许你将一个Custom Pass的渲染结果临时存储在一张独立的纹理中供后续的Custom Pass读取。启用自定义缓冲区首先需要确保HDRP资源支持。选中你的HDRP资源文件如HDRenderPipelineAsset在Inspector中找到Rendering Custom Pass折叠栏。将Custom Buffer Format从None改为一种格式例如R8G8B8A8_SNorm。这就在管线中分配了自定义缓冲区。写入自定义缓冲区在第一个Custom Pass例如一个全屏模糊Pass中将Target Color Buffer设置为Custom。这样该Pass的输出就不会直接画到屏幕上而是存储到自定义缓冲区。从自定义缓冲区读取在第二个Custom Pass例如一个需要模糊结果进行叠加的Pass中你可以通过Shader函数SampleCustomColorBuffer(float2 uv)来采样第一步中写入的纹理。5.2 实现一个简单的两Pass特效模糊 叠加这个例子模拟一种“醉酒”或“回忆”效果先将场景模糊再将原始清晰场景与模糊场景按一定比例混合。Pass 1: 模糊创建一个FullScreenCustomPassInjection Point设为After Opaque Depth And Normal。Target Color Buffer设为Custom。编写一个简单的全屏模糊Shader例如使用一个小的高斯核或Box模糊将其输出到自定义缓冲区。Pass 2: 混合创建另一个FullScreenCustomPassInjection Point设为After Post Process确保它在所有后期之后。Target Color Buffer设为Camera。在它的Shader中使用CustomPassLoadCameraColor采样原始清晰颜色使用SampleCustomColorBuffer采样Pass 1产生的模糊纹理。使用lerp函数将两者混合finalColor lerp(originalColor, blurredColor, _BlendFactor);。通过调整_BlendFactor你可以控制效果的强度。这种多Pass协作的模式极大地扩展了Custom Passes的可能性你可以构建复杂的渲染图Render Graph。5.3 性能优化与最佳实践Custom Passes非常强大但滥用会影响性能。以下是一些关键优化点注入点选择尽可能选择靠后的注入点如After Opaque Depth And Normal避免在管线早期执行昂贵操作导致后续所有阶段都处理了多余的数据。绘制过滤对于DrawRenderersCustomPass务必使用Layer Mask和Queue进行精确过滤只渲染必要的物体。避免使用Everything。全屏Pass优化全屏Shader应尽可能高效。避免复杂的循环和分支。利用HLSL的内置函数和线性代数运算。自定义缓冲区只在必要时使用。每个自定义缓冲区都会增加显存带宽和占用。对于简单的颜色处理直接输出到Camera Buffer可能更高效。动态开关通过脚本控制Custom Pass Volume的enabled属性或根据摄像机与物体的距离、玩家状态等条件来激活/禁用Custom Pass可以节省大量不必要的渲染开销。6. 调试技巧与常见问题实录在实际开发中你一定会遇到各种“为什么没效果”的问题。这里分享一套我常用的调试流程和常见问题清单。6.1 系统性调试检查表当Custom Pass不工作时请按顺序检查以下项目检查项可能的问题与解决方案1. HDRP资源是否配置正确项目Graphics设置中必须指定HDRP资产。检查Edit Project Settings Graphics。2. Custom Pass Volume是否启用确保Inspector中Custom Pass Volume组件的勾选框是选中的。3. 注入点Injection Point是否正确效果可能被后续阶段覆盖。尝试切换到After Post Process看是否出现。4. 目标缓冲区Target Buffer设置对吗全屏Pass想输出到屏幕Target Color Buffer必须设为Camera。如果设为None或Custom但未正确采样则无输出。5. 着色器编译有错误吗查看Console窗口。即使一个警告也可能导致Shader编译失败效果不显示。6. 材质球关联了吗确保Custom Pass条目下的Material字段没有显示“None”。7. 着色器属性暴露了吗在Shader中定义的属性如_OutlineWidth需要在材质球面板上可见并设置了有效值。8. 深度测试/写入冲突对于DrawRenderers Pass检查Override Depth和Depth Test设置。如果物体被自身或其他物体深度剔除尝试改为Always。9. 摄像机裁剪或Layer Mask过滤物体是否在摄像机视野内DrawRenderers Pass的Layer Mask是否包含了目标物体6.2 Shader编写中的“坑”变量命名空间在Custom Pass Shader中你可以访问一些以_CustomPass开头的内置变量如_CustomPassInjectionPoint。但注意并非所有HDRP的通用变量如_Time都自动可用。如果遇到未定义错误可能需要自己传递或计算。坐标空间FullScreenPass函数中的varyings.positionCS是裁剪空间坐标。使用LoadCameraDepth或CustomPassLoadCameraColor时需要屏幕空间整数坐标varyings.positionCS.xy。而通过GetPositionInput获取的posInput.positionWS是世界空间坐标。混淆坐标空间是Shader错误的常见原因。Alpha通道处理全屏Pass中如果你直接返回float4(color.rgb, 1.0)可能会覆盖掉透明通道影响UI或其他透明效果。通常需要保留原始的alpha值return float4(finalColor, color.a);。6.3 性能分析与监控Unity的Frame Debugger和Profiler是你的好朋友。Frame Debugger在Window Analysis Frame Debugger中打开。逐步执行渲染过程你可以清晰地看到你的Custom Pass在哪个阶段被执行它的绘制调用Draw Call是什么以及它输出了什么。这是诊断渲染顺序和效果问题的终极工具。Profiler在Window Analysis Profiler中打开。切换到Rendering标签页查看GPU时间。如果你的Custom Pass导致了明显的GPU峰值就需要考虑优化Shader复杂度或减少绘制范围。掌握HDRP Custom Passes是一个从“使用者”到“创造者”的跨越。它不再让你受限于内置的效果而是打开了自定义渲染效果的大门。从简单的全屏滤镜到复杂的多Pass特效再到基于GPU计算的高级应用其潜力巨大。我个人的体会是初期多花时间理解注入点、缓冲区和坐标空间这些核心概念后期开发效率会成倍提升。当你能够流畅地运用Custom Passes解决各种渲染难题时你会发现它为你的项目带来的视觉表现力和技术灵活性是其他方法难以比拟的。最后一个小技巧将常用的Custom Pass如屏幕扭曲、全局雾效、场景扫描线封装成可复用的Prefab或ScriptableObject能在不同项目中快速搭建起高质量的画面后处理管线。