ENGINE 08 ยท DETERMINISTIC WATER

The water plane is deterministic; only its light moves

Chunk.js first shapes river, lake, coast, and ocean beds through deterministic terrain, then fills every column below the canonical sea level of 96 to one shared plane. A compact column profile preserves that answer, while a top-only visual mesh adds depth tint, ripples, sun light, sparse sparkles, and transparency without moving an authoritative voxel. Rust independently reconstructs the block used by mining validation, where natural water remains unmineable.

10 min read
The canonical orange-haired villager girl measures a shallow stepped riverbed while the black-haired villager boy repeats the survey from a pile-supported pier across the same flat, depth-tinted water plane.

Key points

One plane, not a fluid simulation

River and lake masks lower the deterministic terrain bed. Natural water then fills only columns whose surface lies below sea level 96, always to that same level; it does not flow, spread, refill, or rise independently.

Compact state, animated presentation

One stored water height per horizontal column reconstructs the filled volume. Rendering emits the exposed top surface, converts up to twelve blocks of sampled depth into a packed byte, and moves only fragment shading over the flat geometry.

The chain verifies a block, not a wave

Rust independently reconstructs the canonical block at integer coordinates. A mismatched claimed block fails, natural water is rejected as a mining target, and no wave, sparkle, alpha value, swimming state, or buoyancy result becomes chain state.

DETERMINISTIC TERRAIN FIRST

River and lake masks carve a bed; they do not create another water level

The generation pipeline resolves integer terrain factors before it asks for water. Ocean and coast terms shape the broad profile. River and lake masks flatten floodplains, lower valley cores, and can pull the bed below sea level. Near and beneath the resulting shoreline, the surface material resolves to sand, gravel, clay, or shell bed according to the same deterministic inputs.

After the bed height H is known, natural water has one answer. If H is below the canonical sea level, the column fills from H+1 through 96; otherwise it receives no water. The current generator does not produce elevated lakes, separate river stages, tides, pressure, sources, sinks, spreading, refill, or dynamic level changes.

Canonical sea level Y = 96

Both current JavaScript defaults and Rust canonical constants use the same integer plane.

Filled interval H < y <= 96

Terrain remains at and below H; ordinary water occupies only the cells above that bed.

Horizontal water answers 96 or none

The final current water-level helper has no coordinate-dependent elevation branch.

COMPACT CLIENT STATE AND PROGRAM RECONSTRUCTION

One column value carries the volume; Rust rechecks the block

A normal 16 by 16 base profile stores surfaceY and waterY as signed sixteen-bit arrays and surfaceBlock as an unsigned sixteen-bit array. The no-water sentinel is minY minus one. Those three arrays occupy 1,536 bytes before object overhead and optional data, while the final block query reconstructs all water cells from the two heights instead of retaining a dense base-water volume.

The Rust program independently uses the same below-sea conditional when it reconstructs a block for mining. Chunk.js smoke tests scan below-sea mainnet columns and require ordinary water at sea level 96, while the Rust unit vector asserts ordinary water at (0,96,0) above a surface at 85. These are independent source and representative regression checks, not an exhaustive or formal all-coordinate JavaScript-to-Rust parity proof.

Profile cells 256 columns

A 16 by 16 Chunk needs one surface height, water height, and surface block per column.

Core profile bytes 1,536 bytes

256 columns multiplied by three two-byte array entries, before maps, objects, trees, deltas, or meshes.

Program input Integer coordinate

The expected client block is compared with a block reconstructed from canonical inputs.

VISUAL MESH SURFACE

The renderer submits an exposed top, not every cell in the water column

For a profile-backed Chunk, the visual mesher jumps directly to waterY, confirms that the final block there is fluid, and emits only top face two. The explicit gradient path writes four vertices on that same top boundary; the grouped path can merge compatible top cells. Dense fallback and delta paths likewise skip a fluid cell when another fluid or opaque solid covers it and never emit submerged internal fluid faces.

Water belongs to the separate visual mesh and transparent draw pass rather than the opaque terrain mesh. Alpha blending reveals already-rendered bed blocks through the surface, while depth writes are disabled for the visual pass. That is transparency over a flat top, not refraction, reflected scene geometry, a displaced wave mesh, or a volumetric water renderer.

Natural surface per flooded column One exposed top

The compact profile does not expand every submerged water block into visible geometry.

Surface position Water block top

The explicit quad uses the upper Y boundary of the water block and does not animate its vertices.

Draw behavior Blended visual pass

Terrain remains in the opaque pass; the surface contributes color and alpha without writing transparent depth.

