joy-terrain (joy_engine::terrain) owns terrain data: version 1 heightfield assets, tile pyramids and packages, streaming, quadtree LOD selection, synthesized detail, craters, and sampling. joy-renderer draws TerrainSurface snapshots through RenderScene terrain instances, and joy-physics builds heightfield colliders from them. Coordinates are the terrain's local space: X and Z in world units with finest sample (0, 0) at the layout's origin, and Y the height in world units, before any instance transform. See the terrain example for Dragon Pit's source, units, and controls.
Paths are relative to crates/.
| Rust | Contents |
|---|---|
joy-terrain/src/terrain_asset.rs |
Version 1: TerrainAsset, TerrainManifest, TerrainPatch, TerrainBounds, decode_terrain, parse_terrain, load_terrain, sample_terrain, sample_height_triangle. |
joy-terrain/src/terrain_selection.rs |
Version 1 fixed-leaf selection (the recorded reference): select_terrain, reconcile_terrain_edges, sample_morphed_height, validate_terrain_view, TerrainView, TerrainSelection. |
joy-terrain/src/terrain_layout.rs |
TerrainLayout (pyramid geometry and quantization), TileKey, TILE_APRON. |
joy-terrain/src/terrain_tile.rs |
TerrainTile, .jtile files (encode_tile, decode_tile). |
joy-terrain/src/terrain_pyramid.rs |
build_terrain_pyramid, build_pyramid_from_asset. |
joy-terrain/src/terrain_quadtree.rs |
select_terrain_nodes, coarsen_terrain_nodes, TerrainNode, TerrainNodeSelection, sample_node_height (the vertex shader's CPU reference). |
joy-terrain/src/terrain_surface.rs |
TerrainSurface (snapshots, height_at, resident_height_at, raycast), TerrainRayHit. |
joy-terrain/src/terrain_streamer.rs |
TerrainStreamer, TerrainStreamerOptions, TerrainStreamingBudget, TerrainInterest, TerrainStreamingReport. |
joy-terrain/src/terrain_loader.rs |
TerrainTileLoader, MemoryTileLoader, PackageTileLoader, TerrainByteSource, TerrainPackage, write_terrain_package. |
joy-terrain/src/terrain_detail.rs |
TerrainDetail, TerrainDetailMaterial, TerrainMaterialRules, TerrainMaterialWeights. |
joy-terrain/src/terrain_crater.rs |
TerrainCrater. |
joy-terrain/src/terrain_grid.rs |
create_terrain_grid, TerrainGrid, TerrainIndexRange. |
joy-terrain/src/bin/joy-terrain-package.rs |
The packaging tool. |
joy-renderer/src/terrain/terrain_geometry.rs |
TerrainStats, TerrainDiagnostic, and the tile pool every terrain pass draws (crate-internal). |
joy-renderer/src/terrain/terrain_scene_pass.rs |
The renderer's per-instance adapters (crate-internal). |
joy-renderer/src/terrain/terrain_tile_layers.rs |
Each tile's pool layer: its lattice, apron, and the rings normals read beyond its edges (crate-internal). |
joy-renderer/src/scene/terrain_instance.rs |
TerrainInstance, DEFAULT_TERRAIN_TOLERANCE. |
joy-renderer/src/shaders/terrain-*.wgsl |
terrain-geometry.wgsl (shared morph, normals, and the terrain material group), terrain-material.wgsl (the terrain shading model), terrain-scene.wgsl (the vertex stage feeding the surface passes), terrain-depth.wgsl, and terrain-caster.wgsl. |
joy-physics/src/terrain_colliders.rs, physics_heightfield.rs |
TerrainColliders and heightfield bodies; see simulation. |
joy-app/src/asset_fetch.rs, web_asset_fetch.rs |
AssetFetcher (runtime::AssetFetcher), a TerrainByteSource that reads files on a worker thread natively and fetches them over HTTP in browsers. |
A TerrainLayout names the tile size (tile_cells, a power of two from 2 to 128), the stored levels, the root tiles along X and Z, the origin and sample spacing, and the quantization range of tile files. Level 0 holds the finest authored samples; a level-L tile spans tile_cells * 2^L finest samples, and its lattice is the finest lattice at stride 2^L. Tiles store their lattice with a one-sample apron, the height bounds of the finest surface inside their footprint, and a monotone error table: entry i is the largest error of drawing the footprint with the lattice at level first_error_level + i. build_terrain_pyramid measures level-0 errors exactly as the version 1 offline compiler does; a parent's bounds and shared errors are the maximum over its children, and only its own two-triangle stride is measured directly. build_pyramid_from_asset keeps a version 1 asset's own patch errors for level 0. Builders hold the whole finest grid in memory.
A package is a folder with terrain.json (format version 2: {"version": 2, "layout": {...}, "tiles": "tiles"}) and one .jtile file per stored tile at <tiles>/<level>/<x>-<z>.jtile: a 40-byte little-endian header, the f64 error table, and quantized u16 samples, apron included. cargo run -p joy-terrain --bin joy-terrain-package -- <terrain.json> <folder> [--levels N] packages a version 1 asset; by default it halves the leaves down to one root per side.
use joy_engine::terrain::{MemoryTileLoader, PackageTileLoader, TerrainDetail, TerrainInterest, TerrainStreamer, TerrainStreamerOptions};
use joy_engine::runtime::AssetFetcher;
let loader = PackageTileLoader::new(package, AssetFetcher::new("assets/terrain/dragon-pit"));
let options = TerrainStreamerOptions { detail: Some(TerrainDetail::default()), ..Default::default() };
let mut streamer = TerrainStreamer::new(package_layout, Box::new(loader), options)?;
// Every frame: the camera view and the gameplay regions that must be solid.
let report = streamer.update(&[TerrainInterest::View(view), TerrainInterest::Collision { center, radius }])?;
if report.published {
let surface = streamer.surface().unwrap().clone();
scene.terrain_mut(terrain).unwrap().surface = surface.clone(); // or scene.create_terrain(surface) the first time
colliders.sync(&mut physics, &surface, &anchors)?;
}A streamer requests the roots at once and publishes its first snapshot when they arrive. Each update resolves finished loads (applying craters), marks what each interest uses, requests missing stored tiles coarsest first up to maximum_pending_loads, synthesizes missing detail tiles up to maximum_syntheses_per_update, releases least recently used tiles with no resident children above maximum_resident_tiles (roots and tiles in use stay), and publishes a snapshot when anything changed. Collision interests come before views. Loads never block: a view draws what is resident and refines as tiles arrive. At the residency limit new work waits while stale tiles leave to make room; a tile that fails to load, or leaves before any interest used it (demand beyond the budget), is not requested again for 120 updates, so a saturated stream settles instead of fetching and releasing the same tiles every update. Snapshots of one streamer share a lineage, so the renderer keeps its tile pool across them; a new streamer (for example after toggling detail) starts a new lineage. TerrainSurface::from_asset and from_tiles make fully resident snapshots that never change.
select_terrain_nodes walks down from the resident roots. A tile's fractional detail comes from its error table and the conservative distance to its bounds, as a version 1 leaf's does: projected cells are limited to 32 CSS pixels, a target disk pins the finest authored level (synthesized levels still follow screen error), and the pin fades over one more radius. A tile whose detail falls below its level subdivides when all four children are resident and otherwise draws at its finest and lists the missing children as wanted (children within half a level are prefetched). Children are capped at their parent's level, so at the moment of a switch both draw the parent's lattice; roots keep their whole range. Neighbor levels differ by at most one: a coarse tile beside much finer ones splits, or, when its children are missing, the finer side merges. Each edge takes the coarsest detail along it, and a smaller tile adopts its larger neighbor's whole edge, so both sides evaluate one function from one set of samples and no crack opens; a root beside smaller tiles gives up its single coarsest level. One-level pyramids select exactly like select_terrain (tests compare them on Dragon Pit's recorded views).
Each frame the renderer selects in the terrain's local space (eye and focus from the stable camera, CSS projection scale from the logical viewport height). Selection covers every direction around the eye, so turning the camera never changes detail; each view then keeps only the tiles whose bounds reach it: the camera's frustum for the depth pre-pass and the lit or G-buffer pass, and each shadow view's volume (a directional cascade or a point-light face) for that view. In a cascade, tiles the camera cannot see draw at the coarsest level whose error stays within one of the cascade's texels (coarsen_terrain_nodes keeps the same tiles and reconciles their edges again, so the coarser casters stay crack-free); tiles the camera sees keep their detail, so the surfaces that receive shadows match their casters. Only tiles some view draws upload, into a pooled R32Float texture array, when the pool has not seen them at their current revision (growing it by doubling up to max_texture_array_layers; WebGL 2 guarantees 256, and a frame whose views would draw more tiles than that selects again with a larger tolerance, which TerrainStats::tolerance reports). Each view draws one instanced range per LOD of the shared lattice, with a 48-byte record per tile as instance attributes: placement, texture layer, and detail and edges relative to the tile's level. The vertex shader morphs exactly like version 1 in tile-lattice coordinates.
A pool layer holds the tile's lattice with its apron, then rings: for every stride from 2 to the tile width, the lattice heights one stride beyond each side, read from same-level neighbors or coarser tiles that share those lattice points (beyond the terrain's edge, the surface continues its slope). The lit vertex stage takes central differences at the stride a vertex draws and blends toward its parent stride as the vertex morphs, so terrain shades with the smooth normal of the drawn surface, agrees across tile and level seams, and changes shading as continuously as geometry. A tile re-uploads when its own revision changes or when a tile its rings read arrives or changes.
Terrain shades through the same surface passes as meshes: its lit pipelines compose the engine's surface entry points with a terrain shading model whose material group holds the terrain's bindings. The model matches the lit model's lighting response and G-buffer encoding, so deferred terrain pixels light in the lit model's pass, and lighting, shadows, scene-effect attachments, and renderer debug views (Renderer::set_debug_view) apply to terrain as to any surface. Shading uses slope and height colors from the surface's TerrainMaterialRules (defaults keep the original thresholds) and the TerrainDiagnostic modes; LOD colors follow global detail, so synthesized levels have their own colors. Terrain draws first into the depth pre-pass and first among opaque geometry in the lit or G-buffer pass. Renderer::terrain_stats(id) reports selected tiles, tiles the camera draws, distinct drawn tiles, shadow tile draws and triangles, camera triangles, the finest selected level, CPU prepare time, resident tiles and bytes, pool layers, and per-frame uploads. Invalid settings (non-positive tolerance, negative or non-finite target radius, a non-finite transform) fail the frame with RenderError::Terrain before any pass is recorded; a collapsed transform pins the finest geometry.
TerrainDetail adds levels (1 to 4) synthesized levels below 0. A detail tile samples one continuous surface at its lattice: a Catmull-Rom reconstruction of its level-0 ancestor's samples, plus octaves of gradient noise (wavelengths halving from base_wavelength_samples, amplitudes falling by persistence) blended between each material's amplitude and ridged character by slope and height weights, plus exact crater offsets. The noise is a residual: each sample subtracts the Catmull-Rom interpolation of the noise at the surrounding level-0 lattice points, so detail adds only what the authored lattice cannot draw and vanishes on it. Every level therefore agrees at every lattice point it shares with another, which keeps level-0 tiles crack-free beside their synthesized children and the switch between them pop-free. Everything is integer hashing and f64 arithmetic, so detail is identical on every target, and neighboring detail tiles share edge samples bit for bit. Level-0 and coarser tiles keep their authored samples; their error tables gain the noise error of the materials their own lattice shows, so selection refines into detail where it matters: flat ground with gentle noise stays authored at distances where rock refines, which makes synthesized detail a material-driven alternative to hardware tessellation (which WebGPU lacks). Detail tiles are synthesized on the CPU, so the GPU path, collision, and sampling read the same samples.
TerrainSurface::height_at samples the collision level's triangle surface (TerrainDetail::collision_level, or level 0 without detail) and returns None where that level is not resident; resident_height_at reads the finest resident level for cameras and placement, and raycast marches the finest resident surface and refines by bisection. Positions are local world units.
TerrainStreamer::add_crater takes a TerrainCrater (center, radius, depth, and rim height in local world units): a quartic bowl with a quartic lip that changes nothing beyond 1.6 radii. Offsets add, so craters commute. Stored tiles add the offset at their own samples and widen their bounds and errors; detail tiles add it exactly at their finer lattice points. Resident tiles re-resolve immediately with a new revision (the renderer re-uploads only those, and colliders rebuild only those), and tiles loaded later receive every crater that reaches them. craters() returns the edit log to save or replay.
- Local coordinates are
f32on the GPU, which stays precise to about a millimeter across 16 km worlds; larger worlds need a floating origin. - Detail is geometric to the finest synthesized level; normals follow the drawn lattice, so there is no per-pixel normal detail finer than the geometry yet.
- Craters resolve at the lattice spacing of each level; very small craters on coarse levels only show where detail levels draw.
- Packages hold height only; there is no material or splat channel yet, so materials come from slope and height.
- Pyramid builders hold the whole finest grid in memory.
Assets and snapshots are cheaply cloned shared handles. The streamer owns its loader and resident tiles; dropping it releases them, while published snapshots keep the tiles they hold. The renderer releases an instance's tile pool when the instance leaves the scene, is hidden, or receives a surface of another lineage.
crates/joy-terrain/tests/integration/ covers version 1 selection, sampling, and morphing against tests/fixtures/terrain-reference.json; level-0 measurement against the offline compiler and coarse-tile bounds and errors; tile files and the packaging tool; quadtree parity with version 1, crack-free mixed-level edges (also after coarsening), and continuity while the camera descends; and streaming budgets, residency, collision regions, craters, and detail determinism and agreement with the authored lattice. joy-renderer's terrain_tile_layers.test.rs checks that layers hold the lattice beyond every edge where the shader reads it. crates/joy-renderer/tests/integration/rendering_terrain.rs renders every diagnostic mode, lit terrain with directional and point-light shadows and per-view culling, the normals debug view, scene-effect attachments, native OpenGL, a streamed surface with detail and a crater, and invalid settings. crates/joy-physics/tests/integration/terrain_colliders.rs and tests/integration/example_terrain.rs cover collision and the example.