
1. 为什么要在 HDRP 里自己动手做高亮Unity 的 HDRP 管线在渲染架构上跟内置管线完全是两套思路很多在内置管线里靠OnRenderImage或者简单替换材质就能搞定的事情搬到 HDRP 里就得重新想一遍。物体高亮选中这个需求看起来简单——不就是让选中的物体亮一下、描个边吗但真在 HDRP 里落地你会发现常规的Outline插件要么不兼容要么跟后处理打架要么在透明物体上直接失效。我前后在三个项目里做过这套东西踩的坑足够写一篇长文所以这里把 Custom Pass 实现高亮选中的完整思路拆开讲。先说清楚这个内容是什么它是基于 HDRP 的Custom Pass机制在渲染管线的特定注入点插入一段自定义渲染逻辑把选中的物体用额外的颜色、描边或者菲涅尔效果标记出来。能做什么最典型的就是编辑器里的对象拾取反馈、RTS 或策略游戏里的单位选中、工业可视化里的部件高亮、教育类应用里的结构标注。解决了什么问题解决了 HDRP 下高亮效果与物理光照、后处理、透明排序之间的冲突问题同时保证性能可控、效果可调。适合谁看如果你已经能在 HDRP 里跑起来一个场景知道 Volume 是干嘛的写过简单的 Shader那这篇你能直接抄作业。如果你连 HDRP 的 Frame Settings 都没碰过建议先把官方文档里渲染管线的那几页翻一遍再回来不然中间有些注入点的选择你会看得云里雾里。下面所有内容都围绕“Custom Pass 做高亮”这一条主线展开不跑题。2. 整体方案设计与技术选型拆解2.1 三种常见高亮路线的取舍在动手写 Custom Pass 之前得先想清楚高亮到底要什么视觉效果。市面上常见的做法大致三类我列个表对比一下方便你按项目需求选。方案实现方式优点缺点适用场景替换材质高亮选中时把物体材质换成自发光版本实现最简单几乎零学习成本破坏原有材质表现透明物体穿帮多材质物体要逐个处理原型验证、极简风格项目后处理描边基于深度/法线缓冲做边缘检测全屏统一风格化强无法区分单个物体选中逻辑要额外做 ID 缓冲全局风格化描边Custom Pass 逐物体标记在指定注入点对选中物体额外渲染一遍精准控制单个物体效果可定制与光照解耦需要理解管线注入点代码量中等编辑器拾取、游戏选中、工业标注我选第三条路原因很直接前两种在“只高亮某一个物体”这个需求上都有硬伤。替换材质会污染原始资产后处理描边分不清谁是谁。Custom Pass 的核心优势是它能在渲染管线的任意阶段插入一次额外的绘制而且这次绘制只针对你指定的那批 Renderer天然就是“逐物体”的粒度。2.2 注入点为什么选 AfterOpaque 而不是 BeforeRenderingHDRP 的 Custom Pass 提供了多个注入点常用的有BeforeRendering、AfterOpaque、AfterTransparent、BeforePostProcess等。选哪个直接决定高亮效果跟场景的融合关系。我最初图省事用了BeforeRendering结果高亮物体被后面的不透明物体正常遮挡看起来就像没生效——因为它在深度测试之前就画了后面正常几何体一盖就没了。换成AfterOpaque之后不透明物体已经写好了深度缓冲此时再画高亮深度测试能正确判断遮挡关系同时高亮又不会被后处理比如 Bloom、色调映射过度影响。如果你希望高亮也吃到 Bloom 的辉光那就得放到BeforePostProcess让自发光部分参与后处理计算。这个取舍要看美术想要什么想要“干净利落的描边”就AfterOpaque想要“发光溢出的科技感”就BeforePostProcess。注意注入点选错是高亮“看不见”的头号原因排查时先确认这一点再去看 Shader 和 LayerMask。2.3 LayerMask 与 Rendering Layer 的配合逻辑Custom Pass 靠 LayerMask 决定对哪些物体生效。这里有个容易混淆的点Unity 的 Layer 和 HDRP 的 Rendering Layer 是两套东西。Layer 是 GameObject 上的那个下拉框Rendering Layer 是 HDRP 特有的、在 MeshRenderer 上单独配置的位掩码。Custom Pass 的LayerMask参数过滤的是前者。我的做法是给所有“可被选中”的物体单独分配一个 Layer比如叫Selectable然后 Custom Pass 的 LayerMask 只勾这个 Layer。这样即使场景里有几百个物体Pass 也只会处理这一批性能开销可控。至于“当前到底选中了哪一个”则通过脚本在运行时动态设置每个 Renderer 的renderingLayerMask或者在 Pass 内部用一个MaterialPropertyBlock传选中 ID。前者更灵活后者更省 draw call看你的选中频率决定。3. 核心细节解析与实操要点3.1 Custom Pass 的 C# 骨架长什么样HDRP 的 Custom Pass 有两种写法一种是继承CustomPass的 C# 类另一种是用CustomPassVolume组件配合 FullScreen 或 DrawRenderers 模式。做逐物体高亮我推荐继承CustomPass并重写Execute因为这样能完全控制绘制哪些 Renderer、用什么材质、传什么参数。骨架大致是这样类里持有Material引用和LayerMaskSetup里初始化材质Execute里调用CustomPassUtils.DrawRenderers把目标 Renderer 画一遍。关键参数是RenderQueueType和overrideMaterial。RenderQueueType要设成All或者Opaque取决于你的高亮物体是否包含透明材质overrideMaterial就是你的高亮 Shader 材质。这里有个细节DrawRenderers的overrideMaterialPassIndex要跟 Shader 里 Pass 的顺序对上否则会出现“材质对了但效果不对”的诡异现象。我一般把高亮 Pass 放在 Shader 的第一个 Pass索引填 0省得记。3.2 高亮 Shader 的核心结构高亮 Shader 我通常写成一个 Unlit 风格的简单 Shader核心是两件事颜色叠加和边缘描边。颜色叠加用BaseColor乘一个强度边缘描边用菲涅尔或者屏幕空间法线差分。菲涅尔描边的原理是视线方向与表面法线夹角越大边缘越亮。公式上就是pow(1 - saturate(dot(N, V)), _Power)_Power控制描边宽度。这个做法的好处是不依赖额外缓冲纯靠几何法线就能出效果缺点是对于平面物体比如一个薄片描边会不均匀。屏幕空间法线差分则更稳但需要额外采样法线缓冲在AfterOpaque注入点下是可行的因为此时法线缓冲已经写好了。我实际项目里用的是菲涅尔方案因为选中高亮通常不需要像素级精确的描边菲涅尔的柔和过渡反而更符合“选中态”的视觉语言。如果你做的是工业标注那种要求硬边描边的场景再切到法线差分方案。3.3 深度测试与 ZWrite 的开关策略高亮物体要不要被遮挡这取决于交互设计。编辑器里拾取物体通常希望高亮“穿透”显示让用户知道选中的是哪个哪怕它被挡住了。游戏里则往往希望高亮遵守遮挡关系否则会显得很假。穿透显示的做法是把 Shader 的ZTest设为AlwaysZWrite设为Off。这样高亮永远画在最上层但不会污染深度缓冲影响后续渲染。遵守遮挡则用ZTest LEqualZWrite Off。注意ZWrite一定要关否则高亮会写深度导致后面透明物体排序出错这个坑我踩过一次排查了半天才发现是 ZWrite 没关。提示ZTest Always的穿透高亮在 VR 里要慎用双眼视差下穿透效果容易引起视觉疲劳建议改成半透明叠加。4. 实操过程与核心环节实现4.1 环境准备与项目配置先把基础环境搭好。项目用 HDRP 模板创建确认Project Settings Graphics里指定的 Render Pipeline Asset 是 HDRP 的。然后在场景里放几个不同形状的物体给它们分配一个专用 Layer比如Selectable。这一步别偷懒Layer 分配错了后面 LayerMask 怎么调都不对。接着创建一个CustomPassVolume组件挂在场景里的空物体上Mode选GlobalInjection Point选AfterOpaque。然后在 Volume 里添加一个 Custom Pass 条目类型选Custom Pass对应你写的 C# 类。此时如果 C# 类还没写条目会是空的所以顺序上先写代码再配 Volume。4.2 编写 CustomPass 子类代码部分我按实际项目里的结构简化一下。核心是三个方法Setup、Execute、Cleanup。Setup里用CoreUtils.CreateEngineMaterial创建材质传 Shader 路径Execute里构造RenderTargetIdentifier和RendererList调CustomPassUtils.DrawRenderersCleanup里销毁材质防止内存泄漏。DrawRenderers的参数里RenderQueueType我填AlloverrideMaterial填刚创建的材质overrideMaterialPassIndex填 0。LayerMask用-1表示全部实际过滤靠 Volume 上的 LayerMask 配置。这里有个小技巧如果你想让高亮只对“当前选中”的物体生效可以在Execute里遍历SelectionManager维护的 Renderer 列表逐个设置MaterialPropertyBlock的选中标记Shader 里根据标记决定是否输出高亮色。这样一次 Pass 就能处理多个选中物体不用为每个物体单独建 Pass。4.3 Shader 参数调优与实测记录Shader 里我暴露了几个参数_HighlightColor高亮色、_HighlightIntensity强度、_OutlinePower描边锐度、_PulseSpeed呼吸动画速度。实测下来_HighlightIntensity在 1.5 到 2.5 之间比较舒服太低看不出选中太高会过曝。_OutlinePower在 2 到 4 之间数值越大描边越细。呼吸动画用sin(_Time.y * _PulseSpeed)做强度调制_PulseSpeed设 2 左右有轻微的“心跳感”设 0 则关闭动画。这个动画在编辑器拾取场景里很有用能让用户一眼锁定选中目标但在快节奏游戏里可能显得干扰按需开关。实测数据一个 500 物体的场景开启 Custom Pass 高亮后帧率从 120 掉到 115 左右开销主要来自额外的 draw call。如果选中物体只有几个开销几乎可以忽略。所以性能瓶颈不在 Pass 本身而在你让 Pass 处理了多少物体。4.4 与选中逻辑的联动高亮不是孤立的它得跟选中逻辑接上。我的做法是写一个SelectionManager单例维护一个ListRenderer表示当前选中集合。选中变化时遍历列表给每个 Renderer 设置MaterialPropertyBlock的_Selected属性为 1取消选中的设为 0。Custom Pass 的 Shader 里读这个属性为 1 才输出高亮。这样设计的好处是选中逻辑和高亮渲染解耦选中逻辑随便改点选、框选、多选高亮渲染不用动。缺点是MaterialPropertyBlock会打断 SRP Batcher 的合批如果选中物体很多draw call 会上升。实测 50 个以内选中物体影响不大超过 100 个建议改用renderingLayerMask方案。5. 常见问题与排查技巧实录5.1 高亮完全不显示这是最高频的问题排查顺序我整理成表。排查项检查方法常见原因注入点看 CustomPassVolume 的 Injection Point选了 BeforeRendering 被遮挡LayerMask看 Volume 上的 LayerMask 是否包含目标 LayerLayer 分配错误或掩码没勾Shader 编译看 Console 有无 Shader 报错语法错误或 HDRP 版本不兼容材质创建在 Setup 里打 Log 确认材质非空Shader 路径写错导致材质创建失败深度测试临时把 ZTest 改成 AlwaysZTest 设置导致被深度剔除我遇到最多的是 LayerMask 没勾对因为 HDRP 的 Layer 下拉框和普通 Layer 下拉框长得像但位置不同容易看走眼。5.2 高亮物体边缘有锯齿或闪烁边缘锯齿通常是描边算法的问题。菲涅尔描边在低多边形物体上容易出现硬边解决办法是提高_OutlinePower让描边变细或者改用屏幕空间法线差分。闪烁则多半是深度冲突Z-Fighting检查高亮物体的ZTest是否跟场景几何体深度值太接近把ZTest改成LEqual并加一点深度偏移Offset -1, -1能缓解。5.3 透明物体高亮失效透明物体在AfterOpaque注入点下还没渲染所以高亮画了也会被后面的透明渲染覆盖。解决办法是把注入点改成AfterTransparent但这样高亮就不吃后处理了。折中方案是给透明物体单独建一个 Pass注入点设AfterTransparent不透明物体用AfterOpaque的 Pass。两个 Pass 共用一个 Shader只是注入点不同。5.4 多相机场景下高亮重复绘制HDRP 支持多相机如果场景里有 Game 视图和 Scene 视图同时渲染Custom Pass 可能被调用两次导致高亮叠加变亮。解决办法是在Execute里判断HDCamera的类型只对 Game 相机或只对 Scene 相机生效。这个判断用HDCamera.current.camera.cameraType就能拿到。注意多相机场景下MaterialPropertyBlock的设置时机要小心如果在相机渲染中途改属性可能出现一帧内两个相机看到不同状态的情况建议在LateUpdate里统一设置。6. 我踩过的坑与经验沉淀第一个坑是材质泄漏。Setup里创建的材质如果不在Cleanup里销毁每次进入播放模式都会新建一个编辑器里反复播放几次内存就涨上去了。CoreUtils.Destroy是配套的销毁方法别用DestroyImmediate在运行时会有问题。第二个坑是 Shader 的RenderPipeline标签。HDRP 的 Shader 必须在SubShader里声明RenderPipeline HDRenderPipeline否则 Custom Pass 找不到这个 Shader材质创建出来是粉色的。这个标签在官方模板里都有但自己新建 Shader 时容易漏。第三个坑是呼吸动画的时间基准。用_Time.y在编辑器里没问题但如果项目用了时间缩放比如暂停时Time.timeScale 0_Time.y会停住高亮动画也跟着停。想让高亮在暂停时继续呼吸得用_UnscaledTime或者自己在 C# 里传一个不受缩放影响的时间参数。最后一个经验高亮效果一定要在真实场景里调别在空场景里调完就交付。空场景背景干净什么颜色都好看真实场景有复杂光照和贴图高亮色很容易被淹没。我的习惯是拿项目里最花哨的那个场景当调参基准能在这个场景里看清其他场景就没问题。这套方案后续还能扩展比如把高亮色跟物体状态绑定血量低变红、可交互变绿或者在 Custom Pass 里加一层轮廓光Rim Light让选中物体更有立体感。这些都是在现有骨架上加参数的事不用动整体架构。