A procedural planet showcase built with GlyphEngine. Fly from orbit through the atmosphere to mountains, sandy shores and underwater terrain, then back into space. A seeded 500 km radius world uses adaptive terrain, camera-relative rendering, terrain shadows, grass, rocks and wave-driven water lighting.
| Mountain shadows and atmospheric scattering | Shoreline water and surface reflections |
|---|---|
![]() |
![]() |
These are direct, HUD-free captures from the application. Reproduce them with
python tools/capture_readme.py after building. See
water lighting and its limitations and
rendering performance for the current design.
Later sections of this README include historical implementation notes.
Requires Go 1.27+, CGo/GCC, and a Vulkan-capable GPU/driver. Windows is the
currently validated platform. The engine version is pinned in go.mod; no
sibling engine checkout is required. Compiled shaders are included.
git clone https://github.com/derekmwright/procedural-planet.git
cd procedural-planet
go build -o bin/universebuild.exe .
.\bin\universebuild.exeFor development, go run . also works. The executable retains its original
universebuild.exe name so existing launch and profiling commands keep working.
Shader changes require glslc from the Vulkan SDK and go generate ./atmosphere.
Run go test ./... and go vet ./... to check the Go code.
The adaptive opaque depth prepass is enabled by default. Use
-depth-prepass=off for a baseline or -depth-prepass=on to force it during
profiling. -pipeline-stats adds optional GPU fragment/primitive counters to
the HUD and JSONL profile; leave it off for ordinary timing comparisons.
An experimental stream-power terrain modifier is available with -erosion.
To start above an eroded valley, run:
.\bin\universebuild.exe -erosion-demo -longitude=40Allow about ten seconds for startup erosion preparation. -erosion-strength=0..2
controls incision (default 1). This adds dry valleys and reshapes their side
slopes; flowing rivers and caves are not implemented. Erosion is off by default
while the landforms are evaluated. See terrain erosion
for the method, measurements and remaining work.
To work against a sibling engine checkout, use an ignored local workspace rather than changing the published dependency pin:
go work init . ../GlyphEngineScroll forward to zoom toward the surface; scroll backward to pull away. In orbit mode, the view gradually tilts toward the horizon as you approach. W/S translate along your view direction and A/D yaw left/right. Using these keys automatically takes over from orbit mode without changing the view; there is no need to press Tab before moving. Shift boosts movement speed.
| Control | Orbit / descent | Free flight |
|---|---|---|
| Mouse drag | Orbit the planet | Look around |
| Arrow keys | Orbit the planet | Look around |
| W / S | Enter flight; move forward / backward | Move forward / backward |
| A / D | Enter flight; yaw left / right | Yaw left / right |
| Q / E | Enter flight; move down / up | Move down / up relative to the planet |
| Mouse wheel | Zoom toward / away from surface | Zoom toward / away from surface |
| Shift | Movement speed boost | Movement speed boost |
| Space | Toggle automatic orbit | — |
| Tab | Switch to flight | Switch to orbit / descent |
| Home | Reset to starting orbit | Reset to starting orbit |
| F | Toggle atmosphere | Toggle atmosphere |
| R | Toggle sun rays | Toggle sun rays |
| H | Toggle local cast shadows | Toggle local cast shadows |
| M | Toggle surface material detail | Toggle surface material detail |
| O | Toggle sea-level water | Toggle sea-level water |
| G | Toggle nearby grass blades | Toggle nearby grass blades |
| P | Toggle performance HUD | Toggle performance HUD |
| F8 | Log performance marker | Log performance marker |
| Escape | Quit | Quit |
Flight speed scales with ground clearance; near the surface it slows to walking speeds. The camera has a two-meter floor above both the procedural surface and the currently rendered terrain. This is a flight/hover camera, not a character controller with gravity, slopes, or jumping.
# Another world
go run . -seed 42 -radius-km 750
# Start near the ground; allow a few seconds for refinement
go run . -altitude 100 -flight
# Hands-off 58-second descent, surface hold, and return to orbit
go run . -tourThe same deterministic 3D noise field supplies every detail level: broad basins/highlands, mountain ridges, local hills, and rock-scale relief. Feature wavelengths and amplitudes are in meters, independent of planet radius. The planet uses vertex colors with no external assets.
Localized mountain belts now add nested ridges at 26, 11, 4.2, and 1.5 km wavelengths. Warped range masks preserve the existing lowlands. A 4,000-point sample of seed 7 finds a maximum additional mountain height of 8,160 m, with 1,929 samples outside the mountain belts. This is procedural ridge noise, not an erosion simulation. Cast shadows currently cover nearby terrain only.
Near the surface, seeded cube-face cells scatter small stones and occasional boulders using three irregular mesh variants and three GPU instance sets. Placement persists across travel and cube seams. Rocks follow the current terrain surface, use camera-relative coordinates, and shrink out between 180 and 230 m from the camera. Their scales range from 0.12 to 2.8, with the largest meshes roughly five meters across. They currently have no collision; nearby rocks now cast shadows. The ground check showed 613 visible-range instances; the HUD reports the current instance count, including instances outside the camera frustum.
# Stones and boulders on the default lowland terrain
go run . -altitude 2 -flight
# A rugged mountain basin in daylight
go run . -longitude -76.5681 -latitude -12.3 -altitude 900 -flightRock placement determinism, cube-seam coverage, mesh winding, and mountain
relief/lowland retention are tested. Ground, mountain, and full 3,600-frame
descent/ascent captures passed Vulkan validation. The tour returned to 96
terrain patches after 94 splits and 94 merges. Captures are
captures/rocks-ground.png, captures/mountain-range.png, and
captures/rocks-tour.png.
- Six cube faces start with 96 patches. Nearby patches split into four; distant patches merge. Split and merge thresholds differ to reduce chatter.
- Adjacent patches differ by at most one level, including across cube faces. Refinement demand propagates through neighbours so required patches cannot immediately merge again while waiting for the next split.
- Inward, double-sided edge skirts conceal LOD gaps. These are not watertight stitched edges; edge morphing and smooth visual transitions remain future work.
- One worker builds a group of four children at a time. The main thread uploads at most two child meshes per frame. The parent remains visible until the complete child group is ready, then they replace it together.
- GPU meshes use GlyphEngine's dynamic per-frame buffers and are pooled for reuse. Parents remain cached for immediate coarsening. Active terrain is capped at 960 patches; the pool retains its peak allocation for later travel.
- Coordinates and generation use float64. Mesh vertices are patch-local; the camera is subtracted from patch origins before conversion to float32. Near-ground patches are small enough to preserve local precision.
- The default planet refines to level 16, with at most roughly 0.48-meter vertex spacing at the finest level. The maximum level scales with radius.
The overlay reports active geometry (including skirts), maximum active LOD,
ground clearance, terrain update time, the last four-child generation time,
and GPU mesh count. A GPU mesh owns multiple Vulkan buffers; this is not an
individual buffer count. dt is the smoothed simulation frame interval: with
-frames it is fixed, not a frame-rate benchmark. Generation time is worker
time normally, but part of the main-thread time in -sync-terrain mode.
Lighting uses a fixed directional sun and a spherical atmosphere. Nearby rock and terrain shadows are supported. Distant mountain shadows, material textures, and a walking controller remain unimplemented. Terrain can visibly pop while detail levels change; rapid movement or spawning near the ground can outrun refinement temporarily.
The orbital rim, surface sky, terrain haze, and sunset light use the same spherical single-scattering approximation. Air density falls with height, blue light scatters more strongly, aerosols add haze near the sun, and the solid planet blocks sunlight on the night side. Terrain integrates atmosphere only as far as the visible fragment; it does not receive a transparent shell drawn over foreground geometry. This follows the sky/surface integration approach in Sean O'Neil's GPU Gems 2 chapter.
Press F to compare at the same viewpoint, or launch with -atmosphere=false.
The engine's screen-space sun shafts are enabled by default. R toggles them;
-sun-rays=false disables them at startup. The same shafts remain active in space, with visibility following the spherical
horizon. They are zero when the atmosphere is disabled.
The narrower 0.55-screen-height radius and HDR luminance threshold of 1–2 keep
the effect concentrated near the sun. These are artistic settings, not physical
scattering coefficients. The pass samples scene brightness without depth, so
it approximates rays around ridgelines rather than tracing terrain shadows
through the atmosphere. No engine changes are required.
To reproduce a sun-facing view, use -flight -heading -63.2 -longitude 51.8427734 -latitude 0 -altitude 1000. Heading is degrees east of local north. Changing
longitude to 54 and altitude to 100 places the sun behind a nearby ridge.
Paired 360-frame synchronous on/off captures and the ridge view passed Vulkan
validation; captures are captures/sun-rays-{on,off,ridge}.png.
The default 500 km planet uses a 3.5 km molecular scale height and a 21 km atmosphere cutoff. Parameters scale within bounds for the configurable radius; they are tuned for this smaller world rather than a scientific Earth model. The shader uses 16 view samples and six samples for each sunlight path. It has no clouds, multiple scattering, refraction, or mountain-shadowed scattering. Low sample counts and changing terrain LOD can still show artifacts at grazing angles.
The paired shader overrides and their source live in atmosphere/; the SPIR-V
is embedded, so normal builds need no shader compiler. After changing GLSL:
go generate ./atmosphere
go build -o bin/universebuild.exe .Shader generation needs glslc from the Vulkan SDK on PATH. The application
currently owns the six sky-palette UBO slots as custom atmosphere parameters.
Both shaders decode that same layout. Do not mix them with the stock sky, fog,
or cloud shaders; proper application-owned uniform bindings are tracked in
GlyphEngine #94.
Validation captures include orbit, a 100 m daytime view, the terminator, the
night side, and a full descent/ascent. An initial 600-frame timing sample at
100 m on the RX 7900 XTX / 1440x900 / MSAA 4x measured GPU frame means of
1.578 ms with atmosphere and 0.342 ms without. These are preliminary per-run
means including terrain refinement, not a repeat-run performance guarantee.
Use GLYPHENGINE_TIMING=1 to collect actual GPU timestamps; the fixed dt HUD
value in capture runs is not a benchmark.
go test ./...
go test -race ./...
go vet -unsafeptr=false ./...
New-Item -ItemType Directory -Force captures
# Repeatable full descent/ascent, using the same upload/swap path
go run . -tour -sync-terrain -frames 3600 -vsync=false -validate -screenshot captures/returned-orbit.png
# Ground view on the intersection of three cube faces
go run . -longitude 45 -latitude 35.26438968 -altitude 2 -sync-terrain -frames 800 -vsync=false -validate -screenshot captures/cube-corner.png-frames fixes the simulation clock to 1/60 second; -sync-terrain removes
worker-timing variability for geometry comparisons. Interactive mode uses the
background worker. -longitude, -latitude, and -altitude reproduce a location.
Tests cover seed repeatability, face/patch seams, normals and winding, complete LOD coverage, 2:1 balance after transitions, convergence and coarsening, radial collision against actual mesh triangles, skirt index bounds, worker shutdown, camera clearance, and flight orientation across a pole.
Measured on 2026-09-21 with the default seed/radius on an RX 7900 XTX:
- Default ground view: 378 active patches, level 16, 967,680 triangles including skirts, 472 allocated GPU meshes, two-meter clearance.
- Full 3,600-frame descent/ascent: 94 splits and 94 merges, returning to 96 active patches at level 2. Both synchronous and worker runs completed with no Vulkan validation messages.
- Cube-corner ground view: 222 patches at level 16, with no Vulkan validation
messages. Captures are in
captures/(ignored by Git).
These checks validate the transition and resource path; still images do not establish that motion is free of popping, shimmer, or every possible seam artifact.
GlyphEngine's shaders/lit.frag tilts normals toward global +Y for roughness
above 0.9, a grass-specific lighting heuristic. Planet rock previously used
0.98, incorrectly triggering it. The showcase switched to 0.9 to bypass that
heuristic; the later custom atmosphere shader honors supplied normals directly.
The engine itself is unchanged. Long-term, grass normal treatment
should be independent of material roughness.
Coarse patches also now sample normal derivatives over half a grid cell
(minimum 0.5 m), rather than using sub-meter detail at every resolution.
TestCoarseNormalsFollowResolvedGeometry measures summed 1 - dot(normal, triangle normal) on seed 7, face 0, level 8, cell (27,128): 14.671947 before,
6.324341 after, a 57% reduction. This measures alignment to resolved geometry,
not a claim that every visible terminator artifact is fixed. Normals at differing
LOD boundaries can still differ. Cast shadows now cover nearby terrain only.
Reproduction: -longitude 51.8427734 -latitude 0 -altitude 1000 -frames 500 -sync-terrain -vsync=false. Before, material-only, and filtered-normal captures
are saved as captures/terminator-before.png, terminator-after.png, and
terminator-filtered.png. The final render passed Vulkan validation.
Confirmed findings from the showcase are tracked upstream. Planet generation, LOD, and atmospheric art direction remain consumer responsibilities.
| Issue | Finding |
|---|---|
| #93 | Generic rough materials acquire a grass-specific global-up normal bias |
| #94 | Custom shaders need application-owned per-frame uniform bindings |
| #95 | Device-local mesh uploads wait for the graphics queue per buffer |
| #96 | Distinct geometry cannot share public mesh draw ranges; measure batching before adding indirect draws |
Issue bodies with source references, reproduction details, current workarounds,
and acceptance checks are retained in docs/engine-issues/. Shader parameters
and material correctness are immediate needs; indirect batching is a measured
follow-up, not an established bottleneck. The 100 m view had 294 active patches
but 80 total draws after culling, including non-terrain passes.
Rock and terrain cast shadows are enabled by default; H or -shadows=false
provides an on/off comparison. The near cascade preserves local detail and fades
into the mountain cascade by receiver distance. Both retain distant caster reach,
while atmospheric scattering samples mountain visibility. Shadows turn off at
12 km ground clearance.
Underwater, cast shadows fade out over the first two metres of camera depth, then
caster draws and shadow lookups stop. Surfacing restores them automatically.
Use -underwater-shadows to retain the previous underwater shadows. This is a
visual/performance approximation: it suppresses terrain occlusion rather than
tracing refracted, scattered shadow light. Caustics and underwater shafts remain
active. The HUD reports effective shadow strength alongside the H setting.
See terrain shadow filtering, water lighting and rendering performance for validation and limits.
Procedural surface shading adds soil grain, coarser scree on intermediate
slopes, and cooler exposed rock on steep faces. It adjusts color and the shading
normal on both terrain and scattered rocks without adding triangles or changing
collision. Scree distribution is slope-based, not a simulation of loose-rock
accumulation. M toggles the pass; -materials=false starts without it.
The continuous 3D field needs no UV seams or texture assets. Detail wavelengths are 8 m, 2 m, 25 cm, and 6.25 cm. Pixel-footprint filtering suppresses unresolved frequencies; overall detail fades between 160 and 700 m. Camera coordinates are reduced modulo 4096 m in double precision before upload, keeping the material anchored while preserving close-up precision. The field therefore repeats every 4096 m along each world axis. Custom parameter slots 5.yz and 4.z carry the eye remainder, and slot 4.y controls material detail.
Paired ground captures (captures/materials-on.png, materials-off.png) and
captures/materials-mountain.png were inspected. Coordinate tests check less
than 1 mm drift across camera wrapping boundaries. Tests, vet, and the full
3,600-frame descent/ascent Vulkan validation passed. These checks do not prove
all movement is free of shimmer; use M at the same viewpoint for comparison.
Water is enabled by default at +250 m above the planet reference radius.
-sea-level accepts -2500 to 10000 m; O or -ocean=false toggles water.
The HUD reports both terrain clearance and signed height above sea level.
Flight still follows the terrain collision floor, so descending over an ocean
allows underwater exploration rather than stopping at the water surface.
# A shoreline at the foot of the mountains, seed 7
.\bin\universebuild.exe -longitude -62 -latitude 22 -altitude 2
# Raise the global sea level
.\bin\universebuild.exe -sea-level 600The paired sky and terrain shaders intersect each view ray with an analytic sea-level sphere and composite water only in front of opaque geometry. This adds no water meshes or draw calls. It also means the ocean follows the currently rendered terrain silhouette; coastal details can change as terrain LOD refines. Below-sea-level enclosed basins fill too. These are not independently elevated lakes, and there is no drainage or river simulation yet.
Appearance includes wavelength-dependent absorption, a clear shallows band, Fresnel sky reflection, solar glints, and animated shading ripples that fade with distance and pixel footprint. The reflection evaluates our atmosphere; it does not reflect terrain or rocks. There is no refraction, foam, wave geometry, buoyancy, or underwater total internal reflection. Underwater views attenuate both sunlight and visibility with depth; the surface window is approximate. WATER_CLARITY in atmosphere/ocean.glsl is now 2.0: light and visibility carry twice as far as the original water. This is a shader constant.
The stock engine water targets a flat heightmap. The showcase uses its existing shader override capability for the spherical surface; no engine changes or new engine issue were needed. Parameter slot 4.x holds water radius in km (zero is disabled), and slot 3.z holds periodic wave time. Atmosphere cutoff remains six Rayleigh scale heights; sun energy remains 6.
Orbit, shoreline, underwater, disabled-water, and descent/ascent validation
runs were checked. Captures: captures/ocean-orbit.png, ocean-coast.png,
ocean-underwater.png, and ocean-tour.png. The full tour returned to 96 terrain
patches after 94 splits and 94 merges. Tests and vet passed.
The ocean now skips the seabed atmosphere march wherever water fully replaces it, and skips bed materials/lighting/shadows after 800 m of optical water travel (blue transmission is then below 1e-6). Sky pixels covered by deep water likewise skip their discarded background atmosphere. The reflected sky retains the full original scattering calculation.
Measured on RX 7900 XTX, 1440x900, 4x MSAA, fixed 600-frame runs, synchronous
terrain, VSync disabled, at -longitude -62 -latitude 22 -altitude 2:
| GPU frame time | Run 1 | Run 2 | Run 3 | Mean |
|---|---|---|---|---|
| Before | 5.689 ms | 5.694 ms | 5.682 ms | 5.688 ms |
| After | 4.577 ms | 4.564 ms | 4.620 ms | 4.587 ms |
This is a 19.4% reduction in GPU frame time for this shoreline view. Matched
600-frame captures were pixel-identical below the HUD (rows 210 onward).
This is not a claim about all viewpoints, resolutions, steady-state frame rate,
or measured power/fan speed. Timing logs and captures are in
captures/water-perf-before-* and captures/water-perf-after-*.
Seeded moisture regions blend green lowlands, olive uplands, and golden dry
areas into the existing soil/rock colors. Both terrain vertex color and grass
placement call the same Planet.GroundCover rules. Coverage fades in between
8 and 65 m above sea level, fades out near the configured elevation ceiling,
and excludes steep slopes. Beaches and underwater terrain remain bare.
Coarse terrain uses its filtered surface normal for slope selection; region
boundaries gain detail as the terrain refines.
Nearby grass is a nine-blade mesh, instanced with seeded colors, scale, and yaw. A small CPU-driven lean adds a breeze. Ground material detail remains visible after blades shrink out between 35 and 50 m. Interactive scatter work runs on a background worker; synchronous validation uses deterministic generation. The GPU instance capacity is 20,000, with distance and behind-camera filtering before upload. Blades receive local lighting/shadows but do not cast shadows, have collision, or flatten under the camera. Density is bounded by the upload capacity. Grass materials and placement are showcase code; instancing, buffers, render pipelines, culling, and draw submission come from GlyphEngine.
# Green meadow with distant mountains
.\bin\universebuild.exe -longitude -45 -latitude -13 -altitude 2
# Tunable vegetation band and blade density
.\bin\universebuild.exe -grass-height-max 2200 -grass-slope-max 32 -grass-density 0.7G or -grass=false disables blades while retaining ground vegetation.
-vegetation=false disables both terrain vegetation colors and scatter.
Defaults: elevation ceiling 2600 m above sea level, slope ceiling 38 degrees,
density multiplier 1. Allowed ranges: height 100–10000 m, slope 5–60 degrees,
density 0.1–2.
Mask tests exclude underwater ground, alpine terrain, and cliffs. Scatter tests
check deterministic placement and shared-mask eligibility. Tests, race tests,
vet, worker-mode rendering, and a full descent/ascent Vulkan validation run
passed. Captures include vegetation-orbit.png, grass-ground.png, and
grass-worker.png in captures/.
A preliminary 600-frame meadow comparison at 1440x900, 4x MSAA on RX 7900 XTX measured 4.444 ms GPU with 2,506 tufts vs 4.147 ms without blades (one run each, fixed simulation clock). This preceded the final narrower-blade and color tweak; it is an initial cost indication, not a repeated benchmark or a frame-rate guarantee.
The performance HUD is enabled by default (-perf=false starts hidden).
P toggles it; F8 logs a marker to the console and to the profile file
when one is open. Keep VSync enabled for normal exploration: GPU timestamps
measure actual GPU work independently of presentation pacing.
.\bin\universebuild.exe -profile captures/flight-profile.jsonlThe optional JSONL file appends one sample per wall-clock second plus F8
markers. It records UTC, frame number, actual framebuffer resolution, VSync,
fixed-simulation status, camera position/direction, seed, planet/sea settings,
feature toggles, underwater state, draw/instance/triangle counts, GPU passes,
and CPU measurements. -width and -height set the initial window size;
recorded resolution is the actual framebuffer size after any resizing.
Frame/GPU/update statistics use the last 120 sampled frames and refresh at most four times per second. P95 is the 95th-percentile frame cost. Wall FPS comes from measured wall time, not the fixed simulation delta shown separately. GPU queries lag by a few frames and unavailable timestamps are explicitly shown as unavailable. CPU engine phase means are labeled session averages; the per-update terrain/rock/grass timings are rolling main-thread measurements. They do not measure background worker CPU time. Startup, synchronous generation, and LOD rebuild spikes remain visible in the window rather than being hidden.
Our custom water and atmosphere shaders are charged to opaque and sky. The engine's separate water/grass passes do not account for our analytic ocean or ordinary instanced grass. A large GPU wait can mean pacing or GPU load; compare it with GPU milliseconds. These metrics do not measure temperature, power, utilization percentages, or fan speed.
Initial underwater ablations at default location, 2 m terrain clearance
(about 123 m below sea level), RX 7900 XTX, 1440x900, 4x MSAA, 600 fixed frames,
synchronous terrain, VSync off: 3.132 ms mean GPU normally; 0.928 ms with
atmosphere disabled; 2.628 ms with ocean disabled. One run each: this identifies
atmospheric work as the larger component at this pose, not a general performance
guarantee or a diagnosis of fan behavior at another resolution/viewpoint.
Logs are captures/perf-underwater*.log, perf-no-ocean.log; JSONL samples and
perf-hud.png demonstrate the instrumentation. Tests, vet, and validation passed.
Submerged rays now use water absorption instead of marching the atmosphere through water. Surface windows evaluate air beyond the water exit; obscured geometry skips material and lighting work. Visible seabed retains its detail. The visibility cutoff scales with clarity (originally 400 m, now 800 m).
RX 7900 XTX, 3840x2054, 4x MSAA, 450 fixed frames, synchronous terrain, VSync off, default location, -sea-level 142.5 -altitude 2 (15.1 m deep):
| GPU frame time | Run 1 | Run 2 | Run 3 | Mean |
|---|---|---|---|---|
| Before | 13.008 ms | 12.733 ms | 12.727 ms | 12.823 ms |
| Optimized, original clarity | 3.170 ms | 3.207 ms | 3.198 ms | 3.192 ms |
This is a 75.1% reduction at this pose. Matched captures differed by at most one 8-bit RGB step outside the HUD before clarity tuning. The subsequent clarity increase intentionally changes appearance; its final validation run measured 3.272 ms GPU time. These measurements do not measure fan or power use. Logs/captures: captures/underwater-opt-* and captures/underwater-clear*. Shoreline, disabled-ocean, and full descent/ascent Vulkan validation passed; Go tests and vet passed. This was showcase shader work, requiring no engine fix. Historical shoreline timings above used the original clarity and 400 m cutoff.
Animated procedural caustics now modulate direct lighting on submerged terrain and rocks. They follow world coordinates, respect local shadows, and fade with depth, low sun angle, and pixel footprint. Camera-relative depth avoids stepped pattern edges from subtracting planet-sized floats.
Subtle underwater shafts use four bounded samples of a broad animated light field. R / -sun-rays=false toggles these as well as the existing above-water shafts. The integration ends at visible geometry or the surface. These are artistic approximations, without wave refraction or terrain-shadowed volume lighting; no bubbles or suspended-particle geometry have been added.
A 3840x2054 shallow-water check measured about 3.6 ms GPU versus 3.3 ms before these details; this is a single-run estimate, not a general benchmark. Captures: captures/underwater-caustics.png and captures/caustics-coast.png. Go tests and vet passed, plus Vulkan validation of the descent/ascent tour, coast, and ray/shadow toggles. Slot 5.x now packs local shadows (1) and water shafts (2).
The underwater surface now bends the sky view through animated ripple normals, with transmitted sun glints and caustic highlights concentrated around the refracted sun. Their intensity spreads and fades away from that source. Beyond the sky window, a softened transition shows an approximate dark underside (no reflected terrain). Shafts now use eight bounded samples, follow refracted sunlight, and have stronger directional contrast. R still toggles the shafts.
Reproduce the upward view with -sea-level 142.5 -altitude 2 -flight -heading -72 -pitch 55. The new -pitch option accepts -85 to 85 degrees above the horizon and only sets the starting flight view. Capture: captures/water-look-up.png. A 3840x2054, 450-frame run measured 2.072 ms mean GPU for this upward pose (not comparable to seabed-facing timings). Shader compilation, Go tests, vet, and upward-view/descent-ascent Vulkan validation passed.
Replaced eight discrete volume samples with the analytic integral of a smooth periodic light field and exponential absorption. This removes sample-plane banding. The field is constant along refracted sunlight: each point projects back to its water-entry coordinate. Seabed caustics now use that refracted direction too. Shafts remain an artistic approximation without terrain shadows. Checked 100 integrals against dense numerical quadrature (maximum absolute error below 0.000003), Go tests/vet, and Vulkan validation of shallow-angle, upward, and descent/ascent views. Captures: water-beam-angle.png and water-beam-up.png.
The analytic volume field above was superseded: it still read as nearly uniform haze in shallow forward views. Current shafts are an angular approximation centered on the refracted sun, with irregular animated beam widths, dark gaps, and depth/view-travel attenuation. This deliberately favors visible descending beams over physical volume integration; it is not terrain-shadowed volumetric lighting. R toggles it. Surface glints and seabed caustics remain separate. Visual check at 7.8 m depth: captures/water-shaft-contrast.png. Reproduce with -sea-level 137.3 -altitude 2 -flight -heading -72 -pitch 20.
Fine seabed caustics are now gated by broad moving illumination patches in the same refracted light-entry coordinates. Filtering fades the effect from 80 to 300 m viewing distance and at subpixel scales. From above, an artistic reflected-sun weighting prevents enhancement around every shallow coastline. The underwater surface highlight is more tightly concentrated around the sun. These masks approximate the appearance; individual wave-focused light paths are not simulated. Captures: caustics-localized-above.png and caustics-localized-below.png. Tests, vet, and Vulkan validation passed.
Underwater scattering now uses half-resolution engine render passes by default.
Use -water-passes=false for the original full-resolution A/B baseline. The GPU
HUD reports water scattering/composite costs. See docs/water-lighting.md for
measurements, validation, and remaining above-water costs.
Water now defaults to a GPU-cached broad wave spectrum. Use -wave-cache=false
for the previous five-wave field. Building requires Go 1.27, matching GlyphEngine.
Caustics now default to a forward-projected light atlas that accumulates
overlapping refracted rays, with a filtered shorter-wave band. Use
-caustic-cache=false for the old inverse solver. The GPU HUD reports focusing
separately; see water-lighting.md for reproducible
comparisons, local coverage limits, and remaining overhead-sun limitations.
The projection now integrates four spatial samples per cache texel and applies
depth-dependent diffusion. Grid-aligned cache movement reduces highlight jumps
without changing wave speed or adding screen-space history trails.
Caustic brightness also uses a short, world-aligned temporal filter to reduce
flicker as narrow light folds move between samples. -caustic-temporal=false
compares spatial filtering alone. Both paths keep the same wave motion.
Shoreline foam and animated wet sand are enabled by default. Use
-shore-foam=false for an A/B comparison. Foam is a shallow-water surface effect;
wave geometry does not break or run up the beach yet.
Terrain streams through asynchronous GPU-local uploads by default. Use
-async-terrain-uploads=false to compare the previous pooled dynamic-buffer path.
The HUD reports waiting terrain uploads and skipped draws; parent patches remain
visible until their complete replacement is ready.
Mountain shadows now use the engine's configurable directional shadow coverage: a 90 m near cascade and 120 km far cascade, with 200 km of sunward caster reach. Terrain, water direct light, and atmospheric scattering use this visibility; H toggles shadows. Shadows run below 12 km ground clearance. Ambient sky illumination remains in shaded valleys. Far shadows are approximate and fade at the coverage boundary.
View-path haze now runs in separate half-resolution passes with depth-aware
composition. Direct terrain shadows remain full resolution. Use -air-passes=false
for the inline reference or -air-scale=1 for a full-resolution quality comparison.
The GPU HUD exposes scattering, transmission, composite and copy costs separately.
Underwater rendering keeps its existing path.
python tools/profile_scene.py --scenes ground mountains coast underwater --repeats 2
runs serial fixed-scene comparisons against the canonical executable.
-profile-frame-step=120 records non-overlapping windows for direct profiling.
See rendering-performance.md for architecture,
measurement definitions, limitations, and the remaining cloud integration work.



