Unity Shader 水面 Shader 教程 从 Gerstner 波浪到屏幕折射、焦散、泡沫的完整实现 01 总览Shader 全景架构使用Unlit Transparent渲染模式通过屏幕空间技术折射、深度重建实现高质量水面效果。Gerstner 波浪3 层 Gerstner 波叠加顶点位移 法线计算水面有真实起伏屏幕空间折射采样 _CameraOpaqueTexture法线驱动偏移RefractionMask 防穿帮焦散双纹理 Min 混合基于折射后水底位置同步深度衰减深浅水色 吸收exp2 深度衰减控制深浅色过渡AbsorbStrength 控制水色覆盖平面反射采样 _ReflectionTexFresnel 控制反射强度泡沫 岸边Simplex 噪声 正弦波泡沫岸边浅色着色 边缘高光02 参数属性参数总表Shader 共有30 个属性分为 7 组基础水色、法线、反射、折射、焦散、岸边/泡沫、波浪。属性名类型默认值作用_ShallowRangeRange(0,10)0.13浅水深度范围控制深浅色过渡_DeepColorColor(0,0.21,0.4,1)深水颜色_ShallowColorColor(0.57,0.87,1,0.7)浅水颜色_FresnelPowerFloat10.61水色→反射的 Fresnel 指数_NormalMap2Dbump法线贴图_NormapTilingFloat1法线平铺_NormalSpeedVector(0.1,0.1,0,0)法线滚动速度_NormalScaleRange(0,1)0.06法线强度很小 → 微妙波纹_ReflectionTex2Dwhite平面反射渲染纹理脚本注入_ReflectionFresnelParamVector(0,1,5,0)反射 Fresnel 参数 (offset, scale, power)_RefractionIntensityRange(0,2)0.18折射偏移强度_RefractionMask_FadeStartFloat0折射 Mask 平滑起始_RefractionMask_FadeRangeRange(0,1)0.55折射 Mask 过渡范围_CausticesMap2Dwhite焦散纹理_CausticesTilingRange(0.2,1)1焦散平铺_ShallowRange1Range(0,10)6.76焦散深度衰减范围_CausticsShoreFadeRange(0.001,1)0.536焦散岸边淡出距离_AbsorbStrengthFloat3.78水色吸收强度越大越快不透明_ShoreRangeFloat0.13岸边着色范围_ShoreColorColor(0.57,1,0.99,0)岸边浅色_ShoreEdgeWidthRange(0,1)0.22岸边高光宽度_FoamRangeFloat0.1泡沫深度范围_FoamFrequencyFloat15.6泡沫正弦频率_WaveA/B/CVector4—Gerstner 波参数 (速度XY, 陡度, 波长)03 顶点阶段 VertexGerstner 波浪水面不是平面 —— 顶点阶段使用3 层 Gerstner 波叠加产生真实的波浪起伏和法线。每层波有独立的方向、陡度和波长。核心代码// 单层 Gerstner 波返回位移同时累加切线/副切线 float3 GerstnerWave(float3 position, inout float3 tangent, inout float3 binormal, float4 wave) { const float pi 3.1415926; float steepness wave.z * 0.01; float wavelength wave.w; float k 2.0 * pi / wavelength; // 波数 float c sqrt(9.8 / k); // 波速 √(g/k) float2 d normalize(wave.xy); // 波方向 float f k * (dot(d, position.xz) - c * _Time.y); // 相位 float a steepness / k; // 振幅 // 累加切线和副切线用于叉积求法线 tangent float3(-d.x*d.x*(steepness*sin(f)), d.x*(steepness*cos(f)), -d.x*d.y*(steepness*sin(f))); binormal float3(-d.x*d.y*(steepness*sin(f)), d.y*(steepness*cos(f)), -d.y*d.y*(steepness*sin(f))); return float3(d.x*(a*cos(f)), a*sin(f), d.y*(a*cos(f))); } // 3 层波叠加 → 位移 法线 void ApplyGerstnerWaves(float3 positionWS, out float3 displacedWS, out float3 normalWS) { float3 tangent float3(1,0,0); float3 binormal float3(0,0,1); float3 offset 0; offset GerstnerWave(positionWS, tangent, binormal, _WaveASpeedXYSteepnesswavelength); offset GerstnerWave(positionWS, tangent, binormal, _WaveB); offset GerstnerWave(positionWS, tangent, binormal, _WaveC); displacedWS positionWS offset; normalWS normalize(cross(binormal, tangent)); // 法线 cross(副切线, 切线) }关键点顶点位移后的新位置需要TransformWorldToObject转回 OS再走标准 URPGetVertexPositionInputs确保positionCS正确。原始 OS 位置被保存到positionOSOriginal用于折射 Mask 的深度比较。04 片元阶段 Fragment法线贴图双滚动混合水面法线由两层法线贴图以不同速度和方向滚动、混合而成模拟细密波纹。法线驱动折射偏移和反射 UV。// 双层法线滚动 float2 baseUV positionWS.xz * _NormapTiling; float2 uvA baseUV _NormalSpeed * _Time.y; // 正向 float2 uvB baseUV * 0.5 (_NormalSpeed * float2(-0.5, -0.5)) * _Time.y; // 半速反向 float3 nA UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uvA), _NormalScale); nA.z lerp(1.0, nA.z, saturate(_NormalScale)); // 保持法线平的基底 float3 nB UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uvB), _NormalScale); nB.z lerp(1.0, nB.z, saturate(_NormalScale)); float3 surfaceNormal BlendNormal(nA, nB); // URP 标准法线混合设计技巧_NormalScale默认值仅0.06非常小。这是为了让水面波纹微妙而不夸张 —— 折射偏移和反射扰动都受此控制太大会导致折射重影。法线的z分量用lerp(1, z, saturate(scale))保持基底平整避免水面整体倾斜。05 片元阶段 Fragment屏幕折射与 RefractionMask 防穿帮水面是半透明的需要看到水下场景。本 Shader 使用屏幕空间折射用法线驱动 UV 偏移采样_CameraOpaqueTexture不透明场景颜色。但折射偏移可能穿帮——偏移到水面以上的物体所以需要RefractionMask来缩放偏移量。RefractionMask.hlsl 核心逻辑void RefractionMask( float2 screenUV, // 水面屏幕 UV float2 refractionOffset, // 法线驱动的偏移量 float4 positionCS, // 水面 clip space 位置 (NDC) float fadeStart, // 过渡起始 float fadeRange, // 过渡范围 out float mask, // 输出: 0~1 缩放因子 out float2 finalOffset // 输出: 缩放后的偏移 ) { // 1. 采样折射后位置的场景深度 float rawDepth SampleSceneDepth(screenUV refractionOffset); float depthRefracted LinearEyeDepth(rawDepth, _ZBufferParams); // 2. 水面自身的深度 float waterDepth LinearEyeDepth(positionCS.z, _ZBufferParams); // 3. 深度差 → smoothstep → mask float depthDiff depthRefracted - waterDepth; mask saturate(smoothstep(fadeStart, fadeStart fadeRange, depthDiff)); // 4. 偏移量 × mask → 穿帮时自动归零 finalOffset refractionOffset * mask; }⚠️ 关键positionCS 必须是原始位置的 NDCShader 中保存了波浪位移前的原始 OS 位置positionOSOriginal在片元中重新做TransformObjectToHClip 透视除法得到 NDC。如果直接用波浪位移后的positionCS深度比较会出错。Shader 中的调用// 法线驱动折射偏移 float2 refractionOffset (surfaceNormal * _RefractionIntensity).xy; // 原始位置的 NDC波浪位移前 float4 positionCSOrig TransformObjectToHClip(input.positionOSOriginal); positionCSOrig / positionCSOrig.w; // RefractionMask缩放偏移防止穿帮 float refractionMask; float2 finalOffset; RefractionMask(screenUV, refractionOffset, positionCSOrig, _RefractionMask_FadeStart, _RefractionMask_FadeRange, refractionMask, finalOffset); // 折射后的 UV → 采样场景颜色 float2 refractedUV screenUV finalOffset; float3 sceneColor SampleSceneColor(refractedUV);06 片元阶段 Utility折射后世界坐标重建焦散需要知道水底的世界位置来生成 UV。关键设计用折射偏移后的 UV重建世界坐标确保焦散和折射背景 100% 同步不会重影。// RefractedWorldPos.hlsl float GetRefractedWorldPos(float2 refractedUV, out float3 worldPos) { float rawDepth SampleSceneDepth(refractedUV); worldPos ComputeWorldSpacePosition(refractedUV, rawDepth, UNITY_MATRIX_I_VP); return 1.0; // 占位返回值ASE 兼容 } // shader 中使用 float3 sceneWorldPos; GetRefractedWorldPos(refractedUV, sceneWorldPos); float refractedDepth (positionWS - sceneWorldPos).y; // waterY - sceneY 水深 为什么用折射后的 UV 而不是原始 UV折射背景用偏移后 UV 采样焦散如果用原始 UV 重建坐标两者不同步 → 腿轮廓处焦散和折射错位 → 重影。用同一个refractedUV重建世界坐标焦散 UV 和折射背景 100% 同步。07 片元阶段 Fragment焦散效果焦散是水下光线的聚焦纹理给水底增加动态光影感。本 Shader 使用双纹理 Min 混合取两张图的较小值配合深度衰减和岸边淡出。// 焦散 UV 基于折射后的水底世界坐标与折射同步 float2 causticUV sceneWorldPos.xz * _CausticesTiling; float2 causticScroll _CausticesSpeed * _Time.y * 0.01; // 双纹理反向滚动取 Min模拟光线聚焦的暗纹 float3 causticA SAMPLE_TEXTURE2D(_CausticesMap, sampler_CausticesMap, causticUV causticScroll).rgb; float3 causticB SAMPLE_TEXTURE2D(_CausticesMap, sampler_CausticesMap, -causticUV causticScroll).rgb; // 岸边淡出浅水处焦散渐弱 float causticShore smoothstep(0.0, _CausticsShoreFade, max(refractedDepth, 0.0)); // 深度衰减越深焦散越弱 float3 caustics saturate(exp2(-refractedDepth / _ShallowRange1)) * (min(causticA, causticB) * _CausticsIntensity * causticShore); // 焦散叠加到场景色 float3 underWaterColor sceneColor caustics;⚠️ 踩坑记录焦散深度衰减exp2(-refractedDepth / range)当refractedDepth 0水面以上物体时会返回 1 的值saturate后变成 1导致焦散全亮出现在角色身上。必须加causticShore的smoothstep(0, fade, max(depth,0))来消除。08 片元阶段 Fragment水面颜色与深度吸收水面颜色由三部分混合水下场景色折射焦散、水面颜色深浅色反射、吸收因子控制两者的混合比例。深度越大水色越浓场景越不可见。// --- 水面颜色深浅色 波峰色 --- float aboveWater saturate(-refractedDepth / 0.05); // 水面以上 → 1 float shallowFactor saturate(exp2(-max(refractedDepth, 0.0) / _ShallowRange) * (1.0 - aboveWater)); float3 waterColor lerp(_DeepColor.rgb, _ShallowColor.rgb, shallowFactor) waveColor; // --- 吸收因子 --- float underwaterMask GetRefractedUnderwaterMask(screenUV, finalOffset, positionCSOrig) * saturate(smoothstep(0.0, 0.02, refractedDepth)); float effectiveDepth max(refractedDepth, 0.0) * underwaterMask; float absorb 1.0 - saturate(exp2(-(max(effectiveDepth, 0.0) * _AbsorbStrength))); absorb lerp(absorb, 1.0, aboveWater); // 水面以上 → 完全水色 // --- 最终混合 --- float3 color lerp(underWaterColor, waterAndReflection, absorb); // ↑ 场景色焦散 ↑ 水色反射 关键设计underwaterMask使用GetRefractedUnderwaterMask窄过渡 smoothstepsmoothstep(0, 0.02, refractedDepth)双重判断确保水面以上物体不被水色污染。aboveWater标记让水面以上区域直接走纯水色分支。09 片元阶段 Fragment平面反射与 Fresnel反射来自外部脚本渲染的_ReflectionTexPlanar Reflections。水面法线的xz分量扰动反射 UV再用 Fresnel 控制反射强度——掠射角时反射强正视时反射弱。// 反射 UV 屏幕 UV 法线扰动 float2 reflectionUV PlanarReflectionUV(screenUV) surfaceNormal.xz; // 反射 Fresnel掠射角强正视弱 float reflectionFresnel _ReflectionFresnelParam.x _ReflectionFresnelParam.y * pow(max(1.0 - saturate(dot(normalWS, viewDirWS)), 0.0001), _ReflectionFresnelParam.z); float3 reflectionColor SAMPLE_TEXTURE2D(_ReflectionTex, sampler_ReflectionTex, reflectionUV).rgb * saturate(reflectionFresnel); // 水色 Fresnel另一组参数 float waterFresnel pow(max(1.0 - saturate(dot(normalWS, viewDirWS)), 0.0001), _FresnelPower); // 水色 反射混合 float3 waterAndReflection lerp(waterColor, reflectionColor, waterFresnel);10 片元阶段 Fragment岸边着色岸边浅水与陆地交界处使用独立的_ShoreColor着色并叠加边缘高光让水陆交界更清晰自然。// 岸边着色权重浅水 → 1深水 → 0 float waterShore underwaterMask * saturate(exp2(-effectiveDepth / _ShoreRange)); float3 shoreColor sceneColor * _ShoreColor.rgb; color lerp(color, shoreColor, waterShore); // 岸边边缘高光 float shoreEdge smoothstep(1.0 - _ShoreEdgeWidth, 1.0, waterShore) * _ShoreEdgeIntensity; color shoreEdge;11 片元阶段 Fragment泡沫效果泡沫出现在浅水和岸边使用Simplex 噪声 正弦波生成破碎纹理通过step阈值控制泡沫分布。// 深度因子岸边 → 0深水 → 1 float foamT saturate(effectiveDepth / _FoamRange); float foamShallow 1.0 - smoothstep(_FoamBlend, 1.0, foamT); float foamInv 1.0 - foamT; // Simplex 噪声 正弦波 float foamNoise snoise(input.uv * _FoamNoiseSize); foamNoise foamNoise * 0.5 0.5; float foamPattern step( foamInv - _FoamWidth, sin(foamInv * _FoamFrequency _FoamSpeed * _Time.y) foamNoise - _FoamDissolve); // 水面以上不生成泡沫 float waterDepthUnrefracted positionWS.y - ReconstructWorldPosFromDepth(screenUV).y; float foamAbove saturate(-waterDepthUnrefracted / 0.05); float4 foamColor foamShallow * foamPattern * _FoamColor * underwaterMask * (1.0 - foamAbove); color lerp(color, foamColor.rgb, foamColor.a);12 全流程完整渲染管线流程从顶点输入到最终像素颜色的完整数据流。关键设计总结 核心设计理念这个 Shader 的核心思路是屏幕空间 深度驱动水面不是真正透明地渲染水下几何而是通过屏幕 UV 偏移采样已渲染的不透明场景颜色再用深度差驱动所有效果焦散、吸收、泡沫、岸边。关键设计决策设计点方案原因折射 UV法线驱动 RefractionMask 缩放法线给方向Mask 防穿帮焦散位置折射后 UV 重建 WorldPos焦散和折射 100% 同步无重影深度来源waterY - sceneWorldPos.y世界空间 Y 差直观且准确吸收混合exp2(-depth × strength)指数衰减浅水透明深水不透明水下遮罩GetRefractedUnderwaterMask smoothstep双重判断防止水面以上物体被染色法线强度NormalScale 0.06极小微妙波纹避免折射重影波浪位移3 层 Gerstner保存原始位置多方向叠加原始位置用于深度比较Alpha 1.0不透明混合在 RGB 中完成避免边缘半透泛蓝