Unity URP ShaderGraph实战:从零打造日式卡通风格水面效果 1. 项目概述与核心价值最近在做一个独立游戏项目美术风格定的是那种日式卡通渲染场景里需要一个能跟整体画风匹配的卡通水面。市面上现成的水体插件要么写实过头要么性能开销太大要么就是风格对不上。琢磨了一下决定直接用Unity 2022的URP管线配合ShaderGraph从头搓一个。这玩意儿听起来有点技术含量但实际走下来你会发现它其实是一系列可视化节点逻辑的组合特别适合美术和TA技术美术协作。今天我就把从零搭建这个卡通水面的完整过程包括怎么处理边缘泡沫、如何正确配置深度纹理这些最容易踩坑的环节掰开揉碎了讲清楚。无论你是想给自己的游戏加个有特色的水体还是单纯想深入学习ShaderGraph在URP下的实战应用这篇内容都能给你一套可以直接“抄作业”的方案。这个卡通水面的核心目标很明确它不需要模拟真实水体的复杂光学物理而是要突出干净、明快的色块和风格化的动态效果。我们会重点实现几个关键视觉特征一个基于视角和法线变化的卡通着色Cel Shading效果、模拟水波起伏的顶点动画、水岸交界处的泡沫边缘以及通过深度差实现的水下物体透视效果。整个过程完全在ShaderGraph中完成不需要手写一行HLSL代码这对于不熟悉传统Shader编程的开发者来说是个巨大的福音。2. 环境准备与项目初始设置2.1 URP项目创建与管线配置首先确保你使用的是Unity 2022.3 LTS或更高版本。新建项目时直接选择“Universal RP (URP)”模板是最省事的。如果你是在已有的项目里添加那就需要通过Package Manager安装“Universal RP”这个核心包。安装好后关键的一步是创建并配置URP Asset通用渲染管线资产。在Project窗口右键 - Create - Rendering - URP Asset (with Universal Renderer)。我通常会给它起个清晰的名字比如“URP_ProjectName”。创建后需要把它赋给项目的图形设置Edit - Project Settings - Graphics在Scriptable Render Pipeline Settings栏位拖入你刚创建的URP Asset。注意很多新手卡在第一步创建了URP Asset但忘了把它指定给项目导致整个项目还是用着内置管线ShaderGraph自然无法正常工作。这一步务必检查。接下来双击你创建的URP Asset进行详细配置。对于我们的卡通水面有几个设置需要特别关注Depth Texture深度纹理这是实现水面边缘泡沫和水下透视的基石。在URP Asset的Renderer列表里找到你使用的Renderer通常是Universal Renderer确保其Depth Texture选项是勾选的。默认通常是开启的但检查一下没坏处。Opaque Texture不透明纹理有时也被称为Camera Opaque Texture它是一帧中所有不透明物体渲染完成后的一张“快照”。我们后面会用它与水面Shader做交互模拟折射等效果。这个选项同样在Renderer的配置中名为Opaque Texture确保其开启。渲染缩放Render Scale如果你的项目对性能比较敏感可以考虑在URP Asset主设置里适当调低Render Scale如0.8这能整体降低渲染分辨率来提升帧率。卡通风格本身对分辨率不那么敏感这是个不错的优化点。2.2 ShaderGraph创建与材质球关联环境配置妥当后就可以创建我们的核心——ShaderGraph了。在Project窗口右键 - Create - Shader - Universal Render Pipeline - Blank Shader Graph。我把它命名为“SG_StylizedWater”。双击这个文件会打开ShaderGraph编辑器窗口。首先我们需要设置Graph的精度和渲染路径。在Graph Inspector面板通常位于编辑器左侧将Precision设置为Single。虽然Half在某些移动平台能省点性能但为了确保计算精度避免水面上出现奇怪的条带或噪点这里用Single更稳妥。在Graph Settings里确认Material的Surface Type为Transparent。水是半透明的物体。Blending Mode可以选择Alpha后续我们通过Alpha通道控制边缘泡沫的消散。现在先创建一个最简单的材质球来测试管线。在Project窗口右键 - Create - Material命名为“Mat_StylizedWater”。将其Shader属性选择为我们刚创建的SG_StylizedWater。然后在场景中创建一个Plane平面作为我们的水体基底将这个材质球拖给它。你会看到一个粉红色的平面这是因为我们的ShaderGraph目前还是空的输出节点没有连接任何有效数据。3. 核心视觉模块构建基础着色与波浪动画3.1 构建卡通着色的颜色梯度卡通风格的核心之一是大色块。我们不需要平滑的光照渐变而是需要几个明确的颜色阶梯。在ShaderGraph中这可以通过Sample Gradient采样渐变节点配合Dot Product点积来实现。首先获取光照信息。添加一个Dot Product节点。它的A端口连接一个Normal Vector节点设置Object SpaceB端口连接一个Light Direction节点。这个点积的结果代表了表面法线与光照方向的夹角余弦值即基础的漫反射强度范围在[-1, 1]。但是直接使用这个值会得到平滑的渐变。我们需要将其“阶梯化”。添加一个Remap节点将点积结果的输入范围从[-1, 1]重新映射到[0, 1]。然后将这个重映射后的值输入给一个Sample Gradient节点。Sample Gradient节点是关键。双击它编辑渐变条。对于一个典型的两阶卡通色我会这样设置在位置0.0处选择一个深色比如藏青色 (#0A2E5C)代表背光面。在位置0.5处突然跳到一个亮色比如天蓝色 (#4FA6F0)代表受光面。 这样当光照计算值小于0.5时输出藏青大于等于0.5时输出天蓝形成了清晰的色块边界。你还可以在0.5的位置稍微靠右一点如0.55再设置一个同样的天蓝色形成一个微小的平台可以让色块过渡稍微柔和一丁点避免过于生硬。将这个Sample Gradient节点的输出连接到主节点Master Stack在Unity 2022的ShaderGraph中可能是Fragment或PBR Master节点取决于版本新版本是Master Stack的Base Color输入端口。现在你的平面应该呈现出清晰的明暗两个色块并且会随着光源方向旋转而变化。3.2 实现顶点动画模拟水波静态的水面是死的我们需要让它动起来。这里用正弦波叠加来模拟简单的波浪。思路是在物体空间下沿着平面法线方向Y轴扰动顶点的位置。添加一个Position节点将其Space设置为Object。我们主要扰动它的Y坐标。所以添加一个Split节点将Position的XYZ分量拆开。现在构建波浪函数。添加一个Time节点获取游戏时间乘以一个控制波浪速度的Vector1属性我命名为Wave_Speed。然后加上顶点在世界空间X和Z坐标的某种组合例如Position X * 0.5 Position Z * 0.3这决定了波浪的传播方向和波长。将这个总和输入给Sine正弦函数节点。正弦函数的输出在[-1, 1]之间波动。我们用一个Vector1属性Wave_Height波浪高度乘以它得到最终的Y轴偏移量。最后用一个Combine节点将之前Split出来的X和Z分量与计算出的Y偏移量重新组合形成新的物体空间位置。将这个新位置连接回Master Stack的Vertex Position输入端口注意端口可能需要设置为Object Space。实操心得单一的正弦波会很假。我的经验是至少叠加两层不同频率和方向的波。创建第二套Speed、Frequency通过调整与Position相乘的系数实现和Height参数用另一个正弦波节点计算然后将两个正弦波的Y偏移量Add在一起。这样水面的运动看起来会丰富、自然得多。记得将Wave_Height控制在较小的值如0.1-0.3否则水面会像沸腾一样。4. 关键技术点实现深度纹理与边缘泡沫4.1 深度纹理的原理与采样这是本项目的一个技术重点也是风格化水面实现“水岸交互”视觉效果的关键。深度纹理Depth Texture存储了当前视角下每个像素点到相机最近的物体的深度值Z值。通过比较水面像素自身的深度和深度纹理中对应位置的深度我们就能知道水面下有没有物体比如河床、石头或者水面与岸边、物体的交界在哪里。在ShaderGraph中获取深度纹理非常简单。使用Scene Depth节点即可。这个节点有一个Sample Mode参数通常选择Linear Eye线性眼空间深度这个空间下的深度值是线性的计算差值更方便。但是直接使用Scene Depth节点采样到的深度是水面后方所有物体的深度。对于透明物体比如水面本身我们需要一种方法来获取“水面所在位置的深度”。这通常通过获取水面像素在眼空间下的深度值来实现。添加一个Position节点将其Space设置为View视图空间/眼空间。视图空间下位置的Z分量正值即代表了该点到相机的距离深度。用Split节点取出这个Z值。现在我们有了两个深度值Depth_Scene: 从深度纹理采样得到的水面后方最近不透明物体的深度。Depth_WaterSurface: 水面顶点/片元自身在视图空间的深度。4.2 边缘泡沫效果生成逻辑泡沫效果发生在水面与其他物体河岸、岩石相交的边缘。从深度角度理解就是当水面自身的深度Depth_WaterSurface与从深度纹理读到的后方物体深度Depth_Scene非常接近时意味着水面几乎紧贴着那个物体这里就应该出现泡沫。计算深度差Depth_Difference Depth_Scene - Depth_WaterSurface。在交界处这个差值会是一个很小的正数因为物体在水面下方一点点。离交界处越远差值越大。我们需要将这个深度差转换成一个边缘遮罩。使用一个Smoothstep节点。Smoothstep(Edge0, Edge1, T)函数会在T小于Edge0时返回0大于Edge1时返回1在中间则平滑过渡。我们将Depth_Difference作为输入T。设置Edge0为一个很小的值比如0.01。这意味着深度差小于0.01时我们认为它们“完全接触”泡沫最浓。设置Edge1为一个稍大的值比如0.1。这意味着深度差大于0.1时我们认为它们“远离”没有泡沫。Smoothstep的输出就是一个在[0,1]之间的值0代表无泡沫远处1代表全泡沫交界线。但通常我们希望泡沫在交界线处是白色的值1向外逐渐消失。所以有时我们会用1 - Smoothstep(...)来反转这个遮罩。将这个泡沫遮罩一个0-1的灰度值用于控制两个方面颜色叠加用一个Lerp线性插值节点在基础水色和白色之间以上述泡沫遮罩为系数进行混合。这样交界处的水面颜色就会变白模拟泡沫。透明度/扰动可以将泡沫遮罩乘以一个噪声纹理采样结果然后加到水面法线上让泡沫区域的水面法线更紊乱模拟泡沫聚集处的微观不平整。也可以用它来影响Alpha值让泡沫边缘的透明度有所变化。注意事项深度纹理的精度在远距离会下降这可能导致中远距离的水岸交界处泡沫计算不准确或出现闪烁。一个常见的优化技巧是不要直接用Scene Depth节点而是使用Scene Depth节点的Raw输出然后自己进行线性化转换公式LinearDepth 1.0 / (Z * A B)其中A和B来自相机投影矩阵这样有时能获得更好的控制。但在大多数风格化场景中直接使用Linear Eye模式已经足够。5. 水面质感增强与交互细节5.1 法线贴图与高光闪烁基础颜色和波浪有了但水面还缺乏质感。我们需要模拟水面的微观波纹和光泽感。这可以通过法线贴图来实现。在ShaderGraph中添加一个Texture 2D属性命名为Normal_Map导入一张水波纹法线贴图。使用Sample Texture 2D节点采样它并将其输出连接到Normal节点设置空间为Tangent再将Normal节点连接到Master Stack的Normal输入端口。为了让法线贴图动起来我们需要扰动其UV坐标。使用一个Time节点乘以一个速度属性再与纹理坐标的UV相加。更高级的做法是使用两张不同缩放和速度的法线贴图进行混合Blend节点选择Overlay或Add模式可以极大地增加波纹细节的丰富度和真实性。卡通水面也需要高光但不同于PBR的高光我们可以做得更风格化。使用Specular相关输入端口或者用Dot Product计算视角方向与反射光方向的点积再通过一个Power节点代表光泽度和Step或Smoothstep节点得到一个锐利或带有平滑过渡的高光区域。将这个高光强度与一个亮色白色或淡蓝色相乘然后Add到基础颜色上。这样在光线直射的角度水面就会出现亮晶晶的“闪光点”。5.2 折射效果模拟真实的水面有折射卡通化可以简化它。我们可以利用之前提到的Camera Opaque Texture。添加一个Scene Color节点在URP中它对应Opaque Texture。思路是将水面后方场景的颜色“扭曲”一下再显示出来。使用法线贴图的RG通道代表XY方向的倾斜乘以一个很小的强度系数如0.05作为UV的偏移量。用这个偏移量去扰动采样Scene Color节点的UV坐标。然后用一个Lerp节点在“直接显示水色”和“显示扭曲后的场景色”之间进行混合。混合系数可以用水面透明度或者基于视角菲涅尔效应来计算。菲涅尔效应可以用Fresnel Effect节点轻松实现它能让水面在视角与水面法线夹角小的地方看水面正面更透明折射更强在夹角大的地方看水面边缘更反射显示自身水色。5.3 参数调节与性能考量至此主要的节点网络已经搭建完成。现在需要将过程中所有用到的Vector1属性如Wave_Speed,Wave_Height,Foam_Edge0,Foam_Edge1, 法线强度折射强度等都暴露为材质的可调节参数。在ShaderGraph中选中这些节点在Graph Inspector里勾选Expose并给它们起好直观的名字。调节时建议按顺序进行先调波浪动画让运动看起来自然。再调基础颜色和卡通色阶确定主色调。然后调节泡沫的深度阈值Edge0,Edge1和颜色让泡沫出现在合适的位置且宽度适中。最后加法线、高光、折射这些“锦上添花”的效果并微调其强度。性能方面ShaderGraph生成的Shader是经过优化的但透明物体本身开销就大。确保你的水面网格面数合理不需要太高。如果移动端压力大可以考虑关闭折射效果采样Scene Color有额外开销或者降低法线贴图的混合层数。6. 常见问题排查与优化技巧实录在实际把这套Shader应用到项目里的过程中你几乎一定会遇到下面这几个问题。我把我的排查经验和解决方案整理出来能帮你省下大量折腾的时间。6.1 边缘泡沫不显示或显示错误这是最常见的问题症状可能是完全没有泡沫或者泡沫出现在整个水面上。检查深度纹理是否启用这是首要原因。按照2.1节的方法反复确认URP Renderer上的Depth Texture选项已勾选。有时候在编辑器里改了但运行后没生效可以尝试重新进入播放模式或重启编辑器。检查深度计算空间确保用于计算深度差的Depth_WaterSurface是视图空间View Space的深度而Scene Depth节点的Sample Mode设置为Linear Eye。两者必须在同一个空间线性眼空间下比较才有意义。检查Smoothstep参数Edge0和Edge1的值非常敏感。如果Edge1设置得太小比如0.01可能只有像素级精度的接触才会产生泡沫几乎看不见。如果Edge0设置得比Edge1还大结果会完全反过来。通常Edge0在0.001-0.01Edge1在0.05-0.2之间调试。可以先将泡沫颜色直接输出为Base Color然后实时调节这两个参数直观地观察泡沫范围的变化。检查水面渲染队列水面的材质球Render Queue必须设置为Transparent通常是3000。如果错误地设置为Geometry2000或不透明队列它会在深度纹理写入之前渲染导致深度纹理中记录的是水面背后的物体而不是水面本身与后方物体的关系泡沫计算会完全混乱。6.2 水面闪烁或出现深度冲突Z-fighting当水面与地面或其他物体完全共面或极度接近时会因为深度缓冲精度问题产生闪烁这叫深度冲突。微调水面高度最简单有效的方法是将水面网格的Y轴位置稍微提高一点点例如0.001个单位。让水面略微浮在所有与之接触的物体之上。使用深度偏移Depth Offset在ShaderGraph的Master Stack上通常有Depth Offset输入端口。你可以连接一个很小的负值如-0.001这会在深度测试时让水面“退后”一点点从而避免与地面像素的深度值完全相同。注意值不能太大否则会导致渲染顺序问题。检查网格法线确保你的水面网格如Plane法线是朝上的Y轴正方向。翻转的法线会导致背面剔除和深度计算异常。6.3 折射效果边缘粗糙或扭曲过度采样Scene Color做折射时如果UV偏移量太大或者水面在屏幕边缘可能会采样到纹理边界之外导致颜色错误。钳制UV偏移在对UV进行偏移后使用Clamp节点将UV坐标限制在[0, 1]范围内可以防止采样越界但会导致边缘效果生硬。使用Border模式在Scene Color节点的采样器设置中将Wrap Mode设置为Clamp或Border并指定一个边界色如水的底色。这样当采样超出范围时会返回这个固定颜色而不是错误数据。降低偏移强度风格化水面不需要强烈的折射。将法线影响折射UV的强度系数降到很低如0.01-0.03既能有效果又不易出错。6.4 移动设备上性能不佳卡通Shader虽然比写实PBR水简单但透明、多纹理采样、顶点动画加起来对移动端仍可能有压力。简化波浪计算将两层正弦波减为一层。或者将波浪计算从片元着色器移到顶点着色器在ShaderGraph中波浪节点如果连接到Vertex Position默认就是在顶点阶段计算。降低纹理精度法线贴图使用压缩格式如DXT5nm for PC, ASTC for mobile并将尺寸降低到512x512甚至256x256。关闭次要特效如果帧率吃紧优先考虑关闭折射效果移除Scene Color相关节点它对视觉风格影响相对较小但节省了一次全屏纹理采样。使用LOD为水面材质创建不同复杂度的Shader变体例如一个完整版一个去掉折射和高光的简化版根据相机距离动态切换。这需要在ShaderGraph中通过Branch节点和材质属性开关来实现条件编译稍微复杂一些但对开放世界游戏很有效。调试ShaderGraph时善用Blackboard黑板上的Preview功能可以将任何一个中间节点的输出临时连接到主颜色的输入上从而可视化该节点的计算结果。这是排查逻辑错误最强大的工具。比如你可以直接把深度差、泡沫遮罩、法线通道等可视化出来一眼就能看出哪个环节出了问题。