2026/10/18 13:41:28

UE5鼠标点选模型边缘高亮:Custom Stencil模板描边完整实现

UE5鼠标点选模型边缘高亮:Custom Stencil模板描边完整实现 简介面向UE4/UE5开发者的模型交互高亮参考工程解决鼠标点击选中物体后边缘发光提示的常见交互需求。工程基于第一人称模板完整实现高亮材质通过Emissive Color控制发光可调粗糙度与金属感、射线碰撞检测蓝图、当前选中模型记录与切换并将逻辑挂接至主角组件中附带可运行的FirstPersonDemo示例适合希望快速为场景交互增加视觉反馈的初中级开发者参考也可作为UE4项目升级UE5时的交互示例。包体共485个文件大小约673.69MB资源以401个uasset资产文件为主体配合10个umap场景地图、37个ini配置文件以及少量json/ush脚本和日志构成可直接打开的UE5工程结构目录清晰便于定位材质、蓝图与地图模块。已有4752人学习/下载。除核心工程外还包含编辑器日志等排错参考便于理解功能挂接和调试流程在此基础上可进一步扩展多选模式或动画过渡效果以适配更复杂的项目需求。1. 鼠标点选加描边UE4、UE5 模型点击选中边缘高亮为什么值得认真做美术在编辑器里点一下模型期待它马上被一圈干净亮边框住程序端则在纠结同一件事命中判定写在哪一层描边用顶点膨胀还是后处理。UE4、UE5 模型鼠标点击选中边缘高亮拆开看只是“射线检测加一个描边材质”真落地时却要同时处理输入通道、模板缓冲、Nanite 兼容和性能预算任何一环掉链子功能在验收前都会翻车。这篇文章把整条链路从点击命中到模板写入再到后处理描边与排查手段完整捋一遍给 UE5 工程一套可以直接照做的方案也照顾到从 UE4 迁过来的老工程习惯。适合正在做编辑器工具、展厅交互、RTS 选框这类功能的开发者照着搭完能直接进入业务开发不用再摸黑调参。2. 边缘高亮三条路线对比为什么 Custom Stencil 在 UE5 工程里最稳2.1 三条主流描边方案的原理、成本与适用场景描边在引擎里没有“一键开启”的内置按钮常见做法有三类各有各的账。第一类是顶点膨胀描边业内也叫 Inverted Hull复制一份模型顶点沿法线方向外扩一小段距离渲染时把面朝相机的那一面剔除、只渲染背面材质输出纯描边色。第二类是纯后处理边缘检测对整屏的深度、法线或颜色做 Sobel 类梯度计算哪里梯度大哪里就是边缘。第三类是用 Custom Stencil 配合后处理先把需要描边的物体写进模板缓冲再在后处理材质里只对模板值发生跳变的位置上色。三类方案的差异在工程里非常明显直接决定你能不能接到“点击选中”这个需求上。顶点膨胀的优点是边缘锐利、不占用后处理采样成本但它要求每个可选中物体都有一份外扩副本动态生成网格、骨骼动画、Nanite 网格都会让副本失效纯后处理边缘检测不需要任何预设置可是它会把地形接缝、贴图明暗变化、阴影边界一并当边缘画出来选中与未选中的区分度很差。Custom Stencil 的思路则是把“选中状态”本身作为模板值写入缓冲区后处理只对“值发生变化”的像素做输出。方案实现原理优点缺点适合场景顶点膨胀描边复制模型、法线外扩、剔除正面边缘锐利、不吃后处理额外 Draw Call、动态网格易坏、Nanite 下难复制静态机械、建筑构件的常驻轮廓全屏后处理边缘检测对深度/法线/颜色求梯度全体物体一视同仁误检多、选中态难区分、斜面断线全局氛围轮廓、非交互展示Custom Stencil 后处理写入模板 ID后处理差分精确到组件、支持 ID 映射颜色需开启模板 Pass、ID 池有限点击选中、多选、分级高亮2.2 从点击选中反推选型模板缓冲恰好是最匹配的中间层点击选中这个动作天然产出一个结果你知道“哪个组件被选中”。把这个结果直接写进模板缓冲后处理材质的任务就只剩“找出模板值发生变化的像素”而不是去猜哪些像素属于物体。选中状态与渲染表现之间多了一层模板作为中间介质业务层和后处理层就能各自独立修改这是整套方案在工程上最舒服的地方。顶点膨胀方案没法按点击动态标记因为膨胀壳在构建时就已经存在你不可能为了一个选中状态临时给任意网格生成外扩副本。纯后处理边缘检测更麻烦它不知道你选中了谁全屏找边缘会把背景里的栏杆、树干、地形接缝全描一遍你只能靠“深度范围裁剪”去缩小范围参数调试基本靠玄学。Custom Stencil 则让业务层只做两件事命中的组件写入一个整数 ID未命中的组件什么都不写后处理层只做一件事模板值有跳变就输出描边色。另一个加分项是它对 Nanite 友好。UE5 里 Nanite 网格不能被简单复制膨胀也不能随便修改顶点数据但模板写入是在渲染管线层面完成的不触碰网格几何本身。这意味着哪怕场景里的高模全部走 Nanite这套描边方案依然成立只是要留意后面第 4.3 节讲的开关问题。所以在 UE5 工程里Custom Stencil 基本是点击选中描边的默认解其余方案只会在极特殊的常驻轮廓需求下才拿出来备用。2.3 先把管线打开Custom Depth-Stencil Pass 的项目设置与三个注意点开始写代码前先确认项目把模板通道开了。路径是 Project Settings → Rendering → Post Processing → Custom Depth-Stencil Pass默认是 Off必须改成 Enabled with Stencil。区别在于Enabled without Stencil 只输出自定义深度不写入模板值后处理里读不到 CustomStencil选 Enabled with Stencil 后模板缓冲才会被填充。开启后要在场景里放一个 Post Process Volume勾上 Unbound 让效果覆盖相机所在区域再把描边材质挂到 Post Process Materials 数组。如果没有现成的 Volume也可以在后期处理材质资产里直接以 Blendable 方式拖进场景。材质本身的 Domain 要设为 Post ProcessBlendable Location 建议先选 Before Tonemapping原因后面第 5.4 节专门讲。这里的三个注意点都是老工程从 UE4 迁移到 UE5 时最容易踩到的。第一Custom Depth-Stencil Pass 一旦打开所有参与该 Pass 的物体会被多渲染一次Draw Call 和 Overdraw 都会涨所以默认不要让场景里所有物体都开启相关开关。第二模板值是 8bit范围 02550 表示未写入255 在部分引擎内部路径里被保留业务 ID 最好从 1 开始用。第三CustomStencil 在后处理材质里读出来的是 float4真正的模板值在 R 通道别去采样 RGB 彩色图否则你会得到一张全是 0 的纹理。3. 鼠标点击选中到模板标记C 射线检测与 Stencil 写入的最小实现3.1 从屏幕坐标到世界射线Deproject 与碰撞通道的选择鼠标点选的第一步是把屏幕坐标变成世界空间的一条射线。这个动作官方已经提供好了PlayerController 里的 DeprojectMousePositionToWorld 会把鼠标位置投影到近裁剪面上返回一条从相机位置出发的方向射线。实现时在鼠标左键抬起事件里触发避免按住拖拽过程中反复触发选中逻辑。射线通道的选择直接影响命中率。最常见的是用 ECC_Visibility因为场景里绝大多数可交互物体默认对 Visibility 通道有 Block 响应但 Visibility 也会被触发器、半透明特效、可拾取物响应命中后要做二次过滤。另一个思路是给项目自定义一个 Trace Channel专门给“可选中”的物体配置碰撞响应这样射线检测结果天然就是候选列表只是需要在项目设置里额外建通道并给美术资产配置好碰撞预设。// AHighlightPlayerController.cpp // 鼠标左键抬起时执行选中逻辑避免拖拽过程中被误触发 void AHighlightPlayerController::TrySelectActor() { // 如果鼠标正悬停在 UI 上先不触发场景选中 // IsOverUI() 需要结合你自己的 UI 框架实现 if (IsOverUI()) { return; } FVector WorldOrigin; FVector WorldDir; if (!DeprojectMousePositionToWorld(WorldOrigin, WorldDir)) { return; } const float TraceDistance 10000.f; const FVector TraceEnd WorldOrigin WorldDir * TraceDistance; FHitResult HitResult; FCollisionQueryParams QueryParams; QueryParams.bTraceComplex false; QueryParams.bReturnPhysicalMaterial false; // 用 Visibility 通道做主射线大部分可交互场景物体默认都会响应 if (GetWorld()-LineTraceSingleByChannel( HitResult, WorldOrigin, TraceEnd, ECC_Visibility, QueryParams)) { UPrimitiveComponent* HitComp HitResult.GetComponent(); if (HitComp HitComp-GetOwner()) { // 先复位上一次选中的组件避免模板值残留 if (LastSelectedComp LastSelectedComp ! HitComp) { LastSelectedComp-SetRenderCustomDepth(false); } // 写入模板 ID 1并让该组件进入自定义深度渲染 Pass HitComp-SetCustomDepthStencilValue(1); HitComp-SetRenderCustomDepth(true); LastSelectedComp HitComp; } } }逻辑上这四步是固定套路先排除 UI 命中再把屏幕坐标反投影成射线然后做世界空间射线检测最后把命中的组件写入模板。TraceDistance 的 10000 是经验值展厅项目够用大型开放场景建议改成可配置参数按相机到场景边界的距离动态计算。bTraceComplex 设置成 false 会让射线只检测碰撞简单体对大多数选中交互来说足够也能省一些性能。3.2 命中后写入模板值UPrimitiveComponent 上的两个关键函数射线命中拿到的是 FHitResult组件通过 HitResult.GetComponent() 取得。真正让描边生效的是两个函数SetCustomDepthStencilValue 和 SetRenderCustomDepth。前者写入模板 ID 值后者控制该组件是否参与自定义深度 Pass。很多新手只调第二个不调第一个结果模板值一直是 0后处理里所有选中物体读出来都是同一个值描边范围完全失控。SetCustomDepthStencilValue 的参数范围是 02550 表示不标记1 开始作为业务 ID。如果场景里有多种类型的物体需要不同颜色的高亮这个 ID 就承担了分类职责比如 1 是普通选中、2 是敌人、3 是任务目标后处理材质里用分支判断输出不同颜色。SetRenderCustomDepth(true) 会把这个组件推进额外的一次渲染 Pass它在 Base Pass 结束后单独跑一遍深度和模板写入。这两个函数在运行时调用都会触发渲染状态更新代价是一次很小的重绘不会造成明显卡顿。但要避免每帧对全场景所有可交互物体批量调用那相当于把整个场景强制拉进 Custom Depth Pass性能会掉得很难看。正确的习惯是只对“当前选中”和“上一次选中”两个组件做开关切换其余物体保持关闭。3.3 单选状态管理上一帧选中项如何复位避免描边累积单选模式需要维护一个“当前选中组件”的引用每次新命中先关掉旧的再打开新的。上面代码里用 LastSelectedComp 保存上一次选中的组件在命中新目标时先调用 SetRenderCustomDepth(false) 复位。漏掉这一步的后果非常直观点一个亮一个场景里描边越堆越多最后看起来像开了外挂的框选工具。悬垂指针是这里最常见的崩溃来源。如果被选中组件在运行期被销毁LastSelectedComp 还握着旧地址下次点击时调用 SetRenderCustomDepth 就会访问已释放内存。项目里只要存在运行时 Spawn/销毁逻辑就必须监听销毁事件并把引用清空。// 监听 Actor 销毁避免 LastSelectedComp 变成悬垂指针 void AHighlightPlayerController::OnHighlightedActorDestroyed(AActor* DestroyedActor) { if (LastSelectedComp LastSelectedComp-GetOwner() DestroyedActor) { LastSelectedComp-SetRenderCustomDepth(false); LastSelectedComp nullptr; } }这个监听的注册放在 BeginPlay 里绑定到 World 的 OnActorDestroyed 委托上。如果你做的是多选模式把单个组件引用换成 TArrayUPrimitiveComponent*复位时遍历整个数组关掉模板写入再用一个临时数组存新的选中集合。多选的难点不是画描边而是框选判定后如何合并新旧集合这个在第 6 章展开。4. 后处理材质画描边节点连接、核心参数与 Nanite 兼容4.1 最小后处理材质CustomStencil 差分检测的两组节点材质侧的目标只有一个找出模板值发生跳变的像素。最简单的最小结构需要两组 CustomStencil 采样。第一组采样当前像素的模板值第二组采样偏移后相邻像素的模板值两组输出求差取绝对值差值大于阈值就认为这里是从“选中区域”过渡到“非选中区域”的边缘。材质编辑器里的搭建方式是新建一个材质Material Domain 设为 Post ProcessBlendable Location 选 Before Tonemapping。添加两个 SceneTexture 节点SceneTextureId 都改为 CustomStencil。第一个节点保持默认 UV 不动第二个节点在 UV 输入引脚上接入“默认 UV 加一个偏移向量”偏移量等于 EdgeThickness 乘以单个像素尺寸。每个 SceneTexture 节点取 R 通道作为模板值然后用 Subtract 相减、Abs 取绝对值。最后用 Step 节点做阈值判断差值大于 EdgeThreshold 时输出描边颜色否则输出透明。Step 的输入参数是 EdgeThreshold比较结果会输出 0 或 1正好用来和描边色做乘法。想加一点柔和过渡可以把 Step 换成 SmoothStep让边缘带一圈渐变而不是硬边。注意 CustomStencil 输出的是 float4模板值在 R 通道别去采样 G 或 B那里的值永远是 0。4.2 必调参数表描边颜色、粗细、阈值与放行遮挡描边效果最常调的就是一张参数表。颜色、宽度、阈值这三个是刚需脉冲速度和遮挡放行是进阶项。下面这张表是我在工程里验证过的默认范围直接抄过去再按项目微调即可。参数类型推荐值/范围说明HighlightColorLinearColor0.05.0超过 1.0 的值在 Tonemapping 后会更亮避免发灰EdgeThicknessScalar1.04.0单位为像素乘上单个像素尺寸得到 UV 偏移量EdgeThresholdScalar0.010.1模板差值的判定阈值太大会吞掉细小边缘MatchValueScalar1与 SetCustomDepthStencilValue 写入的 ID 保持一致PulseSpeedScalar0关4呼吸动画速度0 表示静态高亮bShowWhenOccludedBoolfalse控制被遮挡时是否显示描边开启需额外处理深度EdgeThickness 在 1080p 下建议从 2 起步4K 下要适当加大否则同样像素数的描边在高分辨率屏幕上会显得很细。EdgeThreshold 默认给 0.02如果发现远处模型描边断断续续先把阈值往 0.01 降如果出现整个选中物体边缘都被描粗一圈说明阈值太大把 0.05 以上的值往回调。4.3 UE5 Nanite 网格与半透明物体的两个特殊处理UE5 用上 Nanite 后常规描边方案里有一个针对性极强的坑Nanite 网格默认不写入 Custom Depth 和 Custom Stencil 通道。你在普通静态网格上调好的描边套到 Nanite 网格上会直接失效选了半天什么都没画出来。这是因为 Nanite 有自己的渲染路径不走传统 Base Pass 之后的深度模板流程。不同 UE5 小版本对 Nanite 接入 Custom Depth/Stencil 的支持程度不一样有些版本需要在项目设置里打开 Nanite 相关选项下的自定义深度/模板支持开关有些版本干脆还没开放。如果你的 UE5 版本里 Nanite 网格确实写不进模板常见做法是给该网格关闭 Nanite 渲染让它退回传统网格管线代价是损失一部分 Nanite 的 LOD 和裁剪优化另一个做法是额外放一个可选中用的代理网格描边画在代理体上视觉上把描边位置和原模型对齐。前者适合高模但场景数量不多的项目后者适合 Nanite 大场景里少数几个需要交互的物件。半透明物体是另一类特例。Custom Depth 和 Stencil 只在不透明渲染阶段写入半透明材质不会进入这个 Pass所以带 Alpha 的玻璃、水、粒子效果没法用这套方案描边。如果业务上必须高亮半透明物体不要在后处理里苦等模板值直接给该物体套一个顶点膨胀描边壳或者用屏幕空间的深度差补一个虚拟边缘都比改材质 BlendMode 靠谱。5. 避坑描边断线、UI 穿透、Nanite 失效与帧率骤降的排查记录以下五条是我在几个 UE 落地项目里反复遇到的组合坑大多数情况下它们不会同时出现但每一条都足以让描边功能在验收前翻车。按“现象 → 原因 → 解决”的排查顺序整理遇到问题直接对号入座。5.1 描边断线或粗细不均先查 UV 偏移用了像素还是纹素现象是选中一个球体或圆柱描边在侧面转角处断成一截一截远处看更明显近处看粗细还不一样。原因基本出在偏移量上如果第二个 SceneTexture 节点的 UV 偏移用的是固定值比如 0.001它在 1080p 和 4K 下覆盖的像素数就完全不同相机离物体越远物体在屏幕上的纹素密度越低固定 UV 偏移会跨过多个像素甚至跨过整个边缘区域模板值跳变被跳过自然断线。解决方法是把偏移量改成“单个像素尺寸 × EdgeThickness”。单个像素尺寸就是 1 除以视口的分辨率后处理材质里用视口尺寸相关属性计算或者先在调试阶段用 1/1080 这样的近似值把效果调通再换成正式计算。另外把偏移方向从 4 邻域扩到 8 邻域也能明显改善断线多采样左上、右上、左下、右下四个对角点边缘连续性会好很多。5.2 点到 UI 上却选中了后面的模型输入阶段的命中判定顺序现象是界面上的按钮、列表、地图还在鼠标下面点击下去场景里的模型却被选中了描边跟着亮起。原因是射线检测只看世界几何UMG 的控件属于 Slate 层根本不在碰撞系统里射线会直接穿透 UI 命中背后的物体。这不是引擎 bug而是你把 UI 判定漏掉了。解决思路是在触发 TrySelectActor 之前先判断鼠标是否悬停在可交互 UI 上。最简单可靠的做法是在 UI 单例里维护一个“当前鼠标下是否有可交互控件”的状态输入事件里先读这个状态再决定要不要发射线。也可以在 PlayerController 里遍历自己管理的 UserWidget用控件的 IsHovered() 判断鼠标位置。注意 UI 根节点如果设成 Hit Test Invisible子控件也不会响应鼠标要先检查 UI 根节点的命中测试设置。5.3 Nanite 网格不描边版本开关与回退方案现象是部分模型描边正常部分模型无论怎么点都不亮而这些不亮的模型在资产详情里使用的是 Nanite 渲染。原因是 Nanite 网格没有进入传统 Custom Depth/Stencil Pass模板值压根没写进去。这个坑在从 UE4 工程迁移到 UE5 时最容易爆发旧工程里所有网格都是传统渲染切到 UE5 后美术顺手勾了 Nanite描边立刻消失一大片。解决方法是先确认你的 UE5 版本是否支持 Nanite 写自定义深度模板。支持的版本在项目设置里打开对应开关让 Nanite 网格走一遍自定义深度 Pass不支持的版本就只能放弃 Nanite 或做代理网格。排查时可以打开视口的 Buffer Visualization切到 Custom Stencil 视图如果被点中的 Nanite 网格区域在视图里是纯黑说明模板写入环节就没通。5.4 描边发灰发暗后处理位置与线性空间问题现象是描边颜色明明设成了纯白渲染出来却是灰蒙蒙的改成亮黄也像蒙了一层雾。原因大概率是后处理材质跑在 After Tonemapping 阶段。Tonemapping 会把线性空间的 HDR 值压缩到 01 的 LDR 范围你在材质里输入的 1.0 线性白经过压缩后输出到屏幕只剩 0.7 左右看起来就是灰色。这是颜色管理问题跟描边算法本身无关。解决方法是把 Blendable Location 改成 Before Tonemapping或者在 After Tonemapping 下把颜色值直接当成 sRGB 用。Before Tonemapping 更符合渲染管线习惯但颜色值要放大想要视觉上的纯白线性色给到 35 才够否则又被压回灰。如果你需要描边参与 Bloom 或者被镜头光晕拾取就留在 Before Tonemapping如果只是要一个所见即所得的 UI 风格高亮After Tonemapping 反而更直白。5.5 开完模板后帧率掉一半把 Custom Depth 开成动态开关现象是启用描边后GPU 耗时直接翻倍场景复杂度越高掉得越狠。原因是 Custom Depth-Stencil Pass 本质上把开启了写入的物体全部再渲染一遍哪怕物体被完全遮挡也会参与深度测试。很多人图省事在 BeginPlay 里把场景所有可交互物体的 bRenderCustomDepth 都设成 true等于把整个场景塞进了额外 Pass。解决方法是把模板写入做成动态开关默认所有物体关闭只有射线命中的候选物体开启取消选中立刻关闭。配合视锥裁剪或者距离裁剪把可选中范围限制在相机附近的一定距离内性能开销能压到可以忽略。这里没有银弹Custom Depth 的数量直接决定额外 Pass 的负载多一个可写物体就是多一份 Draw Call预算不够时就该考虑把描边改成屏幕空间边缘检测而不是继续堆模板物体。6. 进阶Stencil 值当选中 ID 用以及一个高效的验证视图6.1 把 Stencil 值映射成颜色做多选和分级高亮模板值只存一个“1”太浪费了。给每类物体分配不同 ID后处理材质里就能按 ID 输出不同颜色。做法是在材质里加一个 MatchValue 参数用 If 节点比较当前像素的模板值和 MatchValue相等才输出对应的 HighlightColor。多组 ID 就用多个 If 节点串联业务层在命中时按物体类型写入 1、2、3渲染层再把这些 ID 翻译成蓝、红、黄。实际项目里 4 组以内最划算超过 4 组建议写 Custom 节点用 if-else 链节点图会膨胀到很难维护。6.2 让描边“呼吸”时间节点驱动透明度静态高亮在交互里太生硬脉冲是点选确认最常见的反馈。在后处理材质里加一个 Time 节点输出接 Sine速度用乘法定标最后把正弦值映射到 0.41.0 区间再乘到描边色的亮度上。注意别直接拿 01 的正弦去乘颜色那会让描边周期性变黑正确写法是 0.6 0.4 × Sin亮度始终维持在一个可识别的下限之上。脉冲速度建议控制在每秒 12 个周期太快像故障太慢用户感知不到。6.3 验证技巧用 Buffer Visualization 检查模板通道调试描边时最容易被颜色误导后处理里调了半天的颜色结果问题根本不在颜色而是模板值没写对。我的习惯是先看模板通道再调后处理。编辑器视口左上角把 Lit 切换成 Buffer Visualization找到 Custom Stencil 或 Custom Depth 视图。被标记的物体在这个视图里应该显示成纯色块背景是黑色。如果点中物体后这个视图里什么都没有问题一定出在写入侧跟后处理材质无关省掉一半的玄学排错时间。先确认模板写入正确再切回 Lit 检查描边颜色和粗细最后调脉冲参数。这套顺序帮我避免过无数次在错误图层上反复试参数的无效劳动。希望帮到你。本文还有配套的精品资源点击获取