DEPTH BYTE AND SHORE CONTINUITY

A bounded bed sample travels through one existing vertex byte

An unchanged generated Chunk can calculate visual depth directly from canonical surface and water heights. When chain or pending deltas exist, the sampler falls back to scanning downward through at most twelve cells, skipping fluids, air, and low vegetation until it finds the first remaining block type. Each result is cached for the mesh build.

The smoothed, quantized result is encoded as 150 through 255 in the vertex field normally named AO. When a cardinal neighbor differs by at least ten encoded units, the mesher stops using one flat value and averages up to four adjacent exposed same-family water cells at each corner. The renderer lets ordinary and swamp water share corner continuity, keeps toxic water distinct, and skips water-depth encoding for lava. Those are registered compatibility branches only: current canonical JavaScript and Rust natural generation emits ordinary water 17, not swamp water 18, toxic water 19, or lava 20.

Depth search cap 12 cells

Unchanged generated terrain uses height math; modified terrain scans no farther than the same cap.

Encoded range 150 to 255

The packed byte is decoded by the water fragment path as a shallow-to-deep signal.

Corner-gradient trigger Delta >= 10

A large enough encoded difference replaces the grouped flat-depth top with per-corner depth values.

TIME-BASED PRESENTATION ONLY

Color, apparent normal, sun light, and sparkles move across fixed geometry

The fragment shader combines three broad directional sine signals and three fine line masks. Related cosine gradients produce an apparent water normal for sun specular terms. The decoded depth byte selects shore, clear, sea, and deep-blue mixes; camera distance adds a small contribution, and base alpha moves from 0.60 toward 0.82 before ripple and sparkle additions, horizon-haze reduction, and global-opacity multiplication. Fog blends RGB only.

Clean water also samples three sparse deterministic sparkle-cell scales, then animates each active cell's brightness with presentation time. A separate fine-line sparkle multiplier is present but currently cannot activate: fineA, fineB, and fineC are each at most one, so their 0.16, 0.12, and 0.08 weighted sum is at most 0.36, below smoothstep's 0.58 lower edge. This guide counts the independent cell sparkles and blue sun trail as live, but not that unreachable product.

Broad wave signals 3 directions

Their different speeds, frequencies, and phases reduce obvious repetition without changing mesh positions.

Base clean-water alpha 0.60 to 0.82

Ripple and active sparkle terms add small values before horizon haze can reduce alpha and overall opacity multiplies it; fog changes color, not alpha.

Scene reflection and refraction Not implemented

The current water path has no reflected scene texture, refraction sample, Fresnel pass, or vertex-wave displacement.

RAYCAST, COLLISION, EFFECTS, AND CHAIN

Water can be seen and targeted, but it is not yet a traversal or fluid system

The block raycaster returns the first non-air, non-pass-through-vegetation cell, so transparent water can stop a mining ray before the bed. The mining controller then rejects every fluid target, and the Chunk Program separately rejects ordinary natural water. Water is liquid and transparent but not blocking, so the player-motion controller does not treat it as a body or ground collision. There is no swimming, buoyancy, underwater drag, breath, drowning, boat physics, or water-current response.

Movement near a fluid cell can emit a small splash-particle burst after at least 0.10 horizontal movement and an 88-millisecond cooldown; that effect does not update world state. Foundations require level, blocking, non-fluid ground. Normal block placement requires an empty target and excludes fluid items, while the current chain placement entry point is explicitly disabled.

  1. Raycast

    The transparent water block is still a hit, so the current targeting path does not pass through it to the bed.

  2. Validate

    Client action rules reject fluid mining, dry-ground construction rejects water, and Rust rejects natural-water mining again.

  3. Present

    Local movement may create splash particles. The effect itself changes no water cell or inventory or chain account, and it applies no swimming, buoyancy, or current response.

LEVEL, DEPTH, AND FRAGMENT MATH

One integer plane becomes many visible depths without becoming fluid physics

The first two equations define the canonical water answer. The third mirrors the mesher's bounded, smoothed depth byte. The fourth mirrors the three broad fragment waves that alter shading while every water vertex stays fixed.

Canonical natural-water level

W(x,z) = S if H(x,z) < S; otherwise W(x,z) = none; current canonical S = 96

H is the deterministic terrain surface and S is the shared sea level. The current JavaScript and Rust helpers ignore x and z after H has been resolved: a below-sea column receives exactly S, while a surface at or above S receives no natural water level.

H(x,z)
Canonical terrain surface height at the horizontal coordinate.
S
Canonical sea level, currently 96.
W(x,z)
Stored water height for the column, or no-water state.

