UE5材质系统实现轮廓发光:深度与法线双检测技术详解

📅 2026/7/9 20:36:27 👁️ 阅读次数 📝 编程学习
UE5材质系统实现轮廓发光:深度与法线双检测技术详解

1. 项目概述与核心思路

最近在做一个UE5的Demo,需要给场景里的特定物体加上一个高亮的轮廓发光效果,比如可交互的机关、重要的任务物品。一开始我琢磨着是不是得找插件,或者写个复杂的后处理材质。但转念一想,这种基础又高频的需求,引擎内置的材质系统应该能搞定。果然,研究了一下深度缓冲和法线缓冲,发现不用任何插件,纯用材质蓝图,5分钟就能搭出一个效果相当不错的轮廓发光。而且,只用深度检测容易在复杂场景里“误伤”背景,只用法线检测又可能漏掉平滑曲面,所以我这里把深度和法线检测结合起来,做了一个“双保险”的方案,稳定性和效果都提升了不少。这个方案特别适合游戏里的高亮提示、技能范围指示或者解谜元素强调,无论你是场景美术、TA还是想学点材质技巧的程序,都能快速上手。

2. 核心原理:深度与法线缓冲的双重检测

要理解怎么做出轮廓,得先明白引擎是怎么“看”世界的。我们屏幕上每一个像素的颜色,最终是由一大堆信息混合出来的,其中有两个后台的“隐藏图层”至关重要,就是深度缓冲(Depth Buffer)和法线缓冲(Normal Buffer)。

2.1 深度缓冲:记录“谁在前,谁在后”

你可以把深度缓冲想象成一张黑白的高度图。离摄像机越近的物体,它在深度缓冲里对应的像素就越白(值越小,通常规范化为0-1,近处接近0);离得越远的物体,像素就越黑(值接近1)。当我们渲染一个物体时,引擎会检查:当前要画的这个像素,它的深度值,比深度缓冲里已经记录的那个像素更靠前吗?如果是,就覆盖它;如果不是,就被遮挡。

轮廓发光的关键就在这里:一个物体的边缘,恰恰是它的表面像素与背景(或其他物体)的深度值发生剧烈变化的地方。比如一个盒子放在地面上,盒子侧面的像素和它紧挨着的地面像素,深度值就差了很多。我们只要在材质里,采样当前像素周围一圈的深度值,看看它们和当前像素的深度值相差是否超过一个阈值,就能判断这个像素是不是位于物体的边界上。这就是深度检测的基本原理。

注意:深度检测有个小问题。如果两个物体紧挨着,但它们的表面几乎在同一深度平面上(比如墙上贴着一张海报),深度差很小,深度检测就可能失效,轮廓线会断开。这就是我们需要法线检测来补足的原因。

2.2 法线缓冲:记录“面朝哪里”

法线缓冲存储的是每个像素所对应物体表面的法线向量方向。这个向量通常用RGB颜色来表示:红色分量代表法线指向左右(X轴),绿色代表上下(Y轴),蓝色代表前后(Z轴)。一个朝向正右方的平面,它在法线缓冲里可能就是纯红色(1,0,0)。

物体的轮廓边缘,同样也是法线方向发生突变的地方。还是那个盒子,它侧面的法线是指向侧方的,而它旁边的背景(或地面)的法线是指向上方的,这两个法线向量点乘的结果(衡量方向相似度)会很小。通过采样并比较当前像素与周围像素的法线方向,我们也能捕捉到边界。

2.3 双检测融合:优势互补

单独使用任何一种检测都有其局限性:

  • 纯深度检测:对深度变化敏感,能很好地勾勒出物体与背景的边界。但对于共面的不同物体(如地板上的瓷砖缝)或者平滑曲面上的轮廓,可能无法识别。
  • 纯法线检测:对表面朝向变化敏感,能捕捉到同一个物体上不同面之间的边界(如立方体的棱),甚至能画出平滑曲面上的轮廓。但对于两个法线方向相近但深度不同的物体边界(如平行放置的两块板子),可能失效。

