一、需求来了:一个描边,差点让项目翻车

去年接了个科幻机甲题材的客户端项目,美术总监甩过来一张《高达》的截图:“角色被选中时,要这种描边——边缘干净、不随距离变粗、能穿透遮挡显示,而且性能要稳在 60 帧。” 我当时心想,这不就是后处理描边吗?Stencil 加个 Sobel 算子,半小时搞定。结果这一搞,就是三周。

先说说为什么常规描边方案在这项目里全废了:法线外扩描边(Inverted Hull)在硬表面模型上会炸边,机甲那些锐利倒角直接变成锯齿;后处理深度描边(Depth-based)遇到透明物体和粒子就抓瞎,而且角色和背景深度跳变的地方会误描。最后我们锁定了 Stencil 模板缓冲描边——用自定义深度(Custom Depth)写入模板值,再在后处理里基于 Stencil 做边缘检测。原理上最干净,但坑也最多。

项目复盘:UE5 Stencil 描边从翻车到封神,我踩过的坑比你写的 Shader 都多 - 配图1
项目复盘:UE5 Stencil 描边从翻车到封神,我踩过的坑比你写的 Shader 都多

二、技术选型:为什么是 Stencil,而不是其他?

Stencil 的本质是逐像素的 8 位掩码,渲染管线里每个像素除了颜色、深度,还有一个模板值。我们可以在材质里通过 CustomDepth Stencil Value 给特定物体打标签(比如角色=1,高亮道具=2),然后在后处理材质里读取这个值做逻辑判断。相比其他方案:

  • 法线外扩:需要额外 Pass,模型顶点数翻倍,硬表面容易破面,直接淘汰。
  • 深度描边:依赖场景深度,透明物体不写深度,粒子特效直接缺失,而且深度不连续处(比如角色和墙之间)会误判。
  • Stencil:与深度解耦,可以精确控制哪些物体参与描边,还能做多层描边(不同颜色/粗细),性能开销可控——只多一个后处理 Pass,外加一次 CustomDepth 渲染。

但 Stencil 的坑在于:它和 CustomDepth 是绑定的。要写 Stencil 值,必须开启 CustomDepth Pass,而这个 Pass 默认只渲染不透明物体,且需要手动在项目设置里打开 Custom Depth-Stencil Pass 为 Enabled with Stencil。很多教程没提这一点,导致新手写完材质发现没效果,直接懵圈。

三、踩坑实录:从“能用”到“好用”的四个坎

坑 1:Stencil 值冲突——角色和道具互相覆盖

第一次测试,角色描边正常,但场景里所有带 Stencil=1 的物体全被描了边,包括远处的箱子。原因是 Stencil 值是个全局掩码,只要值匹配就触发。我们当时的逻辑是“Stencil==1 就描边”,结果所有标记为 1 的物体都亮了。解决方案是按类别分配 Stencil 值:角色=1,可交互道具=2,高亮敌人=3,然后在后处理里用 if (Stencil == 1) 精确筛选。但要注意,Stencil 只有 8 位,最多 255 个值,大型项目要规划好,别用超了。

坑 2:边缘锯齿——Sobel 算子直接上,结果像狗啃

最开始我用的是最简单的 4 邻域差分,边缘全是锯齿。后来换成 Sobel 算子(一种 3×3 卷积核,分别计算水平和垂直梯度),效果好了一些,但在斜边上还是不够平滑。关键问题是采样精度:后处理材质里如果直接用 SceneTexture:CustomStencil 采样,默认是点采样(Point),边缘会硬。必须把采样器改成双线性过滤(Bilinear),并且采样坐标要乘上 ViewSize 做像素对齐。另外,Sobel 的阈值要配合 SceneDepth 做动态调整——远处物体边缘对比度低,阈值要降低,否则描边会断。

// 后处理材质中的 Sobel 边缘检测核心逻辑(HLSL 片段)
float2 UV = GetDefaultSceneTextureUV(Parameters, 14); // 14 = CustomStencil
float2 TexelSize = View.ViewSizeAndInvSize.zw;
float StencilCenter = SceneTextureLookup(UV, 14, false).r;
// 采样 8 邻域
float S00 = SceneTextureLookup(UV + float2(-1,-1)*TexelSize, 14, false).r;
float S10 = SceneTextureLookup(UV + float2(0,-1)*TexelSize, 14, false).r;
// ... 省略其他 6 个采样
// Sobel X 和 Y
float Gx = -S00 - 2*S01 - S02 + S20 + 2*S21 + S22;
float Gy = -S00 - 2*S10 - S20 + S02 + 2*S12 + S22;
float Edge = sqrt(Gx*Gx + Gy*Gy);
// 动态阈值:基于深度调整
float Depth = SceneTextureLookup(UV, 1, false).r; // 1 = SceneDepth
float Threshold = lerp(0.1, 0.5, saturate(Depth / 10000.0));
return Edge > Threshold ? 1.0 : 0.0;

注意:SceneTextureLookup 的最后一个参数是“是否使用点采样”,这里必须传 false 才能启用双线性过滤,否则边缘还是硬的。

