Few Juicy Points
• Generated on the shader (no external textures or files)
• An object is added to the water to emphasis various effect (reflection and refraction)
• Object bounces around due to the water
• Pattern on the object is generated using the object shape/position surface
• Interactive so you can move your mouse around to see the shape/water from different angles
Beauty of Water
The ocean is a breathtaking spectacle of beauty and tranquility, a boundless expanse that captivates with its vastness and timeless allure. Its surface shimmers under the sun, shifting in shades from deep sapphire to light turquoise, each wave catching the light in a dance as ancient as the earth itself. The ocean's movement is mesmerizing—gentle ripples glide across calm bays, while powerful waves roll with a grace that's both majestic and soothing. There is a profound simplicity in the way the ocean breathes in rhythmic tides, rising and falling with a calming constancy, connecting shorelines and washing over sands in an eternal cycle. Beneath the surface lies a world of delicate beauty and vibrant life, hidden mysteries that evoke both wonder and peace. The ocean's endless horizons remind us of nature's vastness, inspiring awe and reflection in its quiet, powerful presence.
The output for the example water simulation.
Fragment Shader
The complete implementation is in the fragment shader! Essentially a brute force ray-tracer - for each pixel it calculates the intersection with the water surface and then the lighting calculation (reflections, etc).
Complete fragment shader code is given below:
// Don't use images but is an extra for texturing the shape @group(0) @binding(0) var mySampler: sampler; @group(0) @binding(1) var myTexture: texture_2d<f32>; @group(0) @binding(2) var <uniform> mytimer : f32; @group(0) @binding(3) var <uniform> mymouse : vec2<f32>;
const resolution = vec2<f32>(512, 512);
fn ToGamma( col:vec3<f32> ) -> vec3<f32> { // Gamma correction let GammaFactor:f32 = 2.2; // convert back into color values, so the correct light will come out of the monitor return pow( col, vec3(1.0/GammaFactor) ); }
Calculates camera position and direction based on rotation, zoom, and screen coordinates.
Outputs a camera structure with `pos`, `ray`, and `localRay` for viewing direction and origin.
Smooths random values to create a cohesive noise pattern.
Interpolates values between tile corners, resulting in smooth, blended noise.
You can see what the smooth noise function generates. .
You can try out an interactive noise demo showing the smooth noise scrolling LINK.
fn randomsmooth( st:vec2<f32> ) -> f32 { var i = floor( st * 3.0 ); // uv - 0, 1, 2, 3, var f = fract( st * 3.0 ); // uv - 0-1, 0-1, 0-1, 0-1
// Four corners in 2D of a tile var a = random(i); var b = random(i + vec2<f32>(1.0, 0.0)); var c = random(i + vec2<f32>(0.0, 1.0)); var d = random(i + vec2<f32>(1.0, 1.0));
// Ease-in followed by an ease-out (tweening) for f // f = 0.5 * (1.0 - cos( 3.14 * f ) ); // version without cos/sin // f = 3*f*f - 2*f*f*f;
f = 3*f*f - 2*f*f*f;
// bilinear interpolation to combine the values sampled from the // four corners (a,b,c,d), resulting in a smoothly interpolated value.
// Interpolate Along One Axis - interpolate between `a` and `b` using the fractional coordinate `f.x`, // then interpolate between `c` and `d` using the same `f.x`. // This gives two intermediate values, say `e` and `f`.
// Interpolate Along the Other Axis - linearly interpolate between `e` and `f` // using the fractional coordinate `f.y`. // Final interpolation gives a moothly interpolated value across the square
var x1 = mix( a, b, f.x ); var x2 = mix( c, d, f.x );
var y1 = mix( x1, x2, f.y );
return y1; }
Produces 3D noise using smoothed random functions for multi-dimensional use.
The precise version is to refine the noise by scaling input coordinates, creating a sharper noise effect.
// need to do the octaves from large to small, otherwise things don't line up // (because I rotate by 45 degrees on each octave) pos += mytimer*vec3(0,.1,.1); for ( var i:i32=0; i < octaves; i++ ) { pos = (pos.yzx + pos.zyx*vec3(1,-1,1))/sqrt(2.0); f = f*2.0+abs(NoisePrecise(pos).x-0.5)*2.0; pos *= 2.0; } f /= exp2(f32(octaves));
return (.5-f)*1.0; }
Creates smoother wave effects by applying additional noise blending.
Creates a gentler water surface than other wave functions.
pos += mytimer*vec3(0,.1,.1); for ( var i:i32=0; i < octaves; i++ ) { pos = (pos.yzx + pos.zyx*vec3(1,-1,1))/sqrt(2.0);
f = f*2.0+sqrt(pow(NoisePrecise(pos).x-.5,2.0)+.01)*2.0; pos *= 2.0; } f /= exp2(f32(octaves));
return (.5-f)*1.0; }
Smoothly interpolates values between two edges, `edge0` and `edge1`.
Applies smoothing to input `x` to reduce sharp transitions.
Also a builtin version `smoothstep` but the custom version was used so we could tinker with the values while experimenting.
fn mysmoothstep( edge0:f32, edge1:f32, x:f32 ) -> f32 { // Clamp the input value between 0 and 1 var t:f32 = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); // Apply the smoothstep formula return t * t * (3.0 - 2.0 * t); }
Simulates foam on wave crests based on input position.
Enhances wave realism by adding bright foam to high wave areas.
fn Sky(ray: vec3<f32>) -> vec3<f32> { let horizon_color = vec3(.4,.45,.5); // Light blue near the horizon let zenith_color = vec3(.4,.45,.5)*0.1; // Darker blue for the sky above
// Calculate a factor based on the Y component of the ray direction // Higher y value means looking up, lower means closer to horizon let blend_factor = clamp(ray.y * 0.5 + 0.5, 0.0, 1.0);
// Blend between zenith and horizon colors based on blend_factor return mix(horizon_color, zenith_color, blend_factor); }
Calculates the boat's position, orientation, and movement based on water surface.
Returns a `stBoat` structure with direction vectors and orientation matrix.
struct stBoat { right : vec3<f32>, up : vec3<f32>, forward : vec3<f32>, position : vec3<f32>, rotation : mat3x3<f32> };
fn ComputeBoatTransform() -> stBoat { var samples : array<vec3<f32>, 5>;
Colors the boat surface based on light direction, reflections, and textures.
Calculates the boat's shading using normals, lighting, and fresnel reflections.
fn ShadeBoat( posin:vec3<f32>, ray:vec3<f32> ) -> vec3<f32> { var t:f32 = TraceBoat( posin, ray ); var hp = posin + ray*t; var norm = rayNormal( posin, ray );
var lightDir:vec3<f32> = normalize(vec3(-2,3,1)); var ndotl:f32 = 0.2 + abs( dot(norm,lightDir) );
// allow some light bleed, as if it's subsurface scattering through plastic var light:vec3<f32> = mysmoothstep(-.1,1.0,ndotl)*vec3(1.0,.9,.8)+vec3(.06,.1,.1);
var albedo:vec3<f32> = ShadeBoatPattern(hp);
var col:vec3<f32> = albedo*ndotl;
// specular var h:vec3<f32> = normalize(lightDir-ray); var s:f32 = pow(max(0.0,dot(norm,h)),100.0)*100.0/32.0;
var ndotr:f32 = dot(norm,ray); var fresnel:f32 = pow(1.0-abs(ndotr),5.0); fresnel = mix( .001, 1.0, fresnel );
col = mix( col, specular, fresnel );
return col; }
Calculates distance from a point to the ocean surface, modified by wave height.
Used for ray-marching to the water surface. Refined version is for more detail using additional wave calculations.
Ray-marches through the scene to detect intersections with the ocean surface.
Stops when close to the surface or exceeding max distance.
fn TraceOcean( pos:vec3<f32>, ray:vec3<f32> ) -> f32 { var h:f32 = 1.0; var t:f32 = 0.0; for ( var i:i32=0; i < 100; i++ ) { if ( h < .01 || t > 100.0 ) { break; } h = OceanDistanceField( pos+t*ray ); t += h; }
if ( h > .1 ) { return 0.0; } return t; }
Colors the ocean surface by calculating reflections, foam, and fresnel effect.
Blends refractions and reflections for realistic water appearance.
fn ShadeOcean( pos:vec3<f32>, ray:vec3<f32>, fragCoord:vec2<f32> ) -> vec3<f32> { var norm:vec3<f32> = OceanNormal(pos); var ndotr:f32 = dot(ray,norm);
var fresnel:f32 = pow(1.0-abs(ndotr),5.0);
var reflectedRay:vec3<f32> = ray-2.0*norm*ndotr; var refractedRay:vec3<f32> = ray+(-cos(1.33*acos(-ndotr))-ndotr)*norm; refractedRay = normalize(refractedRay);
const episonFudge:f32 = 0.0;
// reflection var reflection:vec3<f32> = Sky(reflectedRay); var t:f32 = TraceBoat( pos-episonFudge*reflectedRay, reflectedRay );
if ( t > 0.0 ) { reflection = ShadeBoat( pos-episonFudge*reflectedRay, reflectedRay ); }
// refraction t = TraceBoat( pos-episonFudge*refractedRay, refractedRay );
var col:vec3<f32> = vec3(0,.04,.04); // under-sea color if ( t > 0.0 ) { col = mix( col, ShadeBoat( pos-episonFudge*refractedRay, refractedRay ), exp(-t) ); }
col = mix( col, reflection, fresnel );
// foam col = mix( col, vec3(1), WaveCrests(pos,fragCoord) );
return col; }
Primary fragment shader function for rendering the scene.
Determines the camera view and calculates shading for sky, ocean, and boat.
// Shader entry point @fragment fn main(@location(0) uvs : vec2<f32>) -> @location(0) vec4<f32> { var fragCoord = uvs * resolution;
var camRot:vec2<f32> = vec2(.5,.5) + vec2(-.35,4.5)*(mymouse.yx/resolution.yx);
var cam = CamPolar( vec3(0), camRot, 5.0, 1.0, fragCoord );
var to:f32 = TraceOcean( cam.pos, cam.ray ); var tb:f32 = TraceBoat( cam.pos, cam.ray );
var result: vec3<f32>; if ( to > 0.0 && ( to < tb || tb == 0.0 ) ) { result = ShadeOcean( cam.pos+cam.ray*to, cam.ray, fragCoord ); } else if ( tb > 0.0 ) { result = ShadeBoat( cam.pos, cam.ray ); } else { result = Sky( cam.ray ); }
The example as only scratched the surface of what is possible an what you can do next! Some interesting and fun ideas to take it further include:
• Tweak the sea noise generation (it's okay, but could still be improved)
• Different noise patterns can generate different ocean water types (deep, shallow, ...)
• Use 2d map to make different parts of the water use different noise (e.g., close to short or deep water, behaves differently)
• Also modify the noise - so if it's close to the object uses a different noise pattern
• Add some under water creatures, whale or fished?
• Add some 'splash' effects (particles)
• SDF shape - simple box or sphere - but try other shapes
• SDF shape of a 'bottle' - but make it transparent? (glass bottle floating at sea)
• Add background sky/sunset
• Draw lots of things int he water (e.g., rubbish, coke cans, use 'mod' so you can draw hundreds of them)
• Make the water effect into game? little boat driving around?
• Use the texture for the floating object (instead of a generated pattern - texture mapped ot the surface)
• Add interface to allow users to tinker with the variables
• Create a demo scene with sand islands coming out of the water, fish jumping, palm trees, and a sun moving across the sky.