
简介面向UE4/UE5开发者提供模型鼠标点击选中后边缘高亮的完整UE5工程。工程基于第一人称模板内含高亮材质、射线碰撞检测蓝图以及选中状态管理逻辑。包内共485个文件以401个uasset资源、10个umap关卡文件为主配合ini配置与log日志等压缩包大小约673.69MB可视为可直接打开运行的完整项目。已有4749人学习/下载适合希望快速实现交互高亮效果并理解其实现思路的中级开发者。通过该工程可学习并复用材质发光参数控制、射线检测、Actor接口调用等关键技巧也可在原基础上扩展动画效果或多选模式。1. 先从点击选中的刚需讲起为什么这套描边方案在 UE5 工程里最好改做 UE5 工程时被问得最多的一类需求就是“模型鼠标点击选中边缘高亮”。数字孪生、展厅交互、关卡编辑器工具甚至简单的产品选型演示都逃不开这一步鼠标点到模型模型轮廓亮一圈再点空白处取消。这背后的实现路径其实非常收敛——Custom Depth / Stencil 通道加后处理描边材质是 UE5 里最稳、最通用的做法UE4 从 4.26 之后也基本通用。这个方案的好处是不需要给每个模型单独改材质不破坏原有贴图和光照高亮样式统一由后期材质控制换颜色、调宽度都在一个资产里完成。本文适合两类读者一类是刚接触 UE5 交互开发想给现有场景快速加选中高亮另一类是已经把射线点击跑通但描边效果总是不对、想找参数和坑位的人。整套东西拆开看只有三块鼠标点击命中、Stencil 标记、后处理描边下面按落地顺序逐个讲。2. 点击选中这块地基射线命中与组件封装2.1 鼠标位置转 World 射线两种写法与坐标换算UE5 里把鼠标点击变成一条世界射线最常用的入口是 PlayerController 的DeprojectMousePositionToWorld。它做的事就是把屏幕坐标反投影到世界空间得到一条从相机出发的射线然后交给碰撞检测。下面这段是我在工程里常用的写法// 在 APlayerController 子类或自定义交互组件中 bool UInteractionComponent::TraceFromMouse(FHitResult OutHit) { APlayerController* PC GetPlayerController(); if (!PC) return false; FVector WorldOrigin; FVector WorldDirection; if (!PC-DeprojectMousePositionToWorld(WorldOrigin, WorldDirection)) return false; FCollisionQueryParams Params; Params.bTraceComplex false; // 用简化碰撞体便宜且够用 Params.AddIgnoredActor(GetOwner()); // 忽略自己避免射线被自身挡住 return GetWorld()-LineTraceSingleByChannel( OutHit, WorldOrigin, WorldOrigin WorldDirection * HitDistance, ECC_Visibility, Params ); }这段代码的逻辑是先从鼠标位置生成射线起点和方向再用LineTraceSingleByChannel做单次命中检测。通道用ECC_Visibility是经验之谈因为大多数场景里可见的静态网格和动态 Actor 默认都在 Visibility 通道有碰撞射线不容易穿模如果项目里自己建了交互通道也可以把这里的通道换成项目自定义的 Trace 通道设置方法后面避坑章节会说到。要注意的是DeprojectMousePositionToWorld依赖“Enable Input”已经生效。在纯 C 工程里如果 PlayerController 没有启用输入鼠标位置拿不到在蓝图里就是Get Hit Result Under Cursor节点本质也是走这条路径。另一个更容易忽略的点是 UI 遮挡当鼠标停在 Slate 或 UMG 控件上时射线仍然会穿到模型所以需要在调用前先判断当前鼠标是否在 UI 上常见做法是FSlateApplication::Get().GetMouseCaptor或者直接检测IsMouseOverAnyWidget这一点在坑位章节里专门展开。2.2 从命中结果里拿到模型与网格体Component 才是操作对象很多新手拿到FHitResult后第一个反应是拿GetActor()但真正需要做 Stencil 标记的是网格组件也就是UPrimitiveComponent及其子类。静态网格、骨骼网格、程序化网格甚至 Landscap 的每个 Section在底层都是UPrimitiveComponent的实例。所以在封装里我会同时保存命中的 Actor 和 Component描边操作只认 Componentvoid UInteractionComponent::HandleHit(const FHitResult Hit) { UPrimitiveComponent* Comp Hit.Component.Get(); if (!Comp) return; // 如果是同一个组件视为重复点击走取消逻辑 if (Comp CurrentSelectedComponent) { ClearSelection(); return; } // 先把上一次选中的清掉再标记新的 if (CurrentSelectedComponent) { CurrentSelectedComponent-SetRenderCustomDepth(false); CurrentSelectedComponent-SetCustomDepthStencilValue(0); } CurrentSelectedComponent Comp; Comp-SetRenderCustomDepth(true); Comp-SetCustomDepthStencilValue(StencilMaskValue); }这里把“选中状态”完全挂在UPrimitiveComponent上好处是无论场景里挂的是 StaticMesh、SkeletalMesh 还是 InstancedMesh接口都一致不用为每种模型单独写分支。需要额外处理的只有两种情况一是UInstancedStaticMeshComponent它的每个实例有不同的 Transform但 Stencil 是组件级的想做单实例高亮需要额外用 Custom Primitive Data 或拆组件二是带透明材质的模型半透明物体在 Stencil 通道里的表现比较特殊第 5 章会说。2.3 把选中逻辑封装成一个组件单例与多选模式都要考虑实际工程里我不建议直接在 Character 或 Pawn 里写这些逻辑而是封装成一个 ActorComponent 或一个独立的交互管理器。这样关卡里的任何 Pawn 都可以复用换项目时拖着组件走就行。上面两段代码已经基本构成一个最小组件但还需要考虑两种交互模式单选模式和多选模式。单选模式下每次点击先清空上次选中多选模式下把新命中的组件加入一个数组Stencil 值按分组叠加。下面是一个简化的多选实现void UInteractionComponent::ToggleSelect(UPrimitiveComponent* Comp) { if (SelectedComponents.Contains(Comp)) { // 取消选中减去这一组 Stencil 位而不是直接清零 int32 CurrentStencil Comp-GetCustomDepthStencilValue(); Comp-SetCustomDepthStencilValue(CurrentStencil ~StencilMaskValue); if (CurrentStencil StencilMaskValue) Comp-SetRenderCustomDepth(false); SelectedComponents.Remove(Comp); return; } // 新选中把当前组件已有的 Stencil 值叠加新标记 Comp-SetRenderCustomDepth(true); int32 NewStencil Comp-GetCustomDepthStencilValue() | StencilMaskValue; Comp-SetCustomDepthStencilValue(NewStencil); SelectedComponents.Add(Comp); }代码里的~StencilMaskValue是位运算的按位取反用意是只清除当前选中组对应的 Stencil 位保留其他系统可能写入的标记。直接赋 0 很危险因为场景里可能有别的系统已经在用 Stencil 做效果整体清零会造成未知冲突。这个习惯从 UE4 时代一直延续下来在 UE5 工程里更要注意因为 Lumen、Nanite 这些新特性对渲染通道的管理更敏感。蓝图侧的使用方式也很直接把组件挂到 PlayerController 上在Left Mouse Button事件里调用一个自定义事件内部走Get Hit Result Under Cursor然后把 Hit 里的 Component 传给组件。不想写 C 的团队可以把上面逻辑完全用蓝图节点复刻一遍唯一要小心的就是蓝图里对UPrimitiveComponent的 CastMesh 组件类型不对时容易 Cast 失败。3. 让模型真正亮起来Custom Depth / Stencil 标记与描边材质3.1 为什么不用改模型材质也能画一圈边两种描边路线对比UE5 里做选中高亮的路线大致分两种。第一种是给每个模型单独做一套“选中材质”在材质里用 Fresnel 节点做边缘光或者用顶点膨胀法线做轮廓线。这种方案画面效果直接但维护成本很高场景里几十上百个模型每个都要预留一套材质开关换贴图、换 PBR 参数时都要同步改而且 Fresnel 做出来的“边缘”在相机角度变化时粗细不一致。第二种是本文采用的方案——模型本身材质不变只在渲染管线里多写一个 Stencil 标记由后处理材质统一把标记区域边缘画出来。两种方案对比如下对比项模型材质内做边缘高亮CustomDepthStencil 后处理描边是否改造原材质必须额外加材质层完全不用动边缘粗细一致性随视角和模型曲率变化由后处理参数固定控制支持多模型分组需要每个模型区分做法用 Stencil 值天然分组性能代价选中物体多渲染一遍多一个后处理 Pass模型额外跑 CustomDepth工程侵入高美术资产耦合重低纯逻辑控制实际项目里两者不冲突有些团队用模型材质内边缘光做“悬停预览”用后处理描边做“选中确认”两层效果叠加。但如果只做一个方案优先做后处理描边理由很简单它是全局的不是针对单个资产的改需求时只改一个后处理材质。3.2 先把 Custom Depth-Stencil Pass 打开再给模型写标记值后处理描边的前提是场景里有一层“额外深度模板信息”能被后处理材质读取。UE5 里这对应一个项目设置项Project Settings → Rendering → Post Processing → Custom Depth-Stencil Pass选项要设为Enabled with Stencil。只有深度却没有模板值时做不了按模型分组所以必须选带 Stencil 的版本。这一步漏掉的话后处理材质里读到的 CustomStencil 永远是 0后面一切节点连接都白搭。然后是给模型写标记。UE5 里每个UPrimitiveComponent都有两个相关属性Render CustomDepth Pass表示这个模型要不要额外渲染进 CustomDepth 缓冲区Custom Depth Stencil Value表示写入的具体模板值。两者都要设置缺一不可。在蓝图上对应Set Render Custom Depth和Set Custom Depth Stencil Value两个节点C 侧就是第 2 章代码里的SetRenderCustomDepth与SetCustomDepthStencilValue。这里有个细节值得单独说Stencil 值取值范围是 0~255但不要随手填 255。因为后处理描边时常用位运算做分组255 占满了所有二进制位后续叠加状态就不好做。我一般预留 250 以上给纯选中效果项目里如果有 AI 标记、区域提示等其他系统给他们单独分配 1~240 的位段。多组状态叠加时用|按位写入取消时用~清除这样同一模型同时被选中和受 AI 标记时两种描边可以同时存在互不覆盖。3.3 后处理描边材质核心节点怎么连后处理材质是整个方案的灵魂。它的逻辑一句话概括读取每个像素的 CustomStencil 值如果当前像素没有标记但周围像素有标记就判定为边缘用高亮颜色覆盖原画面。判断“周围像素”需要偏移采样这里用材质编辑器原生节点就能实现不需要写 HLSL节点连线如下SceneTexture: CustomStencil → A当前像素模板值 SceneTexture: CustomStencil 偏移(1,0) → B1 SceneTexture: CustomStencil 偏移(-1,0) → B2 SceneTexture: CustomStencil 偏移(0,1) → B3 SceneTexture: CustomStencil 偏移(0,-1) → B4 SceneTexture: CustomStencil 偏移(1,1) → B5 SceneTexture: CustomStencil 偏移(-1,-1) → B6 SceneTexture: CustomStencil 偏移(1,-1) → B7 SceneTexture: CustomStencil 偏移(-1,1) → B8 Max(Max(B1..B8), A) → 周围最大模板值 (周围最大模板值 - A) → 边缘标记信号 Step(0.01, 边缘标记信号) → 边缘 Mask Lerp(PostProcessInput0, 高亮颜色, 边缘 Mask) → 最终输出偏移量不是直接写死坐标而是用 SceneTexture 节点自带的 TexelSize 输出把偏移坐标和 TexelSize 相乘。TexelSize表示当前纹理一个像素占的 UV 大小这样无论最终渲染分辨率怎么变偏移量都对应当前屏幕像素不会出现 4K 和 1080P 下描边粗细不一致的问题。上面连线里SceneTexture: CustomStencil读到的值是 0 或 Stencil 标记值比如 250是一个很大的整数。为什么要用 Max 减当前值而不是直接用 If 判断相等因为 250 在 Step 函数里会被归一化成接近 1 的数而 0 保持 0用 Max 减法能在不做等于判断的情况下捕获“周围有标记但当前没有”的边界像素语义更清晰节点数也更少。实际调时如果把 250 改成 2阈值 Step 也要跟着调这就是为什么推荐 Stencil 值用高位的大数。材质本身的 Blendable Location 放在After Tonemapping还是Before Tonemapping会影响颜色表现。我一般放在After Tonemapping因为这里的 PostProcessInput0 已经是 tonemapping 后的画面高亮颜色是“所见即所得”不会因为色调映射产生色偏代价是高亮色不会参与 Bloom想要发光感的话需要额外加 Bloom。如果放在 Before Tonemapping高亮颜色会被映射一次颜色会偏淡偏灰调试时不好预估。选哪一种没有绝对对错但同一个工程里最好固定一种并把它写进项目规范。4. 让描边从“能亮”到“好看”四个必调参数与分组策略4.1 后处理描边参数表宽度、颜色、阈值、 Mask 的配合描边材质做出来后真正决定效果的是几个参数的配合。我把常用参数整理成一张表在工程里作为默认配置新项目直接复制参数推荐值作用调节注意边缘偏移半径1~3 像素边缘厚度半径越大描边越宽但超过 3 像素锯齿感明显需要配合模糊Step 阈值0.01边缘判定灵敏度抬高到 0.1 会让细碎噪点消失但细模型边缘会断边缘 Mask 模糊1.0边缘平滑度用 Gaussian 模糊一次再乘颜色能消除像素颗粒感高亮颜色自定义描边颜色建议用 2~4 倍亮度值tonemapping 后视觉更明显Stencil Mask 值250分组标记各分组按二进制位分配不要直接用 255这里最容易翻车的组合是“大半径 高阈值”。半径拉到 3 像素后模型边缘往往有 2~3 个像素的过渡带阈值太高会把过渡带砍掉一半描边看起来一边宽一边窄。我通常的做法是先用半径 1 保证基线效果再在材质里加一个 1 像素的高斯模糊最后用 Threshold 收边缘毛刺。如果还有锯齿不要继续加半径改成在后处理链路上再接一个 Blur 节点效果差别很大。4.2 多模型选中与取消选中时Stencil 值怎么交接单模型高亮是最简单的场景但多数实际需求是“允许点选多个模型”或者“一个模型同时有选中态和警告态”。这时候 Stencil 不能当成单一 0/1 开关用要当成一个位掩码表。项目里可以维护一张分组表比如Bit 0 (值 1) → 保留 Bit 1 (值 2) → 悬停预览 Bit 2 (值 4) → 选中态 Bit 3 (值 8) → 警告态 Bit 4~7 → 业务自定义写入时用或运算叠加选中一个模型就在原 Stencil 值上| 4取消选中时 ~4。后处理材质里要区分“边缘来源”时把读取到的 Stencil 值和每个 bit 做 AND 判断分别输出不同颜色。这样同一模型选中又警告时描边可以不同侧不同色或者叠加第二种描边样式。这种做法在纯蓝图工程里也能做节点多几个逻辑不复杂。4.3 边缘发虚和像素颗粒后处理链路顺序与颜色空间描边最丑的两个毛病是“发虚”和“颗粒感”。发虚的原因通常是边缘 Mask 数值太软比如 Step 阈值给的是 0.5 而不是 0.01边缘区域被线性插值成了半透明过渡带颗粒感则多发生在暗色场景因为后处理材质的计算精度在 tonemapping 前是 16 位浮点暗部噪声被描边放大后特别明显。我一般把描边材质放在After Tonemapping此时画面是 8 位整数范围颗粒感会轻很多代价是边缘没法再被 Bloom 照亮发光效果弱一截。这个取舍没有标准答案展厅项目我选 After Tonemapping追求“亮边光晕”视觉的选 Before Tonemapping两种都试一遍再定。5. 高亮选中避坑五个反复踩到的常见问题与排查思路5.1 选中后模型整体变黑或完全看不到Custom Depth Pass 没开现象点击模型后模型变成黑色剪影或者边缘高亮完全没出现但点击逻辑本身是通的。原因项目设置里的 Custom Depth-Stencil Pass 处于 Disabled模型虽然执行了SetRenderCustomDepth(true)但没有写入任何通道数据后处理材质读到的 Stencil 全是 0描边信号算不出来。变黑的情况是后处理材质把“无标记像素”错误输出成了黑色说明材质逻辑也有问题。解决先在 Project Settings → Rendering → Post Processing 里确认 Custom Depth-Stencil Pass 是Enabled with Stencil不是Enabled without Stencil然后在材质编辑器里临时输出一个纯SceneTexture: CustomStencil节点用来调试。如果输出画面能看到模型区域是白色、其他区域是黑色说明数据和后处理材质都正常问题在描边判定逻辑如果全黑说明数据通道根本没写入。5.2 鼠标点击穿透模型碰撞响应通道不匹配现象地面、墙壁都能正常点击但某些模型点不中或者模型能点中但射线会穿透玻璃、粒子等特殊物体。原因LineTraceSingleByChannel用的是 Visibility 通道而有些模型的碰撞预设没有勾选 Visibility 响应。常见于美术从外部导入的模型碰撞预设往往是BlockAll或者自定义的WorldStaticVisibility 通道是 Ignore射线直接穿过去了。解决两个方向。第一个方向是检查模型碰撞预设把 Visibility 响应改为 Block这是最简单的做法第二个方向是改用 Object Type 射线遍历WorldDynamic和WorldStatic两类对象。我不建议把所有物体都改成 Block Visibility因为项目里总有不想被点击挡住的透明装饰物更合理的做法是把射线通道统一到项目自定义的Interaction通道在碰撞预设里按需勾选这样和 UI 点击检测互不干扰。5.3 被 UMG 界面遮挡时点击穿透到模型现象鼠标停在按钮、面板上时点击背后的模型也被选中了。原因射线检测只做了物理碰撞不会自动感知 Slate/UMG 的控件命中。Get Hit Result Under Cursor底层只做几何射线控件层接收鼠标事件后这个射线仍然会打到模型上。解决在射线检测前加一个 UI 命中判断。蓝图里可以在 PlayerController 的事件分发处先调用Get Mouse Position和Get Widget Under Mouse如果在控件上直接返回不执行射线。C 里用FSlateApplication::Get().GetWidgetUnderMouse()判断控件类型如果是 UMG 里的 Panel 或交互控件就跳过。这个坑只影响带 UI 的工程但十个项目里有八个带 UI所以每次新工程我都会第一时间补上。5.4 描边只在高亮模型内部出现外部轮廓不完整半透明材质写入 Stencil 的问题现象用半透明材质做的模型玻璃、显示屏、粒子面片选中后描边出现但只显示在模型内部外部轮廓断断续续甚至完全看不到。原因半透明物体在渲染时不会写入 Custom Depth Stencil或者说需要额外的bAllowCustomDepthStencil开关与半透明渲染兼容设置。UE5 里半透明模型的 Stencil 默认是无效的后处理描边做边缘判定时半透明区域像素贡献的 Stencil 为零边缘信号算不出正确结果。解决给半透明材质开启Render CustomDepth Pass的组件设置并把材质的Translucency Pass调整为After DOF然后测试半透明物体在 CustomDepth 缓冲区的表现是否正常。如果试完仍然不稳定会在工程规范里把“选中高亮能力”限制在不透明模型上半透明物体用另一种高亮方式比如模型中心点发一个点光源。这样最省心。5.5 场景里同时选中十几个模型后帧率掉一半CustomDepth 渲染开销现象小场景没事一旦多选模型数量上来帧率明显下滑尤其是场景里模型面数复杂时。原因每个勾选Render CustomDepth Pass的组件都会在 CustomDepth 阶段被重新渲染一次等于多了一整个渲染 Pass。如果所有模型常驻勾选Draw Call 数量直接翻倍后面还有后处理材质的全屏采样开销。解决严格做到“选中才开取消就关”。这已经写入第 2 章的组件逻辑里选中时SetRenderCustomDepth(true)取消时无论SetCustomDepthStencilValue(0)还是清除位掩码都要同步调用SetRenderCustomDepth(false)。另外不要把所有场景模型都设成可选中给可交互模型单独加一个 Tag在射线命中后先判断 Tag 再决定是否开启描边可以省下大量无效的 CustomDepth 渲染。6. 交付前验证与手感优化一个测试关卡和三个细节6.1 用一个小场景快速验证描边效果是否达标新工程里我一般会搭一个专门测高亮的“样板关卡”里面放四组测试物一个高面数雕像、一个低面数盒子、一个半透明玻璃球、一个多材质组合的汽车模型。点击每个物体检查四件事描边是否完整、内外轮廓是否一致、颜色是否被 tonemapping 改变、取消选中是否彻底干净。这个关卡不建议塞太多装饰物否则画面里其他光效容易干扰判断。测完后把材质参数记录成一份默认值后续项目直接引用省去反复调颜色的时间。6.2 让选中高亮交互更顺手的三个细节第一个细节是悬停和点击分开。悬停预览用淡蓝色、点击选中用亮橙色两者用不同的 Stencil bit 位写入交互层次清晰。很多项目只做点击高亮鼠标划过时没有任何反馈操作感很生硬。第二个细节是重复点击同一模型要能取消选中。这在第 2 章代码里已经做了处理但蓝图工程里很容易漏掉因为蓝图的Get Hit Result Under Cursor每次都是独立事件流忘了保存上一次命中的组件就没有取消逻辑。第三个细节是描边状态跟随相机可见性。模型被遮挡时不显示描边、镜头转过来再出现这是通过 CustomDepth 本身对深度关系的处理但在项目里要确认后处理材质里用的是 Stencil 计算出的边缘而不是单纯在模型像素上叠加颜色否则遮挡部分会被“看穿”。这个差别在移动端上特别明显我自己的习惯是写好描边材质后专门用一个遮挡场景验收一次确认没有穿帮再交付。这整套方案从射线检测、组件封装到后处理描边参数我已经在同一类 UE5 工程上验证过多次踩过的坑基本都收敛在这篇文章里。新项目再接手类似需求时我会先打开项目设置确认 Custom Depth-Stencil Pass再把描边材质烘焙成一个公共资产剩下的只是参数微调。希望帮到你。本文还有配套的精品资源点击获取