所以,最稳妥的办法是两者结合,取并集。只要一个像素在深度上或者法线上被判定为边缘,我们就把它算作轮廓像素。这样既能保证物体外轮廓清晰,也能在物体自身结构变化处产生描边,效果更鲁棒。

3. 材质蓝图实战:一步步构建轮廓

理论说完了,我们直接上引擎操作。在内容浏览器里右键,创建一个新的材质,我给它起名叫M_Outline_Glow

3.1 获取核心纹理采样节点

首先,我们需要把深度和法线缓冲“抓取”到材质里。

  1. 在材质图表空白处右键,搜索SceneTexture
  2. 拖入两个SceneTexture节点。
  3. 在第一个节点的属性面板中,将Scene Texture Id设置为Post Process Input0这里是个关键点:在UE5的移动管线(Mobile)和前向渲染器(Forward Renderer)中,深度信息通常从这里获取。如果是延迟渲染(Deferred Renderer),可能需要选择CustomDepthCustomStencil,但为了通用性,我们使用Post Process Input0,它包含了经过处理后的场景深度。
  4. 在第二个SceneTexture节点的属性面板中,将Scene Texture Id设置为World Normal。这样我们就拿到了世界空间下的法线信息。

为了后续计算方便,我们通常需要把采样到的原始数据转换一下。对于深度,我们可以连一个LinearizeDepth节点(需要搜索添加),将其转换为线性的、易于比较的深度值。对于法线,因为World Normal输出的是0-1范围,我们需要用*2 - 1的运算将其转换回 (-1, 1) 范围的向量。

3.2 实现边缘检测算法

边缘检测的本质是比较一个像素和它邻居的差异。我们采用经典的索贝尔(Sobel)算子思路,但为了简单快速,这里用一个简化版:采样上下左右四个相邻像素。

我们需要用到TextureCoordinate节点(获取当前UV)和TexelSize节点(获取一个像素的UV大小)。具体操作如下:

  1. 计算偏移量:创建四个二维向量常量,分别代表向左、向右、向上、向下偏移一个像素的UV值。例如,向左偏移:(-TexelSize.X, 0)
  2. 采样邻居:将当前UV分别与这四个偏移量相加,得到上、下、左、右四个邻居的UV坐标。然后,用这些UV坐标分别去采样LinearizeDepth后的深度值和转换后的法线向量。
  3. 计算差异
    • 深度差异:分别计算当前像素深度与四个邻居深度的绝对值差,然后取这四个差值中的最大值,作为该像素的“深度边缘强度”。
    • 法线差异:分别计算当前像素法线与四个邻居法线的点积(Dot节点)。因为点积结果越接近1表示方向越相同,越接近-1表示方向相反。我们用1 - DotResult来得到差异度,同样取四个方向差异度的最大值,作为“法线边缘强度”。
  4. 设定阈值与合并:深度和法线的差异值都在0到某个数之间。我们创建两个标量参数,比如Depth_ThresholdNormal_Threshold。将计算出的深度边缘强度与Depth_Threshold比较,大于阈值则输出1,否则为0(可以用StepIf节点,更简单的是用Saturate配合乘除运算来模拟)。法线边缘强度同理处理。最后,将这两个二值化(0或1)的结果用Max节点合并,只要有一个为1,最终输出就是1,表示该像素是轮廓。

3.3 设计发光外观与颜色

得到轮廓掩码(0或1)后,我们要把它变成好看的发光效果。

  1. 轮廓扩张与柔化:直接使用二值化的轮廓会非常生硬。我们可以对上面计算出的“深度边缘强度”和“法线边缘强度”(在二值化之前)先进行一次SmoothStep处理,让边缘有一个平滑的过渡。更好的办法是对最终的轮廓掩码进行一点点模糊。添加一个Blur节点(或通过多次采样取平均模拟),设置一个很小的半径(如0.005),这能让轮廓线变宽变柔和,产生光晕感。将这个模糊后的结果作为发光的强度(Alpha)。
  2. 颜色与叠加:创建一个颜色参数Glow_Color,让美术可以自由调整发光颜色。将颜色与上一步得到的发光强度相乘。然后,最关键的一步是如何将这个发光叠加到原场景。我们需要使用Blend节点。
  3. 选择混合模式:在材质的根节点(即最终输出节点)上,将Blend Mode设置为Additive(相加混合),并将Shading Model设置为Unlit(无光照)。这样,这个材质输出的颜色就会直接加到背景画面上,产生发光效果。
  4. 最终连接:将处理好的颜色连接到Emissive Color引脚,将发光强度连接到Opacity引脚。对于Additive混合模式,Opacity实际上控制着叠加的强度。

