210 lines
10 KiB
Markdown
210 lines
10 KiB
Markdown
# Retail building and environment detail texturing — #226 port note
|
||
|
||
**Date:** 2026-08-21
|
||
**Status:** IMPLEMENTED + CONNECTED-VISUAL-VERIFIED
|
||
|
||
This note is the implementation handoff requested by #226. The measurements
|
||
below come from the already-completed
|
||
[`2026-08-21 terrain and atmospheric rendering findings`](2026-08-21-terrain-and-atmospheric-rendering-findings.md),
|
||
especially §§1–2. They are cited here rather than re-derived. The reachable
|
||
preference/caller chain is also recorded in
|
||
[`2026-07-10 detail texturing`](2026-07-10-detail-texturing.md).
|
||
The A2 terrain-normal verdict and A3 subdivision disposition are recorded in
|
||
the companion [`Terrain fidelity Track A report`](2026-08-21-terrain-fidelity-track-a-report.md).
|
||
|
||
## User-visible target and reachable caller trace
|
||
|
||
The issue title used to say “landscape,” but the Sept-2013 retail client does
|
||
not expose live landscape detail through this option:
|
||
|
||
1. The Options checkbox writes `RenderPrefs.EnvironmentDetailTextures`.
|
||
2. `Render::UpdateFromPreferences` (`0x0054d850`) explicitly changes
|
||
`Current_Render_LandscapeDetailTextures` to `0` and calls
|
||
`SmartBox::SetDetailTexturing(smartbox, 0, environmentEnabled)` at
|
||
`0x0054d9f3`.
|
||
3. `SmartBox::SetDetailTexturing` (`0x00451df0`) forwards
|
||
`LScape::SetDetailTexturing(lscape, landscape, enabled, enabled, 0)`.
|
||
4. `LScape::ChangeRegion` (`0x00506cb0`) independently installs the same
|
||
category state: `(0, EnvDetail, EnvDetail, 0)`.
|
||
|
||
The four positions are landscape (0), building (1), environment/EnvCell (2),
|
||
and ordinary object (3). The only reachable named-retail preference caller
|
||
forces categories 0 and 3 off. `DrawPartCell` also clears ordinary-object
|
||
detail. Therefore #226's scene target is **building shells and interior
|
||
EnvCell geometry**, not outdoor terrain, scenery, creatures, or players. This
|
||
also explains why acdream's existing checkbox is labelled “Building Detail
|
||
Textures.”
|
||
|
||
## Authored source, size, and sampling
|
||
|
||
Detail data is reached through
|
||
`Region(0x13000000).TerrainInfo.LandSurfaces.TexMerge.TerrainDesc[category]`:
|
||
|
||
```text
|
||
SurfaceTextureId = TerrainDesc[category].TerrainTex.DetailTextureId
|
||
tiling = TerrainDesc[category].TerrainTex.DetailTexTiling
|
||
RenderSurfaceId = SurfaceTexture(SurfaceTextureId).Textures[0]
|
||
rgba = decode(RenderSurface(RenderSurfaceId), level 0)
|
||
```
|
||
|
||
For Dereth, enabled categories 1 and 2 both resolve
|
||
`0x05001787 -> 0x06006D58`, a **256 x 256 A8R8G8B8** texture, with tiling
|
||
**4**. The complete measured Dereth population is three textures across 33
|
||
entries: `0x050012AF -> 0x060037D2` (64 x 64, 29 entries),
|
||
`0x05001786 -> 0x06006D57` (256 x 256, two), and the enabled-category texture
|
||
above (256 x 256, two). See the findings §2 table.
|
||
|
||
Retail uses wrap addressing in U and V and linear minification,
|
||
magnification, and mip filtering. Detail UV is `baseUv * tiling`. The port
|
||
therefore uploads each live category as a one-layer RGBA8 texture array with a
|
||
full mip chain and the existing repeat/linear world sampler.
|
||
|
||
## Exact two-pass pseudocode
|
||
|
||
Retail has both a single-pass multitexture route and a two-pass fallback. The
|
||
Vulkan port uses the fallback because it preserves the already-accepted base
|
||
pass byte-for-byte and expresses the retail framebuffer blend directly.
|
||
|
||
```text
|
||
enabled = DisplaySettings.BuildingDetailTextures // existing setting; no new option
|
||
|
||
buildingDetail = load_category(TerrainDesc[1])
|
||
environmentDetail = load_category(TerrainDesc[2])
|
||
|
||
for each retail built-mesh material subset:
|
||
draw_existing_base_subset_unchanged()
|
||
|
||
if enabled and subset belongs to a building or EnvCell:
|
||
draw the same subset with its category detail texture
|
||
// transparent/additive/inverse-alpha: detail follows its base
|
||
// immediately, before the next delayed-alpha subset
|
||
|
||
for each replayed fragment:
|
||
reject ordinary objects / landscape / scenery
|
||
accept opaque, ClipMap, alpha, additive and inverse-alpha subsets
|
||
|
||
zMetres = positive_view_space_depth_in_metres
|
||
fade = clamp((50 m - zMetres) / (50 m - 10 m), 0, 1)
|
||
// full through 10 m; linear 10–50 m; exactly zero at/after 50 m
|
||
|
||
detail = sample(categoryTexture, baseUv * categoryTiling)
|
||
src.rgb = detail.rgb * fade
|
||
src.a = detail.a * fade
|
||
|
||
depth test = EQUAL opaque; LESS_OR_EQUAL transparent
|
||
depth write = preserve base class // ON opaque; OFF transparent
|
||
alpha-to-coverage = OFF // detail alpha is blend input
|
||
blend op = ADD
|
||
source = DEST_COLOR
|
||
destination = ONE_MINUS_SRC_ALPHA
|
||
```
|
||
|
||
Scaling **both** RGB and alpha by the fade is load-bearing. The resulting
|
||
framebuffer multiplier is:
|
||
|
||
```text
|
||
factor = 1 + fade * (detail.rgb - detail.a)
|
||
```
|
||
|
||
Thus fade zero is an exact no-op and the full-strength neutral point is
|
||
`detail.rgb == detail.a` channel-by-channel. It is not 0.5 gray.
|
||
|
||
### Built-mesh material coverage and order
|
||
|
||
The land-polygon `SurfaceType & 4` exclusion does **not** narrow this built-mesh
|
||
port. Named-retail `RenderDeviceD3D::DrawEnvCell` (`0x0059f170`) and
|
||
`DrawBuilding` (`0x0059f2a0`) install `curr_detail_surface` before calling
|
||
`D3DPolyRender::DrawMesh`. `DrawMesh` (`0x0059d4a0`) bypasses delayed-alpha
|
||
queuing while that surface is installed and passes detail enabled to
|
||
`RenderMeshSubset` (`0x0059ca10`) for each material subset. The fallback then
|
||
redraws that exact subset with the detail surface before proceeding. Therefore
|
||
ClipMap, straight-alpha, additive, and inverse-alpha built-mesh subsets are
|
||
included alongside plain opaque ones.
|
||
|
||
The Vulkan port first filters the opaque object command stream to coalesced
|
||
runs containing at least one category-1 building instance; nonbuilding-only
|
||
commands never reach the detail pipeline. A mixed instanced command remains in
|
||
the replay and `mesh_detail` rejects its ordinary instances individually. The
|
||
accepted opaque path stays batched, while transparent subsets preserve
|
||
immediate base/detail adjacency. Their separate detail pipeline
|
||
keeps depth writes disabled, matching the base subset's accepted depth
|
||
contract. This prevents another shell/object contribution from being
|
||
composited between the base and its detail contribution.
|
||
|
||
Opaque detail uses depth compare **EQUAL** against the exact geometry just
|
||
written by the base pass. Vulkan depth is per sample, so on MSAA ClipMap edges
|
||
the detail affects only samples whose base alpha-to-coverage mask wrote depth.
|
||
The detail pipeline itself deliberately keeps alpha-to-coverage off: detail
|
||
alpha controls `ONE_MINUS_SRC_ALPHA` in the retail blend and is not the base
|
||
coverage mask. Transparent bases do not write depth, so their adjacent detail
|
||
uses `LESS_OR_EQUAL` with depth writes still off.
|
||
|
||
One bounded ordering seam is explicit: retail bypasses its delayed-alpha queue
|
||
while `curr_detail_surface` is installed, whereas acdream retains its already-
|
||
authoritative shared alpha-queue order and inserts the detail draw immediately
|
||
after the corresponding base draw. This does not narrow material coverage or
|
||
change base coverage/blend/depth behavior; it avoids making the checkbox
|
||
reorder the default transparent scene. The connected acceptance matrix must
|
||
still exercise overlapping transparent building/EnvCell surfaces.
|
||
|
||
## Brightening decision
|
||
|
||
The port keeps retail's `DEST_COLOR + ONE_MINUS_SRC_ALPHA` verbatim. The
|
||
findings measured factors **1.177**, **1.204**, and **1.033** for the three
|
||
Dereth textures; the live Dereth building/environment category uses the
|
||
1.033-factor texture. That slight brightening is intentional retail parity,
|
||
not an acceptance failure.
|
||
|
||
Changing the destination factor to `ZERO` would be a visual correction rather
|
||
than a port. Exposing both behaviors behind one retail checkbox would also
|
||
make the option ambiguous. If a roughening-corrected material is wanted later,
|
||
it belongs as an explicitly named opt-in enhancement/shader-pack policy with a
|
||
registered divergence. It is not part of #226.
|
||
|
||
## What the reverted experiment got wrong
|
||
|
||
The experiment described by `c25d6186` was never committed as renderer code;
|
||
it was reverted from the worktree with `git checkout`. Its useful failure
|
||
record remains in that issue commit. It differed from the verified contract in
|
||
five material ways:
|
||
|
||
- It targeted outdoor landscape, while the live setting enables building and
|
||
environment categories and forces landscape off.
|
||
- It built a per-terrain-type texture array, while retail selects one
|
||
category-scoped surface and scalar tiling for each draw path.
|
||
- It used `base * detail * 2` (`MODULATE2X`) instead of retail's framebuffer
|
||
blend.
|
||
- It assumed 128 gray was neutral; retail neutral is RGB equal to alpha.
|
||
- Its acceptance prohibited an overall brightness change, although retail's
|
||
measured blend intentionally brightens these textures.
|
||
|
||
The old OpenGL-specific array/bindless wiring is also not reusable in the
|
||
current Vulkan-only RHI.
|
||
|
||
## Corrected acceptance
|
||
|
||
- With `BuildingDetailTextures=false`, no detail replay is submitted and the
|
||
current base rendering remains unchanged.
|
||
- With it `true`, toggling the existing Options checkbox **visibly changes
|
||
buildings and interior/EnvCell surfaces** without a restart. The connected
|
||
2026-08-21 Facility Hub A/B/A gate applied the real Config checkbox on ->
|
||
off -> restored-on and captured the same nearby walls/floor after each
|
||
transition. Static right-wall mean absolute RGB error was 2.132 for on/off
|
||
versus 0.007 for original-on/restored-on; the floor row was 3.385 versus
|
||
0.013. The persisted setting was observed false during B, restored true,
|
||
and the session ended with ACE-confirmed graceful logout.
|
||
- Outdoor terrain, ordinary scenery/objects, creatures, and players do not
|
||
gain this overlay.
|
||
- Every built building/EnvCell material subset is eligible: opaque, ClipMap,
|
||
straight alpha, additive, and inverse alpha. Transparent base/detail draws
|
||
remain adjacent in acdream's authoritative shared alpha order.
|
||
- Opaque object replay submits only command runs containing a building; mixed
|
||
commands are filtered per instance. Depth equality inherits the base pass's
|
||
per-sample ClipMap coverage without applying A2C to detail alpha.
|
||
- Detail is full through positive view depth 10 m, fades linearly over 10–50
|
||
m, and is an exact no-op at and beyond 50 m.
|
||
- Category source, 256 x 256 size, tiling 4, repeat addressing, and linear mip
|
||
sampling match the measured Dereth data.
|
||
- The retail 1.033 live-category brightening is expected. There is no
|
||
`dst=ZERO` correction mode hidden behind the retail checkbox.
|
||
- Physics, collision, walkability, and geometry are untouched.
|