Water-block occupancy and column depth

B(x,y,z) = water(17) iff H(x,z) < y <= S; N(x,z) = max(0, S - H(x,z))

Only cells above the terrain surface and at or below the common plane become natural water. N counts the filled water cells in the column. River and lake masks influence H by carving the bed; they do not assign another water elevation.

B(x,y,z)
Canonical generated block at the integer coordinate.
water(17)
The ordinary natural-water block identifier.
N(x,z)
Number of generated water cells above the bed in this column.

Bounded visual depth encoding

d_c=clamp(trunc(d),1,12); u=(d_c-1)/11; s=u*u*(3-2*u); q=round(6*s)/6; A8=150+round(105*q)

The visual sampler caps its search at twelve blocks, smooths the normalized result, quantizes q in one-sixth increments, and writes 150 through 255 into the packed AO byte. The water shader reinterprets that byte as depth; it is neither physical ambient occlusion nor chain state.

d, d_c
Measured and clamped visual distance from the water-surface block to the first block below it that is neither fluid, air, nor low vegetation.
q
Smoothed depth quantized into six equal intervals, yielding seven possible levels including both endpoints.
A8
Packed unsigned byte delivered to the fragment shader as vAo.

Three broad fragment waves

wave(p,dir,v,f,phase,t)=sin((dot(p,dir)*f+phase+t*v)*pi); r=0.5+0.5*(0.45*w1+0.32*w2+0.23*w3)

The shader evaluates three directions with different speeds, frequencies, and phases, then remaps their weighted sum to r. It uses related gradients for an apparent normal and uses r in color and alpha. There is no vertex displacement, fluid velocity, or authoritative time-varying water coordinate.

p, t
World-space horizontal fragment position and presentation time.
dir, v, f, phase
One wave's direction, speed, frequency, and fixed phase.
r
Broad ripple shading signal, nominally between zero and one.

JavaScript returns only the shared sea-level plane

JavaScript chunk.js/world/world-generator.js
export function waterLevelAt(config, worldX, worldZ, surface = terrainSurfaceHeight(config, worldX, worldZ)) {
  const x = Math.trunc(worldX);
  const z = Math.trunc(worldZ);
  const cacheKey = `${x},${z},${surface}`;
  const cached = config.cache?.water?.get(cacheKey);
  if (cached !== undefined) return cached;
  let waterLevel = null;
  if (surface < config.seaLevel) {
    waterLevel = config.seaLevel;
  }
  cacheSetBounded(config.cache?.water, cacheKey, waterLevel);
  return waterLevel;
}

The coordinate participates in deterministic terrain generation and cache identity, but the final natural-water choice is binary: the canonical sea level or no water. A similarly named inland-water helper exists elsewhere in the file but has no callers and is not part of this path.

The profile path emits one water plane from both sides

JavaScript chunk.js/chunk/chunk-mesher.js
        if (water !== null && water > surface && water >= minY && water < minY + height) {
          const blockId = chunkState.getFinalBlock(x, water, z);
          if (isFluidBlock(blockId)) {
            blockCount += 1;
            if (shouldUseGradientWaterSurface(chunkState, x, water, z, blockId, getBlockAtWorld, waterDepthCache)) {
              quadCount += appendWaterSurfaceQuad(chunkState, x, water, z, blockId, getBlockAtWorld, waterDepthCache, positions, indices);
            } else {
              addWaterSurfaceCells(faceGroups, x, water, z, blockId, waterSurfaceDepthAoCached(chunkState, x, water, z, blockId, getBlockAtWorld, waterDepthCache));
            }
          }
          continue;
        }

The compact profile carries one water height per column. The visual mesher emits upward and downward faces on the same plane, so the surface remains visible from above and underwater without expanding every submerged water cell into geometry.

Presentation time derives ripples and an apparent normal

