Skip to content

feat(tools): proper mesh textures — per-section albedo from material textures (sf-map-renderer)#261

Merged
StuartMeeks merged 6 commits into
mainfrom
feature/246-mesh-textures
Jul 13, 2026
Merged

feat(tools): proper mesh textures — per-section albedo from material textures (sf-map-renderer)#261
StuartMeeks merged 6 commits into
mainfrom
feature/246-mesh-textures

Conversation

@StuartMeeks

@StuartMeeks StuartMeeks commented Jul 12, 2026

Copy link
Copy Markdown
Owner

🤖 AI-generated PR

Refs #246. The "textures" finale.

What

Replaces the hand-picked per-family placeholder colours with the real average albedo of each mesh material, sampled from its diffuse texture.

  • MaterialColourSampler — finds a material's diffuse texture (CMaterialParams2, AllLayers incl. referenced textures), decodes a small mip with the managed AssetRipper BC/DXT decoder (BC1/2/3/7 + raw BGRA/RGBA), and averages the opaque pixels (alpha-cut leaf/coral cards contribute only their lit pixels). When no texture decodes (e.g. DesertRock's virtual-textured blend material) it falls back to a base-colour vector param (ignoring near-white/black tint defaults); only then the family palette. Cached by material path.
  • MeshGeometryCache — samples per LOD0 section, carrying a per-triangle colour. LOD0 excludes the black LOD billboard/imposter materials and gives trunk vs foliage their own real colours.
  • Rasteriser writes the winning triangle's sampled colour.

Dependency note

Uses AssetRipper.TextureDecoder directly (MIT) rather than CUE4Parse-Conversion — that would drag in the paid-line ImageSharp 3.x; the renderer stays on Apache-2.0 ImageSharp 2.1.x. No SkiaSharp, no new licensing exposure.

Result

Cliffs and desert rock read as sand/tan and blend into the dunes, rock greys are real, forests render in true leaf-greens — the map reads like a natural terrain view. (Real AmberTree foliage is green, which the placeholder had guessed as amber — sampling corrects it.)

Testing

  • sf-map-renderer xUnit: 54 pass (host build).
  • probe matcolour smoke test across cliffs (tan), desert rock (sandy via vector fallback), boulders/rubble (grey), blue palm (blue-green), coral, and the trees; channel order verified (not R/B-swapped).
  • Full render --downsample 2 --layers on the host, overlay eyeballed — no billboard/black artifacts, no regressions.

Interactive artifact: https://claude.ai/code/artifact/f71233b1-c70a-49f4-b1bd-7448244ee8c7

Not merged — for your review. Possible future polish: sampling the multi-texture blend materials (rock+sand+wetsand) properly rather than one vector colour; overall brightness/gamma if desired.

…aterial textures (sf-map-renderer)

Replaces the hand-picked per-family placeholder colours with the real average
albedo of each mesh material.

