From 9d1df6ec32437091fcfab80feaddf8c6f5483838 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Thu, 10 Sep 2026 03:31:41 +0300 Subject: [PATCH] chore(release): v0.7.0 --- CHANGELOG.md | 188 +++++++++++++++++++++++++++ VERSION | 2 +- changes/gl-render3d.md | 28 ---- changes/installer-keeps-store.md | 6 - changes/renderer-outside-the-tree.md | 39 ------ changes/resize-depth-fix.md | 27 ---- changes/stream-hitch.md | 36 ----- changes/terrain-perf.md | 54 -------- 8 files changed, 189 insertions(+), 191 deletions(-) delete mode 100644 changes/gl-render3d.md delete mode 100644 changes/installer-keeps-store.md delete mode 100644 changes/renderer-outside-the-tree.md delete mode 100644 changes/resize-depth-fix.md delete mode 100644 changes/stream-hitch.md delete mode 100644 changes/terrain-perf.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d7caa933..2b13765c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,194 @@ change type. Preview the next one with `ludic dev release --dry-run`. A released section may carry a hand-written summary paragraph above its groups (v0.2.0 has one); the generated bullets below it are not edited by hand. +## v0.7.0 — 2026-09-10 + +### Features + +- **A game can use the renderer from outside this repository.** `ludic.render3d` reads two + things from disk at run time — its GLSL, and the scanned CC0 materials — and both were + found only by a path relative to the working directory, so the renderer worked in a + Ludic checkout and nowhere else. A game living in its own repository now needs to copy + neither. + + **The shaders come from the package**, wherever the package is. They belong to + `ludic.render3d` and ship with it, so the renderer looks for them beside the project + first (a Ludic checkout, where they are under `packages/`) and then under the install + root, `$LUDIC_HOME/packages/ludic.render3d` — the same place the compiler already + resolves `import "ludic.render3d/r3d.ludic"` from. Nothing to vendor, and no version of + the shaders that can drift from the version of the code that compiles them. + + **`ludic assets [--force]`** fetches the scanned materials and the HDRI sky into + `assets/polyhaven/` of whatever project you run it in. The list of what to fetch is the + renderer's own — the renderer decides which materials it wants — so it moved out of the + repository's `assets/` and into the package as + `packages/ludic.render3d/assets.manifest`, where it ships with the toolchain. A game + does not keep its own copy of that list and so cannot fall out of step with the + renderer's material set. `ludic dev fetch-assets` is the same command from a checkout. + + **A URL-shaped module built its binary into directories.** `project_name` took + everything after the last dot of the manifest's module path, which for a package + identified the way the package manager identifies them — + `git.host.io/user/name` — is inside the *host*: the build wrote + `build/io/user/name` instead of `build/name`. The last path segment comes first now, and + a dot inside that segment still separates namespace from package, so `ludic.render3d` + still builds as `render3d`. + + **The camping game has moved out** to its own repository — + [Maroon Lake](https://git.workshopsoft.io/workshopsoft/maroon-lake) — taking + `examples/rendering/valley.ludic`, `hiker.ludic`, `camp/`, and 175 MB of survey data and + scanned kit with it. It was here as a demo of the renderer and became a game, and an + engine repository should not be carrying a game's assets. It is now the first consumer + of everything above, which is the point: what the renderer needs a game to be able to do, + it can now do from outside. `examples/rendering/smooth.ludic` stays as the renderer's + example in this tree. +- **OpenGL for Ludic, and a 3D renderer on it.** `Gl.*` binds the whole OpenGL 4.1 + core API — every `gl*` entry point of the platform `gl3.h` as `Gl.(…)` + with every `GL_*` constant, generated by `ludic-dev glgen` with per-call ABI thunks + (`runtime/native/gl_thunks.ll`; float/double parameters take `fixed`). Windowed + builds get an `NSOpenGLContext` on the existing window at Retina resolution + (`cocoa.ll`); headless builds render into an offscreen CGL context, so a program + that uses `Gl.*` renders and screenshots identically under the test harness. + `Gl.open / swap / screenshot / program / vao / floats …` cover the glue, and the + IEEE-float helpers (`f_add`, `mem_put_f32`, …) let Q16.16 programs fill real + float vertex and uniform data. `Gl.*` links `gl.ll + gl_thunks.ll + OpenGL.framework` + only when used; every other build is byte-identical. + + The `ludic.render3d` package (`packages/ludic.render3d`) is a physically based + renderer written on `Gl.*`: HDRI sky with image-based lighting (irradiance, GGX + prefiltered, split-sum BRDF, sun extracted from the map), GPU-generated terrain + with scanned PBR materials (stochastic anti-tiling, triplanar rock, slope/altitude + splatting), cascaded shadow maps with PCF and world-unit biasing, a glTF loader + for scanned models, instanced vegetation with baked impostors, procedural grass + and lupines with wind and translucency, 4x MSAA with alpha-to-coverage, SSAO, + still water, cloud shadows, aerial perspective, an HDR pipeline with bloom, + auto-exposure, ACES tonemapping, grading, sharpening and grain. See + `examples/rendering/smooth.ludic`, and the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake) for a + game built on it. The renderer's CC0 materials are fetched with `ludic assets`. + + **16-bit PNGs**: the renderer's texture loader keeps 16-bit samples (normal / + displacement maps) and uploads them as `RGB16` / `R16`. + +### Fixes + +- **Resizing the window (or entering fullscreen) no longer empties the world.** It left + `gl error 1286` — `GL_INVALID_FRAMEBUFFER_OPERATION` — on every frame from there on, with + the terrain, the trees, the grass and the water gone and only the sky drawn. + + The sun-visibility pass added in `changes/terrain-perf.md` borrows the depth buffer the + frame is about to be drawn with, so that rasterising it doubles as a depth prepass. It + was storing that borrowed texture in its `Target`, and a `Target` deletes whatever its + `depth` names when it is freed. On the first resize the sequence was: `post_free` deletes + the frame's depth texture, `post_init` immediately makes the replacement — and GL hands + back the name that was just freed — and then the visibility target, rebuilt for the new + size, deleted that name believing it was its own. The scene framebuffer lost its depth + attachment. What is left is a colour-only framebuffer, which is *complete*, so drawing + carried on with no depth test at all: the sky is a fullscreen quad drawn last, and with + nothing left to fail against it painted over the entire valley. The 1286s came from the + passes whose own attachment now named a texture that no longer existed. + + A borrowed attachment is never written into the target now, and the frame's depth is + attached afresh at the start of each pass — it is a different texture every time the + screen-sized buffers are rebuilt, and one `glFramebufferTexture2D` per pass is cheaper + than any scheme for noticing that it changed. + + `R3D_RESIZE_AT=` rebuilds every screen-sized buffer from that frame on, cycling + through four drawable sizes every few frames. A window cannot be resized in a headless + run, so this is the only way to reach the path; it reproduced the fault in one frame and + now runs twenty resizes, with the fly camera and with the game, without an error. +- An upgrade keeps the package store. The installer replaces the whole install + root, and `ludic add` caches packages in `~/.ludic/store` — so re-running the + one-liner deleted every package a project had fetched. The store is carried + across now; everything else in the root belongs to the toolchain and is replaced. + +### Performance + +- **Ground cover stops re-growing itself.** Walking a streamed world hitched, and the + hitch got worse the longer you played. Measured in the Maroon Lake game, with a new hitch + report rather than guessed at. + + **The chunk cache had a cliff, not a slope.** A stream cached 4096 chunks and then + stopped remembering: past that the chunk was generated, used for one frame and thrown + away, so every ring walk regenerated it, for the rest of the session. It arrives after + enough of the map has been walked — six evictions' worth over seven kilometres, so an + ordinary session reaches it — and it is the point where cover starts visibly re-growing + as you turn. `stream_evict` now drops the half of the cache nobody has asked for in the + longest time (chunks carry the walk that last wanted them) and rebuilds the index over + what is left. Over a 7 km traversal: generation total **9073 ms → 2230 ms**, the worst + single frame's generation **11.4 ms → 3.1 ms**, median frame 10.8 → 9.0 ms. With a cache + deliberately sized to saturate early, the same run goes from 2748 frames generating to + 1548, and from a 13.3 ms median to 9.2. `R3D_NOEVICT` restores the old behaviour for + comparison, `R3D_STREAM_CAP=` sets the cache size. + + The other half of that hitch was in the game's own cover generator, and went with it to + the Maroon Lake game's repository: its candidates were paying for a second noise field, four + height samples and a path distance before the drift field that rules out most of the + meadow — 2341 µs → 518 µs per chunk, bit-identical output. Worth repeating in any + generator: a `stream_fill` is called for tens of thousands of candidates per chunk, so + the order of its tests is most of its cost. + + **The hitch report** (`R3D_PROF=1`) is what found both. It prints the slowest frames of + the run with what was in each: CPU versus GPU wait, cover generated, instance bytes + uploaded, the game's own tick, and the renderer phase that took longest. Alongside it, + per-chunk generation cost by stream and band, a census of what the caches hold, and a + stutter figure — the frame time a run spent beyond 1.2x its own median — because a mean + cannot show a hitch and a maximum is one unlucky frame. + + It also found two content bugs in the game it was measured on, which is the report doing + its job: a cover stream whose placement rule never fires still pays full generation cost, + and the census makes that visible — 3364 cached chunks holding zero instances. +- **The ground costs half what it did.** Measured in the Maroon Lake game, the terrain was 10.3 ms of + a 22.2 ms frame; it is now 5.4 ms of 16.4 ms — 45 fps to 61 fps at 1080p, with the + frame otherwise unchanged (every viewpoint tested stays above 54 dB PSNR against the + old renderer, with no channel differing by more than 7/255). + + **Measure by frame time, not by the pass timers.** `R3D_PROF`'s per-pass + `GL_TIME_ELAPSED` queries cannot be trusted on this driver: with the ground's shading + work removed the terrain query fell from 10.5 ms to 1.3 ms while the frame time did + not move at all. Every number above and below is a median real frame time, taken by + switching one thing off (`prof_ft_report`); the pass timers are still printed, and are + still useful for spotting a pass that appears out of nowhere, but they cannot size one. + `R3D_NOTERRAIN` skips the ground, `R3D_TNEARONLY` / `R3D_TFARONLY` draw every patch + with one tier's program, and `R3D_RES=x` renders at another size — the three + switches that say whether a cost is the ground, which tier it is in, and whether it is + pixels at all. + + **Each detail tier is its own program.** `terrain.frag` holds a detailed near tier and + a cheap far one and chose between them per pixel, so every pixel of the valley walls + was compiled — and scheduled — for a near path it never ran. CDLOD selection now knows + which tiers a patch can contain: one that never comes within the split draws with + `FAR_ONLY`, one wholly inside it with `NEAR_ONLY`, and only the ring of patches that + straddle the band needs the program that holds both and cross-fades. Pixel-identical, + and it makes the tiers separately measurable: the near tier costs 7.5 ms over a whole + frame, the far tier 2.3 ms. + + **The sun visibility is its own pass** (`tersun.frag`). The same CDLOD patches are + rasterised once into a screen-sized R8 buffer that holds nothing but each ground + pixel's sun visibility, and `terrain.frag` fetches it by fragment coordinate. The pass + costs 0.27 ms, shares the frame's depth buffer so it doubles as a depth prepass, and + takes the cascade read out of the shader that covers the screen. It picks its tier — + filtered PCF near, a single tap far — over the same cross-faded band the ground uses, + so the boundary is not a contour you can find on the hillside. + + **Nothing is sampled for a weight of zero.** The ground sampled all four of its + materials for every pixel and then blended three of them at zero: a meadow pixel took + nine taps of triplanar rock, a cliff pixel nine taps of stochastic grass, and every + pixel in the valley took the snow tile and the four noise fields behind the lake's + shore wash — a wash that is a hairline along one shore within 120 m of the camera. The + survey photograph's classification now runs first, because it is what decides which + materials are present; each material block sits behind its own weight; the ridge field + that ragged the snow line is skipped 160 m below it, where it cannot change anything; + and inside the stochastic blend a cell's rotation, offset and rotated gradients are + computed inside its own test, so a cell whose sharpened weight rounds away costs + nothing. All of it exact where the weight is zero, and it is most of the win. + + Material sampling is what remains (2.8 ms of the 5.4): scanned 2K tiles taken at 16x + anisotropy on ground seen at a grazing angle. `R3D_ANISO=` sets the filter (the + default is unchanged at 16; 4 is worth 1.0 ms and 1 is worth 1.7 ms). + + The shadow pass is 1.2 ms of the frame and has nothing to give: re-using the far + cascades between frames — their windows are snapped to a 14 m and a 64 m grid — is + worth 0.15 ms standing still and nothing while walking, so it is not in the tree. ## v0.6.1 — 2026-09-05 ### Fixes diff --git a/VERSION b/VERSION index ee6cdce3..faef31a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.7.0 diff --git a/changes/gl-render3d.md b/changes/gl-render3d.md deleted file mode 100644 index 45ceb70e..00000000 --- a/changes/gl-render3d.md +++ /dev/null @@ -1,28 +0,0 @@ -bump: minor -type: feat -**OpenGL for Ludic, and a 3D renderer on it.** `Gl.*` binds the whole OpenGL 4.1 -core API — every `gl*` entry point of the platform `gl3.h` as `Gl.(…)` -with every `GL_*` constant, generated by `ludic-dev glgen` with per-call ABI thunks -(`runtime/native/gl_thunks.ll`; float/double parameters take `fixed`). Windowed -builds get an `NSOpenGLContext` on the existing window at Retina resolution -(`cocoa.ll`); headless builds render into an offscreen CGL context, so a program -that uses `Gl.*` renders and screenshots identically under the test harness. -`Gl.open / swap / screenshot / program / vao / floats …` cover the glue, and the -IEEE-float helpers (`f_add`, `mem_put_f32`, …) let Q16.16 programs fill real -float vertex and uniform data. `Gl.*` links `gl.ll + gl_thunks.ll + OpenGL.framework` -only when used; every other build is byte-identical. - -The `ludic.render3d` package (`packages/ludic.render3d`) is a physically based -renderer written on `Gl.*`: HDRI sky with image-based lighting (irradiance, GGX -prefiltered, split-sum BRDF, sun extracted from the map), GPU-generated terrain -with scanned PBR materials (stochastic anti-tiling, triplanar rock, slope/altitude -splatting), cascaded shadow maps with PCF and world-unit biasing, a glTF loader -for scanned models, instanced vegetation with baked impostors, procedural grass -and lupines with wind and translucency, 4x MSAA with alpha-to-coverage, SSAO, -still water, cloud shadows, aerial perspective, an HDR pipeline with bloom, -auto-exposure, ACES tonemapping, grading, sharpening and grain. See -`examples/rendering/smooth.ludic`, and the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake) for a -game built on it. The renderer's CC0 materials are fetched with `ludic assets`. - -**16-bit PNGs**: the renderer's texture loader keeps 16-bit samples (normal / -displacement maps) and uploads them as `RGB16` / `R16`. diff --git a/changes/installer-keeps-store.md b/changes/installer-keeps-store.md deleted file mode 100644 index f5c969c8..00000000 --- a/changes/installer-keeps-store.md +++ /dev/null @@ -1,6 +0,0 @@ -bump: patch -type: fix -An upgrade keeps the package store. The installer replaces the whole install -root, and `ludic add` caches packages in `~/.ludic/store` — so re-running the -one-liner deleted every package a project had fetched. The store is carried -across now; everything else in the root belongs to the toolchain and is replaced. diff --git a/changes/renderer-outside-the-tree.md b/changes/renderer-outside-the-tree.md deleted file mode 100644 index a2095367..00000000 --- a/changes/renderer-outside-the-tree.md +++ /dev/null @@ -1,39 +0,0 @@ -bump: minor -type: feat -**A game can use the renderer from outside this repository.** `ludic.render3d` reads two -things from disk at run time — its GLSL, and the scanned CC0 materials — and both were -found only by a path relative to the working directory, so the renderer worked in a -Ludic checkout and nowhere else. A game living in its own repository now needs to copy -neither. - -**The shaders come from the package**, wherever the package is. They belong to -`ludic.render3d` and ship with it, so the renderer looks for them beside the project -first (a Ludic checkout, where they are under `packages/`) and then under the install -root, `$LUDIC_HOME/packages/ludic.render3d` — the same place the compiler already -resolves `import "ludic.render3d/r3d.ludic"` from. Nothing to vendor, and no version of -the shaders that can drift from the version of the code that compiles them. - -**`ludic assets [--force]`** fetches the scanned materials and the HDRI sky into -`assets/polyhaven/` of whatever project you run it in. The list of what to fetch is the -renderer's own — the renderer decides which materials it wants — so it moved out of the -repository's `assets/` and into the package as -`packages/ludic.render3d/assets.manifest`, where it ships with the toolchain. A game -does not keep its own copy of that list and so cannot fall out of step with the -renderer's material set. `ludic dev fetch-assets` is the same command from a checkout. - -**A URL-shaped module built its binary into directories.** `project_name` took -everything after the last dot of the manifest's module path, which for a package -identified the way the package manager identifies them — -`git.host.io/user/name` — is inside the *host*: the build wrote -`build/io/user/name` instead of `build/name`. The last path segment comes first now, and -a dot inside that segment still separates namespace from package, so `ludic.render3d` -still builds as `render3d`. - -**The camping game has moved out** to its own repository — -[Maroon Lake](https://git.workshopsoft.io/workshopsoft/maroon-lake) — taking -`examples/rendering/valley.ludic`, `hiker.ludic`, `camp/`, and 175 MB of survey data and -scanned kit with it. It was here as a demo of the renderer and became a game, and an -engine repository should not be carrying a game's assets. It is now the first consumer -of everything above, which is the point: what the renderer needs a game to be able to do, -it can now do from outside. `examples/rendering/smooth.ludic` stays as the renderer's -example in this tree. diff --git a/changes/resize-depth-fix.md b/changes/resize-depth-fix.md deleted file mode 100644 index 1825ced7..00000000 --- a/changes/resize-depth-fix.md +++ /dev/null @@ -1,27 +0,0 @@ -bump: patch -type: fix -**Resizing the window (or entering fullscreen) no longer empties the world.** It left -`gl error 1286` — `GL_INVALID_FRAMEBUFFER_OPERATION` — on every frame from there on, with -the terrain, the trees, the grass and the water gone and only the sky drawn. - -The sun-visibility pass added in `changes/terrain-perf.md` borrows the depth buffer the -frame is about to be drawn with, so that rasterising it doubles as a depth prepass. It -was storing that borrowed texture in its `Target`, and a `Target` deletes whatever its -`depth` names when it is freed. On the first resize the sequence was: `post_free` deletes -the frame's depth texture, `post_init` immediately makes the replacement — and GL hands -back the name that was just freed — and then the visibility target, rebuilt for the new -size, deleted that name believing it was its own. The scene framebuffer lost its depth -attachment. What is left is a colour-only framebuffer, which is *complete*, so drawing -carried on with no depth test at all: the sky is a fullscreen quad drawn last, and with -nothing left to fail against it painted over the entire valley. The 1286s came from the -passes whose own attachment now named a texture that no longer existed. - -A borrowed attachment is never written into the target now, and the frame's depth is -attached afresh at the start of each pass — it is a different texture every time the -screen-sized buffers are rebuilt, and one `glFramebufferTexture2D` per pass is cheaper -than any scheme for noticing that it changed. - -`R3D_RESIZE_AT=` rebuilds every screen-sized buffer from that frame on, cycling -through four drawable sizes every few frames. A window cannot be resized in a headless -run, so this is the only way to reach the path; it reproduced the fault in one frame and -now runs twenty resizes, with the fly camera and with the game, without an error. diff --git a/changes/stream-hitch.md b/changes/stream-hitch.md deleted file mode 100644 index 6b32bf7c..00000000 --- a/changes/stream-hitch.md +++ /dev/null @@ -1,36 +0,0 @@ -bump: patch -type: perf -**Ground cover stops re-growing itself.** Walking a streamed world hitched, and the -hitch got worse the longer you played. Measured in the Maroon Lake game, with a new hitch -report rather than guessed at. - -**The chunk cache had a cliff, not a slope.** A stream cached 4096 chunks and then -stopped remembering: past that the chunk was generated, used for one frame and thrown -away, so every ring walk regenerated it, for the rest of the session. It arrives after -enough of the map has been walked — six evictions' worth over seven kilometres, so an -ordinary session reaches it — and it is the point where cover starts visibly re-growing -as you turn. `stream_evict` now drops the half of the cache nobody has asked for in the -longest time (chunks carry the walk that last wanted them) and rebuilds the index over -what is left. Over a 7 km traversal: generation total **9073 ms → 2230 ms**, the worst -single frame's generation **11.4 ms → 3.1 ms**, median frame 10.8 → 9.0 ms. With a cache -deliberately sized to saturate early, the same run goes from 2748 frames generating to -1548, and from a 13.3 ms median to 9.2. `R3D_NOEVICT` restores the old behaviour for -comparison, `R3D_STREAM_CAP=` sets the cache size. - -The other half of that hitch was in the game's own cover generator, and went with it to -the Maroon Lake game's repository: its candidates were paying for a second noise field, four -height samples and a path distance before the drift field that rules out most of the -meadow — 2341 µs → 518 µs per chunk, bit-identical output. Worth repeating in any -generator: a `stream_fill` is called for tens of thousands of candidates per chunk, so -the order of its tests is most of its cost. - -**The hitch report** (`R3D_PROF=1`) is what found both. It prints the slowest frames of -the run with what was in each: CPU versus GPU wait, cover generated, instance bytes -uploaded, the game's own tick, and the renderer phase that took longest. Alongside it, -per-chunk generation cost by stream and band, a census of what the caches hold, and a -stutter figure — the frame time a run spent beyond 1.2x its own median — because a mean -cannot show a hitch and a maximum is one unlucky frame. - -It also found two content bugs in the game it was measured on, which is the report doing -its job: a cover stream whose placement rule never fires still pays full generation cost, -and the census makes that visible — 3364 cached chunks holding zero instances. diff --git a/changes/terrain-perf.md b/changes/terrain-perf.md deleted file mode 100644 index d762a42c..00000000 --- a/changes/terrain-perf.md +++ /dev/null @@ -1,54 +0,0 @@ -bump: patch -type: perf -**The ground costs half what it did.** Measured in the Maroon Lake game, the terrain was 10.3 ms of -a 22.2 ms frame; it is now 5.4 ms of 16.4 ms — 45 fps to 61 fps at 1080p, with the -frame otherwise unchanged (every viewpoint tested stays above 54 dB PSNR against the -old renderer, with no channel differing by more than 7/255). - -**Measure by frame time, not by the pass timers.** `R3D_PROF`'s per-pass -`GL_TIME_ELAPSED` queries cannot be trusted on this driver: with the ground's shading -work removed the terrain query fell from 10.5 ms to 1.3 ms while the frame time did -not move at all. Every number above and below is a median real frame time, taken by -switching one thing off (`prof_ft_report`); the pass timers are still printed, and are -still useful for spotting a pass that appears out of nowhere, but they cannot size one. -`R3D_NOTERRAIN` skips the ground, `R3D_TNEARONLY` / `R3D_TFARONLY` draw every patch -with one tier's program, and `R3D_RES=x` renders at another size — the three -switches that say whether a cost is the ground, which tier it is in, and whether it is -pixels at all. - -**Each detail tier is its own program.** `terrain.frag` holds a detailed near tier and -a cheap far one and chose between them per pixel, so every pixel of the valley walls -was compiled — and scheduled — for a near path it never ran. CDLOD selection now knows -which tiers a patch can contain: one that never comes within the split draws with -`FAR_ONLY`, one wholly inside it with `NEAR_ONLY`, and only the ring of patches that -straddle the band needs the program that holds both and cross-fades. Pixel-identical, -and it makes the tiers separately measurable: the near tier costs 7.5 ms over a whole -frame, the far tier 2.3 ms. - -**The sun visibility is its own pass** (`tersun.frag`). The same CDLOD patches are -rasterised once into a screen-sized R8 buffer that holds nothing but each ground -pixel's sun visibility, and `terrain.frag` fetches it by fragment coordinate. The pass -costs 0.27 ms, shares the frame's depth buffer so it doubles as a depth prepass, and -takes the cascade read out of the shader that covers the screen. It picks its tier — -filtered PCF near, a single tap far — over the same cross-faded band the ground uses, -so the boundary is not a contour you can find on the hillside. - -**Nothing is sampled for a weight of zero.** The ground sampled all four of its -materials for every pixel and then blended three of them at zero: a meadow pixel took -nine taps of triplanar rock, a cliff pixel nine taps of stochastic grass, and every -pixel in the valley took the snow tile and the four noise fields behind the lake's -shore wash — a wash that is a hairline along one shore within 120 m of the camera. The -survey photograph's classification now runs first, because it is what decides which -materials are present; each material block sits behind its own weight; the ridge field -that ragged the snow line is skipped 160 m below it, where it cannot change anything; -and inside the stochastic blend a cell's rotation, offset and rotated gradients are -computed inside its own test, so a cell whose sharpened weight rounds away costs -nothing. All of it exact where the weight is zero, and it is most of the win. - -Material sampling is what remains (2.8 ms of the 5.4): scanned 2K tiles taken at 16x -anisotropy on ground seen at a grazing angle. `R3D_ANISO=` sets the filter (the -default is unchanged at 16; 4 is worth 1.0 ms and 1 is worth 1.7 ms). - -The shadow pass is 1.2 ms of the frame and has nothing to give: re-using the far -cascades between frames — their windows are snapped to a 14 m and a 64 m grid — is -worth 0.15 ms standing still and nothing while walking, so it is not in the tree.