1. 项目概述与核心思路拆解最近在复刻一些经典游戏的美术风格时对《只狼影逝二度》里那个极具辨识度的“危”字提示效果念念不忘。当敌人即将发动无法格挡的致命攻击时角色身上会瞬间迸发出一圈醒目的红色高亮边缘视觉冲击力极强同时信息传达极其清晰。这种效果在技术上属于一种“基于距离和视角的边缘检测与高亮”非常适合用Unity的ShaderGraph来实现尤其是在URP通用渲染管线下我们可以摆脱手写Shader的繁琐用节点可视化地构建这个逻辑。这个效果的核心目标很明确让场景中特定的物体比如敌人在玩家角色靠近到一定距离并且该物体处于玩家视角的边缘区域时自动浮现出一圈动态的高亮边缘。它融合了两种关键的空间信息一是物体与摄像机之间的物理距离Scene Depth二是物体表面相对于摄像机的观察方向View Direction。通过ShaderGraph我们可以巧妙地获取并运算这两种数据最终驱动一个自发光Emission或叠加颜色效果。为什么选择ShaderGraph和URP首先ShaderGraph降低了着色器编程的门槛让美术和TA技术美术能更直观地参与效果制作与迭代。其次URP管线对后处理与屏幕空间数据的支持已经非常成熟我们需要的深度纹理Depth Texture和摄像机向量等信息都是现成可用的。这个项目不仅是为了复现一个酷炫的效果更是为了深入理解如何在ShaderGraph中驾驭Scene Depth和View Direction这两个强大但有时令人困惑的节点掌握将空间信息转化为视觉表现的通用方法论。2. 核心节点原理深度解析在动手连接节点之前必须吃透两个核心节点的“脾气”否则很容易得到莫名其妙的结果。很多ShaderGraph新手在这里栽跟头就是因为对它们输出的数据范围、空间坐标系缺乏清晰认知。2.1 Scene Depth节点你的“距离雷达”Scene Depth节点是本次效果的基石。它输出的不是世界空间的绝对坐标而是当前渲染像素点在摄像机视角下的深度值。这个值通常是非线性的尤其是在透视投影下其范围在近裁剪面Near Clip Plane处接近0在远裁剪面Far Clip Plane处接近1。注意URP中默认不开启深度纹理。你必须在URP Asset的Renderer设置中为你使用的Renderer Data如Universal Renderer Data勾选“Depth Texture”选项。否则Scene Depth节点将无法获取有效数据你的效果会完全失效。这是实操中的第一个大坑。当我们对场景中的一个物体使用Scene Depth节点时节点返回的是该物体表面当前被渲染的片段Fragment所对应的屏幕像素位置的深度值。这意味着如果你在一个物体的Shader中采样Scene Depth你采样到的不一定是该物体本身的深度而是那个屏幕位置所有物体中最靠前离相机最近的物体的深度。这一点至关重要。举个例子一个红色的方块放在一个绿色地板上。在红色方块的Shader里我们用Scene Depth节点。对于被红色方块覆盖的像素Scene Depth返回的是红色方块自己的深度因为它是离相机最近的。但对于红色方块边缘的像素情况可能不同吗实际上在同一个Draw Call渲染红色方块时其片元着色器处理的像素区域就是红色方块覆盖的区域所以采样到的深度就是它自身的深度。真正的差异发生在后处理或屏幕空间效果中那里是对整个屏幕图像进行采样。不过在我们的案例中效果是附着在特定物体敌人上的所以我们是在物体的材质Shader中工作通常采样的是自身深度。但为了做边缘检测我们需要一个“参考深度”这个我们后面会详细说。为了将非线性深度转换回有实际意义的线性距离以米为单位我们通常需要配合使用Linear01Depth节点。这个节点接收一个深度纹理采样值输出一个范围在[0, 1]的线性深度值其中1代表远裁剪面距离。再乘以摄像机的远裁剪面距离_ProjectionParams.z就能得到以米为单位的绝对线性距离。我们的距离检测逻辑将基于这个线性距离来建立。2.2 View Direction节点你的“视角测量仪”View Direction节点输出的是一个从当前着色点指向摄像机的向量。关键在于这个向量的坐标系取决于你如何配置节点。节点有一个“Space”下拉菜单通常有“World”、“View”、“Tangent”等选项。对于我们的效果“World”空间是最直观的选择。在World空间下节点输出的是一个在世界坐标系中定义的、从物体表面点指向摄像机位置的向量。这个向量本身包含了方向信息但其长度模长并不是距离距离需要单独计算通常用物体世界坐标减去摄像机世界坐标再取长度。我们需要的不是向量的具体指向而是这个向量与另一个关键向量——摄像机的前向向量Forward Vector——之间的夹角关系。想象一下当你正对着一个物体时从物体指向你的向量View Direction和你面朝的方向Camera Forward几乎是完全相反的夹角接近180度。当你看向别处物体处于你视野边缘时这两个向量的夹角就会变小。这个夹角的余弦值即点积结果就是我们判断“是否处于视野边缘”的完美指标。在ShaderGraph中我们可以通过Transform节点将“World”空间的View Direction转换到“View”空间也叫摄像机空间。在View空间里摄像机位于原点(0,0,0)其前向轴是-Z轴。此时View Direction向量在View空间下的Z分量就直观地反映了该点与摄像机正方向的偏离程度。不过更通用的做法是直接在世界空间计算点积。3. 效果实现全流程拆解理解了原理我们就可以开始搭建节点网络了。整个Shader Graph可以划分为几个功能模块数据准备模块、距离检测模块、视角检测模块、效果合成模块。3.1 数据准备与参数化首先我们需要创建一些可调节的参数让效果能在不同场景和需求下灵活调整。在Blackboard中创建以下参数_DangerDistance(Float): 危险感知的触发距离。当物体与摄像机距离小于此值时效果开始显现。例如设置为5米。_EdgeWidth(Float): 高亮边缘的宽度控制。通常是一个0到1之间的值用于控制边缘检测的灵敏度。_FadeRange(Float): 效果从无到有的平滑过渡范围。避免效果在阈值处硬切显得生硬。_RimColor(Color): 高亮边缘的颜色比如《只狼》中标志性的红色。_Intensity(Float): 高亮效果的强度。接下来获取系统数据摄像机世界位置通过Camera Position节点获取。物体表面世界位置通过Position节点并将Space设置为World。摄像机前向向量World Space可以通过Transform节点将(0,0,1)或(0,0,-1)从View空间转换到World空间来获得准确值更简单的方法是使用Direction节点创建(0,0,1)然后变换但URP Shader Graph通常提供了Camera Direction节点可以直接选择World Forward。如果找不到一个可靠的方法是Normalize(Camera Position - Object World Position)得到的向量指向摄像机但其反方向才是摄像机前向的近似在距离较远时误差较小。对于精度要求高的建议从C#脚本传递Camera.main.transform.forward到Shader。3.2 距离检测模块实现这个模块负责计算“物体距离摄像机有多近”并生成一个基于距离的权重0到1。计算线性距离Distance(Camera Position, Object World Position)。这会得到世界空间下的直线距离。计算距离因子使用smoothstep函数实现平滑过渡。smoothstep接收三个参数边缘0边缘1和输入值。当输入值小于边缘0时返回0大于边缘1时返回1在中间则进行平滑插值。 我们的逻辑是当距离小于_DangerDistance时效果最强因子为1当距离大于_DangerDistance _FadeRange时效果消失因子为0。因此我们需要一个反向的smoothstep。距离因子 1 - smoothstep(_DangerDistance, _DangerDistance _FadeRange, 实际距离)在ShaderGraph中可以用一个Smoothstep节点然后将它的输出用One Minus节点反转。3.3 视角边缘检测模块实现这个模块负责计算“物体表面某点是否处于摄像机视野的边缘”同样输出一个权重0到1。获取View Direction添加View Direction节点Space设置为World。获取摄像机前向向量如前所述使用可靠方法获取Camera Forward (World)向量。计算点积使用Dot Product节点输入A为Camera Forward输入B为View Direction。注意这里的View Direction是从表面指向摄像机的而Camera Forward是摄像机指向前的。当表面点正对摄像机时这两个向量方向相反点积约为-1。当表面点处于视野边缘即View Direction与Camera Forward垂直时点积接近0。映射到边缘因子我们希望点积值接近0边缘时输出1高亮点积值接近-1或1中心时输出0不高亮。由于我们的点积结果在[-1, 0]区间因为两向量夹角在90度到180度之间我们可以取点积的绝对值然后用1减去它再进行幂运算来控制边缘宽度。// 假设 dotResult 是点积输出 float edge 1 - abs(dotResult); edge pow(edge, _EdgeWidth); // _EdgeWidth 越大边缘越窄在ShaderGraph中abs节点取绝对值Power节点进行幂运算。_EdgeWidth参数可以控制边缘的“硬度”值越大只有非常边缘的部分才会亮起。3.4 效果合成与输出将距离因子和边缘因子相乘得到最终的效果强度掩码Mask。这个掩码结合了“足够近”和“处于视野边缘”两个条件。最终强度 距离因子 * 边缘因子然后用这个强度去调制高亮颜色高亮颜色 _RimColor * _Intensity * 最终强度最后如何将这个颜色应用到物体上有两种主流方式叠加到自发光Emission这是最常用、性能较好的方法。将计算出的高亮颜色加到材质原有的Emission通道上。在ShaderGraph的Master Stack或URP下的Master Node中找到Emission输入端口将我们计算的颜色连上去。记得确保材质的自发光强度Global Illumination设置为“Realtime”或“Baked”否则在场景中可能看不到效果。颜色叠加Additive Overlay通过Mix节点或Lerp节点将高亮颜色与物体原本的Base Color进行插值混合。这种方式更柔和但可能改变物体原本的固有色。为了效果更炫酷我们可以让高亮颜色随时间或距离做一些简单的变化比如使用Time节点驱动一个Sine函数来让边缘颜色有呼吸般的脉动感。4. 关键技巧与深度优化方案按照上述步骤一个基础的效果已经能跑起来了。但要达到《只狼》那种干净利落、富有张力的感觉还需要一些“打磨”技巧。这些往往是教程里不会提的实战经验。4.1 解决深度冲突与Z-Fighting当你把效果应用到一个复杂的模型上特别是靠近其他表面如地面、墙壁时可能会发现高亮边缘在模型与背景的交界处闪烁或不稳定。这是因为我们直接使用了物体自身的表面深度。在模型边缘其屏幕深度与背景的深度值非常接近由于深度缓冲精度限制会产生Z-Fighting导致边缘检测因子剧烈波动。解决方案使用自定义深度Custom Depth或渲染队列Render Queue偏移。方法A推荐轻微后置渲染。在材质的Inspector面板将其“Render Queue”值稍微调大例如从默认的Opaque的2000改为2500。这会让这个物体在所有不透明物体之后渲染确保其边缘检测时背景物体的深度信息已经写入深度缓冲区从而获得稳定的背景深度值用于比较。但这会带来正确的遮挡关系吗会的因为它仍在Opaque队列只是顺序靠后它仍然会写入深度但会在其他不透明物体之后写入所以它的片元如果被遮挡就不会被渲染。这对于附着在角色身上的效果是合适的。方法B使用第二张深度纹理。这是一种更高级的方案需要编写自定义Renderer Feature在渲染这个物体之前先将不包含该物体的场景深度存储到一张单独的纹理中。然后在Shader中比较物体自身深度与这张“背景深度纹理”的差值。差值大的地方就是边缘。这种方法最精确但实现复杂性能开销也更大。对于大多数情况方法A的轻微渲染队列偏移足以解决问题且简单高效。4.2 边缘检测的“软化”与“锐化”控制直接用点积的绝对值做边缘检测得到的边缘可能过于“数学化”是一条硬邦邦的线。为了获得更艺术化的边缘我们可以引入更多控制。法线边缘增强将表面法线Normal与View Direction的点积即经典的菲涅尔效应也作为一个因子乘进去。这样模型轮廓边缘以及那些面向视角的陡峭面都会得到增强效果更立体。公式可以改为边缘因子 (1 - abs(dot(V, N))) * (1 - abs(dot(V, F)))其中V是View DirectionN是NormalF是Camera Forward。噪声扰动采样一张简单的噪声贴图用其灰度值对_EdgeWidth参数或最终强度进行微弱的扰动。这可以打破边缘的完美连续性让高亮效果看起来更有机、更有能量感避免像一层塑料薄膜。距离衰减边缘宽度让_EdgeWidth参数随着距离减小而动态增加。即物体越近边缘越细越锐利物体稍远边缘可以稍微宽一点模糊一点。这符合视觉感知也能增强距离感。4.3 性能优化要点这个效果主要消耗在像素着色器上尤其是全屏的深度纹理采样和向量运算。对于移动平台或低端设备需注意LOD细节层次控制为带有此效果的材质创建不同复杂度的Shader变体或使用Shader LOD在远距离或低端设备上使用简化版例如去掉噪声扰动和复杂的边缘混合。视锥体裁剪Frustum Culling确保效果只在摄像机可见范围内计算。Unity的渲染管线会自动处理但你需要确保物体的Mesh Renderer和Collider设置合理。限制使用范围不要给场景中所有物体都加上这个效果。通常只用于关键的敌人、陷阱或交互物体。可以通过脚本动态启用/禁用材质的这个特性或者通过Shader的Shader Keywords来开关。简化计算在片段着色器中normalize操作和pow操作相对昂贵。如果精度要求不高可以考虑在顶点着色器中计算View Direction和距离因子然后通过插值传到片元但这可能会影响边缘检测的精度需要权衡。5. 实战调试与常见问题排查即使节点连对了第一次运行效果也可能不尽如人意。下面是一个快速排查清单帮你定位问题。问题现象可能原因排查与解决方案完全看不到效果1. URP Depth Texture未开启。2. 材质Emission模式未启用或强度为0。3. 计算出的最终强度始终为0。1. 检查URP Asset设置确保Depth Texture已勾选。2. 检查材质Inspector确保Emission已点亮勾选并尝试调高Emission强度。在ShaderGraph中确保颜色值没有过小。3. 分别将“距离因子”和“边缘因子”直接输出为Emission颜色如红色代表距离因子绿色代表边缘因子可视化调试看哪个部分为0。检查_DangerDistance是否设得太大或物体是否在视野正中心导致边缘因子为0。效果全屏闪烁或仅在特定角度出现深度冲突Z-Fighting。尝试4.1中提到的“轻微后置渲染”方法将材质的Render Queue调高如100。如果问题依旧检查模型是否有重合的面片或尝试调整摄像机的近裁剪面Clipping Planes - Near值不要设得过小如0.010.1或0.3通常更安全。边缘太粗或太细不自然_EdgeWidth参数调节不当。smoothstep过渡范围不合适。_EdgeWidth是幂运算的指数值越大边缘越窄。尝试从1.0开始调节每次增减0.5。同时调整_FadeRange让距离引起的强度变化更平滑。高亮颜色不鲜艳或发白颜色叠加方式问题。HDR颜色值过高导致泛白Bloom后尤其明显。如果使用Additive方式叠加到Emission确保_RimColor是HDR颜色在ShaderGraph中颜色选择器可以开启HDR但强度_Intensity不要一开始就设得太大如超过5。先从小值如1开始测试。如果场景启用了Bloom后处理过高的Emission值会导致严重泛光需要配合Bloom阈值进行调整。性能开销大模型面数过高或效果应用于过多物体。使用Unity Profiler的Rendering模块查看GPU耗时。确保高面数模型使用了合适的LOD。考虑是否可以用更简化的Shader变体例如去掉噪声和法线边缘计算用于次要敌人。调试时善用ShaderGraph的节点预览Preview窗口。你可以右键点击任何节点或连线选择“Create Preview”将该端口的数据可视化成一个灰度图或颜色图。这是理解数据流、定位计算错误最直观的方式。例如将“距离因子”预览出来你就能清晰地看到一个从摄像机中心向外衰减的圆形梯度非常有助于参数调整。最后将这个ShaderGraph保存为一个子图Sub-graph并参数命名规范你就可以将它作为一个可复用的“危险感知效果”模块轻松应用到其他项目的任何角色或物体材质上快速构建起具有统一视觉语言的交互反馈系统。这种将游戏机制与视觉表现深度绑定的手法正是技术美术工作的魅力所在。