1. 项目概述为什么2D角色需要“不规则描边”在2D游戏开发里给角色加描边是个老生常谈的需求。传统的做法比如用多个Mesh往外扩或者用后处理Post-Processing的Sobel算子大家应该都试过。但这些方法有个通病描边永远是均匀的、硬邦邦的一个圈就像用粗记号笔沿着角色轮廓描了一遍。这种“硬边”放在卡通渲染或者一些风格化项目里会显得特别呆板缺乏手绘的灵动感和艺术表现力。我最近在做一个2D横版动作游戏角色是手绘风格原画师在设计时特意强调了轮廓线的粗细变化和笔触感。如果用传统均匀描边这种精心设计的美术细节就全毁了。于是我开始琢磨怎么在Unity里用ShaderGraph实现一种能模拟手绘感的“不规则艺术描边”。核心目标就一个让描边不再是死板的几何外扩而是能根据角色材质、颜色甚至我们自己定义的规则产生粗细、透明度甚至颜色都动态变化的“活”的轮廓线。这不仅仅是技术实现更是为了服务美术表达。想象一下角色在暗处时描边变淡、受击时描边闪烁红色、快速移动时描边产生拖曳残影——这些效果都能极大地增强游戏的视觉反馈和沉浸感。而ShaderGraph尤其是Unlit Graph以其直观的节点化操作成为了实现这个想法最合适的工具。它让我们这些不一定精通HLSL的开发者也能深度参与渲染管线的定制。2. 核心思路拆解从“几何外扩”到“艺术化处理”传统描边的本质是“几何操作”。无论是顶点着色器里将顶点沿法线方向挤出Vertex Offset还是在片元着色器里采样周围像素判断深度/法线其核心逻辑都是基于一个均匀的“偏移量”_OutlineWidth。这就决定了它只能是规则的。我们要做的“不规则艺术描边”则需要将思维从“几何”转向“纹理”和“数据驱动”。我的核心思路分为三步第一步获取轮廓信息。这是基础。我们不再单纯依赖法线或深度而是通过对比角色纹理Albedo的Alpha通道或特定颜色与背景的差异来识别边缘。简单说就是判断当前像素是不是从“透明”或背景色到“不透明”角色颜色的边界点。第二步引入“不规则”因子。这是关键。我们需要一个可控的“噪声”或“图案”来干扰传统的均匀偏移。这个因子可以是一张噪波贴图Noise Texture也可以是基于UV、时间、顶点颜色甚至自定义参数的某种计算函数。它的输出值将用来动态调制描边的宽度、强度甚至偏移方向。第三步进行“艺术化”合成。将前两步的结果结合并赋予描边更多的动态属性。比如用另一张渐变贴图Ramp Texture根据角色局部亮度来决定描边颜色或者根据角色动作状态通过脚本传递参数来改变描边的闪烁频率和波形。最终我们构建的ShaderGraph应该是一个数据流管道输入是基础纹理和一系列控制参数经过“轮廓检测”和“不规则化处理”两个核心模块输出最终带有艺术化描边的片元颜色。使用Unlit Graph是因为对于2D角色我们通常不需要复杂的光照模型Unlit Graph足够轻量且完全可控让我们能聚焦于颜色和轮廓本身的处理。3. 工具选型与ShaderGraph基础环境搭建工欲善其事必先利其器。要实现这个效果我们需要确保Unity工程和ShaderGraph环境配置正确。3.1 Unity版本与渲染管线选择这个项目我使用的是Unity 2022.3 LTS版本。长期支持版稳定性最好避免遇到一些稀奇古怪的编辑器Bug。对于2D项目渲染管线有两种主流选择URP通用渲染管线这是当前和未来的主流对移动端和跨平台支持更好ShaderGraph集成度最高。如果你的项目是新的或者考虑多平台发布强烈建议使用URP。Built-in Render Pipeline内置渲染管线传统管线如果你在维护一个老项目或者需要使用一些尚未迁移到URP的特定Asset Store资源可能还需要用它。ShaderGraph在内置管线中也能用但可能需要安装额外的兼容包。我这里以URP为例进行说明因为它的工作流更现代。在创建项目时请直接选择“Universal Render Pipeline”模板。如果是在已有项目中需要通过Package Manager安装“Universal RP”包并创建URP Asset和渲染器数据Renderer Data。3.2 创建Unlit Shader Graph在Project窗口中右键 -Create - Shader Graph - URP - Unlit Graph。我将其命名为“SG_2D_ArtisticOutline”。双击打开这个ShaderGraph文件我们会进入Shader Graph编辑器窗口。3.3 认识Unlit Graph的基础节点在开始连线之前先快速过一下几个最关键的节点这是我们构建效果的积木块Sample Texture 2D最常用的节点用于采样角色主纹理Albedo。我们需要它的RGBA输出特别是A通道Alpha。Fragment Position提供当前片元在屏幕空间中的位置XY坐标可以用来做基于屏幕坐标的效果。Time输出自游戏开始以来的时间t是制作动态效果如闪烁、流动的必备输入。Noise Texture一种程序化纹理节点可以生成柏林噪声Perlin Noise、单纯形噪声Simplex Noise等是制造“不规则”感的核心。Custom Function如果现有节点无法满足复杂计算我们可以自己写一小段HLSL代码封装成自定义函数节点。这是实现高级轮廓算法的大杀器。注意在ShaderGraph中颜色Color和向量Vector数据通常以四维RGBA或XYZW形式流动。很多数学运算节点如Add, Multiply会自动进行逐分量计算理解这一点对后续调试很重要。4. 核心模块一动态轮廓检测算法实现传统后处理描边依赖Camera的DepthNormal纹理但在纯2D、单层精灵的场景中不适用。我们的轮廓检测必须基于角色自身的纹理信息。4.1 基于Alpha通道的边缘检测这是最直接的方法适用于带有透明通道Alpha Channel的精灵纹理Sprite。在ShaderGraph中创建一个Sample Texture 2D节点采样你的角色主纹理。将其RGB输出连接到主节点的Base ColorAAlpha输出单独拉出来。轮廓检测的原理是如果一个像素自身不透明但其上下左右的某个邻居像素透明那么这个像素就在轮廓上。我们需要获取当前像素周围像素的Alpha值。实现这个“采样邻居”操作需要用到DDX和DDY节点。DDX和DDY是屏幕空间偏导数可以近似地给出当前像素在X和Y方向上与相邻像素的差值。我们可以用它们来估算边缘。具体操作将主纹理Alpha通道的输出同时连接到DDX和DDY节点的输入。然后计算sqrt(DDX^2 DDY^2)。这个值在颜色均匀的区域接近0在Alpha变化剧烈即边缘的区域会得到一个较大的值。用一个Step或Smoothstep节点对这个值进行阈值化处理。Step(threshold, input)会输出0或1产生硬边缘Smoothstep(edge0, edge1, input)会产生平滑过渡让描边边缘有柔和的衰减视觉效果更自然。我通常用Smoothstep。// 节点操作示意非代码是节点连接逻辑 Alpha - [DDX] - dx Alpha - [DDY] - dy [Multiply](dx, dx) - dx2 [Multiply](dy, dy) - dy2 [Add](dx2, dy2) - gradientSqr [Square Root](gradientSqr) - gradientMagnitude [Smoothstep](_EdgeMin, _EdgeMax, gradientMagnitude) - edgeMask这里的edgeMask就是一个黑白掩码Mask白色值1代表轮廓区域黑色值0代表非轮廓区域。_EdgeMin和_EdgeMax是暴露给材质球的参数用于控制轮廓检测的灵敏度。4.2 基于颜色差异的边缘检测无Alpha通道方案如果你的精灵纹理没有Alpha通道例如是一张矩形背景图上的角色或者你想实现更特别的轮廓比如只在内色块边缘描边可以采用基于RGB颜色差异的方案。同样采样主纹理RGB。分别计算当前像素RGB与上、下、左、右四个偏移位置像素RGB的颜色距离。颜色距离可以用distance(color1, color2)函数节点计算它返回两个颜色在RGB空间中的欧几里得距离。取这四个距离中的最大值或者将它们相加。这个值越大说明颜色变化越剧烈越可能是边缘。同样用Smoothstep进行阈值化生成边缘掩码。实操心得基于颜色的检测对纹理质量敏感。如果原图有抗锯齿或噪声可能会检测出大量“假边缘”。通常需要配合一个微小的**模糊Blur**预处理来平滑纹理或者提高检测阈值。这可以通过在采样前对UV坐标进行轻微扰动来实现但会增加计算量。4.3 融合与优化在实际使用中我经常将Alpha检测和颜色检测的结果用Max或Add节点混合起来这样能确保在各种情况下都能稳定地捕捉到轮廓。同时为了性能考虑可以将这些采样操作合并尽可能重用采样结果。5. 核心模块二不规则化因子的设计与应用拿到标准的轮廓掩码edgeMask后我们要对它进行“破坏”使其变得不规则。5.1 使用噪声纹理驱动宽度变化这是实现不规则感最有效的手段。添加一个Sample Texture 2D节点采样一张噪波贴图。推荐使用无缝平铺Seamless Tiling的柏林噪声或瓦片噪声Tileable Noise贴图。将噪波贴图的某个通道通常用R或G通道的输出映射到一个合适的范围比如[-0.5, 0.5]。这可以通过(noise * 2 - 1) * scale来实现其中scale是一个控制强度的新参数_NoiseStrength。将这个噪声值加到我们之前用于控制描边宽度的基础参数_OutlineWidth上。// 节点逻辑 _BaseWidth - [Add] - finalWidth ↗ noiseValue - [Multiply] - _NoiseStrength关键一步将噪声与屏幕空间或世界空间坐标关联。如果我们直接用UV采样噪声那么描边的不规则图案会“粘”在角色的纹理上角色移动时噪声图案不会变看起来不自然。更优的方案是使用屏幕空间位置Screen Position或物体世界位置World Position来采样噪声。使用Screen Position节点取其XY分量除以屏幕尺寸进行归一化然后作为UV去采样噪声图。这样不规则图案是基于屏幕位置的角色在屏幕上移动时其描边的噪波图案会随之变化更有动态感。使用Position节点选择World空间取其XY或XZ分量对于2D角色作为UV。这样不规则图案是基于角色在世界中的位置适合需要与场景互动的效果。5.2 利用顶点颜色或自定义数据流对于需要更精细控制的情况比如希望角色衣服的描边和头发描边有不同的粗细变化我们可以利用顶点颜色Vertex Color或UV通道的第二组坐标UV2。在3D建模软件或Unity的Sprite Editor中预先将需要不同描边强度的区域绘制到顶点颜色如R通道或分配到UV2。在ShaderGraph中使用Vertex Color节点或UV节点选择UV2通道读取这些数据。用这些数据去调制噪声强度或直接调制描边宽度。这样美术人员可以通过修改模型数据来直接控制描边的艺术效果实现数据驱动的艺术风格。5.3 时间动态干扰让不规则因子动起来描边就“活”了。引入Time节点。将Time输出与屏幕位置或世界位置相加后再作为UV去采样噪声图。这样噪声图案会随着时间缓慢流动。// 节点逻辑流动的噪声 [Screen Position].xy - [Divide] by Screen Params - normalizedScreenUV [Time].t - [Multiply] by _FlowSpeed - timeOffset [Add](normalizedScreenUV, timeOffset) - flowingUV [Sample Texture 2D](NoiseTex, flowingUV) - animatedNoise更进一步可以用Time驱动一个Sine或Triangle Wave节点其输出作为噪声强度的周期性调制从而实现描边宽度有节奏地脉动效果非常适合用于表现角色呼吸、蓄力或心跳状态。6. 完整ShaderGraph组装与参数调试将前两个模块的输出结合起来我们就能合成最终的描边。6.1 合成描边颜色与主体颜色假设我们从“轮廓检测模块”得到了edgeMask0到1的值1代表轮廓“不规则化模块”得到了一个动态的宽度/强度系数outlineIntensity。计算最终描边区域finalOutlineArea edgeMask * outlineIntensity。用Saturate节点限制其在0-1范围。定义描边颜色_OutlineColor。你可以让它是一个固定颜色也可以让它更复杂——例如用一张渐变贴图Ramp Texture根据角色主纹理的亮度Luminance来采样不同的描边色。混合计算最终的片元颜色Final Color lerp(mainTextureColor, _OutlineColor, finalOutlineArea)。lerp是线性插值节点根据第三个参数0到1在前两个颜色间混合。将Final Color连接到Unlit Master节点的Color端口。6.2 关键参数暴露与组织为了让美术和策划方便调节必须将关键参数暴露为Material Property。在Blackboard面板中创建_MainTex(Texture2D): 角色主纹理。_OutlineColor(Color): 描边基础颜色。_BaseWidth(Float, 默认0.05): 基础描边宽度/强度。_EdgeMin/_EdgeMax(Float): 轮廓检测的平滑步进范围。_NoiseTex(Texture2D): 噪波贴图。_NoiseStrength(Float, 默认0.1): 噪声影响强度。_FlowSpeed(Float, 默认0.1): 噪声流动速度。_PulseFrequency(Float, 默认1.0): 脉动频率如果做了时间脉动。6.3 调试技巧与视觉化ShaderGraph开发一半是连线一半是调试。多用预览窗口将中间变量如edgeMask,noiseValue,finalOutlineArea临时连接到主输出节点的Color可以直观地看到每一步生成的结果是什么快速定位问题。使用Split和Combine节点方便地查看和操作颜色或向量的各个分量。参数调节策略先调通基础轮廓关掉噪声调_EdgeMin/Max确保轮廓检测准确。然后加入噪声从小强度_NoiseStrength 0.05开始慢慢增加避免效果过于夸张破坏轮廓识别。最后再加入时间动态微调速度。7. 性能优化与多角色适配策略一个好看的Shader如果性能很差也无法在项目中实际使用。7.1 性能开销分析我们这个Shader的主要性能开销点在于纹理采样次数至少2次主纹理噪声纹理。如果用了更复杂的轮廓检测如多次采样周围像素次数会更多。计算复杂度DDX/DDY、distance、sqrt、smoothstep以及噪声计算都属于中等开销操作。Overdraw描边效果通常会导致像素被多绘制一次角色本体和描边区域增加片元着色器负担。7.2 针对性优化方案纹理采样优化确保主纹理和噪声纹理都开启了合适的压缩格式如ASTC并设置了合理的Mipmap。如果可能将噪声图做成小尺寸如64x64或128x128因为噪声不需要高频细节。考虑将噪声图与另一张控制贴图如边缘硬度贴图打包到同一张纹理的不同通道RGBA减少采样指令。计算优化用step代替smoothstep可以获得更硬的边缘但计算更简单。如果美术可以接受这是一个有效的优化。避免在片元着色器中进行复杂的循环或分支判断。对于基于颜色的边缘检测distance计算开销较大。可以尝试用abs(color1.r - color2.r) abs(color1.g - color2.g) abs(color1.b - color2.b)这种曼哈顿距离来近似计算量更小。Draw Call与合批对于2D精灵确保使用相同的材质Material和纹理图集Sprite Atlas以促进Unity的静态/动态合批减少Draw Call。如果不同角色需要不同的描边参数如颜色、宽度不要为每个角色创建单独的材质实例。应该使用Material Property Blocks在运行时动态修改材质参数这样它们仍然可以合批。7.3 适配不同风格的角色一套参数不可能适合所有角色。我们需要建立一个可配置的体系。创建材质预设Material Presets针对“主角”、“敌人”、“NPC”、“场景物体”等不同类别创建几个基础材质预设预设好不同的_OutlineColor、_BaseWidth和_NoiseStrength。脚本驱动参数编写一个C#脚本如OutlineController挂在角色上。脚本中可以定义如“正常状态”、“受击状态”、“隐身状态”等每个状态对应一组描边参数颜色、强度、脉动速度。在状态切换时通过MaterialPropertyBlock动态更新角色渲染器上的材质属性。// 伪代码示例 public class OutlineController : MonoBehaviour { private Renderer renderer; private MaterialPropertyBlock mpb; public Color outlineColorNormal; public Color outlineColorHurt; public float hurtPulseSpeed; void Start() { renderer GetComponentRenderer(); mpb new MaterialPropertyBlock(); renderer.GetPropertyBlock(mpb); mpb.SetColor(_OutlineColor, outlineColorNormal); renderer.SetPropertyBlock(mpb); } public void OnHurt() { renderer.GetPropertyBlock(mpb); mpb.SetColor(_OutlineColor, outlineColorHurt); // 可以同时设置其他参数如触发一个脉动动画 renderer.SetPropertyBlock(mpb); } }与动画系统集成在角色的Animation Clip或Animator Controller中直接使用动画曲线Animation Curve来驱动描边材质参数。这样动画师可以精细地控制某一帧描边变粗、变红然后恢复实现与动作完美匹配的视觉反馈。8. 常见问题、排查技巧与效果扩展8.1 常见问题速查表问题现象可能原因排查与解决思路完全没有描边1. 轮廓检测模块输出始终为0。2. 描边颜色与主体颜色太接近或Alpha为0。3. 主纹理导入设置错误Alpha通道未正确识别。1. 将edgeMask连到Color输出查看是否为全黑。检查_EdgeMin/Max阈值是否设置不当如MinMax。2. 临时将_OutlineColor设为亮红色检查是否可见。3. 在Project窗口选中纹理在Inspector中检查“Alpha Source”和“Alpha Is Transparency”设置。描边断断续续、不连续1. 轮廓检测阈值_EdgeMax过高只检测到最强烈的边缘。2. 噪声强度_NoiseStrength过大将本已微弱的边缘信号淹没了。3. 纹理本身有抗锯齿边缘Alpha是渐变的。1. 降低_EdgeMax值或增大_BaseWidth。2. 降低_NoiseStrength先确保基础轮廓连续。3. 尝试在轮廓检测前对Alpha通道做一个轻微的“硬化”处理例如用step(0.5, Alpha)。描边有锯齿Aliasing1. 使用了step函数产生硬边界。2. 噪声纹理分辨率过低且过滤模式Filter Mode为Point。1. 换用smoothstep函数并给一个合适的平滑区间。2. 将噪声纹理的Filter Mode改为Bilinear或Trilinear。描边在角色移动时闪烁或抖动1. 噪声采样基于UV且纹理Wrap Mode为Clamp时在边界处采样不稳定。2. 基于屏幕空间的噪声在相机抖动时会导致描边整体“滑动”。1. 确保噪声纹理为无缝平铺SeamlessWrap Mode设为Repeat。2. 这是基于屏幕空间效果的固有特性。如果不需要可改用基于世界空间或物体空间的采样。性能开销过大1. 纹理采样次数过多。2. 片元着色器计算过于复杂。3. 角色覆盖屏幕区域很大导致Overdraw严重。1. 检查是否有多余的采样节点尝试合并采样。2. 简化轮廓检测算法或用查找表LUT预计算。3. 考虑对远处或非焦点角色使用简化版Shader如关闭噪声。8.2 效果扩展思路基础的不规则描边实现后可以在此基础上玩出很多花样多色描边不用单一_OutlineColor而是用一张渐变贴图根据边缘检测的强度gradientMagnitude或噪声值来采样颜色实现彩虹色或自定义渐变描边。内外双描边运行两次轮廓检测算法第一次用较大的宽度检测“外轮廓”第二次用较小的宽度和不同的颜色检测“内轮廓”角色内部色块边缘然后将两者叠加层次感立刻提升。动态变形描边将噪声值不仅用于调制宽度还用于调制轮廓点的偏移方向。通过构造一个由噪声生成的2D向量Vector2去扰动采样UV可以让描边产生类似墨水晕开或毛茸茸的变形效果非常适合表现火焰、幽灵等特效角色。与后处理结合将本Shader生成的描边信息finalOutlineArea写入到自定义的渲染缓冲区如使用SetGlobalTexture然后在后处理栈Post-processing Stack中进行二次处理比如做高斯模糊让描边变柔和或者叠加全屏的扭曲、发光效果实现更复杂的视觉融合。实现这些扩展效果本质上就是在我们构建的数据流管道中插入新的处理节点。ShaderGraph的强大之处就在于这些想法都可以通过连线和调试快速验证极大降低了图形编程的尝试成本。