GLSL chunk.js/renderer/shader-manager.js
  if (layer >= 17.0 && layer <= 20.0) {
    vec2 p = vWorldXZ;
    vec2 d1 = normalize(vec2(0.68, 1.0));
    vec2 d2 = normalize(vec2(-0.92, 0.38));
    vec2 d3 = normalize(vec2(0.24, -0.98));
    float w1 = waterWave(p, d1, 0.24, 0.28, 7.0);
    float w2 = waterWave(p, d2, -0.18, 0.42, 11.0);
    float w3 = waterWave(p, d3, 0.11, 0.18, 17.0);
    float ripple = (w1 * 0.45 + w2 * 0.32 + w3 * 0.23) * 0.5 + 0.5;
    float fineA = waterLine(p, normalize(vec2(0.92, -0.24)), 0.22, 0.018, 0.026, 5.0);
    float fineB = waterLine(p, normalize(vec2(0.36, 1.0)), 0.34, -0.014, 0.022, 9.0);
    float fineC = waterLine(p, normalize(vec2(-0.72, 0.42)), 0.48, 0.012, 0.018, 13.0);
    vec2 grad = d1 * cos((dot(p, d1) * 0.64 + uTime * 0.42) * 3.14159) * 0.64 * 0.45;
    grad += d2 * cos((dot(p, d2) * 1.12 - uTime * 0.29) * 3.14159) * 1.12 * 0.32;
    grad += d3 * cos((dot(p, d3) * 0.28 + uTime * 0.17) * 3.14159) * 0.28 * 0.23;
    vec3 waterNormal = normalize(vec3(-grad.x * 0.34, 1.0, -grad.y * 0.34));

These calculations run in the fragment shader after the flat surface has been meshed. They change the sampled color, apparent normal, highlights, and transparency, but they never write a new world position or water block.

Mining re-derives the block and rejects natural water

Rust programs/nicechunk_chunk/src/lib.rs
    let surface = generated_surface_height(global_config_view, args.world_x, args.world_z);
    let block_id = generated_block_id_at_surface(global_config_view, &generated_args, surface);
    if args.expected_block_id != block_id {
        return Err(NicechunkChunkError::GeneratedBlockMismatch.into());
    }
    if matches!(block_id, BLOCK_AIR | BLOCK_WATER | BLOCK_BEDROCK) {
        return Err(NicechunkChunkError::UnmineableBlock.into());
    }

The submitted expected identifier is not authority. The Chunk Program reconstructs the canonical integer-coordinate block, rejects a mismatch, and explicitly excludes ordinary natural water block 17 from the mining state transition.

IMPLEMENTATION EVIDENCE

Where these claims come from

Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.

chunk.js/core/constants.js

Defines the current default sea level of 96, Chunk dimensions, and the water-surface offset constant.

chunk.js/world/world-generator.js

Shapes ocean, coast, river, lake, and valley beds; resolves shore materials; fills only below-sea columns to one plane; and stores compact column profiles.

chunk.js/tests/smoke.test.mjs

Regression-scans natural water for the global plane, absence of suspended water above land, and broad below-sea river columns.

chunk.js/chunk/chunk-state.js

Reconstructs base water blocks from surface and water column heights; getFinalBlock gives pending deltas first priority, then chain deltas, then that base answer.

chunk.js/chunk/chunk-mesher.js

Builds the top-only fluid visual mesh, bounded depth sampler, packed depth byte, neighbor-gradient path, and water surface-family rules.

chunk.js/renderer/shader-manager.js

Implements fragment-only water waves, apparent normals, depth colors, shore blend, sun terms, alpha, fog, sparkle cells, and the currently unreachable fine-line sparkle multiplier.

chunk.js/renderer/webgl2-renderer.js

Draws visual geometry with alpha blending and disabled depth writes after the opaque terrain pass.

chunk.js/world/block-registry.js

Defines ordinary water as transparent liquid block 17 without solid, blocking, or mineable flags, while keeping other fluid identities distinct.

chunk.js/input/raycast.js

Returns the first non-air hit, including water, instead of raycasting through transparent fluid to the bed.

play/mining-controller.js

Rejects fluid blocks before a mining swing can become a submitted mining action.

play/placement-controller.js

Requires an air target and excludes fluid or non-blocking block identifiers from placeable inventory choices.

play/player-motion-controller.js

Uses blocking-block queries for body and ground collision, with no water-specific swimming, buoyancy, drag, breath, or drowning branch.

play/play-effects.js

Emits rate-limited local splash particles from movement near a fluid cell without mutating water or player state.

play/foundation-controller.js

Requires level, blocking, non-fluid ground and clear space before a foundation preview can be valid.

src/chain/nicechunkChain.js

Keeps the current ordinary block-placement chain entry point explicitly disabled.

programs/nicechunk_chunk/src/state.rs

Defines canonical sea level 96, reconstructs ordinary water from the same below-sea rule, and carries representative Rust regression vectors.

programs/nicechunk_chunk/src/lib.rs

Reconstructs the submitted mining coordinate, rejects an expected-block mismatch, and forbids ordinary water as a mined block.

play/play-chain-chunk-deltas.js

Decodes every accepted ChunkBroken record as an air delta; it carries no fluid level, flow, or replenishment state.