源码 / shaders/Ch19/WaterCore.glsl
shaders/Ch19/WaterCore.glsl
// 第 19 章:风格化水面的全部“积木”。Water.glsl 按关键字把它们拼起来。(GLSL 版,对应 WaterCore.hlsl) // 每个函数对应正文里的一个案例:深度 → 折射 → 法线 → 反射 → 高光 → 泡沫 → 焦散 → Gerstner 波。 // Core.glsl / Lighting.glsl 自动包含;_CameraDepthTexture、_CameraOpaqueTexture 也已在 Core.glsl 里声明。 #ifndef COOKBOOK_CH19_WATER_CORE_INCLUDED #define COOKBOOK_CH19_WATER_CORE_INCLUDED uniform sampler2D _NormalMap; uniform sampler2D _FoamNoise; // 平面反射 RT 由 JS 版的 PlanarReflection 脚本设成全局纹理,不属于材质 uniform sampler2D _PlanarReflectionTex; // 深度着色 uniform vec4 _ShallowColor; uniform vec4 _DeepColor; uniform float _DepthMax; uniform vec4 _Absorption; // 每米吸收系数(RGB 分开),用于 Beer-Lambert uniform float _EdgeFade; // 折射 uniform float _RefractionStrength; uniform float _RefractLeakFix; // 法线 uniform vec4 _NormalTiling; // xy:两层法线的平铺(每米几次) uniform vec4 _FlowA; // xy:第一层流向*速度,z:强度 uniform vec4 _FlowB; // 反射 uniform float _ReflectionStrength; uniform float _UseFresnel; uniform float _Roughness; uniform float _PlanarDistortion; // 高光 uniform float _SpecPower; uniform float _SpecIntensity; // 泡沫 uniform vec4 _FoamColor; uniform float _FoamDistance; uniform float _FoamTiling; uniform float _FoamSoftness; // 焦散 uniform float _CausticScale; uniform float _CausticIntensity; uniform float _CausticDepthFade; uniform float _CausticSplit; // Gerstner 波:xy 方向,z 陡度,w 波长(米) uniform vec4 _WaveA; uniform vec4 _WaveB; uniform vec4 _WaveC; // 时间与调试 uniform float _AutoPlay; uniform float _Phase; uniform float _DebugView; float WaterTime() { return _AutoPlay > 0.5 ? _Time.y : _Phase; } // URP 的 LinearEyeDepth(positionWS, viewMatrix) 重载:观察空间 z 的绝对值 float LinearEyeDepth(vec3 positionWS, mat4 viewMatrix) { return abs((viewMatrix * vec4(positionWS, 1.0)).z); } vec2 Hash22(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973)); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.xx + p3.yz) * p3.zy); } // 单个 Gerstner 波。wave = (方向x, 方向z, 陡度 Q, 波长 L)。累加位移,同时累加切线/副切线用于求法线。 // 推导见第 17 章:k = 2π/L,相速度 c = sqrt(g/k),振幅 a = Q/k(Q=1 时波峰刚好变尖) vec3 GerstnerWave(vec4 wave, vec3 p, float t, inout vec3 tangent, inout vec3 binormal) { float steep = wave.z; float k = 2.0 * PI / wave.w; float c = sqrt(9.8 / k); vec2 d = normalize(wave.xy); float f = k * (dot(d, p.xz) - c * t); float a = steep / k; float s = sin(f), co = cos(f); tangent += vec3(-d.x * d.x * steep * s, d.x * steep * co, -d.x * d.y * steep * s); binormal += vec3(-d.x * d.y * steep * s, d.y * steep * co, -d.y * d.y * steep * s); return vec3(d.x * a * co, a * s, d.y * a * co); } // 水下深度:用水面这个像素的屏幕 UV 取场景深度,重建出水底的世界坐标 struct WaterDepth { vec3 sceneWS; // 水底(或水中物体)表面的世界坐标 float vertical; // 竖直水深 = 水面 y - 水底 y(与视角无关) float path; // 视线在水里走过的距离(与视角有关,斜看时更长) float sceneEye; // 场景的观察空间深度 }; WaterDepth SampleWaterDepth(vec2 uv, vec3 surfaceWS) { WaterDepth o; float raw = SampleSceneDepth(uv); o.sceneWS = ComputeWorldSpacePosition(uv, raw, UNITY_MATRIX_I_VP); o.vertical = surfaceWS.y - o.sceneWS.y; o.path = distance(o.sceneWS, surfaceWS); o.sceneEye = LinearEyeDepth(raw, _ZBufferParams); return o; } // 双层法线:同一张 water_normal 以不同尺度、不同方向流动,两层叠加打破重复感 vec3 DualNormalTS(vec2 xz, float t) { vec2 uvA = xz * _NormalTiling.x + _FlowA.xy * t; vec2 uvB = xz * _NormalTiling.y + _FlowB.xy * t; vec3 nA = UnpackNormalScale(texture(_NormalMap, uvA), _FlowA.z); vec3 nB = UnpackNormalScale(texture(_NormalMap, uvB), _FlowB.z); // Whiteout 混合:xy 相加、z 相乘,比简单平均保留更多细节(第 7 章有对比) return normalize(vec3(nA.xy + nB.xy, nA.z * nB.z)); } // 折射:按法线扰动屏幕 UV;如果扰动后取到的是“水面之上的东西”(深度比水面还近),说明发生了前景泄漏,退回原 UV vec2 RefractUV(vec2 screenUV, vec3 normalTS, float surfaceEye, float verticalDepth) { // 越浅扰动越小:岸边几厘米深的水不该把岸上的东西扭得很厉害 vec2 offset = normalTS.xy * _RefractionStrength * saturate(verticalDepth * 2.0); // 远处的水在屏幕上更小,同样的偏移量会显得更夸张,除以深度做个近似的透视补偿 offset /= max(1.0, surfaceEye * 0.25); vec2 uv = screenUV + offset; if (_RefractLeakFix > 0.5) { float eye = LinearEyeDepth(SampleSceneDepth(uv), _ZBufferParams); if (eye < surfaceEye) uv = screenUV; } return uv; } // Beer-Lambert:光在水里每走 1 米,各通道按 exp(-σ) 衰减。红光衰减最快,所以深水偏蓝绿。 // 被吸收掉的部分用“散射色”补上(水体本身被照亮的颜色) vec3 UnderwaterColor(vec3 sceneColor, float pathLength, vec3 scatterColor) { vec3 transmittance = exp(-_Absorption.rgb * pathLength); return sceneColor * transmittance + scatterColor * (1.0 - transmittance); } // Schlick 菲涅尔。水的折射率约 1.33,F0 = ((1.33-1)/(1.33+1))² ≈ 0.02:正看几乎全透,掠射角几乎全反 float WaterFresnel(vec3 normalWS, vec3 viewWS) { float NdotV = saturate(dot(normalWS, viewWS)); return 0.02 + 0.98 * pow(1.0 - NdotV, 5.0); } // 太阳高光:Blinn-Phong,指数很高(水面很光滑),乘上阴影,让高光不会出现在柱子的影子里 vec3 SunSpecular(Light light, vec3 normalWS, vec3 viewWS) { vec3 h = SafeNormalize(light.direction + viewWS); float spec = pow(saturate(dot(normalWS, h)), _SpecPower); // 能量归一化的近似系数 (n+8)/(8π),让指数变大时高光变小但更亮(第 5 章) spec *= (_SpecPower + 8.0) / (8.0 * PI); return light.color * spec * _SpecIntensity * light.shadowAttenuation; } // 岸边泡沫 = 贴岸的一条实心带 + 一圈圈向岸边推进的细线,都用噪声打散 float ShoreFoam(vec2 xz, float verticalDepth, vec3 normalTS, float t) { float shore = saturate(verticalDepth / _FoamDistance); // 0 在岸线,1 在泡沫带外沿 vec2 uv = xz * _FoamTiling + normalTS.xy * 0.1 + vec2(t * 0.02, t * 0.015); float n = texture(_FoamNoise, uv).r; // ① 实心带:阈值被噪声上下扰动,边缘呈不规则的锯齿 float core = 1.0 - smoothstep(0.25 - _FoamSoftness, 0.25 + _FoamSoftness, shore + (n - 0.5) * 0.35); // ② 推进线:shore 是“离岸距离”,减去时间就得到一圈圈朝岸边移动的等深线 float ripple = fract(shore * 2.5 - t * 0.3 + n * 0.6); float lines = smoothstep(0.8, 0.9, ripple) * step(0.45, n) * (1.0 - shore); return saturate(core + lines); } // 程序化焦散:Voronoi 的 F2-F1 在细胞边界处接近 0,取反后就是一张亮线网,很像焦散的光网 vec2 VoronoiF1F2(vec2 p, float t) { vec2 cell = floor(p), f = fract(p); float F1 = 8.0, F2 = 8.0; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(i, j); vec2 o = Hash22(cell + g); o = 0.5 + 0.45 * sin(t + 6.2831 * o); // 特征点绕各自中心转圈,图案就“活”了 vec2 r = g + o - f; float d = dot(r, r); if (d < F1) { F2 = F1; F1 = d; } else if (d < F2) { F2 = d; } } return sqrt(vec2(F1, F2)); } float CausticPattern(vec2 p, float t) { vec2 v1 = VoronoiF1F2(p, t); vec2 v2 = VoronoiF1F2(p * 1.43 + 7.1, t * 1.21); float c1 = 1.0 - smoothstep(0.0, 0.18, v1.y - v1.x); float c2 = 1.0 - smoothstep(0.0, 0.18, v2.y - v2.x); // 两层叠加:交叉处更亮,形成焦散特有的“亮节点” return c1 * c1 + c2 * c2 + 2.0 * c1 * c2; } // 把焦散投到水下表面:沿主光方向把水底点“投影回”水面,在那里查图案,图案就会随太阳方向偏移 vec3 Caustics(vec3 sceneWS, float verticalDepth, float upFacing, Light light, float t) { vec3 L = light.direction; vec2 p = sceneWS.xz + L.xz / max(L.y, 0.2) * verticalDepth; p *= _CausticScale; // RGB 三个通道错开一点点采样,模拟不同波长折射角不同产生的色散 vec2 split = vec2(_CausticSplit, _CausticSplit * 0.5); vec3 c = vec3(CausticPattern(p + split, t), CausticPattern(p, t), CausticPattern(p - split, t)); // 刚入水时焦散还没聚焦,太深又被吸收:用两个衰减把它限制在“中间深度” float fade = saturate(verticalDepth * 4.0) * exp(-verticalDepth / _CausticDepthFade); // 只有朝上的面才接到焦散(upFacing 由调用方用屏幕导数求出,见 SceneUpFacing) fade *= upFacing; // 阴影要在水底那一点上查:柱子的影子里不该有焦散 float shadow = MainLightRealtimeShadow(TransformWorldToShadowCoord(sceneWS)); return c * fade * _CausticIntensity * light.color * shadow; } #ifdef FRAGMENT // dFdx/dFdy 只能在片元着色器里用 // 用重建坐标的屏幕导数求水下表面的法线:dFdx/dFdy 是相邻像素的世界坐标差,叉积就是面法线。 // 屏幕 y 方向在不同平台可能相反,叉积符号会翻转,所以取绝对值。要用“未折射”的坐标: // 折射后的 UV 每个像素扰动不同,求出的导数全是噪声 float SceneUpFacing(vec3 sceneWS) { vec3 n = normalize(cross(dFdy(sceneWS), dFdx(sceneWS))); return saturate(abs(n.y) * 1.5 - 0.3); } #endif #endif