
做Unity项目做到中后期十有八九会碰到一个需求鼠标移到某个物体上或者点击选中它得让这个物体“亮起来”。听起来简单但真在HDRP管线下动手你会发现网上搜到的那些Built-in管线时代的旧方案——什么替换材质、加个Outline脚本、用OnMouseEnter改颜色——搬到HDRP里要么直接报错要么效果惨不忍睹。我自己第一次在HDRP项目里做选中高亮时就踩了整整两天的坑最后才摸到Custom Pass这条正路。这篇内容就是把我从原理到落地这一路的经验完整摊开讲。核心围绕Unity HDRP的Custom Pass展开讲清楚它为什么是HDRP下做物体高亮选中效果最靠谱的方案、它的渲染管线原理是什么、怎么一步步配置出可用的高亮效果、以及实测中那些文档里不会写的坑。适合已经上手Unity、正在用HDRP管线、需要做交互选中反馈的开发者参考哪怕你之前没碰过Custom Pass跟着思路走也能落地。1. 为什么HDRP下做高亮不能照搬旧方案1.1 Built-in管线那套思路在HDRP里为什么失效先说清楚问题根源不然后面配置Custom Pass你会觉得是在瞎折腾。Built-in管线时代做物体高亮最常见的手段是给目标物体换一个带描边Shader的材质或者用Graphics.DrawMesh在物体外面再画一层放大的壳。这套逻辑成立的前提是渲染顺序和材质替换完全由你手动控制管线本身不插手。但HDRP是延迟渲染为主、前向为辅的混合管线它内部有一整套基于物理的光照计算、GBuffer写入、屏幕空间效果链。你直接替换材质会破坏HDRP的材质属性约定比如_BaseColor、_Smoothness这些Lit Shader的关键字轻则高亮物体变成一片死黑重则整个场景的光照都跟着乱。更麻烦的是HDRP的渲染流程是分Pass的你没法像以前那样简单地在某个时机“插一脚”。我实测过最典型的翻车场景给选中物体挂一个自制的描边材质结果物体表面原本的PBR质感全没了变成纯色块而且描边在物体被其他物体遮挡时依然穿透显示视觉上非常假。这就是因为旧方案根本没考虑HDRP的深度测试和GBuffer结构。1.2 Custom Pass在HDRP渲染链中的位置Custom Pass本质上是HDRP开放给开发者的一根“注入管”允许你在HDRP预设的渲染阶段之间插入自己的渲染指令。HDRP的渲染流程大致可以理解为不透明物体渲染 → 深度预计算 → 光照计算 → 透明物体渲染 → 后处理。Custom Pass可以挂在其中多个注入点Injection Point上比如Before Rendering Transparent、After Opaque Depth And Normal、Before Post Process等等。这个机制的价值在于你注入的渲染指令是跑在HDRP自己的渲染上下文里的能拿到正确的深度缓冲、法线缓冲、相机矩阵不会破坏原有光照。做物体高亮最常用的注入点是After Opaque Depth And Normal——此时不透明物体的深度和法线已经写好了你可以基于这些信息做边缘检测、做遮罩、做描边而且能正确处理遮挡关系。打个生活化的比方HDRP是一条流水线每个工位负责一道工序Custom Pass就是你申请到的一个“临时工位”你可以在这个工位上对半成品做加工但不会打乱整条流水线的节奏。这就是它比替换材质高明的地方。1.3 高亮效果的技术选型对比在动手前值得把几种可行方案摆在一起对比避免走弯路。下面这张表是我实际项目中测试过的几种方案对比方案实现难度遮挡处理对原材质影响性能开销适用场景替换材质描边低差易穿透大破坏PBR低已淘汰不推荐额外Mesh壳放大中中需手动处理无中简单物体静态场景屏幕空间后处理描边中高好无中全屏统一描边Custom Pass 遮罩中好无可控HDRP下首选Custom Pass 深度边缘检测高很好无可控高质量选中反馈从表里能看出来Custom Pass方案在遮挡处理和材质保护上都有明显优势代价是需要理解渲染管线。而“Custom Pass 遮罩”和“Custom Pass 深度边缘检测”的区别在于前者是先把选中物体渲染到一个遮罩纹理再基于遮罩做高亮后者是直接基于深度和法线缓冲做屏幕空间边缘检测。前者实现更直观、可控性强后者效果更“聪明”但调参更费劲。我建议先从遮罩方案入手跑通后再考虑升级。2. Custom Pass的核心机制拆解2.1 注入点选择背后的逻辑注入点选错后面全是白费功夫。HDRP提供的注入点有好几个做物体高亮最相关的是这三个Before Rendering Transparent透明物体渲染之前。适合你想让高亮效果被透明物体正确遮挡的场景。After Opaque Depth And Normal不透明物体的深度和法线写完之后。这是做边缘检测和遮罩的黄金位置因为此时深度缓冲已经可用。Before Post Process后处理之前。适合做全屏级别的效果叠加。我最初图省事选了Before Post Process结果发现拿不到干净的法线信息边缘检测出来的线又粗又抖。换成After Opaque Depth And Normal之后深度和法线都是现成的边缘检测的精度立刻上来了。这个选择的逻辑是你要用的数据在哪个阶段准备好就注入在它之后。深度和法线在After Opaque Depth And Normal这个点已经完整所以做基于深度的效果就该选它。2.2 遮罩渲染与深度测试的配合遮罩方案的核心思路分两步第一步把选中物体单独渲染到一张遮罩纹理RenderTexture上此时只写颜色不写深度或者用一个纯色Shader第二步在Custom Pass里读取这张遮罩对遮罩覆盖的区域做高亮叠加。这里的关键是深度测试。如果你希望高亮只在物体可见部分显示被墙挡住的部分不亮那遮罩渲染时就要开启深度测试让被遮挡的像素被剔除。但如果你希望做一个“透视高亮”比如选中物体在墙后也能看到轮廓那就要关闭深度测试或者用模板缓冲做特殊处理。我实际项目里两种都做过。普通选中反馈用开启深度测试的版本玩家看到的就是物体本身发光而做“任务目标指引”时用关闭深度测试的版本让目标物体隔着墙也能透出轮廓。这个取舍完全取决于你的交互设计没有绝对对错但一定要在遮罩渲染阶段就定好因为深度测试的开关直接影响后面高亮叠加的逻辑。2.3 全屏三角形与Blit的底层操作Custom Pass里做屏幕空间效果绕不开Blit操作和全屏三角形。简单说Blit就是把一张源纹理“画”到目标纹理上中间可以跑一个自定义Shader做像素级处理。HDRP的Custom Pass提供了CustomPassUtils.RenderFromCamera、CustomPassUtils.DrawRenderers等工具方法但做遮罩叠加时最直接的还是自己写一个全屏Pass。全屏三角形的原理是不用两个三角形拼一个矩形而是用一个覆盖整个裁剪空间的大三角形顶点坐标直接给(-1,-1)、(3,-1)、(-1,3)这样光栅化后正好铺满屏幕还省去了矩形拼接的接缝问题。这个技巧在写全屏Shader时很常用性能上也更友好。在Custom Pass的Execute方法里典型操作是设置好目标RenderTexture调用CoreUtils.SetRenderTarget然后用CommandBuffer.DrawProcedural画全屏三角形材质里采样遮罩纹理做高亮计算。这一套流程听起来抽象但代码结构其实很固定后面实操部分我会给出可直接参考的骨架。3. 从零搭建高亮选中效果的完整步骤3.1 环境准备与Custom Pass Volume配置动手前先确认环境Unity版本建议2021.3 LTS及以上HDRP包版本与Unity版本匹配用Package Manager里的推荐版本最稳。项目里需要有一个已经配置好HDRP Asset的场景这是前提不然Custom Pass根本不会生效。第一步在场景里创建一个空物体挂上Custom Pass Volume组件。这个组件是Custom Pass的“容器”所有自定义Pass都挂在它下面。关键配置项Mode选Global表示全场景生效选Local表示只在Volume范围内生效。做选中高亮一般用Global。Fade Radius / Fade HeightLocal模式下才有效Global模式忽略。Priority多个Volume时的执行优先级数值大的先执行。然后在Volume里点“Add Pass”选择FullScreen Custom Pass或者Draw Renderers Custom Pass。做遮罩方案我建议用FullScreen Custom Pass作为主Pass因为它能自由控制全屏Blit逻辑如果需要单独渲染选中物体到遮罩可以再配一个Draw Renderers Custom Pass。提示Custom Pass Volume组件在场景里不显示任何可见物体容易让人以为没生效。配置完后一定要在Game视图或Scene视图里确认效果别对着空场景干等。3.2 遮罩纹理的创建与选中物体渲染遮罩纹理建议用RenderTexture分辨率跟屏幕一致或者减半都行。减半能省性能但边缘会略糊做精细描边时建议全分辨率。创建代码大致如下// 在Custom Pass的setup阶段创建遮罩纹理 m_MaskRT RTHandles.Alloc( Vector2.one, // 相对屏幕尺寸one表示全分辨率 TextureXR.slices, dimension: TextureXR.dimension, colorFormat: GraphicsFormat.R8G8B8A8_UNorm, useMipMap: false, name: SelectionMask );这里用RTHandles.Alloc而不是直接new RenderTexture是因为HDRP内部用RTHandles管理纹理生命周期直接new容易和管线自己的纹理管理冲突导致纹理被提前释放或者内存泄漏。这个细节文档里提得不多但实测中直接new RenderTexture在多次进出Play模式后会出现纹理丢失换成RTHandles就稳了。渲染选中物体到遮罩时用一个极简的Shader只输出纯色比如白色不参与光照计算。通过CustomPassUtils.DrawRenderers指定渲染层Layer只把选中物体所在的Layer画进去。这里建议给可选中物体单独分一个Layer比如“Selectable”这样渲染时用LayerMask过滤干净利落。3.3 高亮叠加Shader的编写要点高亮叠加Shader跑在全屏三角形上输入是遮罩纹理和相机颜色纹理输出是叠加后的结果。核心逻辑是采样遮罩如果遮罩值大于阈值就在原颜色上叠加高亮色同时可以基于遮罩做边缘检测让高亮只出现在物体轮廓上而不是整个物体泛白。写这个Shader有几个要点采样遮罩时用点采样还是线性采样做纯色填充用线性采样边缘更柔和做硬描边用点采样更锐利。我一般用线性采样加一个smoothstep做过渡。高亮色叠加方式直接lerp会显得平用Add叠加再配合Saturate能保留原物体细节视觉上更像“发光”而不是“涂色”。边缘检测的采样偏移基于遮罩做边缘检测时采样偏移量要跟屏幕分辨率挂钩用_ScreenParams的倒数做偏移不然不同分辨率下描边粗细不一致。一个容易忽略的点全屏Shader里做遮罩采样UV要用屏幕空间UV而不是物体UV。Custom Pass的全屏Pass里顶点着色器输出的UV直接就是裁剪空间映射过来的屏幕UV直接用就行别自己再算一遍。3.4 选中逻辑与Custom Pass的联动Custom Pass本身不知道“哪个物体被选中了”这个信息得从游戏逻辑层传进来。常见做法是维护一个选中物体列表在Custom Pass的Execute之前更新这个列表Pass里根据列表决定渲染哪些物体到遮罩。联动的关键是时机。选中状态可能在鼠标点击的瞬间改变而Custom Pass每帧执行。如果选中列表在Pass执行后才更新会出现一帧的延迟表现为点击后高亮慢半拍。解决办法是在Update里更新选中列表确保它在渲染之前完成。我踩过这个坑一开始在LateUpdate里更新结果高亮总是慢一帧排查了半天才定位到执行顺序问题。另外选中物体被销毁时要及时从列表移除否则Pass里访问到已销毁的GameObject会报空引用。建议在物体销毁的回调里做清理或者每帧渲染前过滤一遍列表。4. 实测中那些文档不会告诉你的坑4.1 遮罩纹理的格式与精度陷阱遮罩纹理的格式选择看着不起眼但直接影响效果。我最初用R8_UNorm单通道8位想着遮罩只需要一个值省内存。结果做边缘检测时发现描边有严重的阶梯感因为8位精度在渐变区域不够用。换成R8G8B8A8_UNorm后阶梯感消失。如果你的高亮需要做柔和渐变或者多层叠加建议直接用RGBA格式别在格式上抠性能这点显存开销换来的是肉眼可见的画质提升。还有一个坑是纹理的sRGB设置。遮罩纹理如果被标记为sRGB采样出来的值会经过gamma校正导致阈值判断失准。创建RTHandles时确保colorFormat选的是线性格式不带_SRGB后缀或者在Shader里手动做转换。这个细节不处理高亮的亮度会莫名其妙偏暗或偏亮。4.2 多相机与Scene视图下的重复渲染项目里如果有多个相机比如主相机加一个小地图相机Custom Pass Volume默认会对所有相机生效导致遮罩被渲染多次高亮在小地图里也出现甚至出现闪烁。解决办法是给Custom Pass加相机过滤在Execute里判断当前相机只对主相机执行高亮逻辑。Scene视图下也有类似问题。编辑器里Scene视图本身就是一个相机Custom Pass在Scene视图里也会跑有时候你在Game视图调好的效果在Scene视图里看着完全不一样。调试时建议明确区分两个视图的表现别在Scene视图里调参数然后被误导。我一般调试高亮效果时把Scene视图的相机关掉Custom Pass通过Volume的相机过滤只在Game视图里看真实效果。4.3 性能开销的实测数据与优化方向Custom Pass做高亮不是免费的。我在一个中等复杂度场景约200个可选中物体屏幕1080p里实测遮罩渲染阶段的开销主要来自DrawCall如果选中物体多DrawCall会上去全屏叠加阶段的开销跟分辨率强相关4K下全屏Blit的带宽压力明显。优化方向有几个遮罩纹理降分辨率半分辨率能省约一半带宽代价是边缘精度限制同时高亮的物体数量比如最多同时高亮5个把遮罩渲染的Shader做到极简去掉一切不必要的计算。实测下来半分辨率遮罩加极简Shader在1080p下开销能压到1ms以内对大多数项目够用了。还有一个隐蔽的性能坑每帧重新分配RenderTexture。如果在Execute里每次都new一个RTGC压力会很大帧率抖动明显。正确做法是在Setup阶段分配一次Cleanup阶段释放Execute里复用。这个模式跟HDRP内部管理纹理的思路一致养成习惯后能避免很多莫名其妙的卡顿。4.4 高亮边缘的锯齿与抗锯齿冲突HDRP自带抗锯齿TAA、MSAA等但Custom Pass渲染的遮罩和全屏叠加不一定能吃到抗锯齿。我遇到过开启TAA后高亮描边依然有明显锯齿的情况原因是遮罩纹理在渲染时没有走TAA的时序累积。解决思路有两个一是遮罩渲染时手动做超采样渲染到2倍分辨率再降采样代价是性能二是在全屏叠加Shader里对遮罩采样做多重采样平滑比如采样周围几个像素做平均用Shader换性能。我一般选后者在描边计算里加一个3x3的采样平均锯齿能明显改善开销也可控。另外如果项目开了MSAA要注意Custom Pass的RenderTexture需要匹配MSAA采样数否则会出现解析错误或者效果丢失。这个在HDRP的RenderTexture创建参数里有对应设置别漏了。5. 效果调优与进阶玩法5.1 高亮强度、颜色与呼吸感的参数化基础高亮跑通后接下来就是调效果。高亮强度、颜色这些参数建议全部暴露成Material Property或者通过MaterialPropertyBlock动态设置方便运行时调整。我习惯把高亮色、强度、描边宽度、呼吸频率这几个参数做成一个配置结构策划或美术可以直接在Inspector里调不用改代码。“呼吸感”是个加分项——让高亮强度随时间做正弦波动视觉上更抓眼球。实现很简单在Shader里用_Time.y做正弦乘到高亮强度上。但要注意频率别太高0.5到1.5赫兹比较舒服太快会显得焦躁。这个细节在交互反馈里很实用尤其是做任务目标指引时。5.2 遮挡透视高亮的实现变体前面提过关闭深度测试能做透视高亮。具体实现是在遮罩渲染阶段把深度测试设为Always这样被遮挡的像素也会写入遮罩。但直接这么做会让整个物体轮廓都透出来包括背面视觉上有点乱。进阶做法是用模板缓冲或者两次渲染先渲染可见部分用正常高亮再渲染被遮挡部分用半透明高亮形成“可见部分实、遮挡部分虚”的层次感。这个变体在解谜类或者探索类项目里很有用玩家能隔着墙看到目标的大致位置但又不至于完全透视破坏沉浸感。实现上比基础版多一个Pass但逻辑不复杂值得一试。5.3 多物体同时选中的层级处理框选多个物体时高亮效果要能区分“主选中”和“次选中”。我的做法是给不同选中状态用不同颜色或不同强度主选中用亮色加粗描边次选中用暗色细描边。实现上在遮罩渲染时按状态分组渲染到不同的遮罩通道RGBA四个通道正好够用全屏叠加时按通道分别处理。这里要注意遮罩通道的分配和Shader里的采样对应关系别搞混了通道导致颜色错乱。建议在代码里用常量定义通道索引别用魔法数字后期维护会轻松很多。5.4 与交互系统的解耦设计最后说一个工程层面的经验高亮系统最好和具体的交互逻辑解耦。Custom Pass只负责“给我一个物体列表我把它们高亮”至于这个列表怎么来的鼠标点击、射线检测、UI选择交给上层逻辑。这样高亮系统可以复用到不同场景也方便单独测试。我见过不少项目把选中逻辑和高亮渲染揉在一起结果换个交互方式就得重写渲染部分。解耦之后高亮系统就是一个纯粹的“渲染服务”输入物体列表输出视觉效果职责清晰维护成本低。这个设计思路在项目规模变大后价值会越来越明显。我在实际项目里把这套方案跑通后最大的体会是Custom Pass的学习曲线主要卡在“理解渲染阶段”这一步一旦想明白注入点和数据可用性的关系后面的配置和Shader编写都是水到渠成。真正费时间的反而是那些细节坑——纹理格式、相机过滤、执行时机这些没有文档会主动告诉你只能靠实测踩出来。如果你也在HDRP下做选中高亮建议先把基础遮罩方案跑通别一上来就追求透视和呼吸感稳扎稳打反而更快。