Taos Engine

Terrain & Worlds

Geo Fox in the Rain — Physics, Particles & Decals on Streamed Terrain

Run the sample · geo_fox_rain.ts

A fox runs across the real Grand Canyon rim in a moonlit downpour. The rain collides with the terrain and with a giant Luxo-style ball, bursting into splashes on impact; the fox kicks up splashes at its feet and leaves a fading trail of water-ripple decals; and the ball — pushable by the fox — bounces down the canyon under physics. Every surface here is streamed geospatial terrain (AWS Terrarium elevation draped with Esri aerial imagery), not authored geometry.

This sample is mostly integration: it bridges four subsystems that were each built in isolation and never designed to meet. The interesting work is in the seams between them.

OSM raster-DEM terrain  (geo_osm_terrain)  ─┐
fox + atmosphere/fog/stars (standalone/geo_fox) ─┤
rain + on_death splash  (mountain_fox)      ─┼──►  geo_fox_rain
Jolt terrain colliders  (geo_physics)       ─┘

The floating-origin world#

The scene is anchored at Mather Point (lon −112.1071°, lat 36.0608°, rim ≈ 2100 m) via a GeoFrame, so world-space coordinates stay small (f32-safe) even though the underlying positions are ECEF meters in the millions. The origin is pinned — we never re-anchor — because the Jolt terrain colliders are baked into the current frame, and re-anchoring would slide the terrain out from under them. The canyon play area is local, so f32 stays precise across it.

Terrain streams through a GeoScene with a RasterDemTileset: the app calls geo.update(...) each frame with the camera's ECEF position and cull frustum, and the returned draw items are mirrored into the deferred geometry pass. The fox (skinned) and ball (static mesh) are appended to the same draw lists, so they share the terrain's lighting, shadows, AO, atmosphere and tonemap.

Seam 1 — Rain that collides with terrain and a moving ball#

mountain_fox's rain bakes its collision heightmap on the GPU from the terrain's virtual-texture atlas. Geospatial terrain has no such atlas, so that path can't be reused. Instead, geo_fox_rain.ts defines a sample-local GeoRainFeature that bakes the heightmap on the CPU each frame: it walks a grid around the camera, and for each cell converts world XZ → lon/lat → GeoScene.heightAt → ellipsoid height → world Y (the same geodetic↔ECEF↔world path the rendered mesh was baked with), then uploads it via ParticlePass.updateHeightmap. The rain config's block_collision modifier (kill-on-contact) and its on_death splash split then "just work" against the streamed canyon surface.

A movable particle sphere collider (engine change)#

The ball is a moving sphere — the heightmap (single-valued, terrain-shaped) can't represent it. The particle system already had a sphere_collision modifier, but it baked the center and radius as WGSL constants, so the collider couldn't move. This sample motivated a small engine addition:

GeoRainFeature pushes the ball's world center + radius every frame, so rain drops that strike the ball die and spawn the same on_death splash burst as terrain hits. Because the collider is a uniform, not baked geometry, it tracks the ball as it rolls — at zero extra draw cost.

Seam 2 — A physics fox & camera on streamed colliders#

Following geo_physics, each rendered quantized-mesh terrain tile is fed to Jolt as a static triangle-mesh collider via TerrainContent.sampler.collisionGeometry(...), added and removed as tiles stream in and out of the rendered set (capped to a few per frame to avoid hitches). See the engine's PhysicsWorld.

The fox is a kinematically-driven dynamic sphere: each frame we set its horizontal velocity from camera-relative input and let gravity + the colliders resolve the vertical, then read the body position back to place the GameObject. It rolls under the hood; the visible fox is yaw-oriented from its movement direction and animated (idle/walk/run gaits via AnimatedModel). The camera ground-clamps with a downward PhysicsWorld.rayCast against the same colliders.

Three subtleties were worth solving:

Seam 3 — Waiting for the world to settle#

Streamed terrain changes shape as finer LODs arrive. A ball placed on a coarse tile would roll off when that tile re-resolves a meter lower. So the ball is held by a world-load gate: it stays pinned on the ledge (showing "Waiting for the world to load…") until a minimum time has passed and the collider set has been stable for a beat and a ground height under it is available — then it's released to the simulation exactly once. syncTerrainColliders reports whether the collider set changed this frame, which drives the stability timer.

Because the fox and ball are both dynamic bodies in Jolt's moving layer, the fox can shove the ball around with no extra code — the contact solver does it.

The trimmings#

Known follow-ups#