- `MaterialColourSampler` finds a material's diffuse texture (`CMaterialParams2`,
  AllLayers incl. referenced textures), decodes a small mip with the managed
  AssetRipper BC/DXT decoder (BC1/2/3/7 + raw BGRA/RGBA), and averages the opaque
  pixels (alpha-cut leaf/coral cards contribute only their lit pixels). When no
  texture decodes (e.g. DesertRock's virtual-textured blend material) it falls
  back to a base-colour vector param, ignoring near-white/black tint defaults;
  only then the family palette. Cached by material path.
- `MeshGeometryCache` samples per LOD0 section, carrying a per-triangle colour
  (LOD0 excludes the black LOD billboard/imposter materials, and gives trunk vs
  foliage their own real colours). The rasteriser writes the winning triangle's
  colour.

Uses AssetRipper.TextureDecoder directly (MIT) rather than CUE4Parse-Conversion,
which would force the paid-line ImageSharp 3.x — the renderer stays on Apache-2.0
ImageSharp 2.1.x. Adds a `probe matcolour` smoke test.

Result: cliffs and desert rock read as sand/tan and blend into the dunes, rock
greys are real, forests render in true leaf-greens — the map reads like a natural
terrain view.

Refs #246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@StuartMeeks
StuartMeeks force-pushed the feature/246-mesh-textures branch from e0ee80d to 915aff2 Compare July 12, 2026 17:59
StuartMeeks and others added 5 commits July 13, 2026 02:22
…trast (sf-map-renderer)

Three review tweaks on top of the texture pass:

- All coral (trees + shells) renders one fixed purple-ish hue (152,100,170)
  rather than its sampled albedo — in-game the alien coral glows magenta/purple
  from its emissive, which the base-texture albedo (green/brown) misses. The
  rasteriser skips the sampled colour for coral; the per-species coral table is
  dropped in favour of the single default.
- Ocean + wet-sand surface raised a touch, -1755 → -1730cm, so the shoreline
  east of the Dune Desert sits a little higher.
- Inland (non-ocean) water depth ramp widened for more contrast: deep end darker
  (8,28,72) and shallow end slightly brighter (70,136,190). The ocean band keeps
  its own ramp.

Refs #246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(sf-map-renderer)

The terrain was coloured by a hand-picked colour per weightmap layer, so the
desert read green-yellow rather than its true warm sand. Now each layer's colour
is sampled from the landscape material itself.

- `TerrainLayerColours` maps each weightmap layer (Sand, SandRock, Gravel,
  SandPebbles, WetSand, CoralRock, Forest, Grass, …) to its diffuse texture in
  the shared `ULandscapeMaterialInstanceConstant` and multiplies the average
  albedo by the layer's base-colour tint in linear space — e.g. Sand =
  TX_Sand_BC × "Sand BaseColor", SandRock = TX_SandRock_Alb × "Sand Rock
  BaseColor" (its reddish sandstone). Falls back to `TerrainPalette` for any
  layer whose texture doesn't decode.
- `SceneCollector` captures the landscape material instance on each tile;
  `HeightmapDecoder` builds the colour map once from it and uses it in the
  weight-blend. `MaterialColourSampler.AverageTexture` is exposed for reuse.
- DesertRock meshes track the desert sand (light sandstone) rather than their
  pale virtual-textured fallback, so rock and sand read alike as in-game.
- New `probe terrainmat` dumps the landscape material's textures/tints.

Note: the single Sand layer paints dune desert, rocky desert and beaches alike,
so the in-game regional variation (orange dune vs yellow rocky) — driven by the
material's macro world-space variation, not the weightmap — is not reproduced.

Refs #246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rrain colour (sf-map-renderer)

The single Sand weightmap layer paints dune desert, rocky desert and beaches
alike, so a flat per-layer colour couldn't split the in-game orange dune from
the yellow rocky desert. The landscape material carries that variation in a
world-aligned "PigmentMap" (its LandscapeCoords switch is set): a 1024² colour
map, mostly white/neutral with large tinted regions — a salmon Dune Desert, a
mauve Red Jungle, a blue Blue Crater.

- `MacroPigment` decodes the PigmentMap once from the shared landscape material
  and, per cell, multiplies the weight-blended terrain colour toward the pigment
  at the cell's landscape UV (column/width, row/height — the frame is the
  landscape section extent), scaled by a strength. White pigment leaves a cell
  unchanged, so only the tinted regions shift.
- `--pigment <0..1>` (default 0.6, 0 disables) via RenderOptions.PigmentStrength.
- `MaterialColourSampler.DecodeRgb` decodes a texture to an RGB grid (reused by
  the new `probe pigment`, which dumps the map + its scalar/switch params).

Result: the Dune Desert now reads distinctly orange while the Rocky Desert stays
pale yellow-tan, and the Red Jungle/Red Bamboo Fields gain their rust cast —
regional variation matching the game, precisely aligned to the biomes.

Refs #246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rer)

The world-aligned pigment tinted only the landscape terrain, so placed rock
meshes stayed a flat neutral colour — desert rock read grey against the orange
sand around it. The pigment is now built once in the pipeline and shared: the
rock rasteriser multiplies each rock cell's colour by the pigment at its
landscape UV, exactly as the terrain does.

Rock (not trees or coral) now varies by region — orange in the dune desert to
sit with the sand, rust in the Red Jungle — instead of one uniform tone.

Refs #246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…renderer)

Every rock of a family shared one sampled material colour, so cliffs/boulders
read as a single flat tone within a region. `RockJitter` derives a stable
per-instance multiplier — a brightness shift plus a small warm/cool tilt, hashed
from the instance's world position (reproducible across renders) — applied to
each rock before the macro pigment.

`--rock-jitter <0..1>` (default 0.18, 0 disables) via RenderOptions.RockJitter.
Same-family rock now varies naturally between lighter/darker, warmer/cooler
shades instead of one uniform colour.

Refs #246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@StuartMeeks
StuartMeeks merged commit 03bb070 into main Jul 13, 2026
7 checks passed
@StuartMeeks
StuartMeeks deleted the feature/246-mesh-textures branch July 13, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant