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 Wave Animation

Per-frame vertex displacement system that animates water surfaces on the terrain grid. Water cells are identified by texture index, then their height values are replaced with a sine-table lookup producing radial concentric ripples.

Water Tile Detection

A grid cell is classified as water when all four corner vertices have a texture index (lower 6 bits of Map 3) in the set {0, 5, 30, 31}. The check runs per-cell inside the wave renderer — only cells passing all four corners receive wave displacement. Non-water cells retain their static height-table value.

Wave Parameters

Three values configure the wave system per world, set via world_wave[N] in WORLD.INI:

ParameterINI positionConsole commandDescription
Amplitude1stfxwaveampVertical scale of wave displacement. Multiplies the sine-table value.
Speed2ndfxwavespeedRate of phase advance per frame. Higher values = faster ripple animation.
Spatial frequency3rdfxwavefreqControls ripple density. Multiplies the squared-distance term in the phase calculation.

The setup function stores amplitude and spatial frequency directly; speed passes through a conversion function before storage.

Displacement Formula

For each water vertex at grid position (x, z), the engine computes:

distance_sq = x_offset² + z_offset²
phase       = (distance_sq * spatial_freq + frame_count * speed) & 0x7FF
wave_offset = sine_table[phase] * amplitude
bias        = amplitude * centering_constant

vertex.y    = wave_offset + height_table[heightmap_byte] - bias

Where:

  • x_offset, z_offset — grid-relative coordinates from the center of the visible terrain window
  • frame_count — global frame counter, advances each tick
  • & 0x7FF — wraps the phase to 2048 entries (the sine table length)
  • height_table — the same 128-entry height lookup table used for all terrain
  • bias — centers the oscillation so waves ripple symmetrically around the base water level

The squared-distance term produces concentric circular ripples radiating outward. This is not a planar wave — the phase depends on radial distance from the grid center, so ripples form rings rather than parallel lines.

Sine Lookup Table

The wave animation indexes a 2048-entry float table allocated at runtime. The table is addressed as:

value = table[(phase & 0x7FF) * 4]    (byte offset; effectively table[phase & 0x7FF] as float)

The table stores one full period of a periodic waveform across 2048 samples. Multiple engine systems share this table — it is also used for camera rotation interpolation and sky animation, confirming it is a general-purpose sine/cosine lookup rather than a water-specific waveform.

Water Level Initialization

The terrain height table has two initialization modes:

ModeFormulaWhen used
Defaultheight[i] = -ABS(source[i])No water level specified
Water-relativeheight[i] = water_level - ABS(source[i])Water level parameter is non-zero

When a non-zero water level is provided, a secondary rendering flag is set that enables the wave displacement pass. The same 128-entry source table is used in both modes — only the sign/offset changes.

Rendering Pipeline

The wave renderer runs each frame as part of the terrain update:

1. Build vertex grid
   └─ 33×33 vertices, each with X, Y (height), Z, texture index
   └─ stride: 76 bytes per vertex, 2584 bytes per row

2. Wave displacement
   ├─ Clear dirty flags
   ├─ For each grid cell:
   │   ├─ Check all 4 corners for water texture indices {0, 5, 30, 31}
   │   ├─ If water: replace vertex.y with sine_table[phase] * amplitude + height - bias
   │   └─ Set dirty flags on affected cells and neighbors
   └─ Recompute normals on displaced geometry:
       ├─ Face normals   (cross products per triangle)
       ├─ Vertex normals  (average adjacent face normals)
       └─ Smooth normals  (per-vertex lighting pass)

3. Lighting
   └─ Per-vertex RGB from ambient + directional dot-product, clamped to [0, 255]

4. Rasterize

The normal recomputation after displacement ensures water surfaces receive correct per-frame lighting as waves move — the normals tilt with the displaced geometry rather than remaining flat.

Terrain Vertex Layout

Each vertex in the 33×33 grid occupies 76 bytes:

OffsetSizeTypeNameDescription
0x004f32xWorld X position (grid_x * 256.0)
0x044f32yHeight (from lookup table, or wave-displaced)
0x084f32zWorld Z position (grid_z * 256.0)
0x18–0x30face_normalsTwo face normals per cell (upper/lower triangle)
0x30–0x3Cvertex_normalAveraged vertex normal for lighting
0x484u32texture_indexTerrain texture ID (lower 6 bits used for water detection)

Grid stride: 76 bytes between adjacent X-axis vertices, 2584 bytes (34 × 76) between rows.

Console Commands

Three runtime console commands allow tuning wave parameters without restarting:

CommandSyntaxEffect
fxwaveampfxwaveamp <value>Set wave amplitude
fxwavespeedfxwavespeed <value>Set wave speed
fxwavefreqfxwavefreq <value>Set spatial frequency

These modify the same globals as the INI parameters and take effect on the next frame.

External References