
01 总览
Shader 全景架构
使用 Unlit + Transparent 渲染模式,通过屏幕空间技术(折射、深度重建)实现高质量水面效果。
Gerstner 波浪
3 层 Gerstner 波叠加,顶点位移 + 法线计算,水面有真实起伏
屏幕空间折射
采样 _CameraOpaqueTexture,法线驱动偏移,RefractionMask 防穿帮
焦散
双纹理 Min 混合,基于折射后水底位置同步,深度衰减
深浅水色 + 吸收
exp2 深度衰减控制深浅色过渡,AbsorbStrength 控制水色覆盖
平面反射
采样 _ReflectionTex,Fresnel 控制反射强度
泡沫 + 岸边
Simplex 噪声 + 正弦波泡沫,岸边浅色着色 + 边缘高光

02 参数
属性参数总表
Shader 共有 30+ 个属性,分为 7 组:基础水色、法线、反射、折射、焦散、岸边/泡沫、波浪。
| 属性名 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| _ShallowRange | Range(0,10) | 0.13 | 浅水深度范围,控制深浅色过渡 |
| _DeepColor | Color | (0,0.21,0.4,1) | 深水颜色 |
| _ShallowColor | Color | (0.57,0.87,1,0.7) | 浅水颜色 |
| _FresnelPower | Float | 10.61 | 水色→反射的 Fresnel 指数 |
| _NormalMap | 2D | bump | 法线贴图 |
| _NormapTiling | Float | 1 | 法线平铺 |
| _NormalSpeed | Vector | (0.1,0.1,0,0) | 法线滚动速度 |
| _NormalScale | Range(0,1) | 0.06 | 法线强度(很小 → 微妙波纹) |
| _ReflectionTex | 2D | white | 平面反射渲染纹理(脚本注入) |
| _ReflectionFresnelParam | Vector | (0,1,5,0) | 反射 Fresnel 参数 (offset, scale, power) |
| _RefractionIntensity | Range(0,2) | 0.18 | 折射偏移强度 |
| _RefractionMask_FadeStart | Float | 0 | 折射 Mask 平滑起始 |
| _RefractionMask_FadeRange | Range(0,1) | 0.55 | 折射 Mask 过渡范围 |
| _CausticesMap | 2D | white | 焦散纹理 |
| _CausticesTiling | Range(0.2,1) | 1 | 焦散平铺 |
| _ShallowRange1 | Range(0,10) | 6.76 | 焦散深度衰减范围 |
| _CausticsShoreFade | Range(0.001,1) | 0.536 | 焦散岸边淡出距离 |
| _AbsorbStrength | Float | 3.78 | 水色吸收强度(越大越快不透明) |
| _ShoreRange | Float | 0.13 | 岸边着色范围 |
| _ShoreColor | Color | (0.57,1,0.99,0) | 岸边浅色 |
| _ShoreEdgeWidth | Range(0,1) | 0.22 | 岸边高光宽度 |
| _FoamRange | Float | 0.1 | 泡沫深度范围 |
| _FoamFrequency | Float | 15.6 | 泡沫正弦频率 |
| _WaveA/B/C | Vector4 | --- | Gerstner 波参数 (速度XY, 陡度, 波长) |
03 顶点阶段 Vertex
Gerstner 波浪
水面不是平面 ------ 顶点阶段使用 3 层 Gerstner 波叠加,产生真实的波浪起伏和法线。每层波有独立的方向、陡度和波长。

核心代码
cs
// 单层 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,再走标准 URP GetVertexPositionInputs,确保 positionCS 正确。原始 OS 位置被保存到 positionOSOriginal,用于折射 Mask 的深度比较。
04 片元阶段 Fragment
法线贴图双滚动混合
水面法线由两层法线贴图以不同速度和方向滚动、混合而成,模拟细密波纹。法线驱动折射偏移和反射 UV。

cs
// 双层法线滚动
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 核心逻辑
cs
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 必须是原始位置的 NDC
Shader 中保存了波浪位移前的原始 OS 位置 positionOSOriginal,在片元中重新做 TransformObjectToHClip + 透视除法得到 NDC。如果直接用波浪位移后的 positionCS,深度比较会出错。
Shader 中的调用
cs
// 法线驱动折射偏移
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% 同步,不会重影。

cs
// 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 混合(取两张图的较小值),配合深度衰减和岸边淡出。

cs
// 焦散 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
水面颜色与深度吸收
水面颜色由三部分混合:水下场景色(折射+焦散) 、水面颜色(深浅色+反射) 、吸收因子控制两者的混合比例。深度越大,水色越浓,场景越不可见。

cs
// --- 水面颜色(深浅色 + 波峰色) ---
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(窄过渡 smoothstep)+ smoothstep(0, 0.02, refractedDepth) 双重判断,确保水面以上物体不被水色污染。aboveWater 标记让水面以上区域直接走纯水色分支。
09 片元阶段 Fragment
平面反射与 Fresnel
反射来自外部脚本渲染的 _ReflectionTex(Planar Reflections)。水面法线的 xz 分量扰动反射 UV,再用 Fresnel 控制反射强度------掠射角时反射强,正视时反射弱。

cs
// 反射 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 着色,并叠加边缘高光,让水陆交界更清晰自然。

cs
// 岸边着色权重:浅水 → 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 阈值控制泡沫分布。

cs
// 深度因子:岸边 → 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 中完成,避免边缘半透泛蓝 |