Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Water

Live Demo: Water

Nightshade includes a procedural water system with three rendering paths: ray-marched surface water, mesh-based water with vertex displacement, and volumetric water for waterfalls and mist.

How Water Rendering Works

Wave Generation

Water surfaces are animated using multi-octave procedural noise. The sea_octave function creates wave-like patterns by combining abs(sin) and abs(cos) of noise-distorted UV coordinates, then raising the result to a choppiness exponent. Higher choppiness values produce sharper wave peaks.

The height map evaluates 5 octaves of this function. Each octave doubles the frequency and reduces amplitude by 0.22x, while increasing choppiness by 20%. Between octaves, UV coordinates are rotated by a fixed 2x2 matrix [1.6, 1.2; -1.2, 1.6] to prevent visible repetition. Two offset directions (uv + time and uv - time) create coherent wave interference patterns.

Fresnel Reflections

The water shader computes the Fresnel effect to blend between refraction (seeing into the water) and reflection (seeing the sky):

fresnel = pow(1.0 - dot(normal, view_direction), fresnel_power)

At steep viewing angles (looking straight down), fresnel is near 0 and the water shows its base color. At grazing angles (looking across the surface), fresnel approaches 1 and the water reflects the sky. The fresnel_power parameter (default 3.0) controls how quickly this transition happens.

Specular sun reflections use a cosine power of 60 for tight, bright highlights on wave crests.

Three Rendering Paths

Path 1: Ray-Marched Surface Water - For bounded water regions without mesh geometry. The fragment shader traces rays from the camera through the 3D height field using heightmap_tracing() with 32 march steps and geometric refinement iterations. This produces per-pixel correct reflections and refractions. Supports polygon bounds with soft edge feathering. Limited to 16 simultaneous water regions.

Path 2: Mesh-Based Water - For large flat surfaces. The vertex shader applies procedural wave displacement to mesh vertices using the same water_height() function. The fragment shader computes normals from height gradients, applies Fresnel-based sky reflection, and adds subsurface scattering: pow(max(dot(view, -sun_dir), 0.0), 2.0) * 0.2. More efficient than ray-marching and integrates properly with the depth buffer.

Path 3: Volumetric Water - For waterfalls, mist, and cascading water. A per-pixel ray-marching shader traces through SDF-bounded volumes (box, cylinder, or sphere) with up to 64 steps. Three flow types have different density functions:

  • Waterfall: High vertical stretch with turbulence, top-to-bottom falloff
  • Mist: Rising motion with horizontal drift, wispy patterns using 3D FBM noise
  • Cascade: Multiple parallel streams using 3 layered FBM noise functions

Volumetric water is lit with sun shadowing through the volume and foam blending based on accumulated density.

Vertical Water

For waterfall surfaces, a separate shader (water_mesh_vertical.wgsl) displaces vertices along the surface normal instead of the Y-axis. Wave frequency is stretched (2.0x horizontally, 0.5x vertically) to create vertical streaks. Foam patterns are generated from layered noise and blended with the water color.

Frustum Culling

A compute shader (water_mesh_culling.wgsl) tests each water object's bounding sphere against the 6 frustum planes. Culled objects skip rendering entirely. For volumetric water, the bounding sphere is derived from the volume's half-size. Visible instances are appended to an indirect draw buffer via atomic operations.

Water Component

#![allow(unused)]
fn main() {
pub struct Water {
    pub wave_height: f32,           // Wave amplitude (default: 0.6)
    pub choppy: f32,                // Wave sharpness (default: 4.0, higher = sharper peaks)
    pub speed: f32,                 // Animation speed (default: 0.8)
    pub frequency: f32,             // Wave frequency (default: 0.16, lower = longer waves)
    pub base_height: f32,           // Water level (default: 0.0)
    pub base_color: Vec4,           // Dark water color
    pub water_color: Vec4,          // Light water color
    pub specular_strength: f32,     // Sun reflection intensity (default: 1.0)
    pub fresnel_power: f32,         // Reflection balance (default: 3.0)
    pub edge_feather_distance: f32, // Shore softness (default: 2.0)
    pub is_vertical: bool,          // Waterfall mode
    pub is_volumetric: bool,        // 3D volume mode
    pub volume_shape: VolumeShape,  // Box, Cylinder, or Sphere
    pub volume_flow_type: VolumeFlowType, // Waterfall, Mist, or Cascade
    pub volume_size: Vec3,          // Volume dimensions
    pub flow_direction: Vec2,       // Normalized flow direction
    pub flow_strength: f32,         // Flow intensity
}
}

Spawning Water

Planar Water

#![allow(unused)]
fn main() {
let water_entity = world.spawn_entities(
    LOCAL_TRANSFORM | GLOBAL_TRANSFORM | WATER,
    1
)[0];

world.core.set_local_transform(water_entity, LocalTransform {
    translation: Vec3::new(0.0, 0.0, 0.0),
    ..Default::default()
});

world.core.set_water(water_entity, Water {
    wave_height: 0.5,
    speed: 1.0,
    frequency: 0.5,
    choppy: 4.0,
    base_color: Vec4::new(0.0, 0.1, 0.2, 1.0),
    water_color: Vec4::new(0.0, 0.3, 0.5, 1.0),
    fresnel_power: 3.0,
    specular_strength: 1.0,
    edge_feather_distance: 1.0,
    ..Default::default()
});
}

Volumetric Water (Waterfall)

#![allow(unused)]
fn main() {
world.core.set_water(waterfall_entity, Water {
    is_volumetric: true,
    volume_shape: VolumeShape::Box,
    volume_flow_type: VolumeFlowType::Waterfall,
    volume_size: Vec3::new(2.0, 10.0, 1.0),
    flow_direction: Vec2::new(0.0, -1.0),
    flow_strength: 2.0,
    base_color: Vec4::new(0.0, 0.15, 0.25, 1.0),
    water_color: Vec4::new(0.1, 0.4, 0.6, 1.0),
    ..Default::default()
});
}

Wave Parameters

ParameterRangeEffect
wave_height0.1-2.0Vertical amplitude of waves
choppy1.0-8.0Higher = sharper peaks, lower = smooth rounded waves
speed0.1-2.0Animation speed (time multiplier)
frequency0.05-0.5Lower = longer wavelengths, higher = finer detail
fresnel_power1.0-10.0Higher = stronger reflection at grazing angles
specular_strength0.0-2.0Sun reflection intensity

Dynamic Weather

Water properties can be changed at runtime for weather transitions:

#![allow(unused)]
fn main() {
fn stormy_weather(world: &mut World, water: Entity) {
    if let Some(w) = world.core.get_water_mut(water) {
        w.wave_height = 3.0;
        w.speed = 2.0;
        w.choppy = 6.0;
    }
}

fn calm_weather(world: &mut World, water: Entity) {
    if let Some(w) = world.core.get_water_mut(water) {
        w.wave_height = 0.3;
        w.speed = 0.5;
        w.choppy = 2.0;
    }
}
}