3.4 完整材质节点图与参数化

把上述所有步骤连起来,你的材质图大概会包含以下几个功能模块:纹理采样与数据转换模块、深度边缘检测模块、法线边缘检测模块、阈值判断与合并模块、后处理模糊与颜色混合模块。

为了灵活控制,我们应该把所有可调节的部分都做成参数:

  • Depth_Threshold:深度检测灵敏度。值越小,对微小的深度变化越敏感,轮廓越粗(可能包含更多噪点)。
  • Normal_Threshold:法线检测灵敏度。值越小,对细微的法线变化越敏感。
  • Glow_Color:发光颜色。
  • Glow_Intensity:发光强度,可以乘在颜色或Alpha上。
  • Outline_Width:轮廓宽度,可以通过控制模糊半径或采样偏移倍数来实现。

完成后,你的材质资产大概长这样:

材质:M_Outline_Glow 混合模式:Additive 着色模型:Unlit 参数组: - [Threshold] - Depth_Threshold (Scalar, Default: 0.02) - Normal_Threshold (Scalar, Default: 0.4) - [Appearance] - Glow_Color (Vector3, Default: (0, 1, 1) // 青色) - Glow_Intensity (Scalar, Default: 5.0) - Outline_Width (Scalar, Default: 1.0)

4. 场景应用与材质实例化

材质做好了,怎么用到物体上呢?这里有两种主流方法,推荐第二种。

4.1 方法一:直接应用于模型材质

创建一个材质实例(Material Instance)基于M_Outline_Glow。然后,将这个材质实例赋给需要发光的模型。这种方法最直接,但有个大问题:发光的轮廓可能会被模型自身遮挡。因为这是在模型表面绘制的,如果摄像机看到的是模型的背面,或者模型自身结构复杂,轮廓光可能显示不正确。

4.2 方法二:通过后期处理盒子(推荐)

这是更稳定、更通用的方法。我们不在物体本身的材质上画,而是在整个屏幕的后处理阶段来画。

  1. 在场景中放置一个Post Process Volume
  2. 将这个体积框设置为Infinite Extent(无限范围),影响整个场景。
  3. 在体积的Rendering Features->Post Process Materials数组中,添加我们的M_Outline_Glow材质。
  4. 但是,这样所有物体都会有轮廓。我们需要告诉材质:“只给特定的物体画轮廓”。这就需要用到一个关键技巧:自定义深度(Custom Depth)

4.3 结合自定义深度实现精准控制

自定义深度是引擎为选定的物体单独渲染的一层深度缓冲。我们可以利用它作为“标记”。

  1. 标记物体:在需要显示轮廓的物体(静态网格体或角色骨骼网格体)的细节面板中,找到Rendering部分,勾选Render CustomDepth Pass
  2. 修改材质:回到我们的M_Outline_Glow材质。我们不再(或不仅)与普通的场景深度比较,而是与自定义深度比较。
    • 添加第三个SceneTexture节点,设置其Scene Texture IdCustomDepth
    • 修改边缘检测逻辑。我们可以将原来的深度检测分支,改为同时检测“当前像素有自定义深度(即是要高亮的物体),而它的邻居像素没有自定义深度或自定义深度值相差很大”。这样就精确地只在高亮物体与周围环境的边界处产生轮廓。
    • 甚至可以简化:直接采样自定义深度缓冲,然后对其做边缘检测,得到的边缘就是高亮物体的轮廓。因为只有被标记的物体才会写入这个缓冲。
  3. 优化性能:只对需要轮廓的物体开启Render CustomDepth Pass,避免不必要的渲染开销。在后期处理材质中,因为基于全屏缓冲操作,无论场景中有多少个高亮物体,都只执行一次材质计算,效率很高。

5. 性能优化与常见问题排查

一个效果再好,如果太耗性能也白搭。这里分享几个优化点和踩坑记录。

5.1 性能优化要点

  1. 控制采样次数:我们的方案采样了中心点、上、下、左、右共5个点。这是性能和质量的平衡点。如果追求极致性能,可以只采样左右和上下,用4次采样。但不要低于4次,否则边缘可能不连续。
  2. 降低渲染分辨率:轮廓发光通常不需要全分辨率渲染。可以在Post Process Volume中,将M_Outline_Glow材质添加到Post Process Materials时,通过Blendable Location设置为Before Tonemapping等阶段,并考虑配合一个半分辨率或四分之一分辨率的渲染目标来实现,大幅减少像素计算量。模糊操作本身也有降分辨率的效果。
  3. 使用材质实例参数:所有调节项(阈值、颜色、强度)都必须通过材质实例参数暴露。这样,美术可以在运行时动态调整,而无需重新编译材质。这也是为什么我之前强调要参数化。
  4. 避免每帧动态计算偏移TexelSize是常量,不要放在复杂的动态计算网络中。确保它只被计算一次。

5.2 常见问题与解决方案

问题现象可能原因排查与解决思路
轮廓闪烁或抖动深度阈值Depth_Threshold设置过低,在摄像机或物体微动时,深度值的微小波动被误判为边缘。适当提高Depth_Threshold值。检查深度值是否已线性化,非线性深度在远处精度不足,更容易抖动。
轮廓线太粗或包含噪点边缘检测的采样偏移量(一个像素的UV偏移)过大,或者模糊半径Outline_Width太大。深度/法线阈值太低,将非边缘信息也包含了进来。减小采样偏移倍数(保持为1倍TexelSize)。先调高Depth_ThresholdNormal_Threshold,让轮廓线变细、干净,再用模糊来柔和和加宽。
轮廓在特定角度消失法线检测分支可能出了问题。当物体表面法线与视线方向平行时,屏幕空间法线变化不剧烈。确保法线检测的阈值Normal_Threshold设置合理(通常0.3-0.5)。可以适当调低,或增加法线检测的权重。归根结底,这正是需要深度+法线双检测的原因,一个失效时另一个能补上。
没有轮廓效果材质混合模式未设置为AdditiveTranslucent。后期处理体积未启用或优先级不够。自定义深度未开启。1. 检查材质根节点Blend Mode。2. 检查Post Process VolumeEnabledPriority(确保大于其他可能覆盖的体积)。3. 检查目标物体是否勾选Render CustomDepth Pass。4. 在材质中连一个Emissive Color为纯色的测试节点,看后期材质是否生效。
轮廓颜色不亮或发灰发光强度Glow_Intensity不够。在Additive混合下,颜色是线性叠加的,需要较高的值(如5.0, 10.0)才在屏幕上显得亮。HDR和Tonemapping也会影响。提高Glow_Intensity。检查场景是否启用了HDR,Glow_Color可以尝试使用超过1.0的亮度值。确保颜色连接到了Emissive Color,并且Opacity有正确的强度输入。
移动端上效果差或性能低下移动设备对全屏后处理和非必需的高精度计算敏感。可能使用了高消耗的节点如DDX/DDY(屏幕空间导数)。简化方案:考虑在移动端只使用深度检测,或使用更低的采样数。确保所有计算都在Shader Model 5或移动端支持的特性内。使用材质质量开关,为移动设备创建简化版的材质实例。

5.3 一个实用的调试技巧

当效果不对时,可以快速隔离问题。创建三个简单的测试材质:

  1. 深度可视化材质:直接将LinearizeDepth的结果作为灰度色输出。观察深度变化是否连续、合理。
  2. 法线可视化材质:将World Normal转换后,用ComponentMask拆分RGB查看,或直接作为颜色输出。
  3. 轮廓掩码可视化材质:将最终合并前的深度边缘强度/法线边缘强度,或最终的二值化掩码,作为灰度色输出。

将这三种材质分别作为后期材质应用,你就能清晰地看到到底是深度信息有问题,还是法线信息有问题,或者是边缘检测算法本身的计算逻辑出了问题。