@group(0) @binding(0) var heightField : array; @group(0) @binding(1) var renderParams : array; @group(0) @binding(2) var viewProjection : array; @group(0) @binding(3) var cameraPosition : array; @group(0) @binding(4) var bottomColorTex : texture_2d; @group(0) @binding(5) var bottomHeightTex : texture_2d; @group(0) @binding(6) var bottomSampler : sampler; struct VertexOutput { @builtin(position) position : vec4, @location(0) worldPosition : vec3, @location(1) normal : vec3, @location(2) heightValue : f32, }; fn indexFromCoord( x : u32, y : u32, size : u32 ) -> u32 { return y * size + x; } fn loadViewProjection( ) -> mat4x4 { return mat4x4( vec4( viewProjection[ 0 ], viewProjection[ 1 ], viewProjection[ 2 ], viewProjection[ 3 ] ), vec4( viewProjection[ 4 ], viewProjection[ 5 ], viewProjection[ 6 ], viewProjection[ 7 ] ), vec4( viewProjection[ 8 ], viewProjection[ 9 ], viewProjection[ 10 ], viewProjection[ 11 ] ), vec4( viewProjection[ 12 ], viewProjection[ 13 ], viewProjection[ 14 ], viewProjection[ 15 ] ) ); } fn sampleHeight( fx : f32, fy : f32, simSize : u32, heightAmp : f32 ) -> f32 { let sx : f32 = clamp( fx, 0.0, f32( simSize - 1u ) ); let sy : f32 = clamp( fy, 0.0, f32( simSize - 1u ) ); let ix0 : u32 = u32( sx ); let iy0 : u32 = u32( sy ); let ix1 : u32 = min( simSize - 1u, ix0 + 1u ); let iy1 : u32 = min( simSize - 1u, iy0 + 1u ); let tx : f32 = sx - f32( ix0 ); let ty : f32 = sy - f32( iy0 ); let idx00 : u32 = indexFromCoord( ix0, iy0, simSize ); let idx10 : u32 = indexFromCoord( ix1, iy0, simSize ); let idx01 : u32 = indexFromCoord( ix0, iy1, simSize ); let idx11 : u32 = indexFromCoord( ix1, iy1, simSize ); let parity00 : u32 = ( ix0 + iy0 ) & 1u; let parity10 : u32 = ( ix1 + iy0 ) & 1u; let parity01 : u32 = ( ix0 + iy1 ) & 1u; let parity11 : u32 = ( ix1 + iy1 ) & 1u; var sign00 : f32 = 1.0; var sign10 : f32 = 1.0; var sign01 : f32 = 1.0; var sign11 : f32 = 1.0; if ( parity00 == 1u ) { sign00 = -1.0; } if ( parity10 == 1u ) { sign10 = -1.0; } if ( parity01 == 1u ) { sign01 = -1.0; } if ( parity11 == 1u ) { sign11 = -1.0; } let h00 : f32 = heightField[ idx00 ] * heightAmp * sign00; let h10 : f32 = heightField[ idx10 ] * heightAmp * sign10; let h01 : f32 = heightField[ idx01 ] * heightAmp * sign01; let h11 : f32 = heightField[ idx11 ] * heightAmp * sign11; let hx0 : f32 = mix( h00, h10, tx ); let hx1 : f32 = mix( h01, h11, tx ); return mix( hx0, hx1, ty ); } @vertex fn v_main( @location(0) position : vec3, @location(1) uv : vec2, @builtin(instance_index) instanceIndex : u32 ) -> VertexOutput { let simSize : u32 = u32( renderParams[ 0 ] ); let meshSize : f32 = renderParams[ 1 ]; let heightAmp : f32 = renderParams[ 2 ]; let meshRes : u32 = u32( renderParams[ 4 ] ); // Tiling amount (slider 1..N). We clamp to at least 1. let tilingF : f32 = max( 1.0, renderParams[ 5 ] ); let tiling : u32 = max( 1u, u32( tilingF + 0.5 ) ); let tileRange : u32 = tiling - 1u; let tilesPerRow : u32 = tileRange * 2u + 1u; let tileIndex : u32 = instanceIndex; let tileXIndex : u32 = tileIndex % tilesPerRow; let tileZIndex : u32 = tileIndex / tilesPerRow; let offsetGridX : i32 = i32( tileXIndex ) - i32( tileRange ); let offsetGridZ : i32 = i32( tileZIndex ) - i32( tileRange ); let offsetX : f32 = f32( offsetGridX ) * meshSize; let offsetZ : f32 = f32( offsetGridZ ) * meshSize; let denom : f32 = max( 1.0, f32( meshRes - 1u ) ); let fxNorm : f32 = clamp( uv.x / denom, 0.0, 1.0 ); let fyNorm : f32 = clamp( uv.y / denom, 0.0, 1.0 ); let fx : f32 = fxNorm * f32( simSize - 1u ); let fy : f32 = fyNorm * f32( simSize - 1u ); let hCenter : f32 = sampleHeight( fx, fy, simSize, heightAmp ); let worldX : f32 = ( fxNorm - 0.5 ) * meshSize + offsetX; let worldZ : f32 = ( fyNorm - 0.5 ) * meshSize + offsetZ; // Derivatives via central differences on the sampled height field let fxStep : f32 = 1.0; let fyStep : f32 = 1.0; let hXp : f32 = sampleHeight( fx + fxStep, fy, simSize, heightAmp ); let hXm : f32 = sampleHeight( fx - fxStep, fy, simSize, heightAmp ); let hYp : f32 = sampleHeight( fx, fy + fyStep, simSize, heightAmp ); let hYm : f32 = sampleHeight( fx, fy - fyStep, simSize, heightAmp ); let h : f32 = hCenter; let dx : f32 = hXp - hXm; let dz : f32 = hYp - hYm; let normal : vec3 = normalize( vec3( -dx, 2.0, -dz ) ); let vp : mat4x4 = loadViewProjection(); var output : VertexOutput; output.position = vp * vec4( worldX, h, worldZ, 1.0 ); output.worldPosition = vec3( worldX, h, worldZ ); output.normal = normal; output.heightValue = h; return output; } @fragment fn f_main( input : VertexOutput ) -> @location(0) vec4 { let mode : u32 = u32( renderParams[ 3 ] ); let N : vec3 = normalize( input.normal ); // Debug: visualize normals if ( mode == 1u ) { let normalColor : vec3 = 0.5 * ( N + vec3( 1.0, 1.0, 1.0 ) ); return vec4( normalColor, 1.0 ); } // Debug: true solid color (no height variation) if ( mode == 2u ) { let flatColor : vec3 = vec3( 0.05, 0.4, 0.8 ); return vec4( flatColor, 1.0 ); } // Debug: visualize height field as grayscale if ( mode == 3u ) { // Scale and bias height into a visible range let h : f32 = input.heightValue; let hNorm : f32 = clamp( h * 0.05 + 0.5, 0.0, 1.0 ); let c : vec3 = vec3( hNorm, hNorm, hNorm ); return vec4( c, 1.0 ); } // Sun / light setup let lightDir : vec3 = normalize( vec3( 0.2, 0.85, 0.35 ) ); let sunColor : vec3 = vec3( 1.0, 0.97, 0.9 ); // Camera + view let camPos : vec3 = vec3( cameraPosition[ 0 ], cameraPosition[ 1 ], cameraPosition[ 2 ] ); let viewDir : vec3 = normalize( camPos - input.worldPosition ); // Normal and basic terms let NdotL : f32 = max( dot( N, lightDir ), 0.0 ); let NdotV : f32 = max( dot( N, viewDir ), 0.0 ); // Water body base color let waterBase : vec3 = vec3( 0.01, 0.18, 0.55 ); // Realistic mode: reuse simple lighting but with sand/depth-based underwater color if ( mode == 4u ) { let meshSize : f32 = renderParams[ 1 ]; let uvBase : vec2 = input.worldPosition.xz / meshSize + vec2( 0.5, 0.5 ); let noise : vec2 = vec2( sin( input.worldPosition.x * 0.05 + input.worldPosition.z * 0.12 ), cos( input.worldPosition.x * 0.04 - input.worldPosition.z * 0.09 ) ) * 0.03; let uvMain : vec2 = uvBase + noise; let uvAlt : vec2 = uvBase + vec2( 0.25, 0.43 ) + noise * 0.8; let colorSample : vec4 = textureSample( bottomColorTex, bottomSampler, uvMain ); let colorAlt : vec4 = textureSample( bottomColorTex, bottomSampler, uvAlt ); let finalColor : vec3 = mix( colorSample.rgb, colorAlt.rgb, 0.38 ); let heightSample: vec4 = textureSample( bottomHeightTex, bottomSampler, uvMain ); let heightAlt : vec4 = textureSample( bottomHeightTex, bottomSampler, uvAlt ); let heightVal : f32 = mix( heightSample.r, heightAlt.r, 0.4 ); let baseColor : vec3 = finalColor; let poolY : f32 = -6.0; let depthGeom : f32 = clamp( poolY - input.worldPosition.y, 0.0, 20.0 ); let shallowT : f32 = clamp( ( heightVal - 0.4 ) / 0.6, 0.0, 1.0 ); let depthT : f32 = clamp( depthGeom / 8.0, 0.0, 1.0 ); let sandWeight : f32 = shallowT * ( 1.0 - depthT ); let waterWeight : f32 = 1.0 - sandWeight; let sandColor : vec3 = baseColor; let wetColor : vec3 = mix( baseColor, waterBase, 0.6 ); let bottomColor : vec3 = sandColor * sandWeight + wetColor * waterWeight; let absorbCoeff : vec3 = vec3( 0.04, 0.09, 0.16 ); let absorb : vec3 = exp( -absorbCoeff * depthGeom ); let bodyColor : vec3 = bottomColor * absorb; let up : vec3 = vec3( 0.0, 1.0, 0.0 ); let reflDirEnv : vec3 = normalize( reflect( -viewDir, N ) ); let horizonT : f32 = clamp( pow( 1.0 - max( dot( reflDirEnv, up ), 0.0 ), 1.5 ), 0.0, 1.0 ); let skyZenith : vec3 = vec3( 0.02, 0.12, 0.28 ); let skyHorizon : vec3 = vec3( 0.30, 0.45, 0.70 ); let skyAnalytic : vec3 = mix( skyZenith, skyHorizon, horizonT ); let skyMask : vec3 = vec3( 0.08, 0.18, 0.32 ); let skyColor : vec3 = mix( skyAnalytic, skyMask, 0.4 ); let halfDir : vec3 = normalize( lightDir + viewDir ); let NdotH : f32 = max( dot( N, halfDir ), 0.0 ); let spec : f32 = pow( NdotH, 140.0 ) * NdotL; let specTerm : vec3 = spec * vec3( 1.0, 1.0, 1.0 ) * 1.3; let F0 : vec3 = vec3( 0.02, 0.025, 0.03 ); let oneMinus : f32 = 1.0 - NdotV; let fresnel : vec3 = F0 + ( vec3( 1.0, 1.0, 1.0 ) - F0 ) * pow( oneMinus, 5.0 ); let diffuse : vec3 = bodyColor * ( 0.15 + 0.85 * NdotL ) * sunColor; let envBlend : vec3 = mix( skyColor, vec3( 0.08, 0.25, 0.55 ), 0.35 ); let reflection : vec3 = envBlend + specTerm; let color : vec3 = diffuse * ( vec3( 1.0, 1.0, 1.0 ) - fresnel ) + reflection * fresnel; let colorGamma : vec3 = pow( clamp( color, vec3( 0.0, 0.0, 0.0 ), vec3( 1.0, 1.0, 1.0 ) ), vec3( 1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2 ) ); return vec4( colorGamma, 1.0 ); } // Simple sky / environment tint (towards horizon) let skyZenith : vec3 = vec3( 0.02, 0.12, 0.28 ); let skyHorizon : vec3 = vec3( 0.30, 0.45, 0.70 ); // Reflection direction for environment lookup let reflDirEnv : vec3 = normalize( reflect( -viewDir, N ) ); let up : vec3 = vec3( 0.0, 1.0, 0.0 ); let horizonT : f32 = clamp( pow( 1.0 - max( dot( reflDirEnv, up ), 0.0 ), 1.5 ), 0.0, 1.0 ); let skyAnalytic : vec3 = mix( skyZenith, skyHorizon, horizonT ); let skyMask : vec3 = vec3( 0.08, 0.18, 0.32 ); let skyColor : vec3 = mix( skyAnalytic, skyMask, 0.4 ); // Specular highlight using Blinn-Phong let halfDir : vec3 = normalize( lightDir + viewDir ); let NdotH : f32 = max( dot( N, halfDir ), 0.0 ); let spec : f32 = pow( NdotH, 140.0 ) * NdotL; let specTerm : vec3 = spec * vec3( 1.0, 1.0, 1.0 ) * 1.3; // Fresnel using Schlick's approximation (F0 ~ 0.02 for water) let F0 : vec3 = vec3( 0.02, 0.025, 0.03 ); let oneMinus : f32 = 1.0 - NdotV; let fresnel : vec3 = F0 + ( vec3( 1.0, 1.0, 1.0 ) - F0 ) * pow( oneMinus, 5.0 ); // Diffuse-ish underwater term (absorbed light) let diffuse : vec3 = waterBase * ( 0.15 + 0.85 * NdotL ) * sunColor; // Reflected environment (sky + sun highlight) let envBlend : vec3 = mix( skyColor, vec3( 0.08, 0.25, 0.55 ), 0.35 ); let reflection : vec3 = envBlend + specTerm; // Mix refraction (underwater) and reflection via Fresnel let color : vec3 = diffuse * ( vec3( 1.0, 1.0, 1.0 ) - fresnel ) + reflection * fresnel; // Mild contrast to keep it punchy let colorGamma : vec3 = pow( clamp( color, vec3( 0.0, 0.0, 0.0 ), vec3( 1.0, 1.0, 1.0 ) ), vec3( 1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2 ) ); return vec4( colorGamma, 1.0 ); }