项目复盘:UE5 Stencil 描边从翻车到封神,我踩过的坑比你写的 Shader 都多 - 配图2
项目复盘:UE5 Stencil 描边从翻车到封神,我踩过的坑比你写的 Shader 都多

坑 3:透明物体不写 Stencil——粒子特效直接消失

项目里有个技能特效是半透明护盾,需要描边。但 CustomDepth Pass 默认只渲染不透明物体,半透明材质根本不会写入 Stencil。解决方案有两个:一是把材质改成Masked(遮罩)模式,并勾选 Render CustomDepth Pass,这样它就能参与 Stencil 写入,但会失去半透明渐变;二是用后处理材质单独处理,通过 SceneTexture:CustomStencil 读取不到,就改用 SceneTexture:PostProcessInput0 的 Alpha 通道做辅助判断。我们最终选了方案一,因为护盾本身是硬边效果,Masked 足够用。如果项目里大量半透明描边,建议用 Custom Stencil + 自定义深度写入 的材质函数封装,避免每个材质手动设置。

坑 4:性能爆炸——移动端直接掉到 20 帧

PC 上跑得挺欢,一到移动端就跪了。分析发现两个性能杀手:一是 CustomDepth Pass 额外渲染了一遍场景,Draw Call 翻倍;二是后处理里 8 次采样 + 多次 SceneTexture 读取,带宽压力大。优化手段:

  • 限制 CustomDepth 的渲染范围:在项目设置里把 Custom Depth-Stencil Pass 改为 Enabled with Stencil 后,只有勾选了 Render CustomDepth Pass 的物体才会被渲染,务必只给需要描边的物体勾选,别全局开。
  • 降低采样次数:把 8 邻域改成 4 邻域(十字采样),边缘质量略降但性能提升 40%。
  • 使用半分辨率后处理:在材质里把 SceneTexture 的 UV 乘 0.5,然后输出时再放大,描边会稍微模糊但可接受。
  • 合并描边 Pass:如果项目里还有其它后处理(如 Bloom、DOF),尽量合并到一个材质里,减少全屏 Pass 数量。

最终移动端稳定在 55-60 帧,Draw Call 只增加了 12 个(角色和主要道具),可以接受。

四、交付与反思:Stencil 描边的边界在哪里

项目上线后,描边效果成了美术总监的“心头好”,甚至衍生出了“选中描边”“受击闪白描边”“可交互高亮”三种变体。但我也清楚它的局限:Stencil 描边不适合大世界场景,因为 CustomDepth Pass 会渲染所有标记物体,如果开放世界有几百个可交互物,性能直接崩。这时候还是得回到屏幕空间边缘检测或者基于距离的 LOD 方案。

另外,Stencil 的 8 位掩码在复杂项目里很容易不够用,我们后来引入了多层 Stencil 值编码——低 4 位表示类别,高 4 位表示子类型,算是打了个擦边球。如果项目对描边要求极高,可以考虑 自定义深度 + 模板缓冲 + 后处理材质 的组合拳,但一定要在项目初期就规划好 Stencil 值分配表,否则后期改起来牵一发动全身。

项目复盘:UE5 Stencil 描边从翻车到封神,我踩过的坑比你写的 Shader 都多 - 配图3
项目复盘:UE5 Stencil 描边从翻车到封神,我踩过的坑比你写的 Shader 都多

五、给进阶者的三个硬核建议

  • 别迷信教程里的“一键描边”:网上很多 Stencil 描边教程只讲了材质节点,没讲项目设置和性能陷阱。实际项目里,CustomDepth Pass 的开销、Stencil 值冲突、透明物体处理,每一个都能让你加班到凌晨。
  • 用材质函数封装通用逻辑:把 Sobel 边缘检测、动态阈值、采样优化做成 Material Function,不同项目直接复用,改参数就行。我们团队现在有一个 MF_StencilOutline,支持 4 种描边样式,省了大量重复劳动。
  • 性能分析要趁早:别等到优化阶段才看 GPU 开销。在编辑器里用 ProfileGPU 命令,重点看 PostProcessing 和 CustomDepth 的耗时,一旦超过 2ms 就要警惕。

最后说一句,Stencil 描边是个“看起来简单、做起来要命”的技术点,但一旦吃透,你就能在项目中游刃有余地处理各种高亮需求。如果你也想系统掌握 UE5 的渲染管线、材质系统和性能优化,欢迎来火星人教育聊聊——我们不讲虚的,只教项目里真能用的东西。

关于火星人教育:火星人教育创办24年来,专注数字艺术与 AIGC 实战职业技能培训,开设课程涵盖 AI 漫剧创作、AIGC 视觉设计、UE5 游戏特效、室内全屋定制、MAYA 三维动画、C4D 动态设计、工业设计、NUKE 影视后期、摄影摄像实训。北京、深圳双校区开设线下面授班,面向美术生、在校大学生及意向转行的职场人群,以项目化实训教学,夯实专业技能,助力就业与职业提升。咨询电话:010-65918670。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。