1. 项目概述:从“描边”到“空间感知”的材质艺术
在UE4(或UE5)里折腾过材质的朋友,肯定都动过给模型加描边的念头。这玩意儿看着简单,不就是给模型外轮廓加一圈颜色嘛,但真上手做,你会发现从简单的卡通渲染到复杂的场景高亮,再到数字孪生里对设备状态的视觉强调,描边都是个绕不开的基础又核心的技术点。我见过不少项目,初期随便找个教程抄个后处理描边,结果一到复杂场景就性能吃紧或者边缘闪烁,后期重构成本巨大。
描边材质远不止是“加个边”。它本质上是一种对模型空间关系的视觉化增强手段。在游戏里,它能让角色在混乱的场景中脱颖而出;在交互应用或数字孪生中(比如你提到的“智慧工厂”),精准、稳定的描边能清晰指示出可交互物体或异常设备,是提升用户体验和操作效率的关键。今天,我就结合自己踩过的坑和项目经验,把UE4里描边材质的几种主流实现原理、各自的“脾气秉性”,以及怎么根据你的项目需求(是手机游戏还是高端数字孪生)来选择和优化,给你彻底讲透。我们会从最基础的网格扩张法,讲到更灵活但稍复杂的后处理法,最后聊聊一些高级混合技巧和避坑指南。
2. 描边核心原理与方案选型:知其所以然
在动手写任何一行材质节点之前,我们必须搞清楚:我们到底要识别和渲染什么?答案是“边缘”。在三维空间中,“边缘”通常指代两种东西:一是模型自身的轮廓(Silhouette Edge),即背向摄像面的部分与面向摄像面部分的交界;二是模型表面的尖锐折痕(Crease Edge)。不同的描边技术,核心就在于用不同的方法去检测和凸显这些“边缘”。
2.1 主流实现方案深度对比
市面上主流的UE4描边方案,归根结底就三大类,每一类都有其鲜明的优缺点和适用场景,选错了后期会非常痛苦。
方案一:基于网格扩张的材质描边(Mesh Expansion)这是最经典、最直观的方法。思路很简单:复制一个原始网格,用材质让它沿着顶点法线方向“膨胀”一圈,只渲染这个膨胀体的背面,从而形成轮廓边。
- 原理:在材质中利用
World Position Offset(WPO)或Vertex Normal驱动顶点位置沿法线外扩。通常我们会将原网格和膨胀网格渲染两次,原网格正常渲染,膨胀网格只渲染背面(通过Two Sided材质并设置Cull Mode为CW或CCW),并赋予一个纯色。 - 优点:
- 原理简单,控制精细:每个模型独立计算,描边粗细、颜色可以按材质、甚至按模型顶点颜色进行个性化控制,非常适合角色、道具等需要差异化表现的物体。
- 稳定性高:描边是模型的一部分,不受场景深度复杂度影响,几乎不会出现闪烁或断裂。
- 性能可预测:开销主要来自多渲染一次网格(Draw Call增加)和顶点变换,对于描边物体数量不多的情况(如主要角色和关键物品)非常高效。
- 缺点:
- 粗细不均:在模型表面曲率变化大的地方(如角色鼻尖、耳朵),由于法线方向发散,描边会不自然地变粗。
- 无法处理遮挡:膨胀的网格如果被其他物体遮挡,描边也会被硬生生切断,看起来不自然。
- 不适合复杂场景:如果场景中成百上千个物体都需要描边,每个都渲染两遍,Draw Call会爆炸。
方案二:基于后处理的屏幕空间描边(Post Process)这是目前大型场景和开放世界最常用的方案。它不再关心单个模型,而是站在整个屏幕的视角,通过分析渲染完成的画面信息(深度、法线)来找出边缘。
- 原理:在后处理材质(Post Process Material)中,获取当前像素的场景深度(Scene Depth)和场景法线(Scene Normal)。通过对比当前像素与周围像素的深度值和法线方向,如果发现差异超过某个阈值,就判定该像素位于“边缘”上,然后对其进行着色。
- 深度边缘检测:能捕捉所有物体的轮廓和物体间的遮挡边界。
- 法线边缘检测:能捕捉物体表面本身的尖锐棱角。 通常两者结合使用,效果最全面。
- 优点:
- 全局性与一致性:一次性处理整个屏幕的所有边缘,无论物体多少,性能开销相对恒定(与屏幕分辨率相关)。
- 效果统一:整个画面的描边风格(颜色、粗细)是完全统一的,视觉上很协调。
- 天然处理遮挡:基于深度信息,能正确处理物体间的遮挡关系。
- 缺点:
- 控制粒度粗:难以对场景中某个特定物体进行差异化描边(比如主角用红色,敌人用黄色)。虽然可以通过
Custom Depth和Custom Stencil通道来实现物体级别的控制,但这需要额外的渲染开销和设置。 - 依赖画面信息:在屏幕空间信息不足或失效的区域(如透明物体、粒子效果后面),描边会消失或出错。
- 性能与分辨率绑定:高分辨率下,全屏像素处理开销不小。
- 控制粒度粗:难以对场景中某个特定物体进行差异化描边(比如主角用红色,敌人用黄色)。虽然可以通过
方案三:基于Sobel算子的边缘检测这其实是后处理方案的一种具体实现算法,但因为太经典且高效,值得单独拿出来说。Sobel算子是一种图像处理中的边缘检测算子,通过两个3x3的卷积核(分别检测水平和垂直方向的梯度)来快速计算图像中每个像素点的梯度幅值,梯度大的地方就是边缘。
- 在UE4中的实现:在后处理材质中,对Scene Depth或Scene Normal纹理进行Sobel滤波。具体操作是,对当前像素及其周围8个像素进行采样,按照Sobel算子的权重进行加权计算,得到梯度值。这个值越大,说明该点变化越剧烈,越可能是边缘。
- 优点:计算高效,效果清晰,是屏幕空间边缘检测的“标准答案”。
- 缺点:继承了所有后处理描边的缺点,并且对深度/法线纹理的噪声比较敏感,容易产生“毛刺”。
实操心得:怎么选?
- 选网格扩张:如果你的项目是角色/道具导向的(如ACT、RPG),需要差异化、高质量、稳定的描边,且描边物体数量可控(<50个),这就是你的首选。很多二次元风格化渲染的角色轮廓光,本质就是它的变种。
- 选后处理(Sobel):如果你的项目是场景/环境导向的(如开放世界、RTS、数字孪生),需要为大量物体提供统一的视觉高亮或轮廓提示,那么后处理方案是唯一可行的选择。数字孪生中高亮整个设备或区域,用它就对了。
- 高级操作:混合使用:成熟项目往往会混合使用。例如,用后处理处理场景建筑、地形的整体轮廓,保证性能;同时用网格扩张法为关键交互角色和物品赋予更精美、可定制的描边,保证品质。这需要良好的架构设计。
3. 核心细节解析与实操要点
选定方案后,我们深入每个方案的实现细节。这里藏着大量的“魔鬼”,参数调不好,效果就是天壤之别。
3.1 网格扩张法:顶点偏移的艺术与陷阱
创建一个新的材质,命名为M_Outline_MeshExpansion。关键步骤如下:
材质基础设置:
Material Domain设置为Surface。Blend Mode设置为Opaque。Shading Model设置为Unlit(因为我们只需要纯色描边)。- 最关键的一步:勾选
Two Sided,并将Two Sided Sign连接到后续计算中(可选,用于控制方向)。在Details面板的Rendering部分,将Cull Mode设置为Two Sided。但更精细的控制是:复制原始网格时,在材质的Customized UVs或通过参数控制,让复制体的材质Cull Mode设为CW(顺时针剔除),而原始网格材质保持CCW(逆时针剔除,默认)。这样,复制体只渲染背面(即膨胀出去的部分)。
顶点偏移(World Position Offset)实现: 这是核心中的核心。我们需要将顶点沿着其法线方向向外移动。
- 获取顶点在世界空间中的法线:使用
TransformVector节点,将Vertex Normal WS(或Absolute World Position派生出的向量)从世界空间转换到切线空间?不,这里通常需要的是视图空间(View Space)的法线,因为扩张方向最好相对于摄像机。 - 更常见的做法是:直接使用
CameraVector节点?不对。标准做法是使用Pixel Normal WS吗?也不是。对于网格扩张,我们通常使用Object Normal或Vertex Normal。 - 推荐节点链:
Vertex Normal WS->Normalize->Multiplyby aScalar Parameter(比如叫Outline Thickness) -> 连接到World Position Offset。 - 但这样扩张是均匀的,会带来“粗细不均”问题。为了解决这个问题,我们需要一个“基于摄像机视角的缩放因子”。
- 高级技巧:视角相关扩张:计算顶点到摄像机的方向向量(
Camera Position-World Position),点乘(Dot)顶点法线。当法线朝向摄像机时(点乘结果接近1),我们减少扩张量;当法线垂直于视线时(点乘结果接近0,即轮廓边缘),我们保持或增加扩张量。这能有效缓解曲面上的描边过粗问题。
// 伪代码思路,在材质蓝图中实现: float3 ViewDir = normalize(CameraPosition - WorldPosition); float NdotV = 1 - abs(dot(VertexNormalWS, ViewDir)); // 边缘处值大,正面处值小 float3 FinalOffset = VertexNormalWS * OutlineThickness * NdotV * (可选的控制曲线);- 将这个
FinalOffset输出到World Position Offset。
- 获取顶点在世界空间中的法线:使用
颜色与渲染:
Base Color连接一个Vector Parameter(如Outline Color)。Opacity设置为 1。Metallic,Specular,Roughness全部设为0。
注意事项与避坑:
- 深度冲突(Z-fighting):膨胀的网格和原始网格距离太近,会导致深度测试时像素闪烁。务必让膨胀网格的渲染深度比原始网格稍微靠后。有两种方法:1)在材质中轻微增加
World Position Offset的偏移量,使其略微远离相机;2)更可靠的是,在复制网格的材质上,设置一个很小的Depth Bias(例如0.01到0.05)。 - 性能开销:每个描边物体至少增加1个Draw Call。对于静态物体,考虑是否可以将描边材质合并到同一张Draw Call中(通过材质参数集合或纹理数组)。对于动态物体,这是不可避免的开销。
- 复杂网格问题:对于有复杂内部结构或非常薄的网格(如纸张),背面扩张可能会穿帮。可能需要调整扩张方向或使用
Two Sided Sign进行更精细的控制。
3.2 后处理Sobel法:屏幕空间的卷积计算
创建一个后处理材质M_PP_Outline_Sobel,并将其添加到项目的后处理体积(Post Process Volume)中,或通过C++/蓝图动态添加。
材质基础设置:
Material Domain设置为Post Process。Blendable Location设置为Before Tonemapping(在色调映射前处理,颜色更准确)。
获取屏幕纹理:
- 我们需要
Scene Texture节点。分别获取Scene Depth和World Normal(或Separate Translucency通道的法线,质量更高)。Scene Depth是核心。
- 我们需要
实现Sobel滤波:
- Sobel算子需要当前像素周围3x3区域共9个像素的值。我们需要定义一个偏移量,这个偏移量是单个像素的屏幕UV大小。
1.0 / Viewport Size可以得到这个值。 - 创建9个
Scene Texture采样节点,分别对应当前像素 (0,0) 和其上下左右及四个对角方向的偏移位置。例如:- Top-Left: UV + (-1, -1) * PixelSize
- Top: UV + (0, -1) * PixelSize
- Top-Right: UV + (1, -1) * PixelSize
- ... 以此类推
- 深度边缘检测:分别采样这9个点的深度值。Sobel算子有两个3x3卷积核:
Gx = [-1, 0, 1] Gy = [-1, -2, -1] [-2, 0, 2] [ 0, 0, 0] [-1, 0, 1] [ 1, 2, 1] - 在材质蓝图中,我们需要手动计算卷积。这有点繁琐,但逻辑清晰:
- 计算水平梯度
Gx_depth= (TopRight + 2Right + BottomRight) - (TopLeft + 2Left + BottomLeft) - 计算垂直梯度
Gy_depth= (BottomLeft + 2Bottom + BottomRight) - (TopLeft + 2Top + TopRight) - 深度边缘强度
EdgeDepth= sqrt(Gx_depth² + Gy_depth²)
- 计算水平梯度
- 法线边缘检测:同理,对9个点的法线向量(取RGB通道)进行同样的Sobel卷积计算。但由于法线是向量,我们需要分别对R、G、B三个分量做卷积,然后合并计算梯度幅值。计算量更大,但能捕捉表面棱角。
- 合并边缘:
FinalEdge = max(EdgeDepth, EdgeNormal)。或者用lerp进行加权混合。
- Sobel算子需要当前像素周围3x3区域共9个像素的值。我们需要定义一个偏移量,这个偏移量是单个像素的屏幕UV大小。
阈值化与描边生成:
- 计算出的
FinalEdge是一个连续值。我们需要通过一个阈值(Threshold)将其二值化或平滑化。 float OutlineMask = smoothstep(Threshold, Threshold + Softness, FinalEdge);- 这里的
Softness参数可以实现描边的抗锯齿平滑过渡,避免生硬的锯齿边。
- 计算出的
应用颜色与混合:
float3 OutlineColor = ...;float3 FinalColor = lerp(OriginalSceneColor, OutlineColor, OutlineMask);- 将
FinalColor输出到Emissive Color或直接作为最终颜色。
注意事项与避坑:
- 深度纹理精度:移动平台或远距离物体,深度缓冲精度不足会导致边缘检测出现噪点或断裂。可以尝试对深度值进行非线性重映射或使用更高的精度格式(如果平台支持)。
- 性能优化:全屏Sobel滤波采样9次,开销不小。优化方法:
- 降低采样率:可以先对深度/法线纹理进行半分辨率或四分之一分辨率的下采样,在低分辨率下进行边缘检测,然后再上采样回原分辨率。边缘通常是大块区域,对分辨率不敏感。
- 分离渲染:不是所有物体都需要后处理描边。可以为需要描边的物体单独渲染
Custom Depth,然后在后处理中只对Custom Depth进行边缘检测,大幅减少计算量。这是实现物体差异化描边的关键!
- 透明物体处理:标准的
Scene Depth不包含透明物体。如果需要为透明物体加描边,需要额外渲染透明物体的深度或使用Custom Depth。
4. 实操过程与核心环节实现
让我们以一个具体的、更高级的需求为例,来串联整个实操流程:在一个数字孪生智慧工厂项目中,我们需要实现:1)对所有设备进行统一的白色细描边;2)对处于“报警”状态的设备进行闪烁的红色粗描边。
这个需求混合了后处理(统一描边)和网格扩张/自定义深度(差异化描边)的思想。我们将采用“后处理为主,Custom Depth通道为辅”的混合方案。
4.1 场景与材质准备
- 为所有工厂设备模型创建一个共用的主材质
M_Device_Base,包含基础的PBR属性。 - 创建描边专用材质:
M_PP_Outline_Global:后处理材质,用于实现全局白色细描边(基于Scene Depth和Scene Normal的Sobel检测)。M_Outline_Alert:用于报警设备的网格扩张材质,或用于渲染到Custom Depth通道的特殊材质。
4.2 全局白色描边实现(后处理)
按照3.2节的步骤创建M_PP_Outline_Global。这里重点讲参数调节:
Outline Thickness:通过控制Sobel采样偏移的倍数(PixelSize * ThicknessScale)来间接控制描边粗细。值越大,采样范围越广,检测到的边缘越“粗”。Depth Threshold和Normal Threshold:分别控制深度和法线边缘的灵敏度。在工厂场景中,设备棱角分明,可以适当提高Normal Threshold来减少不必要的内部线条;设备间可能有微小间隙,Depth Threshold要设得足够小才能捕捉到。Outline Color:设为白色(1,1,1)。Intensity:控制描边颜色的强度,设为0.8左右,避免过于刺眼。
将这个材质添加到主后处理体积中。
4.3 报警红色描边实现(Custom Depth通道法)
这是实现差异化描边的关键。我们不希望为每个报警设备单独渲染两遍网格(性能差),而是利用UE4的Custom Depth和Custom Stencil通道。
启用报警设备的Custom Depth渲染:
- 选中所有可能报警的设备模型(或在其基类蓝图中设置)。
- 在它们的
Rendering属性中,勾选Render CustomDepth Pass。 - 可以进一步设置
Custom Depth Stencil Value。例如,设定报警设备为1,其他物体为默认0。这样我们就用模板值区分了物体类型。
创建报警描边后处理材质:
- 新建一个后处理材质
M_PP_Outline_Alert。 - 在这个材质里,我们不采样Scene Depth,而是采样
Custom Depth纹理 (Scene Texture节点选择Custom Depth)。 - 同样对Custom Depth进行Sobel边缘检测。但关键点来了:在采样Custom Depth时,我们可以同时采样
Custom Stencil纹理。 - 逻辑判断:在边缘检测计算完成后,我们增加一个判断:如果当前像素对应的
Custom Stencil Value等于我们设定的报警值(例如1),并且它位于边缘上(EdgeMask > Threshold),那么我们才应用红色描边。 - 实现闪烁:添加一个基于游戏时间
Time节点的正弦波或方波运算,输出一个在0和1之间周期性变化的Pulse值。用这个Pulse值去调制描边颜色的Alpha或强度,或者控制边缘检测阈值的动态变化,就能实现“呼吸”或“闪烁”效果。
// 伪代码思路 float EdgeMask = Sobel(CustomDepthTex, UV); float StencilValue = Sample(CustomStencilTex, UV).r; float AlertPulse = 0.5 + 0.5 * sin(Time * FlashSpeed); // 闪烁控制 if (EdgeMask > Threshold && StencilValue == 1.0) { float3 AlertOutlineColor = float3(1.0, 0.0, 0.0) * AlertPulse; // 混合到最终颜色... // 注意:这里可以设置比全局描边更大的ThicknessScale,实现“更粗”的效果 }- 新建一个后处理材质
混合两个后处理效果:
- 可以将
M_PP_Outline_Global和M_PP_Outline_Alert的逻辑合并到一个后处理材质中,通过参数开关控制。 - 更清晰的做法是,创建两个独立的后处理材质,通过
Blendable的优先级顺序叠加。将报警描边材质的优先级设得更高,确保它覆盖在全局描边之上。
- 可以将
4.4 性能与质量平衡点
- 下采样策略:对于智慧工厂这种静态场景居多的应用,可以对
Custom Depth和Scene Normal纹理进行1/2下采样,边缘检测计算量降至1/4,在1080P屏幕上几乎看不出质量损失,性能提升显著。 - 距离剔除:通过后处理材质的
Sphere Mask或计算像素的世界位置距离,可以实现远距离物体不渲染描边或使用更简化的描边,进一步节省性能。 - 报警描边的优化:报警设备通常是少数。我们可以动态地将报警设备的
Custom Depth渲染开关打开,非报警状态则关闭,避免一直渲染Custom Depth带来的开销。
5. 常见问题与排查技巧实录
即使原理都懂,实操中还是会遇到各种妖魔鬼怪。下面是我总结的“排坑手册”。
5.1 网格扩张法常见问题
问题1:描边在特定角度或模型部分消失。
- 排查:检查模型的法线。可能是模型本身法线有问题(如翻转或不连续)。在建模软件中检查并重新计算平滑组或法线。
- 技巧:在材质中,尝试将顶点偏移的基准从
Vertex Normal WS切换为Camera Vector与某个固定向量的混合,作为保底偏移方向,确保无论如何都有一个最小幅度的扩张。
问题2:描边闪烁(Z-fighting)。
- 解决:这是最常见的问题。立即检查并调整描边材质的
Depth Bias(深度偏移)属性。从0.01开始尝试,逐渐增加直到闪烁消失。同时,确保World Position Offset的扩张量不要太小(至少0.5-1个世界单位以上,具体看模型比例)。
问题3:曲面模型(如球体)描边粗细极端不均。
- 解决:必须实现“视角相关扩张”,如3.1节所述。核心是
NdotV(法线与视线点乘)因子。可以尝试1 - abs(dot(N, V))或pow(1 - abs(dot(N, V)), EdgeFalloff)来控制,EdgeFalloff参数用于控制衰减曲线。
5.2 后处理描边法常见问题
问题1:描边出现大量“毛刺”或噪点,尤其在平坦表面上。
- 排查:这是深度纹理精度不足或场景中有细微深度变化的物体(如细小网格、粒子)导致的。
- 解决:
- 提高阈值:适当增加
Depth Threshold,过滤掉微小的深度变化。 - 深度滤波:在对
Scene Depth进行Sobel采样前,先对其进行一次简单的3x3或5x5的高斯模糊(单独采样模糊,不要用后处理的模糊),平滑深度值的突变。这能有效去除噪点,但会让边缘稍微变软。 - 法线权重:增加法线边缘检测的权重。因为平坦表面的法线是均匀的,不会被检测为边缘,可以中和掉深度噪点。
- 提高阈值:适当增加
问题2:透明物体(如玻璃、粒子)没有描边。
- 原因:
Scene Depth默认不包含透明物体的深度。 - 解决:
- 对于重要透明物体:单独为其开启
Render CustomDepth Pass,并在后处理中同时结合Scene Depth和Custom Depth进行边缘检测。 - 使用Separate Translucency:UE4的
Separate Translucency通道提供了半透明物体的法线信息(如果材质启用了Output Velocity)。可以在后处理中采样Separate Translucency Normal来为半透明物体添加基于法线的边缘。
- 对于重要透明物体:单独为其开启
问题3:描边在物体边缘内部“渗出”或太粗,遮盖了本体。
- 排查:Sobel采样步长(
ThicknessScale)设置过大。 - 解决:减小
ThicknessScale。更精细的方法是使用“距离场边缘检测”的变种。不是简单比较相邻像素,而是计算当前像素深度与周围一小块区域(比如5x5)内最大/最小深度的差值。这能产生更准确、更“细”的边缘线,但计算开销更大。
问题4:性能开销大,移动端帧率下降严重。
- 解决:
- 首要方案:下采样。将边缘检测的全过程在1/2甚至1/4分辨率下进行,这是最有效的优化手段。
- 减少采样次数:尝试使用Roberts Cross或Prewitt算子,它们只需要4个或6个采样点,比Sobel的9个点更快,虽然边缘质量稍差。
- 分帧渲染:如果不是每帧都需要更新描边(比如场景变化缓慢),可以尝试每两帧或每三帧计算一次描边,中间帧复用上一帧的结果。
- 限制范围:使用遮罩(如通过
Scene Color的亮度或饱和度)只对画面中重要的部分(如中心区域)进行全精度边缘检测,其他区域使用低精度或直接关闭。
5.3 混合方案专属问题
问题:Custom Depth描边与全局Scene Depth描边重叠、错位或闪烁。
- 原因:
Custom Depth的渲染时机和默认的Scene Depth可能略有不同,或者深度值范围不一致。 - 解决:
- 深度值统一:确保在着色器中对两者使用相同的线性化深度转换公式。
- 优先级处理:在着色器逻辑中明确优先级。例如,当
Custom Depth检测到边缘(报警设备)时,直接覆盖或混合掉该位置的全局描边结果,避免双重描边。 - 检查渲染顺序:确保报警设备在渲染
Custom Depth时,其深度写入是正确的,没有被其他透明物体干扰。
描边效果的实现,从简单的功能到生产级的稳定、高效应用,中间隔着无数个细节和权衡。没有一种方案是银弹,最好的方案永远是贴合你项目具体需求、经过充分测试和优化的那一个。理解原理,大胆尝试,细致调试,你就能掌控这条“线”,让它为你的场景赋予清晰的空间层次和精准的视觉引导。