Repository navigation
Ground tile variants and surface blending - #333
Merged
Merged
Conversation
tessellateAsset took its base index as a uint16 with no limit check, so an asset over 65,535 vertices wrapped its indices and drew garbage without a word. It now packs paths into as many meshes as it takes, in path order, and AssetRegistry::buildMeshes hands them all back. buildMesh stays for callers that draw one mesh (flora, groundcover, the asset-manager thumbnails) and now fails with an error when the asset needs more than one, instead of corrupting. A single path that tessellates past the limit on its own is dropped with a warning, the way SVGLoader does. GeneratedPath gains minPixelsPerMeter (0 = always drawn), and buildMeshes takes a pixel density that drops the paths asking for more, which is what the ground tile bake will use to leave fine detail out of coarse mips.
noise2(x, y, seed) is salal's gradientNoise2, about -1 to 1, so a generator gets the same deterministic field the rest of the engine uses. periodicNoise2 (x, y, period, seed) wraps the integer lattice every `period` cells, so the field tiles seamlessly (ground tiles need that); inside cells 0 to period - 2 it equals noise2 bit for bit. The period arrives as a double because P / F is a float in Lua even when it divides evenly, and anything that isn't a whole number >= 1 is a script error. path:setLod(n) sets GeneratedPath::minPixelsPerMeter. Existing bindings are unchanged.
A ground def (role ground, at assets/world/ground/<Surface>/<Surface>.xml) is the tile art for one land surface. It names its <surface>, and optionally a <period> in meters (default 4), <pixelsPerMeter> (64), <rotate> free, quarter or none (free), <variantCount> (8 here, 1 elsewhere) and a <macro> with dry and lush r,g,b tints, an amplitude and two wavelengths. The generator or svgPath are the existing ones. assets already links world, so the surface is a world::Surface, parsed with a new surfaceFromString next to surfaceToString; AssetDefinition only forward-declares the enum, as PlacementExecutor.h does. The validator checks that period divides 512 and fills whole pixels at the chosen density, that variantCount is 1 to 32, pixelsPerMeter is a power of two from 16 to 512, the surface is a land surface, that no two ground defs paint the same surface, and that a ground def has no <biome> rules (placement already skips defs without any). Once the tree has any ground def it warns for each land surface that lacks one; that becomes an error when the content lands. Ground-only elements on other roles warn. Ground defs are skipped by the eager collision capture, which would otherwise run their (heavy) generators at load for nothing.
… split Validator rules for period, pixelsPerMeter, variantCount, surface, duplicates and missing surfaces; schema parse and defaults; noise2 against salal's gradientNoise2 and per seed; periodicNoise2 repeating along x, y and both, including across the period seam and negative cells; setLod landing on the path; a 22,000 triangle asset splitting into two valid meshes with every vertex kept; and the pixel density filter.
…gent-a8260b19db14da839
…sRGB encode, metrics Every downsample is a Kaiser-windowed sinc (alpha 4, 3 texels each way) that wraps, clamped to the min and max under its main lobe, then given the source's mean back: the clamp alone brightened a coarse mip of small light detail by 25%. The mean lock adds a 2-texel blur of the residual against the reference, so the mean moves and surviving detail keeps its contrast. Encoding dithers half a code so flat far mips keep their mean.
GroundTileBaker draws each mip from the def's meshes at that mip's texel density, so setLod paths leave at the level their author chose. A triangle's wrap copies follow it, which keeps the generator's paint order at the edges. Mips 0 and 1 render at 8x8, the rest at 4x4, tiled past 4096 px, then go through the Kaiser downsample, the mean lock against mip 0 and sRGB encoding. It returns CPU chains plus per-mip metrics (mean, delta E, variant spread, windowed contrast) and timings, logs the table, and puts the coordinate system, viewport and bound target back.
Every level costs one full generator run whatever survives the LOD cut, about 95 ms a level on the snow placeholder, which the per-mip column makes plain.
A ground def's detail pane gets a Vectors / Bake switch. Bake runs the game's GroundTileBaker one variant a frame and shows the variant's mips at one texel a pixel, a strip of what the game draws at 160 down to 3 px/m (a 2x2 of four variants, one LOD a draw with the +0.25 bias, trilinear in linear light, computed on the CPU since salal textures clamp), and the metrics table. Next variant steps through the chain; Reload drops the cached bakes so the def bakes again.
Drawing a tile's whole mesh 3x3 doesn't wrap it: each copy's full-tile base paints over the overhang of the copies drawn before it, and its own overhang lands on top of everything drawn after, so blades get cut at two edges and float over the other two. buildMeshes now repeats every path of a procedural ground def that crosses an edge of its [0, period] square at the offsets it crosses into, right after the original, so paint order holds across the seam. Clipped to the square, the meshes are the seamless tile, which means the baker draws them once instead of 3x3; the spec says so now.
A dense ground def needs several meshes, and buildMesh refuses it, so the asset render path takes them all from buildMeshes. A ground def is framed on its [0, period] square rather than its mesh bounds, and drawn clipped to it, since its art crosses the edges. The detail pane shows the first variant tiled 3x3, each copy clipped to its own cell, beside the usual variant grid, so a seam would show. asset-cli's render smoke pass uses buildMeshes too, so a big ground def isn't reported as producing nothing. Drawing a ground def's 15 copies is slow in Debug, about 2 s a frame with the grass tile selected.
ground_grass.lua paints one 4 m tile dark to light: a far stack for the coarse mips (mean base, soil, clover and tuft proxies), a gap-shadow cover tagged setLod(20), then soil peeks with pebbles, dark undergrowth fans, two-tone tuft fans grown in clumps that share a tone and a lean and shade like a dome from above, broadleaf rosettes, clover in Thomas clusters, and top blades with centered underlays, broad blades, straw and dry tips. Density fields are periodicNoise2 at 0.8 and 0.4 m, counts are per square meter, and every look knob is a param, so the other grass surfaces can reuse the script. Ground_Grass is the Grass surface's def with the tuned params. Mean is about sRGB (73, 103, 49), the eight variants agree within 0.6% in luma, and the far stack lands within 0.1% of the full-detail mean. In RelWithDebInfo a variant is about 190 ms of Lua and 340 ms with wrap and tessellation, 27.7k paths and 230k vertices.
…ures-1cd28e # Conflicts: # apps/asset-manager/AssetDetailView.h
buildMeshes repeats every ground path that crosses the tile edge right after the original, so the baker no longer emits 3x3 copies of each triangle. It scales the meshes into render pixels, drops what misses the target, and draws them in paint order; the target's clip is the wrap.
buildPaths runs the generator once and tessellates each path once, tagged with its setLod density, and TessellatedPaths::meshesAt packs the paths a density keeps into the meshes buildMeshes returned there. buildMeshes is now a thin call over the two, and tessellateAsset is gone. The registry also resolves a def's script path for callers, and remembers which XML file a def came from.
…kes on disk The baker builds every variant's paths at once on the TaskPool, one generator run and one tessellation each, then draws each mip from them. Each def's bake is kept in cache/ground beside the exe, keyed by its XML, its script, its shape and the baker's version, so an unchanged def comes back in about 20 ms. Ground_Grass in RelWithDebInfo goes from 19.6 s to 5.8 s cold; what's left is the GPU finishing salal's batched draws. The asset-manager's Bake view takes the same steps, the paths in one frame and a variant's render a frame, and Reload bakes a def again once its files change.
GrassShort, GrassMeadow, GrassTall, Dirt, Sand, Mud, Rock and Snow get placeholder defs that share ground_placeholder.lua: a flat base in the surface's old color under a light, low-contrast speckle, 4 variants at 64 px/m. Real art replaces them later. With all nine covered, the validator's missing-surface warning is now an error; the ground tests build a full tree so they still check one def at a time.
GroundTileBank bakes every ground def with GroundTileBaker once the registry has loaded, a variant a call so the splash keeps drawing (its boot log counts them; the loading scene finishes the bake after a --scene jump), and uploads them into GL_SRGB8_ALPHA8 texture arrays, one per layer size, every mip, with a per-surface table of layers, period, density, rotation and macro tints. land.glsl samples them on a chunk-continuous hex lattice (4 m columns, 148 rows a chunk): per-cell variant, rotation and offset, Mikkelsen's luminance-weighted blend with a gain that relaxes with zoom, one sample where a cell holds 98% or below terrain/ground/hexMinPxPerMeter, textureLod at a per-surface LOD the CPU computes once a frame. The fringe's other surface takes the dominant cell alone. Samples blend in linear light and are encoded back to sRGB for the rest of the pass. The chunk worker fills a 257x257 macro texture at 2 m a texel, uploaded with the render tiles: a dry to lush mix at 36 and 14 m and value noise at 10 and 5 m. Its last row and column are the next chunk's first, so borders match, and each lattice corner is hashed once a chunk (3 ms a chunk). Deleted: the tile atlas (GameBootstrap's initializeTileAtlas and surfacePatternPath), the old pattern SVGs in assets/tiles/surfaces, and Chunk::getSurfaceColor and getBiomeColor.
The near stack now runs down to mip 3, so the default 24 px/m view (two thirds mip 3) shows real clumps: tufts around a lit crown, light cores, dark ground between them, clover and the odd soil peek. The far stack for mips 4 to 6 is a coarse tone model of the near stack averaged into one rect a mip 4 texel, so it keeps mip 0's mean and patches in the same places instead of bright clover and tuft proxies. Blades take the model's tone where they lie, over a proxy strip of that tone kept to mip 3, so a light blade and its dark underlay average to what the coarser mips show. Clump sizes and tones are normalized per variant. The mean lock is under 2.3% on every mip (it was 8 to 17%), mips 4 to 6 sit at 72 to 94% contrast, and the palette leans a little more olive.
…res-1cd28e The baker builds every variant's paths once on the pool, bakes a variant from them on the GL thread, and keeps each def's bake on disk under cache/ground beside the exe.
…-1cd28e The bank now bakes through the staged baker, so there's one baker API and the game reads the same kind of disk cache the asset-manager does. Each step uploads every def in a row the cache holds, and at the first it doesn't, builds that def's paths on the pool; later steps bake and upload a variant each, and the last one stores the def. Cold, the loading scene's bake takes 4.5 s of steps; warm, 36 ms.
Each hex cell flips left to right or not off a hash bit, with its random uv offset and no rotation, so art with an up (grass blades) still breaks repetition without spinning sideways.
The camera is a hair off top-down, so the tile is drawn that way now: tufts are upright fans rooted at their base, blades lean within about 30 degrees of vertical, and everything standing is painted back to front by root. Shapes crossing the top or bottom edge are split there, so the depth order holds across the wrap. Undergrowth goes where a cover grid says the ground would show, until about a tenth of the tile is left open, so every variant shows the same 15-20% of earthy soil and litter at close zoom. Blade proxies take the blade's own average, the far stack is pinned to the measured mip 0 mean, and the tile doesn't rotate.
On light views the driver drops the 3090 to P3 or P2 and tileGpuMs then measures the clock, not the code (WOR-553). GpuHold draws a throwaway shader into its own 512x512 target after the scene's GPU timer closes, so no scene timer counts it and the frame is untouched, and sizes that work each frame so scene plus hold GPU time sits near 0.8 of the frame. Off by default, toggled with /api/control?action=gpuhold&value=0|1 and read back from /api/state?what=gpuhold, so only builds with the dev server can turn it on. perf-capture.ps1 turns it on for the whole capture (-NoGpuHold skips it), records the P-state and clocks per sample from a read-only nvidia-smi query, and warns in red when a sample isn't P0.
ground-textures.md describes how captures hold P0 now (WOR-553), with the on and off numbers for main and this branch, and the testing README lists the gpuhold control and state view. The terrain spec's perf section points at the hold instead of nvidia-smi -lgc.
The tile pass is short of quarter-rate work (int/float conversions and MUFU ops), and the hex sampler added a dozen conversions a pixel. Cell ids stay floats and cross to int through the 1.5 * 2^23 bias, the variant pick rounds in float on the hash's low 12 bits instead of an integer modulo by a uniform (the same pick for every power-of-two variant count), and the CPU folds the chunk's cell origin and the macro tint and value constants. River view: tileGpuMs down 0.01 to 0.02 ms in every cell.
Each inner tile's 3x3 binomial sum comes from its rows' 1 2 1 sums, read from a nibble table, and the thin-feature test runs on the whole 4x4 mask at once. The sums, and so the field, are bit-identical to the per-bit version and to SurfaceField.cpp. The distance to an isoline takes one inversesqrt in place of length() and a divide. tileGpuMs on the river view drops 0.02 to 0.07 ms against the previous commit in every cell; with both commits, coast and desert drop 0.03 to 0.09 ms against e533e5b.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The pixel-width floor can extend a transition beyond the field’s supported reach, causing an abrupt color change when tint is enabled.
1 open issue
What changed in this PR
Builds on #332’s baked ground textures with height-based surface blending and organic biome boundaries, connecting terrain generation, asset definitions, rendering, and preview tools.
Changes:
- Replaces sector-based biome selection and value noise with shared gradient-noise fields.
- Adds baked heights, validated transition recipes, shared grass-family seeds, and rendering optimizations.
- Retunes ground art and adds height previews, automated coverage, and a manual regression scenario.
| File | Description |
|---|---|
libs/world/world/generation/WetlandGenerator.h |
Uses shared noise for marsh islands. |
libs/world/world/generation/TundraGenerator.h |
Uses shared noise for rock outcrops. |
libs/world/world/generation/PatchNoise.h |
Defines lazily cached patch fields. |
libs/world/world/generation/PatchNoise.cpp |
Builds seeded gradient-noise octaves. |
libs/world/world/generation/OceanGenerator.h |
Returns the water surface directly. |
libs/world/world/generation/NoiseUtils.h |
Removes obsolete value-noise utilities. |
libs/world/world/generation/MountainGenerator.h |
Uses shared noise for snow patches. |
libs/world/world/generation/GrasslandGenerator.h |
Selects grass variants through shared fields. |
libs/world/world/generation/GenerationResult.h |
Removes the unused result wrapper. |
libs/world/world/generation/GenerationContext.h |
Exposes shared patch-noise sampling. |
libs/world/world/generation/ForestGenerator.h |
Uses shared moisture and outcrop fields. |
libs/world/world/generation/DesertGenerator.h |
Updates rock-outcrop selection. |
libs/world/world/generation/BiomeDispatcher.h |
Dispatches generators returning surfaces directly. |
libs/world/world/generation/BeachGenerator.h |
Updates beach-outcrop selection. |
libs/world/world/chunk/TileBiomes.h |
Defines per-tile biome selection interfaces. |
libs/world/world/chunk/TerrainPolygonTestSupport.h |
Updates a comment about sampler resolution. |
libs/world/world/chunk/TerrainPolygonSeams.test.cpp |
Migrates seam fixtures to chunk neighborhoods. |
libs/world/world/chunk/TerrainPolygonBuilder.test.cpp |
Removes sector-grid setup from fixtures. |
libs/world/world/chunk/TerrainPolygonBuilder.bench.cpp |
Uses neighborhood biome and apron queries. |
libs/world/world/chunk/SurfaceField.test.cpp |
Tests the tighter shortcut and noise bounds. |
libs/world/world/chunk/SurfaceField.h |
Documents the expanded near-interior shortcut. |
libs/world/world/chunk/SurfaceField.cpp |
Checks for early exit after one fine octave. |
libs/world/world/chunk/NoiseLattice.h |
Defines reusable cached noise grids. |
libs/world/world/chunk/NoiseLattice.cpp |
Implements lattice caching and sampling coordinates. |
libs/world/world/chunk/MockWorldSampler.test.cpp |
Tests corner-derived tile biomes. |
libs/world/world/chunk/MockWorldSampler.cpp |
Removes sector-grid generation. |
libs/world/world/chunk/GroundMacro.cpp |
Reuses the shared noise-grid implementation. |
libs/world/world/chunk/GeneratedWorldSampler.cpp |
Removes sector-grid generation. |
libs/world/world/chunk/ChunkSampleResult.h |
Removes sector storage and lookup helpers. |
libs/world/world/chunk/Chunk.h |
Updates tile-generation inputs and interfaces. |
libs/world/world/chunk/Chunk.cpp |
Integrates neighborhood biomes and patch fields. |
libs/world/world/chunk/ApronField.test.cpp |
Adapts apron tests to neighborhood inputs. |
libs/world/world/chunk/ApronField.h |
Introduces shared chunk-neighborhood state. |
libs/world/world/chunk/ApronField.cpp |
Resolves apron tiles from owning chunks. |
libs/world/CMakeLists.txt |
Registers new terrain-generation sources. |
libs/world-view/world/rendering/shaders/tile.frag |
Updates the land-color call. |
libs/world-view/world/rendering/shaders/includes/hash_noise.glsl |
Optimizes conversions and separates octave evaluation. |
libs/world-view/world/rendering/GroundTileFilter.test.cpp |
Tests height filtering and rank normalization. |
libs/world-view/world/rendering/GroundTileFilter.h |
Defines height-data filtering interfaces. |
libs/world-view/world/rendering/GroundTileCache.test.cpp |
Tests height metrics and transition-independent keys. |
libs/world-view/world/rendering/GroundTileCache.h |
Documents revised cache-key inputs. |
libs/world-view/world/rendering/GroundTileCache.cpp |
Versions height data and excludes transition XML. |
libs/world-view/world/rendering/GroundTileBank.h |
Adds pair tables and shared hash identities. |
libs/world-view/world/rendering/GroundTileBank.cpp |
Integrates height baking and transition tables. |
libs/world-view/world/rendering/GroundTileBaker.test.cpp |
Tests explicit heights, paint order, and mip preservation. |
libs/world-view/world/rendering/GroundTileBaker.h |
Adds height-baking interfaces and metrics. |
libs/world-view/world/rendering/GroundPairs.test.cpp |
Tests recipe mapping and uniform packing. |
libs/world-view/world/rendering/GroundPairs.h |
Defines surface-pair rendering parameters. |
libs/world-view/world/rendering/GroundPairs.cpp |
Packs zoom-dependent transition uniforms. |
libs/world-view/world/rendering/ChunkRenderer.h |
Replaces fringe uniforms with pair-table state. |
libs/world-view/world/rendering/ChunkRenderer.cpp |
Uploads transition recipes and shared grass hashes. |
libs/world-view/CMakeLists.txt |
Registers pair-table implementation. |
libs/sim/ecs/systems/VisionSystem.test.cpp |
Replaces sector-based lake fixtures with ponds. |
libs/assets/CMakeLists.txt |
Registers transition parsing. |
libs/assets/assets/TessellatedPaths.h |
Adds path heights and height-mesh output. |
libs/assets/assets/TessellatedPaths.cpp |
Shares mesh packing with the height pass. |
libs/assets/assets/lua/LuaEngine.cpp |
Adds validated setHeight bindings. |
libs/assets/assets/IAssetGenerator.h |
Stores optional generated-path heights. |
libs/assets/assets/GroundTransition.h |
Defines transition recipes and validation interfaces. |
libs/assets/assets/GroundTransition.cpp |
Parses recipes and strips them from bake-key XML. |
libs/assets/assets/GroundAssets.test.cpp |
Tests heights, seed groups, and transition loading. |
libs/assets/assets/AssetValidator.test.cpp |
Covers transition validation rules. |
libs/assets/assets/AssetValidator.cpp |
Reports invalid or misplaced transition fields. |
libs/assets/assets/AssetRegistry.cpp |
Loads recipes and assigns paint-order heights. |
libs/assets/assets/AssetDefinition.h |
Adds seed groups and transition settings. |
docs/testing/scenarios/08-ground-tiles.md |
Adds ground-rendering regression steps. |
docs/technical/chunk-management-system.md |
Marks the sector-grid design as superseded. |
docs/technical/3d-to-2d-sampling.md |
Documents replacement of sector interpolation. |
assets/world/ground/Snow/Snow.xml |
Retunes snow art, heights, and transitions. |
assets/world/ground/Sand/Sand.xml |
Retunes sand detail and mud transitions. |
assets/world/ground/Mud/Mud.xml |
Retunes puddles, colors, and distant appearance. |
assets/world/ground/GrassTall/GrassTall.xml |
Shares grass seeds and defines transitions. |
assets/world/ground/GrassShort/GrassShort.xml |
Shares grass seeds and defines transitions. |
assets/world/ground/GrassMeadow/GrassMeadow.xml |
Retunes grass appearance and transitions. |
assets/world/ground/Grass/Grass.xml |
Adds shared seeds and transition recipes. |
assets/world/ground/Dirt/Dirt.xml |
Documents heights and adds transition recipes. |
apps/asset-manager/GroundBakeView.h |
Adds height-preview state. |
apps/asset-manager/GroundBakeView.cpp |
Displays baked heights separately from opaque color. |
Review effort: Balanced
- One layer-size rule (GroundLayer.h: period x pixelsPerMeter a whole number of texels, 1 to 1024) shared by AssetValidator, GroundTileBaker::begin and GroundTileBank::begin, so an 8192 texel layer is rejected at validation and the bank allocates no array for it. - Validation reports an SVG (assetType simple) ground def as an error; the baker only runs generators. The spec says so. - periodicNoise2 wraps the lattice index without overflowing int32 for periods near INT32_MAX. - perf-capture.ps1's cleanup resets the pan too, so a failed scroll scenario doesn't leave the camera moving.
The key hashed the def's XML as it was on disk when the bake began, while the generator runs the params the registry parsed at load, so an edit between the two stored a stale bake under a fresh key. It now hashes the parsed params, and groundBakeXml and AssetDefinition::sourceFile go. Cache files are named from a sanitized stem and a hash of the exact name, so names differing in case or in path characters never share a file or leave the directory.
… and vertices A ground def's <period> is now its only period: generateAsset passes it in the GenerationContext and Lua reads it as the global period, so ground_grass.lua and ground_bare.lua no longer read a <params><period> that had to match it by hand. Every def's period is 4, and all nine bake bit for bit as before. periodicNoise2 takes a period within 1e-6 of a whole number as that number, since P / F lands a hair off one, and addVertex raises a Lua error for a NaN or infinite coordinate.
…em at bake readGroundMacro (GroundTint.cpp) reads <macro> for the loader and the validator alike: three finite tints from 0 to 2, an amplitude from 0 to 1 and two positive wavelengths, so a NaN no longer tints a whole surface. GroundLayer.h now holds every bake limit, and the validator reports ground defs that need more distinct layer sizes than the bank's four arrays. GroundTileBaker::begin refuses a variant count outside 1 to 32 before anything is generated, the bake keeps the count it began with, and the asset-manager's Bake view shows a def's validation errors instead of baking it.
…ke path Each bank step now catches what it throws and fails its def with the message, so salal's guard no longer reruns the same step every frame with the splash stuck on its count. The bank checks GL_MAX_TEXTURE_SIZE and GL_MAX_ARRAY_TEXTURE_LAYERS before it allocates, fits the baker's render tile under the first, and checks glGetError after each array, dropping the defs of one GL refuses. world_sim::stepGroundBake is the one way the scenes bake, behind the splash's validation gate: the splash, the loading scene after a --scene jump, and a dev switch straight to the game scene, which goes to the splash on failure. The bank's bakeAll and started go, and the splash's new members lose their m_ prefix.
The value and target rules move from GpuHold::configure into parseGpuHoldCommand (GpuHoldCommand.cpp), which world-sim-tests covers the way it covers CameraCommand, the 400 paths included. GameScene's new GpuHold member loses its m_ prefix.
prepareAsset builds a ground tile at the pixels a meter its square lands at, device pixels included, so paths tagged finer than a thumbnail's pixels are neither tessellated nor drawn; Grass's tree thumbnail no longer holds the full-detail tile. The thumbnails' shared mesh cache drops its least recently fetched entries past 128 MiB, and a thumbnail keeps the mesh it draws through a shared_ptr, so a Randomize or a resize no longer adds an entry without limit.
The period source, the macro and limit rules, the Lua guards, the one bake path and its errors, and the cache's key and file names.
At a rounding tie the prune's b + reach <= a dropped a runner-up that the settled test's a - b >= reach had just called open, so rank() came back without one and the pick read past its candidates. Both compare the leader's lead now. The tie tile is a regression test, and the sector-grid test asserts what its comment and the spec claim.
…an't fail NoiseOctaveGrid matches gradientNoise2 bit for bit wherever that function's float position is exact, at origins on both sides of zero, and gives a shared point the same bits from any grid origin. TileBiomesComeFromTheCorners could only ever pass: the pick's candidates come from the corners.
GenerationContext::elevation, selectSurfaceFor's elevationMeters and rankNormalize's tieBreak were never read or passed. The biome water comment names what Chunk::generate answers from now.
The staircase lake modeled the deleted 16 m sector shoreline. LakeShore runs Chunk::generate on a planet blend band across the chunk, so the per-tile pick draws the shore with its bays and islands: 723 ring vertices where the staircase had 405.
…le sums The 36 pairs a band reads take 72 vec4 at upper * (upper - 1) / 2 + lower, where the square table took 162, 648 of GL 4.1's 1024 guaranteed fragment uniform components. The 1.5 x 2^23 bias is exact only in source order, so it no longer meets a sum a compiler could fold it through: the noise's offset in its cell and the gather corner convert with float(), the variant pick rounds a floor, and a cell's uv offset and angle come from a mantissa. Same values, and on the desert view the same tileGpuMs as the tip in every cell; precise held the order too but cost 0.02 to 0.05 ms.
Two more regions: the middle of a chunk 70 km out across the origin, and the far corner of chunk (-1, -1), whose reads cross zero, so the noise's floor division and hashing run on negative tiles on the GPU. The band distance and far-zoom tests cover them as well.
The co-bake's shared hex hash, what the golden test covers and exempts, the triangular pair table, the mip window as the code computes it, the bias's fold safety and its A/Bs, the tightened sector-grid numbers, the sector grid's wording outside its banners, and scenarios 05, 06 and 08 in the testing index.
Both epics move to Recently Completed with dev log entries, the follow-ups become a planned epic, the ground spec drops its draft-PR wording, and status.md keeps only the last four completed epics.
kcjonson
changed the base branch from
kj/terrain-edges-grass-textures-1cd28e
to
main
November 2, 2026 20:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Ground tile variants (#332) and ground surface blending, landing together. This branch carries #332's commits, so merging it closes both; #332 is the place to read the first half on its own.
From #332: ground tiles are Lua-generated assets (role
ground), baked at load into mipmapped texture arrays with a disk cache, hex-tiled inland.glslwith a per-chunk macro tint. The atlas is gone, and with it the faint tile-edge lines that flickered while panning.This half:
path:setHeight), and a height contest inland.glslthat replaces the fringe, its breakup and Rock's rim. Each pair's recipe comes from<transition>blocks in the ground defs.SurfaceField.Since the manual test:
textureGatheris core and macOS accepts it.<macro>, variant counts and layer sizes, and the def's period as the only period;Perf (RTX 3090, 3072x1728, GPU held at P0): the tile pass is at or under main in 26 of 27 cells. Desert idle zoom 3 is 0.58 ms against main's 0.56. Cold bake is about 21 s and warm about 40 ms, and the arrays are 56 MiB. The tables are in
docs/technical/ground-textures.md.Tests: world 193, world-view 37, assets 242, sim 678 and world-sim 7 pass. Regression scenarios 01, 06 and 09 pass on the final build, including the direct
reloadentry into the game scene.Follow-ups are listed in status.md under Ground Tile Follow-ups:
data-lodSVG ground defs, BC3/BC7, golden on a software GL in CI, salal requesting a 4.1 context, and the cache location and key.