|
| 1 | +// Animated water surface shader. |
| 2 | +// Multi-octave sine wave vertex displacement, fresnel transparency, specular highlights. |
| 3 | +// |
| 4 | +// Uses the same instance (group 0) and scene (group 1) bind groups as the PBR |
| 5 | +// shader — no per-material group needed since water properties are baked in. |
| 6 | + |
| 7 | +diagnostic(off, derivative_uniformity); |
| 8 | + |
| 9 | +// --------------------------------------------------------------------------- |
| 10 | +// Shared structures (must match pbr.wgsl layout exactly) |
| 11 | +// --------------------------------------------------------------------------- |
| 12 | + |
| 13 | +struct InstanceData { |
| 14 | + model: mat4x4<f32>, |
| 15 | + normal_matrix: mat4x4<f32>, |
| 16 | + material_id: u32, |
| 17 | + _pad0: u32, |
| 18 | + _pad1: u32, |
| 19 | + _pad2: u32, |
| 20 | +} |
| 21 | + |
| 22 | +struct PointLightData { |
| 23 | + position: vec4<f32>, |
| 24 | + color: vec4<f32>, |
| 25 | +} |
| 26 | + |
| 27 | +struct SpotLightData { |
| 28 | + position: vec4<f32>, |
| 29 | + direction: vec4<f32>, |
| 30 | + color: vec4<f32>, |
| 31 | + cone: vec4<f32>, |
| 32 | +} |
| 33 | + |
| 34 | +struct SceneUniforms { |
| 35 | + camera_pos: vec4<f32>, |
| 36 | + light_direction: vec4<f32>, |
| 37 | + light_color: vec4<f32>, |
| 38 | + ambient_color: vec4<f32>, |
| 39 | + camera_vp: mat4x4<f32>, |
| 40 | + light_vp: mat4x4<f32>, |
| 41 | + inv_vp: mat4x4<f32>, |
| 42 | + cascade_vps: array<mat4x4<f32>, 3>, |
| 43 | + cascade_splits: vec4<f32>, |
| 44 | + point_lights: array<PointLightData, 4>, |
| 45 | + spot_lights: array<SpotLightData, 2>, |
| 46 | + num_point_lights: vec4<f32>, |
| 47 | + num_spot_lights: vec4<f32>, |
| 48 | + probe_sh: array<vec4<f32>, 9>, |
| 49 | + probe_enabled: vec4<f32>, |
| 50 | + shadow_params: vec4<f32>, |
| 51 | + ibl_params: vec4<f32>, |
| 52 | + elapsed_time: vec4<f32>, |
| 53 | +} |
| 54 | + |
| 55 | +// --------------------------------------------------------------------------- |
| 56 | +// Bindings |
| 57 | +// --------------------------------------------------------------------------- |
| 58 | + |
| 59 | +@group(0) @binding(0) var<storage, read> instances: array<InstanceData>; |
| 60 | +@group(1) @binding(0) var<uniform> scene: SceneUniforms; |
| 61 | + |
| 62 | +// --------------------------------------------------------------------------- |
| 63 | +// Constants |
| 64 | +// --------------------------------------------------------------------------- |
| 65 | + |
| 66 | +const WATER_COLOR_DEEP: vec3<f32> = vec3<f32>(0.02, 0.12, 0.22); |
| 67 | +const WATER_COLOR_SHALLOW: vec3<f32> = vec3<f32>(0.10, 0.35, 0.45); |
| 68 | +const SPECULAR_COLOR: vec3<f32> = vec3<f32>(1.0, 0.95, 0.85); |
| 69 | +const WAVE_HEIGHT: f32 = 0.15; |
| 70 | +const BASE_ALPHA: f32 = 0.55; |
| 71 | + |
| 72 | +// --------------------------------------------------------------------------- |
| 73 | +// Vertex stage |
| 74 | +// --------------------------------------------------------------------------- |
| 75 | + |
| 76 | +struct VertexInput { |
| 77 | + @location(0) position: vec3<f32>, |
| 78 | + @location(1) normal: vec3<f32>, |
| 79 | + @location(2) tangent: vec3<f32>, |
| 80 | + @location(3) uv: vec2<f32>, |
| 81 | +} |
| 82 | + |
| 83 | +struct VertexOutput { |
| 84 | + @builtin(position) clip_position: vec4<f32>, |
| 85 | + @location(0) world_pos: vec3<f32>, |
| 86 | + @location(1) world_normal: vec3<f32>, |
| 87 | + @location(2) uv: vec2<f32>, |
| 88 | +} |
| 89 | + |
| 90 | +// Multi-octave wave displacement. Each octave has a different direction, |
| 91 | +// frequency, and phase speed so the surface looks organic rather than tiled. |
| 92 | +fn wave_height(xz: vec2<f32>, t: f32) -> f32 { |
| 93 | + var h: f32 = 0.0; |
| 94 | + // Octave 1: broad swell |
| 95 | + h += sin(xz.x * 1.2 + xz.y * 0.8 + t * 1.4) * 0.45; |
| 96 | + // Octave 2: cross chop |
| 97 | + h += sin(xz.x * 2.5 - xz.y * 1.7 + t * 2.1) * 0.25; |
| 98 | + // Octave 3: fine ripple |
| 99 | + h += sin(xz.x * 5.0 + xz.y * 4.3 + t * 3.6) * 0.12; |
| 100 | + // Octave 4: micro detail |
| 101 | + h += sin(xz.x * 8.7 - xz.y * 7.1 + t * 5.0) * 0.06; |
| 102 | + return h * WAVE_HEIGHT; |
| 103 | +} |
| 104 | + |
| 105 | +// Analytical normal from the partial derivatives of the wave function. |
| 106 | +fn wave_normal(xz: vec2<f32>, t: f32) -> vec3<f32> { |
| 107 | + let eps = 0.05; |
| 108 | + let hc = wave_height(xz, t); |
| 109 | + let hx = wave_height(xz + vec2<f32>(eps, 0.0), t); |
| 110 | + let hz = wave_height(xz + vec2<f32>(0.0, eps), t); |
| 111 | + let dx = (hx - hc) / eps; |
| 112 | + let dz = (hz - hc) / eps; |
| 113 | + return normalize(vec3<f32>(-dx, 1.0, -dz)); |
| 114 | +} |
| 115 | + |
| 116 | +@vertex |
| 117 | +fn vs_main(in: VertexInput, @builtin(instance_index) iid: u32) -> VertexOutput { |
| 118 | + let model = instances[iid].model; |
| 119 | + let t = scene.elapsed_time.x; |
| 120 | + |
| 121 | + var world_pos = (model * vec4<f32>(in.position, 1.0)).xyz; |
| 122 | + |
| 123 | + // Displace Y by the wave function |
| 124 | + world_pos.y += wave_height(world_pos.xz, t); |
| 125 | + |
| 126 | + var out: VertexOutput; |
| 127 | + out.clip_position = scene.camera_vp * vec4<f32>(world_pos, 1.0); |
| 128 | + out.world_pos = world_pos; |
| 129 | + out.world_normal = wave_normal(world_pos.xz, t); |
| 130 | + out.uv = in.uv; |
| 131 | + return out; |
| 132 | +} |
| 133 | + |
| 134 | +// --------------------------------------------------------------------------- |
| 135 | +// Fragment stage |
| 136 | +// --------------------------------------------------------------------------- |
| 137 | + |
| 138 | +@fragment |
| 139 | +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { |
| 140 | + let N = normalize(in.world_normal); |
| 141 | + let V = normalize(scene.camera_pos.xyz - in.world_pos); |
| 142 | + let L = normalize(-scene.light_direction.xyz); |
| 143 | + |
| 144 | + // Fresnel: more reflective at glancing angles |
| 145 | + let NdotV = max(dot(N, V), 0.0); |
| 146 | + let fresnel = pow(1.0 - NdotV, 4.0) * 0.8 + 0.2; |
| 147 | + |
| 148 | + // Blend between deep and shallow water based on view angle |
| 149 | + let water_color = mix(WATER_COLOR_DEEP, WATER_COLOR_SHALLOW, NdotV); |
| 150 | + |
| 151 | + // Ambient contribution |
| 152 | + let ambient = water_color * scene.ambient_color.xyz * scene.ambient_color.w; |
| 153 | + |
| 154 | + // Diffuse (wrap lighting for softer underwater look) |
| 155 | + let NdotL = max(dot(N, L), 0.0); |
| 156 | + let diffuse = water_color * scene.light_color.xyz * scene.light_color.w * NdotL * 0.6; |
| 157 | + |
| 158 | + // Specular (Blinn-Phong, tight highlight for water) |
| 159 | + let H = normalize(L + V); |
| 160 | + let NdotH = max(dot(N, H), 0.0); |
| 161 | + let spec = pow(NdotH, 128.0) * fresnel; |
| 162 | + let specular = SPECULAR_COLOR * scene.light_color.xyz * scene.light_color.w * spec; |
| 163 | + |
| 164 | + let color = ambient + diffuse + specular; |
| 165 | + |
| 166 | + // Alpha: base transparency boosted at glancing angles (fresnel) |
| 167 | + let alpha = mix(BASE_ALPHA, 0.85, fresnel); |
| 168 | + |
| 169 | + return vec4<f32>(color, alpha); |
| 170 | +} |
0 commit comments