Unity Shader Graph 实现法线扩张外描边:零代码高效方案 1. 项目概述为什么我们需要一个简单的外描边在Unity里做项目尤其是涉及到角色、UI或者一些需要突出显示的游戏元素时描边效果几乎是绕不开的话题。你可能试过用UI组件的Outline组件或者在网上找一些复杂的Shader代码但前者效果僵硬、性能消耗大后者对不熟悉Shader的开发者来说又像天书。我自己在项目里就遇到过这种尴尬一个需要高亮提示的交互道具用UI Outline锯齿严重用传统Shader又调试了半天参数对不上。所以当我们需要一个简单、高效、可控的外描边方案时Shader Graph就成了一个绝佳的选择。它把复杂的着色器代码可视化让你能用连线的方式“画”出效果。今天要聊的这个“Unity 外描边简单实现”核心目标就是不写一行代码完全在Shader Graph中实现一个适用于大多数3D物体的、可自定义颜色和宽度的平滑外描边效果。这个方案特别适合独立开发者、技术美术入门者或者任何想在项目中快速添加高质量描边功能的人。它解决了传统方案的几个痛点避免了多重绘制带来的性能问题实现了真正基于模型法线扩张的平滑描边并且所有参数颜色、宽度、甚至剔除方式都可以在材质面板上实时调整迭代效率极高。2. 核心思路拆解从“描边”到“法线扩张”在动手连节点之前我们必须先搞清楚外描边的本质是什么。很多人第一反应是“在物体外面再画一圈”这个思路没错但具体到GPU渲染层面如何“再画一圈”最经典且高效的实现思路叫做“法线扩张”Normal Extrusion也叫“壳渲染”Shell Rendering的简化版。它的核心思想可以这样通俗理解我们准备两套一模一样的模型网格。第一套正常渲染也就是我们的物体本体。第二套我们沿着每个顶点自身的法线Normal方向把顶点“往外推”一点点。这样第二套网格就变成了一个比原模型大一圈的“壳”。我们只渲染这个“壳”并且通常用一种纯色比如你的描边颜色来渲染它同时通过深度测试等技术确保这个“壳”只出现在物体本体的外围从而形成描边效果。在Shader Graph里我们不需要真的准备两套网格。我们可以通过顶点着色器阶段的操作在一个Pass或通过多Pass渲染中实现这个“扩张”过程。简单来说流程分三步顶点位置偏移在顶点着色器中获取模型顶点的法线信息并让顶点位置沿法线方向向外移动一段距离这个距离就是描边宽度。纯色渲染对这个扩张后的“壳”使用一个单一的、可配置的颜色进行片元着色。深度处理通过恰当的渲染队列和深度测试设置确保这个描边“壳”永远被正确绘制在物体本体的后面从而只显示出外围的一圈。理解了这三步我们再看Shader Graph的实现就会觉得每一个节点都目的明确而不是在盲目连线。3. Shader Graph环境准备与关键节点解析3.1 创建Shader Graph与基本设置打开Unity在Project窗口右键 - Create - Shader - Universal Render Pipeline - Shader Graph (URP)。给它起个名字比如“SimpleOutline”。双击打开创建的Shader Graph文件我们会进入编辑界面。首先关注右侧的Graph Inspector面板。这里有几个关键设置Active Targets确保是“Universal”。这决定了我们使用URP渲染管线。Graph SettingsMaterial保持默认的“Lit”其实不太合适因为我们的描边“壳”不需要复杂的光照计算。更优的做法是在创建时就选择“Unlit”或者之后将主节点换成“Unlit Master”。这里我推荐直接使用Unlit Master节点它只处理颜色和透明度效率更高也更符合描边纯色渲染的需求。你可以在Blackboard主面板上右键删除默认的PBR Master然后从节点菜单搜索添加Unlit Master。Surface选择Transparent。这是因为描边通常需要叠加在场景其他物体之上且可能带有透明度。选择Transparent后渲染队列会自动调整。Blend保持SrcAlpha / OneMinusSrcAlpha即Alpha混合。这样我们可以通过调整颜色Alpha值来控制描边透明度。注意如果你的项目是内置渲染管线Built-in RP创建流程是 Create - Shader - Shader Graph。但核心节点逻辑是相通的。本文以URP为例因为这是Unity当前和未来的主流方向。3.2 理解实现描边的几个核心节点在开始连线前先认识几个老朋友和新朋友Position节点这是我们的操作对象。选择Vertex Position-Object Space。我们将在物体空间下对顶点位置进行修改。Normal Vector节点同样选择Object Space。它提供了每个顶点原始的、未经变换的法线方向是我们进行“扩张”的依据。Multiply节点乘法节点。我们将用它将法线向量与一个代表“宽度”的标量值相乘得到具体的偏移向量。Add节点加法节点。用于将计算出的偏移向量加到原始的顶点位置上。Color节点一个颜色属性用于定义描边的颜色。我们将把它暴露给材质面板。Float节点一个浮点数属性用于定义描边的宽度。同样需要暴露。4. 完整Shader Graph构建流程4.1 构建顶点偏移网络这是实现描边最核心的一步目的是计算出扩张后的顶点位置。创建属性在Blackboard黑板区域点击“”号创建两个属性。OutlineColor类型Color默认值可以设为一个醒目的颜色如红色(1, 0, 0, 1)。OutlineWidth类型Float默认值设为0.01。这个值很小因为是在物体空间通常是米制下的偏移0.01米1厘米对于大多数模型来说是一个不错的起始值。这里有个关键点直接使用这个值描边宽度会受模型本身缩放Scale的影响。一个Scale为10的模型0.01的偏移看起来会非常细。为了解决这个问题一个常见的技巧是让宽度与模型缩放的一个分量比如Y轴或平均缩放关联但这会引入更多复杂性。对于“简单实现”我们通常先接受这个特性或者提示美术同学在建模和放置时注意模型缩放的一致性。计算偏移向量从节点菜单添加Normal Vector节点Space设置为Object。从Blackboard将OutlineWidth属性拖入画布生成一个属性节点。添加一个Multiply节点。将Normal Vector节点的输出口连接到Multiply节点的A端口将OutlineWidth属性节点连接到B端口。这样我们就得到了一个向量法线方向 * 描边宽度。这个向量的长度就是描边宽度方向就是顶点外扩的方向。应用偏移到顶点位置添加Position节点Space设置为Object。添加Add节点。将Position节点的输出连接到Add节点的A端口。将上一步Multiply节点的输出连接到Add节点的B端口。现在Add节点的输出结果就是原始顶点位置 法线方向 * 描边宽度。这就是我们扩张后的新顶点位置。输出到主节点找到你的主节点如果是Unlit Master它通常有Vertex Position输入口。将上一步Add节点的输出连接到主节点的Vertex Position输入口。这一步至关重要它告诉Shader Graph我们修改了顶点的最终位置。4.2 设置片元颜色与透明度顶点位置搞定了接下来要决定这个“壳”渲染成什么颜色。从Blackboard将OutlineColor属性拖入画布。将这个OutlineColor属性节点的输出直接连接到主节点Unlit Master的Color输入口。如果你希望描边有透明度可以调整OutlineColor的Alpha通道A值。因为之前设置了透明混合Alpha值会生效。4.3 配置渲染状态以处理深度如果我们现在创建一个材质球并用上这个Shader可能会发现描边把物体本身都盖住了或者描边在物体内部也显示了出来。这是因为深度测试和渲染队列没设置对。我们需要确保描边“壳”先被绘制写入深度缓冲区然后物体本体再绘制并且本体通过深度测试覆盖掉“壳”位于物体内部的部分。在URP Shader Graph中这主要通过主节点的设置和Depth节点来控制。利用渲染队列因为我们之前将Surface设为TransparentShader会自动使用透明渲染队列。在URP中透明物体通常是在不透明物体之后渲染的。为了确保描边先于本体渲染一个实用的技巧是创建两个材质。一个材质用这个描边Shader另一个材质用物体原本的Shader如Lit。然后通过脚本控制渲染顺序或者手动调整材质的Render Queue优先级描边材质用更小的Queue值如1999本体用2000。但在单个Shader内实现“单Pass”描边更优雅的方式是下面这个。深度偏移Depth Offset这是一个更强大的技巧。我们可以让描边“壳”在深度上稍微向后偏移一点这样即使它和本体在同一个渲染队列本体也能通过深度测试覆盖它。在Shader Graph画布中添加一个Depth节点。它会输出当前片元的深度值。添加一个Subtract减法节点。将Depth节点连接到Subtract的A口。创建一个新的Float属性命名为DepthBias默认值设为0.001拖入画布。将这个DepthBias属性节点连接到Subtract的B口。最后将Subtract节点的输出连接到主节点如果有Fragment Depth输入口的Fragment Depth。注意Unlit Master节点可能没有直接的深度输入。对于URP更标准的做法是使用Universal Master节点或保持PBR Master但关闭光照它提供了Depth Offset输入口。你可以直接将一个很小的负值如-0.001连接到Depth Offset也能达到类似“让描边往后退一点”的效果。深度写入与测试在主节点的Graph Inspector或节点属性中我们可以找到深度相关的设置。Depth Write对于描边通常可以保持On。让它写入深度有助于后续物体的正确遮挡。Depth Test必须设置为Less EqualLEqual。这意味着只有当片元深度值小于或等于深度缓冲区中的值时才会被绘制。结合Depth Offset让描边深度稍微变大更远就能保证物体本体深度更小/更近绘制时会覆盖掉同一位置的描边部分。经过以上设置一个基础但可用的外描边Shader Graph就完成了。保存你的Graph。5. 材质应用与参数调试实战5.1 创建并应用材质在Project窗口右键 - Create - Material命名为“Mat_Outline”。选中这个材质在Inspector面板顶部将Shader选择为我们刚刚创建的SimpleOutline或者你起的名字。将这个材质拖拽到场景中的一个3D物体比如一个Cube或一个角色模型上。此时你可能看不到任何效果因为默认的OutlineWidth (0.01) 可能太小或者物体是纯色描边颜色融入了背景。5.2 参数调试技巧现在在材质的Inspector面板你应该能看到我们暴露的Outline Color和Outline Width属性。调整Outline Width这是最关键的参数。尝试将值从0.01逐步调大比如0.02 0.05 0.1。你会看到模型的边缘出现了一条彩色的线。注意观察在模型折角尖锐的地方描边可能会断开或变形。这是因为顶点法线在硬边Hard Edge处是不连续的扩张会导致裂缝。这是法线扩张法的一个固有缺陷。对于低模这个问题更明显。调整Outline Color选择一个与模型本体对比强烈的颜色比如模型是灰色就选亮黄或亮蓝。也可以调整Alpha值实现半透明描边会有一种发光的效果。DepthBias调试如果你实现了深度偏移调整这个值例如0.0001到0.01之间可以微调描边与本体之间的前后关系。值越大描边“退”得越远可能在某些视角下完全被本体覆盖值太小则描边可能“钻”到本体前面造成穿帮。需要根据具体模型和摄像机视角进行微调。5.3 处理硬边与平滑问题如前所述模型硬边会导致描边不连续。这里有几种应对策略模型预处理推荐这是最根本的解决方法。让建模人员在导出模型前软化硬边Smooth Edge。在大多数3D建模软件中这被称为设置“平滑组”或“自动平滑”。一个完全平滑的模型其顶点法线是连续插值的描边效果会非常均匀流畅。在Shader中平滑法线这是一个进阶技巧。我们可以不使用原始的顶点法线而是在Shader Graph中通过相邻顶点的信息计算一个“平滑后的法线”。但这需要访问更多的网格数据在Shader Graph中实现较为复杂通常需要编写自定义节点Custom Function Node这超出了“简单实现”的范围。但对于追求完美效果的项目这是一个值得研究的方向。接受风格化效果对于一些卡通、低多边形的艺术风格有断点的描边反而可能成为一种特色。你可以通过调整宽度让断点看起来像是故意设计的笔触效果。6. 性能考量与优化建议这个简单的法线扩张描边性能消耗主要在哪里顶点变换每个顶点都进行了一次加法运算计算量可以忽略不计。Overdraw描边“壳”覆盖的屏幕像素区域会比原模型大一圈。这意味着GPU需要为这些额外的像素执行片元着色器。如果描边很粗或者模型在屏幕上很大这部分开销会增加。优化建议在保证视觉效果的前提下尽量使用较小的OutlineWidth。透明混合如果使用了透明度会开启混合计算比不透明渲染更耗性能。优化建议如果不需要透明描边尽量将颜色Alpha设为1并将Surface类型改为Opaque同时调整深度测试确保正确遮挡。多材质与Draw Call如果采用“描边材质”“本体材质”的双材质方案意味着同一个物体会被渲染两次产生两个Draw Call。对于大量需要描边的物体这会显著增加CPU的渲染准备负担。优化建议对于大量、同类的物体比如一群小兵可以考虑将描边功能集成到它们原本的Shader中通过一个开关_OutlineEnabled来控制这样不会增加额外的Draw Call。这就是所谓的“单Shader双Pass”或“单Pass模板测试”等更高级的方案但实现复杂度也更高。对于我们这个“简单实现”的目标当前方案在中小型项目、主要角色或少量关键物体上使用性能是完全可接受的。它的最大优势是开发效率高、调整直观。7. 常见问题与排查实录在实际使用中你肯定会遇到一些奇怪的现象。下面是我踩过坑之后总结的排查清单问题现象可能原因解决方案完全看不到描边1.OutlineWidth值太小。2. 模型缩放Scale极大导致0.01的偏移相对比例太小。3. 描边颜色与背景或模型颜色太接近。4. 深度测试设置错误描边被本体完全覆盖。1. 调大OutlineWidth(如0.1)。2. 在材质上调整宽度或考虑在Shader中将宽度与模型缩放倒数相乘进行补偿。3. 使用对比强烈的颜色。4. 检查主节点的Depth Test是否为LEqual并尝试调整DepthBias或渲染队列。描边闪烁Z-fighting描边与物体本体的深度值过于接近在不同像素上深度测试结果随机。增加DepthBias的值让描边明确地“退后”一些。描边内部也可见深度测试设置可能为Always或者DepthBias为正值导致描边更靠前。确保Depth Test为LEqual并检查DepthBias应为很小的负值如果节点是Depth Offset或通过减法使深度变大。描边在硬边处断开模型存在硬边顶点法线不连续。软化模型硬边。或在建模时对需要描边的区域专门处理平滑组。描边粗细不均匀模型本身的顶点分布不均匀。稀疏区域的顶点扩张后三角形被拉得很大视觉上描边变粗。这是顶点扩张法的另一个局限。可以考虑使用屏幕空间方法如Sobel边缘检测但那完全是另一种实现更耗性能但稳定性与模型无关。对于简单实现可以尝试在建模时保持网格分布相对均匀。移动端上效果差或性能低透明混合开销大或Overdraw严重。1. 尽可能使用不透明描边Alpha1, SurfaceOpaque。2. 严格控制描边宽度。3. 对于低端机可以考虑降低渲染分辨率或关闭此效果。一个关键的实操心得调试Shader Graph时善用Preview窗口。你可以将任何一个中间节点比如计算后的法线、偏移向量的输出端口拖拽到Preview窗口实时查看它的值表现为灰度图或向量图。这是定位问题最快的方式比如你可以检查Normal Vector节点输出是否正确就能立刻知道硬边问题是不是法线数据导致的。8. 方案扩展与变体思路掌握了基础版本后你可以根据项目需求轻松扩展出更酷的效果渐变描边不使用单一的OutlineColor而是根据顶点法线与视角方向的点积Dot Product来计算一个从Color A到Color B的渐变。这可以用来模拟边缘光Rim Light效果让描边在侧边缘和正对视角的边缘有颜色变化。噪声扰动描边在计算偏移向量时不只是沿法线方向可以叠加一个基于模型空间或屏幕空间坐标的噪声Noise纹理。这样可以让描边产生一种手绘、墨水晕开或能量波动的动态效果。仅对特定区域描边通过一张额外的贴图如顶点色或一张遮罩贴图来定义模型的哪些部分需要描边。在Shader Graph中用这张贴图的值去乘以OutlineWidth在不需要描边的地方将宽度设为0。动画化描边将OutlineWidth或OutlineColor与Time节点连接可以做出描边脉动、呼吸、颜色循环的动画效果非常适合用于技能提示、可交互物品高亮。这个简单的Shader Graph外描边方案就像给你了一把瑞士军刀的基础刀片。它可能不是最强大、最完美的工具但它绝对是最容易上手、最能快速解决问题的那个。从理解原理到连出节点再到调试参数和排查问题整个流程走一遍你不仅得到了一个可用的描边效果更重要的是理解了实时渲染中一个经典效果背后的图形学思想。下次当你再看到任何描边效果时你大概都能在心里勾勒出它的实现节点图了。