
简介面向 UE4/UE5 开发者的模型拾取交互 Demo解决鼠标点击选中模型时边缘高亮显示的交互问题适用于第一人称射击、虚拟展厅等需要物体拾取反馈的场景。压缩包内含完整的 UE5 工程共 485 个文件大小约 673.69MB其中以 401 个 uasset 资产文件材质、蓝图等、10 个 umap 关卡文件及 uproject 工程文件为主体另含引擎配置与调试日志便于排查运行环境问题。已有 4752 人学习下载。工程内置高亮材质与射线碰撞检测蓝图通过自定义接口统一控制模型选中与高亮切换各可选模型均可通过该接口开启或关闭高亮便于扩展多选与状态恢复逻辑。可直接整合进新建或现有 UE5/UE4 项目快速实现点击选中、边缘高亮与状态恢复的完整交互链路适合需要落地交互高亮功能的初中级开发者。1. 为什么UE5工程的鼠标点选高亮不是换个材质那么简单UE5工程里做“模型鼠标点击选中边缘高亮”很多新手第一反应是给模型换一个带菲涅尔边缘的材质或者干脆叠加一个半透明发光层。但一旦场景里有几十个Actor、有人点空白处、有Nanite高模、开了TSR抗锯齿这套方案立刻翻车边缘参差不齐、高亮盖不住模型、换材质瞬间闪一下。真正可靠的落地做法是把“点选”和“描边”拆成两件事物理射线命中负责选后处理描边负责亮。标题里特意写“UE5工程”说明不是拖一个节点就行而是要从项目设置、材质管线、碰撞通道、渲染开关一路打通。本文这套方案就是按工程标准做的读者按步骤能自己在UE5工程里复现出来。2. 点选命中与高亮方案选型先分清“选中谁”和“怎么亮”2.1 鼠标射线命中从屏幕坐标打出一条可见性检测线点击选中这件事UE里最常见的做法是用PlayerController的射线检测。UE5工程里我一般直接用GetHitResultUnderCursorByChannel传ECC_Visibility通道。这个通道的好处是它只关心“能不能被看到”不会把场景里那些只参与物理碰撞、但视觉上不该被拾取的碰撞体误选进来。下面是C版的最小实现蓝图的节点Get Hit Result Under Cursor by Channel逻辑完全相同// AMyPlayerController.h DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnActorSelected, AActor*, SelectedActor); // AMyPlayerController.cpp绑定到鼠标左键输入事件 void AMyPlayerController::OnLeftClick() { FHitResult Hit; // bTraceComplex false走简单碰撞拾取开销小 if (GetHitResultUnderCursorByChannel(ECC_Visibility, false, Hit)) { AActor* HitActor Hit.GetActor(); if (HitActor HitActor-ImplementsUHighlightInterface()) { // 通过接口通知目标Actor“你被选中了” IHighlightInterface::Execute_SetHighlighted(HitActor, true); } else { // 点击非高亮对象或空白处清空上一次选中 ClearCurrentHighlight(); } } }这段代码里有三个参数值得解释。bTraceComplex false表示用简单碰撞通常是一个凸包近似拾取高模时不会因为碰撞面太多而卡顿代价是精细凹槽处可能点不中比如一个有很多洞的雕塑点中洞边缘时射线会打到洞背后的表面。ECC_Visibility通道默认会忽略掉bHiddenInGame的物体UI界面、布告板这类也应该从该通道排除。接口UHighlightInterface不是引擎内置的是工程里自己定义的一个纯虚接口这样鼠标逻辑不关心具体是高亮组件还是高亮Actor解耦更干净。2.2 三种边缘高亮方案对比菲涅尔、后处理深度边缘、Custom Stencil做完拾取真正的重点在“边缘高亮怎么画”。UE工程里能见到的方案大致三类但适用场景差很多。我整理了一张对比表读者可以直接按自己的项目阶段选方案实现成本描边效果Nanite多对象区分性能菲涅尔自发光材质最低改材质只亮内部边缘外轮廓不明显支持不可区分全都一个样式无额外开销后处理Sobel深度检测中一个后处理材质全场景边缘需加遮罩支持需配合Stencil才能区分全屏采样较高Custom Stencil 后处理描边较高需开项目Stencil精准轮廓颜色可定制UE5.1部分版本支持可按Stencil ID区分阵营选中对象才写入后处理一次菲涅尔方案适合原型Demo、临时给策划看效果。它的本质是让材质边缘处自发光变强模型内部也会跟着有一圈亮边视觉上更像“LED灯条贴在模型表面”而不是清晰的轮廓线。后处理深度检测方案是在全屏场景深度图上做边缘提取问题是场景里所有物体都有边缘必须额外像素来判断“这条边是不是选中对象的边”所以最终还是要回到Stencil。2.3 为什么Custom Stencil方案在UE5工程里更稳Custom Stencil自定义深度模具缓冲方案的思路很直接给选中的模型写入一个模板值然后在一个全屏后处理材质里读取这个模板值配合场景深度做边缘检测只对被选中的模型描边。它稳在三点不修改原材质模型原来的贴图、法线、粗糙度完全不动支持同时选中多个对象模板值可以区分阵营或类型后处理描边是屏幕空间的不受模型面数影响哪怕模型有几十万面描边粗细看起来也是均匀的。UE5工程里开启自定义深度只需要在项目设置中做一次之后所有Mesh都多了一个“允许写入模板”的开关运行时再按需打开不选中的对象不写入性能损失很小。这也是目前运行时交互选中效果的主流做法编辑器自带的选中描边本质上也走类似机制。3. 从点击到高亮UE5工程里的组件设计与项目设置3.1 一个可复用的Actor高亮组件HighlightComponent我不想把高亮逻辑写在每个Actor的蓝图里那样后期维护成本太高。常见做法是写一个UActorHighlightComponent挂在需要被选中的Actor上它负责遍历当前Actor上所有可见Mesh打开它们的自定义深度写入并设置Stencil值。这样鼠标点击只关心“告诉组件你被选中了”组件内部管理网格体职责清晰。// UActorHighlightComponent.h UCLASS(Blueprintable, meta (BlueprintSpawnableComponent)) class HIGHLIGHTMODULE_API UActorHighlightComponent : public UActorComponent { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category Highlight) void SetHighlighted(bool bOn, int32 StencilValue 1); UPROPERTY(EditAnywhere, Category Highlight) int32 DefaultStencilValue 1; UPROPERTY(EditAnywhere, Category Highlight) bool bHighlightByDefault false; }; // UActorHighlightComponent.cpp void UActorHighlightComponent::SetHighlighted(bool bOn, int32 StencilValue) { // 遍历Owner身上所有UMeshComponent子类包括StaticMesh和SkeletalMesh TArrayUActorComponent* MeshComps; GetOwner()-GetComponents(UMeshComponent::StaticClass(), MeshComps, true); for (UActorComponent* Comp : MeshComps) { UMeshComponent* Mesh CastUMeshComponent(Comp); if (Mesh Mesh-IsVisible()) { Mesh-SetRenderCustomDepth(bOn); Mesh-SetCustomDepthStencilValue(StencilValue); } } }这段代码有两个容易被忽略的细节。GetComponents的第三个参数设为true表示包含子Actor上的组件如果模型是由很多子网格体拼装的比如一个机器人由头、身、手、脚多个StaticMesh组合这个参数决定描边能不能覆盖全身。另一个是IsVisible()判断被隐藏的武器、隐身道具不应该被描边否则视觉上会穿帮。别忘了在组件的OnRegister里读入bHighlightByDefault这样编辑器里预先勾选的Actor进游戏就直接高亮省去一次点击操作。3.2 点击与高亮的对接蓝图还是C都行但接口要统一组件写好后点击检测那边可以通过两种方式通知它。如果是纯蓝图工程直接在角色控制器的鼠标左键事件里拖Line Trace by Channel然后Cast到HighlightComponent调用Set Highlighted。C工程则建议定义一个接口UHighlightInterface组件实现接口鼠标控制器只管调接口不关心具体实现。这样将来如果不想用后处理描边、想改成缩放弹窗或者飘字提示控制器代码完全不用动。// UHighlightInterface.h UINTERFACE(Blueprintable) class UHighlightInterface : public UInterface { GENERATED_BODY() }; class HIGHLIGHTMODULE_API IHighlightInterface { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category Highlight) void SetHighlighted(bool bOn, int32 StencilValue 1); };接口函数用BlueprintNativeEvent是为了兼容蓝图子类。有的策划会在蓝图里重写这个事件比如在进入高亮时顺带播放一个音效C默认实现已经处理了Mesh蓝图重写后调Parent: SetHighlighted即可。如果读者没有多阵营区分需求StencilValue可以固定为1少暴露一个参数蓝图里更简单。3.3 项目设置开Custom Depth-Stencil Pass并注意Nanite选项这一步是整个方案里最容易忘的而且忘了之后后处理材质怎么调都读不到模板值。打开UE5工程的Project Settings - Engine - Rendering把Custom Depth-Stencil Pass从Disabled改成Enabled with Stencil。注意不要选成Enabled without Stencil那能读深度但读不了模板值我们这个方案的核心目的就没了。之后如果你场景里有Nanite高模还要确认Nanite分段里的Custom Depth相关选项是打开的。UE5不同小版本的入口位置不一样有的版本叫Use Custom Depth/Stencil in Nanite有的直接包含在Nanite全局选项里。我踩过坑UE5.0早期版本对Nanite网格体的自定义深度写入支持不完整描边对Nanite模型无效后来升级到高版本小版本才解决。所以读者如果用的是UE5.0且必须保留Nanite建议先拿一个Nanite模型做测试不行就临时给Nanite网格体开Fallback Mesh渲染或者干脆关闭Nanite的CustomStencil那条分支。项目设置改完要重启编辑器不然材质编译出来可能还是拿不到Stencil数据。3.4 运行时“按需开关”比全局常开更省性能很多团队把工程里所有模型都默认勾上Render CustomDepth省事但DrawCall和GPU写入都会持续产生开销。我的习惯是默认全部关闭选中时才打开取消选中的瞬间关闭。组件的SetHighlighted(false)里要还原成SetRenderCustomDepth(false)不要只改模板值。否则物体已经被取消选中了模板里还残留着旧值万一后处理材质里没做“区域限定”残留的模板会让已经被取消选中的物体依然带一圈边这种诡异问题排查起来非常费时间。4. 边缘高亮材质从模板值到屏幕空间描边4.1 用后处理材质把“模板区域”变成“边缘线”工程里的高亮材质我倾向用Post Process Material实现而不是直接给模型材质加节点。原因很简单模型材质要管贴图、法线、金属度再加描边逻辑会让材质分支变多编译时间变长。后处理材质独立一张不选中任何物体时整张图直接输出SceneColor几乎零开销。做法是新建一个MaterialMaterial Domain设为Post ProcessBlendable Location选Before Tonemapping放在色调映射之前。后处理材质里要输出的是一张带有描边颜色的图。// PostProcessOutline.usf 关键代码放到Material的Custom节点 float2 TexelSize View.ViewSizeInPixels.zw; // 每像素对应的UV大小 float DepthC SceneTextureLookup(UV, SceneTextureId_SceneDepth, false).r; // 对深度做四方向差分检测边缘 float DepthL SceneTextureLookup(UV - float2(TexelSize.x, 0), SceneTextureId_SceneDepth, false).r; float DepthR SceneTextureLookup(UV float2(TexelSize.x, 0), SceneTextureId_SceneDepth, false).r; float DepthU SceneTextureLookup(UV - float2(0, TexelSize.y), SceneTextureId_SceneDepth, false).r; float DepthD SceneTextureLookup(UV float2(0, TexelSize.y), SceneTextureId_SceneDepth, false).r; float Edge 0; Edge abs(DepthC - DepthL) EdgeThreshold ? 1 : 0; Edge abs(DepthC - DepthR) EdgeThreshold ? 1 : 0; Edge abs(DepthC - DepthU) EdgeThreshold ? 1 : 0; // 只对Stencil值等于选中ID的区域应用描边 float StencilC SceneTextureLookup(UV, SceneTextureId_CustomStencil, false).r; if (StencilC ! SelectedStencilValue) { Edge 0; } return float4(OutlineColor, saturate(Edge));这段HLSL写了三层逻辑深度差检测找到屏幕上的所有几何边缘模板值过滤出“我选中的那个物体”最后把边缘颜色叠加到画面。View.ViewSizeInPixels.zw是UE里现成的像素尺寸倒数用它做采样偏移避免UV偏移量在不同分辨率下描边粗细不一样。SelectedStencilValue要在材质里暴露成标量参数这样同一个后处理材质可以被多个选中的对象复用只需要在点击时给组件传入不同的模板值。4.2 描边粗细、颜色、闪烁节奏三个必调参数这个材质有三个参数我几乎每次换项目都要重新调。第一个是EdgeThickness对应代码里的采样偏移倍数如果材质节点里把TexelSize乘以一个标量值越大描边越粗但超过3个像素后边缘会发虚建议范围0.8到2.5。第二个是EdgeThreshold这是深度差阈值决定了“多深的深度差算边缘”。场景是走廊这类深度变化平缓的地方阈值要调小否则只有门窗边缘有边模型的整体轮廓出不来露天大平地则相反。第三个是OutlineColor配OutlineIntensity后处理材质输出的是HDR颜色直接给纯白会过曝工程里一般给颜色×亮度例如红色就是(1,0.2,0.2)*3。除了这三个还有一个容易被忽略的参数BlendableLocation和After Tonemapping的取舍。如果放在After Tonemapping描边颜色会受到色调映射影响偏灰。放在Before Tonemapping颜色更饱和但会和泛光、Bloom相互作用选中一个很亮的物体时边缘会“晕开”。我的习惯是放Before Tonemapping然后在后处理Volume里把Bloom强度压到0.2以下。4.3 多选与颜色区分Stencil ID按位分组单选高亮只需要一个模板值。但工程里经常出现“选中敌人显示红色、选中队友显示绿色”的需求这时候模板值就要分区。回看第3章的组件代码SetCustomDepthStencilValue的值范围是0到255我们可以约定1到16是敌方阵营17到32是友方阵营33以上是中立物体。后处理材质里就变成float3 OutColor SceneColor; if (StencilC 1 StencilC 16) { OutColor float3(1, 0, 0) * Edge * OutlineIntensity; } else if (StencilC 17 StencilC 32) { OutColor float3(0, 1, 0) * Edge * OutlineIntensity; }注意这里加到SceneColor上而不是直接输出描边色这样描边像素能保留模型原来的明暗关系红色敌人身上被光源照亮的部位描边也会更亮视觉上更真实。多选时组件要维护一个“当前已选中Actor列表”每次点击新的Actor先把上一个的模板值清掉再设置新的否则后处理材质里会出现两坨重叠的边缘颜色看似无害但选中对象多了之后模板值会被后写入的覆盖导致颜色错乱。5. 选中高亮的避坑与排查UE4/UE5工程里最容易翻车的5个地方5.1 模型边缘出现颗粒状闪烁远看像坏帧现象描边在摄像机移动时不连续边缘像素一会儿出现一会儿消失尤其高光区域明显。原因后处理材质里直接对SceneDepth做差分深度值在远距离上的精度不够Micron级的深度差会抖动另外很多调整过对比度的后处理链会放大这种噪声。解决不要拿原始深度做差先把深度做一次LinearEyeDepth转换再用转换后的线性深度做索贝尔。UE的材质节点里可以直接连SceneDepth到LinearizeSceneDepth节点再采样线性空间下深度变化更平滑边缘更稳。5.2 CustomStencil完全读不到值后处理材质里一片黑现象项目设置改了组件也开启了SetRenderCustomDepth(true)但后处理材质里采样CustomStencil永远是0。常见原因有两个一是Custom Depth-Stencil Pass选成了Enabled without Stencil这只能读深度读不到模板二是材质里使用SceneTextureCustomStencil时采样坐标写死成了UV但在有些渲染管线里后处理全屏贴图是反UV的需要翻转Y轴。解决先用SceneTexture面板下拉确认CustomStencil当选了哪一张RT再在后处理材质里加一个if(UV.y 0.5)的测试节点确认UV方向。最快的方法是临时把后处理输出改成StencilC * 255如果屏幕上有白色物体轮廓说明读到了再调UV。5.3 高亮描边盖不住物体边缘线被模型本身遮挡现象描边画在了模型的正中心区域而不是边缘轮廓上或者描边被前面更远的物体压住。原因深度差分提取的边缘只认“深度突变”模型内部曲率大的地方——比如雕像的鼻子、衣服褶皱——深度变化同样剧烈也会被判成边缘。解决给材质加一个“区域内限定”条件即if (StencilC SelectedStencilValue)才输出描边。同时把深度阈值调高正常情况下模型内部的深度差不会超过边缘处的一半阈值先给0.5再往下压。如果还是有噪点可以在输出前对Edge做一次exp( -Edge * Edge * 2 )软化宁可描边淡一点不要出现斑驳的“地图等高线”。5.4 Nanite高模描边失效普通网格体却正常现象同一个场景里普通StaticMesh描边正常设为Nanite的高模点选了没反应。原因UE5早期版本的Nanite网格体不写入CustomDepth/Stencil后处理里读到的模板值是0。解决优先升级UE5高版本小版本新版项目设置里有Nanite相关的CustomDepth开关打开即可。如果版本受限临时做法是给Nanite网格体设置Runtime Virtual Texture那种思路不通用更实际的是把需要描边的关键物体从Nanite排除改用普通StaticMesh或Fallback Mesh。工程里通常需要描边的都是可交互对象数量一般不多性能影响完全可接受没必要为了一个交互效果让所有模型都硬上Nanite。5.5 UE4工程迁移到UE5之后描边变粗或偏移现象同一个材质从UE4工程迁到UE5工程描边线明显变粗而且歪向一边。原因UE5默认的Pixel Format和深度缓冲布局有变化View.ViewSizeInPixels.zw在不同分辨率规范下数值不同。另外UE5的TAA升级到TSR后后处理内部采样数量变了固定偏移的UV采样会落在新的像素网格上。解决把后处理材质里的TexelSize改成通过GetViewResolution节点动态计算不要用四舍五入的常数。如果偏移还是存在检查项目设置的Default RHI和分辨率缩放确认后处理材质跑在最终显示分辨率而不是渲染分辨率上。这类问题排查时开控制台命令r.SceneColorFormat0对比一下很多所谓的“描边歪”其实是渲染分辨率缩放引起的。6. 进阶让高亮跟手、不穿帮的几个小技巧6.1 把描边收敛进模型轮廓而不是浮在表面外屏幕空间描边天然有一到两个像素的“外扩”模型越小这个外扩越明显照片视角下会有描边脱离模型轮廓的违和感。常见修法是在后处理最后一步把描边像素向模型中心方向收缩采样时朝物体内侧的方向取偏移用模板值做引导只有模板值内部的像素才允许被描边覆盖。具体做法是在深度差的基础上再判断当前像素的外侧像素是否属于同一模板区域如果属于则描边向内偏移一个像素。这个技巧能让高模、远景物体的描边看起来像“从模型表面长出来的”而不是贴上去的贴纸。6.2 用时间轴做高亮淡入淡出避免突然闪亮选中高亮直接跳变会让眼睛很难受尤其是连续点击多个物体时。我一般会在HighlightComponent里放一个Timeline节点控制一个HighlightIntensity浮点参数变化范围从0到1周期0.15秒。后处理材质里最终输出的描边颜色乘以这个强度函数取消选中时反向播放淡出。有一个坑如果模型在淡出过程中被再次点击要先把正在播放的Timeline停止再重新正向播放不然强度会从0.5跳回1视觉上又一次闪变。材质里的参数名我用的是PostProcessOutlineIntensity在材质编辑器里暴露为标量参数组件通过SetScalarParameterValue驱动它。6.3 验证高亮可靠性的三个测试视角做完这套方案我建议读者用三个固定视角测试。第一是俯视视角看描边粗细屏幕空间描边在俯视时单个像素占比变大如果工程里物体都是地面装备这个视角最容易暴露描边过粗问题。第二是物体被遮挡一半的视角把选中物体放进一堵墙后面确保描边不会隔墙透出。第三是快速连续点击两个相邻物体确认模板值不会互相覆盖导致高亮残留。这三个测试能覆盖绝大多数实际项目中玩家手速快导致的交互Bug。我早期在某个模拟项目里就是因为模板值没清干净连续选中两个物体后前一个物体边缘一直亮着策划还以为是特效没结束排查了整整一天最后发现就是组件里少了取消选中的SetRenderCustomDepth(false)。后来我把这条逻辑写进了工程开发规范新同事做交互选中时都先查模板残留。希望帮到你。本文还有配套的精品资源点击获取