HDRP Custom Pass实现电影级物体描边:从原理到实战优化

📅 2026/7/11 11:14:30
HDRP Custom Pass实现电影级物体描边:从原理到实战优化
1. 项目概述与核心价值最近在捣鼓一个HDRP的圣诞市场场景想给那些精致的圣诞树、礼物盒和雪人加上一层电影感十足的描边效果就像《蜘蛛侠平行宇宙》或者很多3A游戏里那种角色和重要物体自带一圈发光轮廓能瞬间从背景中“跳”出来。Unity内置的Outline方案比如用第二个摄像机渲染纯色或者后处理描边在HDRP管线里要么效果生硬要么性能开销大要么对复杂透明物体的支持不够好。折腾了一圈最终把目光锁定在了HDRP的Custom Pass功能上特别是利用它来操作Outline通道这几乎是目前HDRP下实现高质量、可定制描边效果的“标准答案”。简单来说这个项目就是教你如何从零开始利用HDRP的Custom Pass亲手打造一个完全可控的、电影级的物体描边效果。它不仅仅是画个边而是涉及到如何精准地分离出你想要描边的物体、如何计算边缘、如何控制描边的颜色、宽度、平滑度甚至动态效果。无论你是想为自己的独立游戏增加视觉亮点还是为某个商业项目实现特定的艺术需求掌握这套方法都能让你对HDRP的渲染控制能力提升一个档次。接下来我会把整个实现过程、背后的原理、以及我踩过的坑和优化心得毫无保留地拆解给你看。2. HDRP Custom Pass与Outline通道核心原理拆解2.1 为什么是Custom Pass在进入实操之前必须得先搞明白我们手里的“工具”到底是什么。HDRPHigh Definition Render Pipeline作为Unity的高清渲染管线其设计哲学就是高度可定制和模块化。Custom Pass正是这一哲学的核心体现之一。你可以把它理解为一个“钩子”或者一个“插槽”允许你在HDRP渲染流程的特定阶段比如在渲染不透明物体之后、在后处理之前插入你自己编写的Shader代码。传统的描边方法比如在Forward Rendering里给物体加一个正面剔除Cull Front的Pass并放大模型在HDRP的复杂光照和材质体系下会问题百出难以与HDRP的Lit Shader完美结合且无法处理物体之间的遮挡关系。而后处理屏幕空间描边虽然通用但难以区分特定物体容易把背景里杂乱的边缘也描上精度和可控性都欠佳。Custom Pass的优势就在于精准与灵活物体级控制你可以指定哪些Layer的物体参与描边实现“指哪打哪”。管线深度集成你的描边计算可以发生在HDRP管线的精确阶段例如在深度和法线缓冲区已经准备好之后这为我们提供了高质量的边缘信息源。完整的Shader能力在Custom Pass的Shader里你可以像写普通Surface Shader或Unlit Shader一样访问各种纹理如_CameraDepthTexture, _CameraNormalsTexture和全局属性实现复杂的边缘检测算法。2.2 Outline通道的本质与工作流在HDRP的语境下“Outline通道”并不是一个现成的、开关一拨就有的东西。它是一套工作流和策略其核心思想是利用Custom Pass在渲染管线的后期为选定的物体单独渲染一次轮廓信息到一张额外的纹理即Outline Buffer然后再与主场景图像进行合成。这个工作流通常包含以下几个关键步骤目标选择通过Layer、Renderer或自定义组件标记出需要描边的物体。轮廓渲染在一个Custom Pass中将这些被标记的物体以特定的方式通常是纯色或基于法线/深度的边缘高亮渲染到一张Render Texture上。此时场景中其他物体不参与渲染。边缘处理对上一步得到的纯色轮廓图进行图像处理如扩张/腐蚀来模拟描边宽度或直接利用几何信息如背面膨胀生成轮廓。图像合成在另一个Custom Pass或后处理Pass中将处理好的轮廓图以“叠加”Additive或“屏幕”Screen等混合模式合成到最终的摄像机画面上。我们常说的“Outline通道”指的就是第2步中用于存储轮廓信息的那个Render Texture以及围绕它建立的一整套渲染和合成逻辑。3. 打造电影级描边效果的完整实操流程3.1 环境准备与项目设置首先确保你有一个使用HDRP的项目。如果是从头创建在Unity Hub中选择“High Definition RP (Universal RP is also available)”模板即可。对于现有项目需要通过Package Manager导入“High Definition RP”包并创建HDRP资产HDRP Asset和默认场景设置。关键设置检查HDRP Asset配置在项目设置的Graphics中确认使用的HDRP Asset已启用Custom Pass选项。通常这是默认开启的。摄像机设置选中你的主摄像机在Inspector面板找到“Rendering”部分确保“Custom Pass”选项是启用的。你需要在这里挂载我们即将创建的Custom Pass Volume。Layer准备在Tags Layers设置中新建一个Layer命名为“Outline”或类似名称。我们将把需要描边的物体分配到这个Layer上。3.2 创建Custom Pass Volume与ShaderCustom Pass需要通过一个Custom Pass Volume组件来管理。这类似于HDRP的后处理Volume系统。在场景中创建一个空物体命名为“Outline Custom Pass Volume”。为其添加Custom Pass Volume组件。将该Volume的“Mode”设置为“Camera”并将“Target Camera”拖拽指定为你的主摄像机。这样它的效果就只作用于这个摄像机。在“Custom Passes”列表下方点击“Add Pass”按钮选择“Fullscreen Custom Pass”。我们第一个Pass先用全屏Pass来清除和准备我们的Outline缓冲区。接下来是重头戏编写Shader。在Project窗口中右键Create Shader HDRP Custom Pass。将其命名为“OutlineCustomPass”。双击在Shader编辑器中打开推荐使用Visual Studio或Rider。这个模板Shader已经为我们搭建好了HDRP Custom Pass的基本结构。我们需要修改的主要是FullScreenPass函数。3.3 核心Shader代码解析深度-法线边缘检测电影级描边的质量很大程度上取决于边缘检测的精度。基于模型膨胀的方法在复杂网格上容易变形而基于深度和法线缓冲区的图像后处理方法则更加稳健和通用。这里我们实现一个经典的深度-法线边缘检测算法。首先在Shader的Properties块和HLSLINCLUDE部分定义我们需要的参数和纹理采样器。Shader Hidden/Custom/Outline { Properties { _OutlineColor(Outline Color, Color) (0, 1, 0, 1) _OutlineWidth(Outline Width, Range(0, 10)) 2 _DepthThreshold(Depth Threshold, Range(0, 1)) 0.01 _NormalThreshold(Normal Threshold, Range(0, 1)) 0.5 } HLSLINCLUDE #include Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl #include Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl TEXTURE2D_X(_CameraDepthTexture); TEXTURE2D_X(_CameraNormalsTexture); SAMPLER(sampler_CameraDepthTexture); SAMPLER(sampler_CameraNormalsTexture); float4 _OutlineColor; float _OutlineWidth; float _DepthThreshold; float _NormalThreshold; // 深度值解码从深度纹理中获取线性深度 float GetLinearDepth(float2 uv) { float depth LOAD_TEXTURE2D_X(_CameraDepthTexture, uv * _ScreenSize.xy).r; return LinearEyeDepth(depth, _ZBufferParams); } // 法线值解码 float3 GetNormal(float2 uv) { float4 normalEncoded LOAD_TEXTURE2D_X(_CameraNormalsTexture, uv * _ScreenSize.xy); return normalEncoded.xyz * 2.0 - 1.0; } ENDHLSL // ... SubShader和Pass定义 }核心的边缘检测函数CalculateOutline如下。它的原理是对于当前像素采样其上下左右四个偏移位置的深度和法线值与中心点进行比较。如果深度差或法线方向差超过阈值就认为该像素位于边缘。float CalculateOutline(float2 uv, float2 texelSize) { float centerDepth GetLinearDepth(uv); float3 centerNormal GetNormal(uv); // 定义采样偏移_OutlineWidth控制采样距离模拟描边粗细 float2 offsets[4] {float2(1, 0), float2(-1, 0), float2(0, 1), float2(0, -1)}; float edge 0.0; for (int i 0; i 4; i) { float2 sampleUV uv offsets[i] * texelSize * _OutlineWidth; float sampleDepth GetLinearDepth(sampleUV); float3 sampleNormal GetNormal(sampleUV); // 深度边缘检测 float depthDiff abs(centerDepth - sampleDepth); if (depthDiff _DepthThreshold * centerDepth) // 使用相对阈值适应远近物体 { edge 1.0; break; } // 法线边缘检测 float normalDiff dot(centerNormal, sampleNormal); if (normalDiff _NormalThreshold) // 法线点积越小夹角越大越是边缘 { edge 1.0; break; } } return edge; }最后在FullScreenPass函数中调用它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); float2 uv posInput.positionNDC.xy; float2 texelSize 1.0 / _ScreenSize.xy; float outline CalculateOutline(uv, texelSize); return float4(_OutlineColor.rgb * outline, outline); }这个Pass的输出白色或你设定的颜色代表边缘黑色代表非边缘形成一张轮廓遮罩图。3.4 配置Custom Pass实现物体过滤与合成仅有全屏边缘检测还不够它会把所有物体的边缘都描出来。我们需要限制只对特定物体进行描边。这需要用到DrawRenderers Custom Pass。回到“Outline Custom Pass Volume”对象。在Custom Passes列表中在刚才的“Fullscreen Custom Pass”之前添加一个“DrawRenderers Custom Pass”。顺序很重要我们需要先画出轮廓再进行全屏处理。配置这个DrawRenderers PassName: “Outline Mask Renderers”Target Color Buffer: 选择“Custom”。这是关键这意味着我们会把物体渲染到一个自定义的缓冲区而不是主颜色缓冲区。在“Renderers Filter”下将“Layer Mask”设置为之前创建的“Outline”层。Override Material: 这里需要一个新的材质。创建一个新的材质使用一个极其简单的Unlit Shader将其颜色设置为白色。将这个材质拖拽到这里。这个Pass的目的就是用纯白色渲染所有“Outline”层的物体到自定义缓冲区生成一个最原始的物体遮罩。Clear Flags: 设置为“Color”确保每一帧开始前这个自定义缓冲区被清除。现在我们有了两个Pass第一个PassDrawRenderers将轮廓物体以白色渲染到自定义缓冲区我们称之为_MaskBuffer。第二个PassFullscreen读取全屏的深度和法线但只对_MaskBuffer中为白色的区域即轮廓物体进行边缘检测计算。这就需要修改我们的Fullscreen Shader让它能采样到这个_MaskBuffer。在Shader中声明并采样这个自定义缓冲区TEXTURE2D_X(_CustomBuffer); SAMPLER(sampler_CustomBuffer);在CalculateOutline函数开始或FullScreenPass中先采样遮罩float mask LOAD_TEXTURE2D_X(_CustomBuffer, uv * _ScreenSize.xy).r; if (mask 0.1) // 如果不是轮廓物体直接返回黑色 { return 0.0; } // ... 否则进行正常的边缘检测计算最后我们需要第三个Pass将描边合成到最终画面。在Volume中再添加一个“Fullscreen Custom Pass”放在最后。这个Pass的Shader非常简单就是采样第二个Pass输出的轮廓图假设它输出到了_CameraColorTexture然后以“叠加”方式加到主画面上。float4 compositeColor LOAD_TEXTURE2D_X(_CameraColorTexture, uv * _ScreenSize.xy); float4 outlineColor // ... 采样第二个Pass生成的轮廓纹理; // 简单的叠加混合 return compositeColor outlineColor * outlineColor.a;在实际操作中HDRP的Custom Pass系统会自动管理这些中间纹理。你需要通过“Injection Point”来控制每个Pass的执行顺序例如在“After Post-Process”合成并通过“Target Color Buffer”和“Target Depth Buffer”的绑定来传递数据。3.5 参数调节与视觉打磨实现基础功能后调节参数以达到“电影级”效果至关重要_OutlineColor (描边颜色)不要只用纯白或纯黑。尝试使用与物体主色调互补的颜色或者场景氛围光的主色调。对于圣诞场景给金色铃铛加上暖白色的柔光描边给深绿色圣诞树加上淡金色的描边会非常出彩。_OutlineWidth (描边宽度)此参数控制边缘检测的采样距离。值太大会导致描边粗糙、溢出太小则描边不明显。建议根据物体在屏幕上的大小动态调整或者为近处物体设置更宽的描边以增强景深感。_DepthThreshold (深度阈值)控制多大深度差被视为边缘。对于复杂的、前后重叠的物体如一堆礼物盒需要调高此值以避免内部产生过多杂边。对于光滑曲面则需要调低以捕捉细微的深度变化。_NormalThreshold (法线阈值)控制法线夹角多大被视为边缘。这是捕捉物体轮廓如立方体边缘的关键参数。调低它会让你得到更“硬”的几何轮廓调高则会得到更平滑的过渡适合有机体。实操心得单独调整深度和法线阈值往往能解决大部分问题。如果物体内部出现了不应有的“龟裂”状描边通常是法线阈值太低将模型接缝或平滑组边界误判为边缘适当调高即可。如果描边在物体与背景交界处断裂可能是深度阈值太高未能检测到前景与背景的深度落差适当调低。4. 性能优化与高级技巧4.1 性能瓶颈分析与优化Custom Pass虽然强大但滥用会影响性能。主要开销在于额外的Draw CallDrawRenderers Pass会为轮廓物体增加一次渲染。全屏Shader处理边缘检测和合成Pass是像素着色器密集型操作。优化策略严格控制Layer只将最关键、最需要突出显示的物体如主角、可交互物品、BOSS放入“Outline”层。避免给背景或大量小物件添加描边。降低采样精度在全屏Pass的Shader中可以考虑以半分辨率uv * 0.5对深度/法线纹理进行采样和计算然后再上采样回全分辨率。这对描边效果的影响很小但能显著降低像素着色器的计算量。使用Stencil Buffer进行优化这是更高级的优化。在第一个DrawRenderers Pass中不仅输出颜色到自定义缓冲区还可以向Stencil Buffer写入一个特定值如1。然后在全屏边缘检测Pass中设置Stencil Test只对Stencil值为1的像素进行处理。这样可以完全避免对非轮廓物体区域的任何像素计算效率最高。这需要在Shader中编写Stencil相关代码并在Custom Pass Volume中配置相应的Stencil状态。分帧处理对于非关键或移动缓慢的物体可以考虑每2帧或3帧更新一次其描边通过时间插值来平滑变化适用于性能极度紧张的场景。4.2 实现动态与交互式描边静态描边只是开始动态描边更能增强表现力。脉动描边在Shader中让_OutlineWidth或_OutlineColor的强度随时间正弦变化。这非常适合用于提示可交互物品、任务目标或角色技能就绪状态。float pulse sin(_Time.y * _PulseSpeed) * 0.5 0.5; // 生成0-1的脉动值 float dynamicWidth _OutlineWidth * (1.0 pulse * _PulseStrength);击中高亮当角色被击中时瞬间将描边颜色变为红色并提高亮度然后快速衰减回原色。这可以通过脚本修改Custom Pass Volume中暴露的Shader参数如_OutlineColor来实现。基于距离的描边在Shader中根据像素的深度即距离摄像机的远近来调整描边宽度或颜色强度模拟景深效果让近处物体描边更实远处更虚。float depth GetLinearDepth(uv); float distanceFade saturate(1.0 - depth / _MaxFadeDistance); // 计算距离衰减 float finalWidth _OutlineWidth * distanceFade;4.3 与HDRP圣诞场景等艺术资产结合回到我们最初的热词“unity modular stylized christmas market bundle hdrp圣诞场景”。对于这类风格化场景描边效果可以玩出更多花样风格化强化对于Low Poly或手绘风格场景可以使用更“硬”、颜色更鲜艳的描边提高_NormalThreshold使用饱和色来强化其卡通渲染Cel-shading的感觉。雪景适配场景中可能有大量积雪。如果给雪地也加上描边会显得很乱。此时可以利用Shader中的深度阈值设置一个上限让深度差过大的地方如物体嵌入雪地的部分不产生描边或者让描边颜色与雪地颜色接近淡蓝色使其更自然地融入环境。灯光交互让描边颜色受到场景环境光或主要点光源颜色的影响。例如在圣诞市场温暖的灯笼光下描边可以偏橙黄色在冰冷的月光下描边可以偏蓝白色。这需要在Shader中采样环境光探针或主要光源颜色进行混合。5. 常见问题排查与调试实录在实际操作中你几乎一定会遇到下面这些问题。这里是我的排查记录和解决方案。问题现象可能原因排查步骤与解决方案完全看不到描边1. Custom Pass Volume未启用或未绑定正确摄像机。2. 物体Layer未设置为“Outline”。3. Custom Pass执行顺序错误合成Pass在最后。1. 检查Volume组件的“Mode”和“Target Camera”。2. 检查物体Layer和DrawRenderers Pass的Layer Mask。3. 在Volume中确保Pass顺序为DrawRenderers(生成遮罩) - Fullscreen(边缘检测) - Fullscreen(合成)。检查每个Pass的“Injection Point”是否合理如边缘检测在“Before Post-Process”合成在“After Post-Process”。描边闪烁或抖动1. 深度/法线纹理采样时UV计算受TAA时域抗锯齿或动态分辨率影响。2. 边缘检测算法没有处理屏幕边界。1. 在Shader中使用GetScreenSpaceTAAJitter函数获取并补偿TAA抖动偏移。对于动态分辨率确保使用_ScreenSize.xy和_ScreenSize.zw即1/屏幕尺寸进行正确的UV和Texel计算。2. 在CalculateOutline函数中对采样UV进行钳制clamp(sampleUV, 0, 1)防止采样到纹理边界外。描边内部有杂点或破碎1. 法线阈值(_NormalThreshold)设置过低将模型表面的平滑组边界或纹理法线细节误判为边缘。2. 深度阈值(_DepthThreshold)设置不合理对于连续曲面内部的微小深度波动过于敏感。1.逐步调高_NormalThreshold直到模型表面的杂点消失只保留主要轮廓。这是最常见有效的解决方法。2. 将深度阈值从绝对阈值改为相对阈值如if (depthDiff _DepthThreshold * centerDepth)这样能根据物体远近自适应。描边太粗或溢出到背景1._OutlineWidth值过大。2. 边缘检测采样时相邻像素可能采样到了背景物体。1. 降低_OutlineWidth通常2-5之间是安全范围。2. 在深度检测逻辑中加入背景判断如果中心点是物体有遮罩但采样点是背景深度接近远平面或遮罩为0则也应判定为边缘。这能更好地处理物体与背景的边界。性能开销过大1. Outline层物体过多或面数过高。2. 全屏Pass分辨率过高。1. 严格管理Outline层物体使用LOD或为复杂物体使用简化的代理网格Proxy Mesh进行轮廓渲染。2. 实施“半分辨率渲染上采样”优化或引入Stencil Buffer优化彻底剔除非轮廓像素的计算。透明物体描边异常HDRP的深度/法线纹理默认可能不包含透明物体的正确信息。透明物体通常在不透明物体之后渲染。方案一将透明物体的渲染队列改为“Geometry”或使用“Opaque”材质来生成轮廓遮罩仅用于描边不影响主外观。方案二使用一个专门的、只渲染透明物体的Custom Pass在其“After Post-Process”阶段进行轮廓绘制和合成避开深度缓冲区的问题。调试时善用Frame Debugger和RenderDoc工具至关重要。在Frame Debugger中你可以一步步查看每个Custom Pass的输入输出纹理清晰看到遮罩图、边缘检测图是如何生成的精准定位问题发生在哪个环节。最后这套Custom Pass Outline方案是一个强大的框架。掌握了它你不仅能做描边其思想可以扩展到很多其他效果比如为特定物体施加全局的扭曲效果热浪、局部的颜色替换角色中毒变色、或者高级的遮挡高亮X-Ray效果。关键在于理解“通过Custom Pass分离渲染目标再进行图像处理与合成”这一核心工作流。在圣诞市场的雪夜中为你精心布置的圣诞树挂上那圈温暖的光晕轮廓时你会觉得这一切的折腾都是值得的。