1. 为什么我要自己写一个高亮插件
在 Unreal Engine 项目里做交互开发,尤其是涉及编辑器工具、关卡设计辅助或者调试可视化的时候,物体高亮几乎是一个绕不开的需求。你可能想快速定位某个 Actor,想在编辑器里一眼看出哪些对象被选中了,或者想在运行时给玩家一个明确的视觉反馈。UE 本身提供了自定义深度模板(Custom Depth/Stencil)这套机制,但真到用的时候你会发现,从零搭一套稳定、可复用、不污染项目代码的高亮方案,其实要踩不少坑。
HighLightActors 这个插件就是我在反复折腾这些需求之后沉淀下来的一个完全开源、纯免费的解决方案。它的核心目标很明确:用最少的接入成本,给任意 Actor 加上可配置的高亮描边效果,同时保证在编辑器视口和运行时 PIE 里都能正常工作。它不依赖任何第三方库,纯 C++ 实现,基于 UE 的 Custom Depth Stencil 通道做渲染,性能开销可控,适合中小型项目直接拿来用,也适合想学习 UE 渲染管线接入方式的开发者参考。
这篇文章我会从设计思路、核心原理、代码结构、实操接入、参数调优到常见问题排查,完整拆一遍。如果你正在做 UE 的编辑器工具链、关卡交互或者调试可视化,这个插件应该能帮你省下不少时间。即使你只是想搞清楚 UE 里高亮描边到底是怎么实现的,下面的内容也能让你少走弯路。
2. 高亮描边的核心原理与方案选型
2.1 为什么选 Custom Depth Stencil 而不是后处理材质硬写
UE 里实现物体描边高亮,常见路子有这么几条:一是用后处理材质配合场景深度做边缘检测,二是用 Custom Depth Stencil 标记目标物体再在后处理里读取,三是直接改材质做发光或者叠加。第一种方案对全屏做边缘检测,容易把场景里所有物体的边缘都描出来,想只高亮特定对象就得额外做遮罩,逻辑复杂且性能不划算。第三种方案侵入性太强,每个要高的物体都得改材质,维护成本高。
Custom Depth Stencil 的优势在于它是 UE 原生支持的逐物体标记机制。你只需要把目标 Actor 的 Mesh 组件设置SetRenderCustomDepth(true),再指定一个 Stencil 值,后处理材质里就能通过SceneTexture:CustomStencil精确读取到这个值,从而只对标记过的像素做描边处理。这样高亮逻辑和物体本身的材质完全解耦,想高亮谁就标记谁,取消也只是一行代码的事。
HighLightActors 正是基于这套机制。插件内部封装了 Stencil 值的分配、Mesh 组件的遍历、后处理材质的动态加载和参数传递,对外只暴露简单的接口,比如HighlightActor(Actor)和UnHighlightActor(Actor)。你不需要关心底层 Stencil 怎么分配,也不需要手动去挂后处理体积。
2.2 Stencil 值分配策略与冲突规避
Custom Stencil 是一个 0 到 255 的整数值,UE 默认把 0 当作“未标记”。理论上你可以给每个高亮对象分配不同的 Stencil 值,然后在后处理里根据值做不同颜色的描边。但实际项目里,Stencil 通道可能已经被其他系统占用了,比如某些描边插件、选中反馈、特殊渲染效果。如果大家各用各的,很容易冲突。
我的做法是在插件里维护一个 Stencil 值池,默认从某个高位段开始分配,比如 200 到 255,尽量避开常见系统占用的低位段。插件初始化时会扫描当前场景里已经使用了 Custom Stencil 的组件,记录已占用的值,然后从池子里取空闲的分配。释放时把值归还池子,保证复用。这样即使项目里还有其他系统在用 Stencil,也能最大程度减少冲突。
注意:如果你的项目里已经有其他系统在用 Custom Stencil,建议先确认它们占用的值范围,然后在插件配置里把起始值调到不重叠的区间。这个配置我放在了插件设置里,改起来很方便。
2.3 编辑器视口与运行时的一致性处理
很多高亮方案在编辑器里看着没问题,一进 PIE 就失效,或者反过来。原因通常是后处理体积的挂载方式不对,或者渲染标记没有在正确的时机设置。HighLightActors 的做法是区分编辑器世界和运行世界,分别管理后处理体积的创建和销毁。
在编辑器视口里,插件会往编辑器世界场景里动态添加一个后处理体积,范围覆盖整个视口,保证编辑器里也能看到高亮效果。运行时则在当前 Game World 里创建对应的后处理体积,跟随玩家相机或者固定在场景里。两者的后处理材质是同一套,只是挂载的 World 不同。这样无论你在编辑器里调试还是打包后运行,高亮表现都是一致的。
3. 插件代码结构与核心模块拆解
3.1 模块划分与文件组织
HighLightActors 的代码结构尽量保持扁平,方便阅读和二次修改。主要分为这几个部分:
- HighLightActors.uplugin:插件描述文件,声明模块、依赖和加载阶段。
- Source/HighLightActors/:核心模块目录,包含 Public 和 Private 两个子目录。
- Public/HighLightActorsSubsystem.h:对外暴露的子系统接口,负责高亮管理的入口。
- Public/HighLightActorTypes.h:定义高亮配置结构体、枚举和委托。
- Private/HighLightActorsSubsystem.cpp:子系统实现,包含 Stencil 分配、Actor 标记、后处理体积管理等核心逻辑。
- Private/HighLightActorPostProcess.cpp:后处理材质的动态加载和参数设置。
- Content/:存放后处理材质资源,插件自带一套默认描边材质。
这种划分的好处是接口和实现分离,使用者只需要包含 Public 头文件,不需要关心 Private 里的实现细节。如果你要扩展功能,比如增加不同颜色的高亮,也只需要在 Public 接口上做文章,底层逻辑不用大改。
3.2 子系统设计:为什么用 WorldSubsystem
UE 的 Subsystem 机制是管理全局逻辑的好工具。HighLightActors 选择继承UWorldSubsystem,而不是 GameInstanceSubsystem 或者 EditorSubsystem,原因是高亮逻辑和 World 强相关。每个 World 有自己的场景、自己的 Actor、自己的后处理体积,用 WorldSubsystem 可以天然地做到按 World 隔离,编辑器世界和运行世界互不干扰。
子系统在Initialize阶段会做几件事:加载默认的后处理材质、初始化 Stencil 值池、注册 World 的 Actor 销毁回调(用于自动清理已销毁 Actor 的高亮状态)。在Deinitialize阶段则负责释放所有资源、销毁后处理体积、归还 Stencil 值。这样即使 World 切换或者编辑器关闭,也不会留下悬挂的渲染标记。
3.3 后处理材质的动态加载与参数传递
插件自带的后处理材质是一个基于 Custom Stencil 的描边材质。它的核心逻辑是:采样SceneTexture:CustomStencil,如果当前像素的 Stencil 值在有效范围内,就根据周围像素的 Stencil 差异计算边缘,然后叠加描边颜色。材质里暴露了几个参数:描边颜色、描边宽度、是否启用发光、发光强度。
动态加载方面,插件在初始化时通过LoadObject加载材质资源,然后创建动态材质实例(MID),把 MID 设置到后处理体积的WeightedBlendables里。每次高亮配置变化时,只需要更新 MID 的参数,不需要重新创建材质实例。这样性能开销很小,切换高亮颜色几乎是即时的。
实操心得:如果你要自定义描边效果,建议直接复制插件自带的材质,改完之后在插件设置里指向你的材质路径。不要直接改插件原始材质,否则插件更新时你的修改会被覆盖。
4. 完整接入流程与实操步骤
4.1 插件安装与项目配置
拿到插件后,第一步是把它放到项目的Plugins目录下。如果项目还没有 Plugins 目录,手动建一个。目录结构应该是YourProject/Plugins/HighLightActors/,里面包含HighLightActors.uplugin和Source、Content等文件夹。
放好之后,右键项目文件生成工程文件,或者直接打开编辑器。UE 会自动检测到新插件并提示编译。编译通过后,在编辑器的插件列表里启用 HighLightActors,重启编辑器生效。
如果你用的是 C++ 项目,还需要在项目的Build.cs里添加对 HighLightActors 模块的依赖:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "HighLightActors" });这一步很多人会忘,结果代码里 include 头文件报找不到。加上之后重新生成工程文件即可。
4.2 在 C++ 中调用高亮接口
插件对外的主要接口都在UHighLightActorsSubsystem里。获取子系统的方式是:
UHighLightActorsSubsystem* HighlightSubsystem = GetWorld()->GetSubsystem<UHighLightActorsSubsystem>();拿到之后就可以调用高亮和取消高亮:
// 高亮一个 Actor HighlightSubsystem->HighlightActor(MyActor); // 取消高亮 HighlightSubsystem->UnHighlightActor(MyActor); // 取消所有高亮 HighlightSubsystem->UnHighlightAllActors();如果你需要自定义描边颜色和宽度,可以用带配置参数的版本:
FHighLightConfig Config; Config.Color = FLinearColor::Red; Config.Width = 3.0f; Config.bEnableGlow = true; HighlightSubsystem->HighlightActor(MyActor, Config);配置结构体里还支持设置优先级,当同一个 Actor 被多次高亮时,优先级高的配置会覆盖低的。这个在复杂交互场景里很有用,比如选中高亮和警告高亮同时存在时,警告应该优先显示。
4.3 在蓝图中使用高亮功能
不是所有项目都方便写 C++,所以插件也暴露了蓝图接口。你可以在蓝图里通过Get World Subsystem节点拿到 HighLightActorsSubsystem,然后调用对应的函数。蓝图节点的参数和 C++ 接口一一对应,配置结构体在蓝图里也可以直接编辑。
一个常见的用法是在 Actor 的BeginPlay里注册高亮,在EndPlay里取消。或者在做交互检测时,射线打到的 Actor 调用高亮,离开时取消。蓝图里操作起来很直观,不需要写一行代码。
注意:蓝图里调用子系统接口时,确保传入的 Actor 是有效的,并且它的 Mesh 组件已经注册到场景里。如果 Actor 还没 BeginPlay 或者 Mesh 还没创建,高亮可能会失败。建议在 Actor 初始化完成之后再调用。
4.4 编辑器视口高亮的启用方式
编辑器视口里的高亮默认是开启的,但如果你发现编辑器里看不到效果,先检查这几个地方:一是插件的编辑器模式是否启用,二是后处理体积是否被正确添加到编辑器世界,三是当前视口是否开启了后处理显示。在编辑器视口的显示设置里,确保Post Processing是勾选的。
如果你只想在运行时高亮,不想在编辑器里看到,可以在插件设置里关掉编辑器视口支持。这样插件就不会往编辑器世界添加后处理体积,减少不必要的开销。
5. 参数调优与视觉效果打磨
5.1 描边宽度与分辨率的适配关系
描边宽度这个参数,很多人直接给个固定值,结果在不同分辨率下表现不一致。原因是后处理材质里的描边计算是基于屏幕像素的,分辨率越高,同样的像素宽度看起来越细。我的建议是把描边宽度和屏幕高度关联起来,比如宽度值乘以ViewSize.y / 1080,这样在 1080p 和 4K 下视觉粗细基本一致。
插件里默认做了这个适配,但如果你自己改材质,记得把这个逻辑加进去。具体做法是在后处理材质里用ViewSize节点获取当前视口尺寸,然后和基准分辨率做比值,乘到采样偏移上。
5.2 颜色选择与场景对比度
高亮颜色不是随便选一个亮色就行。如果场景本身很亮,你用亮黄色描边,可能根本看不出来。如果场景偏暗,用深色描边也会糊成一团。我的经验是:先看场景的主色调,然后选一个对比度高的颜色。比如森林场景偏绿,用橙红或者品红描边就很跳;工业场景偏灰,用青色或者亮黄效果不错。
另外,描边颜色最好带一点自发光,这样在暗处也能看清。插件里的bEnableGlow和GlowIntensity就是干这个的。发光强度不要太高,否则会过曝,一般 1.5 到 2.5 之间比较合适。
5.3 多对象高亮时的性能考量
同时高亮大量对象时,性能主要花在后处理的边缘检测上。因为后处理是全屏的,不管高亮一个还是十个对象,全屏采样的开销是一样的。真正增加的开销在于 Custom Depth 的渲染,每个被标记的 Mesh 都要多渲染一遍深度。如果高亮几百个高面数模型,深度渲染的开销会明显上升。
优化思路有几个:一是限制同时高亮的对象数量,比如只高亮视野内的;二是对于远处的小物件,可以降低高亮优先级或者直接不高亮;三是如果项目允许,可以把 Custom Depth 的分辨率调低,减少深度渲染的像素量。插件里提供了最大高亮数量的配置,超过之后会自动取消最早的高亮,避免无限增长。
6. 常见问题排查与避坑指南
6.1 高亮不显示或显示异常
这是最常见的问题,排查顺序可以按下面这个表来:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全看不到高亮 | 后处理体积未创建 | 检查 World 里是否有 HighLightActors 的后处理体积 |
| 高亮闪烁或不稳定 | Stencil 值冲突 | 查看是否有其他系统占用了相同 Stencil 值 |
| 描边颜色不对 | MID 参数未更新 | 确认配置变更后调用了 UpdateConfig |
| 编辑器里正常,PIE 里没有 | 后处理体积挂载 World 错误 | 检查运行时 World 的后处理体积是否存在 |
| 只有部分 Mesh 高亮 | 组件遍历不完整 | 确认 Actor 下所有 PrimitiveComponent 都被标记 |
我遇到最多的是 Stencil 值冲突。项目里如果有其他描边或者选中效果,它们可能也在用 Custom Stencil。解决办法是先把其他系统的 Stencil 占用范围搞清楚,然后把插件的起始值调到不重叠的区间。插件设置里可以改这个起始值,改完重启编辑器生效。
6.2 打包后高亮失效的处理
编辑器里好好的,一打包就失效,通常是因为后处理材质没有被正确打包。UE 在打包时只会包含被引用的资源,如果插件是通过代码动态加载材质路径,而该路径没有被任何地方硬引用,打包时可能会被忽略。
解决办法是在插件设置里把材质路径配置成一个硬引用,或者在插件的启动模块里用ConstructorHelpers或者FSoftObjectPath显式引用一下。另外,确保插件的 Content 目录被正确包含在打包范围内。可以在打包设置里检查一下插件资源的包含规则。
6.3 与其他渲染插件的兼容性
如果你的项目里还有其他改渲染管线的插件,比如自定义后处理、描边、抗锯齿替换等,可能会和 HighLightActors 冲突。冲突点主要在两个方面:一是后处理体积的 Blendable 顺序,二是 Custom Stencil 的占用。
后处理体积的 Blendable 是有顺序的,如果两个插件都往同一个后处理体积里加 Blendable,顺序不同可能导致效果叠加异常。HighLightActors 默认创建自己的后处理体积,不和其他插件共用,这样能减少冲突。但如果你的项目强制要求所有后处理走同一个体积,就需要手动调整 Blendable 的插入位置。
Custom Stencil 的冲突前面说过了,这里再强调一下:如果两个系统用了相同的 Stencil 值,后处理材质读取到的值就是一样的,会导致不该高亮的物体也被描边。所以 Stencil 值池的管理很重要,插件里做了自动分配,但前提是其他系统也遵守类似的分配规则。如果其他系统是硬编码的,你就得手动避让。
6.4 移动端和主机平台的注意事项
Custom Depth Stencil 在移动端和主机平台上的支持情况不太一样。移动端默认可能不开启 Custom Depth,需要在项目设置里手动启用。具体路径是Project Settings -> Rendering -> Mobile -> Enable Custom Depth。启用之后会增加一定的渲染开销,需要根据目标机型评估。
主机平台一般支持没问题,但要注意后处理材质的复杂度。主机上后处理是全屏的,如果材质指令数太多,可能会成为性能瓶颈。建议在主机平台上简化描边算法,比如减少采样次数,或者用更低精度的计算。
实操心得:在移动端上,如果高亮对象不多,可以考虑不用后处理,直接用材质发光或者叠加一个高亮 Mesh 来替代。虽然后处理效果更好,但移动端的性能预算很紧,能省则省。
7. 扩展思路与二次开发建议
7.1 增加多种高亮样式
插件默认只提供一种描边样式,但实际项目里可能需要多种,比如选中高亮、警告高亮、任务目标高亮,每种颜色和粗细不同。扩展方式很简单:在FHighLightConfig里增加一个样式枚举,然后在后处理材质里根据 Stencil 值或者额外的参数通道来区分样式。
具体做法是给每种样式分配不同的 Stencil 值段,后处理材质里根据 Stencil 值范围选择不同的颜色和宽度。这样一次后处理就能处理多种样式,不需要多个后处理体积。缺点是 Stencil 值段有限,样式太多会不够用。如果样式超过五六种,建议改用额外的参数纹理来传递样式信息。
7.2 与选中系统、交互系统的联动
高亮很少单独存在,通常和选中、交互、任务系统联动。比如玩家看向某个物体时高亮,按下交互键后高亮变成另一种颜色,任务完成后高亮消失。这些逻辑都可以在子系统之上封装一层管理器来实现。
我的建议是把高亮状态和业务状态分开管理。高亮子系统只负责“谁被高亮了、什么样式”,业务系统负责“什么时候高亮、什么时候取消”。两者通过事件或者接口通信,不要耦合在一起。这样业务逻辑变化时,高亮部分不用改。
7.3 性能分析与优化方向
如果项目里高亮用得很多,建议用 Unreal Insights 或者 Stat 命令看一下开销。重点看PostProcessing和CustomDepth这两项。如果 CustomDepth 开销大,说明高亮对象太多或者面数太高,需要做裁剪。如果 PostProcessing 开销大,说明后处理材质太重,需要简化算法。
一个实用的优化是给高亮对象做距离裁剪,超过一定距离就不标记 Custom Depth。这个逻辑可以在子系统里做,每帧或者每隔几帧检查一次高亮对象和相机的距离,超出范围就临时取消标记,进入范围再重新标记。这样远处的高亮不参与深度渲染,能省不少性能。
7.4 源码阅读与学习建议
如果你想通过这个插件学习 UE 的渲染接入,建议按这个顺序读代码:先看HighLightActorsSubsystem.h了解对外接口,然后看HighLightActorsSubsystem.cpp里的HighlightActor和UnHighlightActor实现,理解 Stencil 分配和组件标记的逻辑。接着看HighLightActorPostProcess.cpp,理解后处理体积和 MID 的创建过程。最后看后处理材质的节点图,理解描边算法。
读的时候重点关注几个问题:Stencil 值是怎么分配和回收的,后处理体积是在哪个 World 创建的,MID 参数是怎么传递的,Actor 销毁时高亮状态是怎么清理的。把这几个问题搞清楚,你对 UE 的 Custom Depth 高亮方案就基本通了。
这个插件我一直在自己的项目里用,从 UE4 到 UE5 都跑过,稳定性没问题。代码量不大,但该有的都有,适合直接拿来用,也适合当学习材料。如果你在使用中遇到问题,或者有更好的实现思路,欢迎一起交流。