docs(research): terrain detail/normals + atmospheric rendering findings
Measured, not theorised. Records for the next session:
- #226 detail overlay is NOT implemented at 255b0aae, and the
BuildingDetailTextures Options checkbox is wired to NOTHING (settings +
UI exist; no consumer in Rendering/). The port must consume that
existing setting.
- Retail authors detail per terrain entry; Dereth has 33 entries but only
3 distinct detail textures, one 64x64 backing 88% of them.
- The detail pass is a framebuffer blend, not DOT3: environment uses
DSTCOLOR+INVSRCALPHA under BLENDOP_ADD, landscape uses SRCALPHA. Decoded
texture means give factors 1.03-1.20, so retail's own pass BRIGHTENS
rather than roughens - a real design decision for the port.
- EnvironmentDetailTextures gates buildings+interiors, NOT landscape
(ChangeRegion passes landscape=0).
- Refuted and recorded so nobody re-chases: highres is NOT an override dat
(20,684/2,294 ids, zero overlap - we already use it); the detail
textures are NOT normal maps (24%/8%/15% unit-length); but the engine
DOES have BumpMap/DotProduct3 machinery.
- Terrain normals are flat per-face with no averaging - whether retail
smooths is the open parity question.
- Atmospheric rendering want captured: opt-in shader packs, tier table,
and the shadow constraints (alpha-tested casters, animated casters via
the N.5 SSBO, indoor gating, streaming-bounded cascades, the #129 bias
trap, and the shadow_objects naming trap).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
255b0aaeda
commit
5ca029d3ea
1 changed files with 322 additions and 0 deletions
|
|
@ -0,0 +1,322 @@
|
|||
# Terrain detail, terrain normals, and atmospheric rendering — verified findings
|
||||
|
||||
**Date:** 2026-08-21 · **Status:** RESEARCH / HANDOFF — no code written
|
||||
**Tree:** `main` @ `255b0aae` (branch `claude/git-sync-status-5fb1d2`, level with main)
|
||||
|
||||
Everything below was **measured** against the installed DATs, the current
|
||||
source tree, or the named-retail decomp. It exists so the next session does not
|
||||
re-derive it. Where a claim was tested and **refuted**, that is recorded too —
|
||||
those are the expensive ones to rediscover.
|
||||
|
||||
---
|
||||
|
||||
## 1. The detail-texture overlay is NOT implemented (and the setting lies)
|
||||
|
||||
**Renderer:** `terrain_modern.frag` / `.vert` contain **zero** detail
|
||||
references — no second sampler, no detail UV, no distance-fade term. Tree-wide
|
||||
there is no `DetailTex` / detail-surface concept in
|
||||
`src/AcDream.App/Rendering/`. Verified at `255b0aae`, not only at the older
|
||||
`bb1640f7`.
|
||||
|
||||
**But the SETTING exists and is DEAD:**
|
||||
|
||||
| Piece | Location |
|
||||
|---|---|
|
||||
| `bool BuildingDetailTextures = true` | `AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs:77` |
|
||||
| persisted as `"buildingDetailTextures"` | `SettingsStore.cs:99` (read), `:677` (write) |
|
||||
| Options checkbox, retail string `ID_Graphics_BuildingDetailTextures` | `ConfigOptionsPageController.cs:772-774` |
|
||||
| **consumers in `Rendering/`** | **none** |
|
||||
|
||||
The Options window shows "Building Detail Textures", checked by default,
|
||||
persisting across sessions — wired to nothing. This is worse than not having
|
||||
the setting: the UI implies a feature that does not exist. **#226's port must
|
||||
consume this existing setting, not invent a new one**, and its acceptance test
|
||||
must include "toggling it visibly changes the scene".
|
||||
|
||||
Already shipped and NOT to be confused with this: **#155 base tiling**
|
||||
(`bb5acab9`, ported `TexMerge::CopyAndTile`/`Merge`) — that fixed the
|
||||
*stretched* look. #155 and #226 were conflated once already.
|
||||
|
||||
---
|
||||
|
||||
## 2. How retail's detail pass actually works
|
||||
|
||||
### Authored data (measured from the installed DATs)
|
||||
|
||||
Detail textures are authored **per terrain entry**:
|
||||
`TerrainTex.DetailTextureId` + `TerrainTex.DetailTexTiling`, reached via
|
||||
`LandSurf::GetDetailTex` / `GetDetailTiling`.
|
||||
|
||||
Region `0x13000000` "Dereth": **33 terrain entries, only 3 distinct detail
|
||||
textures.**
|
||||
|
||||
| Detail texture | RenderSurface | Size | Used by |
|
||||
|---|---|---|---|
|
||||
| `0x050012AF` | `0x060037D2` | **64x64** A8R8G8B8 | **29 of 33 (88%)** |
|
||||
| `0x05001786` | `0x06006D57` | 256x256 A8R8G8B8 | 2 — BarrenRock, LushGrass |
|
||||
| `0x05001787` | `0x06006D58` | 256x256 A8R8G8B8 | 2 — Grassland, Ice |
|
||||
|
||||
`DetailTexTiling` is mostly **1** (4 for grass/rock types, 8 for
|
||||
FauxWaterRunning, 2 for RoadType) against a base `TexTiling` of 2.
|
||||
|
||||
This confirms the community observation that the client "uses the same noise
|
||||
texture for everything" — one 64x64 texture backs 88% of Dereth's terrain.
|
||||
|
||||
### Four categories, and which the setting actually gates
|
||||
|
||||
`LScape::SetDetailTexturing` (0x00506b40) manages **four** independent detail
|
||||
surfaces + tilings: `0 = landscape`, `1 = building`, `2 = environment`,
|
||||
`3 = object`. Also `GenerateDetailSurface` (0x00506230),
|
||||
`CleanupDetailSurfaces` (0x00504ae0).
|
||||
|
||||
`LScape::ChangeRegion` (0x00506cb0) calls:
|
||||
|
||||
```c
|
||||
SetDetailTexturing(this, 0, EnvDetail, EnvDetail, 0);
|
||||
// ^landscape=OFF ^building ^environment ^object=OFF
|
||||
```
|
||||
|
||||
gated on the single `Render::m_RenderPrefs.EnvironmentDetailTextures`. **So
|
||||
retail's setting is a BUILDINGS-AND-INTERIORS setting, not a terrain
|
||||
setting** — which explains why its UI label is "Building Detail Textures".
|
||||
|
||||
`SmartBox::SetDetailTexturing` (0x00451df0) *can* pass landscape through — its
|
||||
callers are **untraced**; that is the open question for whether terrain detail
|
||||
is ever enabled in practice.
|
||||
|
||||
### The blend — and retail's own bug
|
||||
|
||||
`ACRender::SetDetailSurfaceInternal` (0x006b6280):
|
||||
|
||||
```c
|
||||
SetStageTexture(stage, detailTex);
|
||||
SetSamplerAddressMode(stage, TEXADDRESS_WRAP, TEXADDRESS_WRAP);
|
||||
SetSamplerFilterMode(stage, TEXFILTER_LINEAR x3);
|
||||
if (stage == 0) {
|
||||
SetBlendFunction(curr_detail_src_blend, curr_detail_dst_blend, BLENDOP_ADD);
|
||||
SetAlphaBlendEnable(1);
|
||||
SetDepthBufferMode(DEPTHTEST_LESSEQUAL, 1);
|
||||
}
|
||||
```
|
||||
|
||||
It is a **framebuffer blend, not a DOT3/normal-map path.** Factors differ per
|
||||
category:
|
||||
|
||||
| Path | site | src | dst |
|
||||
|---|---|---|---|
|
||||
| environment / interiors | `0059f1c2` | `9` = `BLEND_DSTCOLOR` | `6` = `BLEND_INVSRCALPHA` |
|
||||
| landscape | `005a19a1` | `5` = `BLEND_SRCALPHA` | `6` = `BLEND_INVSRCALPHA` |
|
||||
| static default (`.data`) | `0081eca0` | 5 | 6 |
|
||||
|
||||
`DSTCOLOR + INVSRCALPHA` under `BLENDOP_ADD` evaluates to
|
||||
`dest x (detailLum + 1 - alpha)`. Decoded means of the three real textures:
|
||||
|
||||
| texture | lum | alpha | factor | effect |
|
||||
|---|---|---|---|---|
|
||||
| `0x060037D2` | 0.459 | 0.282 | **1.177** | **+18% brighter** |
|
||||
| `0x06006D57` | 0.414 | 0.210 | **1.204** | **+20% brighter** |
|
||||
| `0x06006D58` | 0.165 | 0.132 | **1.033** | **+3% brighter** |
|
||||
|
||||
**Retail's own detail pass BRIGHTENS rather than roughens.** A proper
|
||||
roughening modulate needs `dst = ZERO` so the result centres on 1.0; here the
|
||||
`dest x (1-alpha)` term adds most of the original back on top. This
|
||||
independently confirms the community observation that it looks like surfaces
|
||||
"reflect more instead of being rougher", and it raises a real design decision
|
||||
for #226 (see open questions).
|
||||
|
||||
### Terrain draw path
|
||||
|
||||
`ACRender::landPolyDraw` — **two overloads**, `0x006b6320` (single-poly) and
|
||||
`0x006b6760` (two-poly). Detail is gated on
|
||||
`trysinglepass && m_caps.bCanDoSinglePassDetailing && curr_detail_surface != 0`,
|
||||
then `SetDetailSurfaceInternal(1)`. Single-pass multitexture, with a
|
||||
non-single-pass fallback that is **untraced**. Vertex lighting comes from
|
||||
`ACRender::curLandBlockVertexLighting`.
|
||||
|
||||
---
|
||||
|
||||
## 3. REFUTED claims — do not re-chase these
|
||||
|
||||
- **"client_highres.dat is an override dat and we pick the low-res copy."**
|
||||
FALSE. Measured twice, including at `255b0aae`: portal **20,684**
|
||||
RenderSurfaces, highres **2,294**, **overlap 0** — fully disjoint IDs.
|
||||
`TextureCache` (`Portal.TryGet || HighRes.TryGet`) and
|
||||
`DatCollectionAdapter.TryResolvePreferred` (Portal then HighRes) both reach
|
||||
them, so every high-res surface resolves. **We already use them.** The
|
||||
"Portal precedence is load-bearing" comment in `DatCollectionAdapter` is
|
||||
vestigial given zero overlap. QUESTION CLOSED.
|
||||
- **"The detail textures are normal maps."** FALSE. The 64x64 one is
|
||||
blue-dominant (RGB 0.303/0.469/0.824), which *looks* like a tangent-space
|
||||
normal map, but unpacking RGB as a vector and measuring length gives
|
||||
**24.3% / 7.7% / 14.9%** unit-length pixels (a real normal map is ~100%).
|
||||
They are colour+alpha detail textures.
|
||||
- **"AC has no normal-map capability at all."** ALSO FALSE — the engine has a
|
||||
`BumpMap` parameter, the `DotProduct3` texture op, and a hardware capability
|
||||
probe `m_caps.bTexOpDotProduct3` (read at `0x0059f5c7`). The machinery
|
||||
exists; the detail textures simply are not normals. What *uses* the bump path
|
||||
is untraced.
|
||||
|
||||
---
|
||||
|
||||
## 4. Terrain geometry and normals
|
||||
|
||||
- Landblock = **192 m**; 8x8 cells @ **24 m**; 9x9 = **81 height samples**;
|
||||
2 triangles per cell => **128 triangles per landblock**.
|
||||
- Heights are **quantised**: each sample indexes a **256-entry**
|
||||
`region.LandDefs.LandHeightTable`. This caps what smoothing or subdivision
|
||||
can achieve — they smooth quantisation, they do not recover unsampled detail.
|
||||
- Diagonal split direction comes from the `FSplitNESW` hash of world cell
|
||||
coords (constants `0x0CCAC033`, `0x421BE3BD`, `0x6C1AC587`, `0x519B8F25`).
|
||||
- `src/AcDream.Core/Rendering/Wb/TerrainUtils.cs` `GetNormal` returns the
|
||||
**flat per-face normal** (cross product of whichever triangle the sample
|
||||
lands in). **No averaging anywhere in the tree.** That is what produces the
|
||||
faceted look.
|
||||
|
||||
**Open question, must be answered from the decomp, no guessing:** does retail
|
||||
smooth terrain vertex normals? Entry points: `ACRender::landPolyDraw` (both
|
||||
overloads, note `ACRender::curLandBlockVertexLighting`),
|
||||
`LScape::calc_object_light` (0x00455730), the LScape sunlight/ambient block
|
||||
~`0x00455b51`, and how a terrain `CPolygon`'s vertex normals/colours are built.
|
||||
`TerrainUtils` is WorldBuilder-derived, so WB may have simplified — **check
|
||||
retail, not WB.**
|
||||
|
||||
- retail smooths => **parity gap**, schedule like #226.
|
||||
- retail is faceted => opt-in enhancement, belongs with the shader-pack work.
|
||||
|
||||
Smoothing costs **zero geometry change** and therefore **zero physics risk**:
|
||||
collision, walkability, slope tests and the 4M-cell conformance sweep all see
|
||||
identical triangles.
|
||||
|
||||
**Subdivision is NOT recommended** and the next session should try to refute
|
||||
rather than confirm that: perceived softness is texture-frequency (§1-2) plus
|
||||
flat shading (this section), not silhouette resolution; the 256-step
|
||||
quantisation caps the gain; and it is the only one of the three that touches
|
||||
the physics contract — those triangles ARE the collision surface (FloorZ /
|
||||
ValidateWalkable, walkable-polygon tracking, precipice/cliff slide, the 4M-cell
|
||||
conformance sweep, and the triangle-boundary Z bug that cost five failed fix
|
||||
attempts). If ever wanted, the only defensible forms are coplanar
|
||||
surface-preserving subdivision (identical surface, better Gouraud gradients) or
|
||||
an interpolating spline through the original 81 samples, render-only, with a
|
||||
registered bounded divergence.
|
||||
|
||||
---
|
||||
|
||||
## 5. Renderer state relevant to atmospheric work
|
||||
|
||||
- **9 shader pairs** (`debug_line`, `mesh_modern`, `particle`, `particle_mesh`,
|
||||
`portal_depth`, `sky`, `terrain_modern`, `ui_text`, `vk_probe`) plus a
|
||||
compiled `spv/` directory.
|
||||
- `mesh_modern` uses **per-vertex Gouraud** lighting deliberately — the A7
|
||||
comment records that a per-pixel evaluation produced a hard "spotlight pool"
|
||||
unlike retail's fixed-function T&L. 8-light `SceneLighting` UBO carrying
|
||||
`uFogParams` / `uFogColor` / `uCameraAndTime`. Two-pass alpha (opaque discard
|
||||
`<0.95`, translucent discard `>=0.95` and `<0.05`).
|
||||
- **Sun direction already exists and already tracks the day/night cycle:**
|
||||
`WorldRenderFrameBuilder.cs:526` —
|
||||
`-SkyStateProvider.SunDirectionFromKeyframe(keyframe)`, supplied as
|
||||
`LightKind.Directional` (`:531`, `:546`). The sun is a **real drawn object**
|
||||
(`dayGroup.SkyObjects` via `SkyPesFrameController`).
|
||||
- **A depth-only pipeline shape already exists:** `portal_depth.vert/.frag`
|
||||
with `GpuPipelineDescription.ColorWrite = false`
|
||||
(`Gpu/GpuPipelineDescription.cs:276`, honoured in
|
||||
`Gpu/Vk/VulkanGpuPipeline.cs:205`) and an empty fragment `main()`. **A shadow
|
||||
map is that pipeline aimed at the sun.**
|
||||
- `activeDayGroup` (weather: Clear / Cloudy / Overcast / Rainy) reaches the
|
||||
frame builder.
|
||||
- Campaign V (OpenGL -> Vulkan) closed 2026-07-29: pass-based RHI
|
||||
(`IGpuDevice` / `IGpuFrame` / `IGpuPassEncoder`, explicit
|
||||
`GpuPassDescription` + `GpuPipelineDescription`) over bindless + MDI.
|
||||
|
||||
### Performance baseline and the binding constraint
|
||||
|
||||
**519.7 FPS; CPU/GPU p50 1.869 / 1.096 ms** (CLAUDE.md), and the dense-town
|
||||
profile is **CPU-SUBMISSION-BOUND** (memory:
|
||||
`feedback_render_perf_measurement`).
|
||||
|
||||
Consequences:
|
||||
|
||||
- GPU-side **fullscreen** work is nearly free in observed FPS — it fills GPU
|
||||
idle time. Tier-1 post-processing costs little.
|
||||
- Anything that adds **CPU submissions** is expensive. **Shadow cascades must
|
||||
not re-run visibility culling per cascade on the CPU** — draw the full
|
||||
resident set per cascade, or move culling to a GPU compute pass.
|
||||
- `glFinish`-style profilers **inflate** GPU timings. Measure with the existing
|
||||
gates (capped/uncapped, p50/p99 CPU+GPU, pinned dense Arwic).
|
||||
|
||||
---
|
||||
|
||||
## 6. Wanted work — atmospheric rendering (user-stated)
|
||||
|
||||
Opt-in **shader packs**, modelled on Minecraft's Iris/OptiFine: the
|
||||
retail-faithful path stays the **default and authoritative**; enhanced
|
||||
rendering is a toggle, exposed through the **plugin API**. This framing is what
|
||||
resolves the parity tension — the divergence register is untouched because the
|
||||
faithful path still exists and is still what we test.
|
||||
|
||||
| Tier | Contents | Prerequisite |
|
||||
|---|---|---|
|
||||
| 1 | bloom, ACES filmic tonemap, colour grade, vignette | none — fullscreen, scales with pixels |
|
||||
| 1 | screen-space sun rays (crepuscular) | sun screen-pos + occlusion mask; **no shadow maps** |
|
||||
| 2 | **cascaded directional shadows** — trees, monsters, houses | second scene pass |
|
||||
| 2+ | volumetric light shafts | reuses the shadow map — nearly free after shadows |
|
||||
| later | SSAO, water reflections | depth + normals |
|
||||
| out | true PBR | AC has no per-texture normal/roughness maps |
|
||||
|
||||
**Dynamic shadows are an explicit user want** — trees, monsters and buildings
|
||||
casting real sun shadows — not merely a design exercise.
|
||||
|
||||
Design points the user asked for:
|
||||
|
||||
- Ray/shaft intensity driven by **AC's own authored weather**
|
||||
(`activeDayGroup`) rather than invented constants — strong shafts at clear
|
||||
dawn, muted under Overcast. Combined with the already-authored moving sun
|
||||
this yields rays that rake low at dawn, vanish at noon, return at dusk.
|
||||
Retail never had this.
|
||||
- Composite sun rays **before** tonemapping so bloom picks them up and they
|
||||
roll off the filmic curve instead of clipping.
|
||||
|
||||
### Shadow-specific constraints
|
||||
|
||||
1. **Alpha-tested casters.** Foliage is cutout geometry (two-pass alpha), so
|
||||
the shadow pass **cannot** use `portal_depth`'s empty fragment shader — it
|
||||
must sample and discard, or every tree casts a solid rectangle.
|
||||
2. **Animated casters.** Monsters need the same per-part transforms as the main
|
||||
pass; those already live in the N.5 SSBO, so the shadow vertex shader can
|
||||
read the same buffer.
|
||||
3. **Indoors has no sun.** Gate to outdoor cells; dungeon cells use authored
|
||||
per-cell ambient and must not be fought.
|
||||
4. **Cascades must be camera-relative**, bounded by the two-tier streaming
|
||||
window (memory: `reference_two_tier_streaming`), not a fixed world extent.
|
||||
5. **Depth bias will hit issue #129's bug class** — an NDC-space bias constant
|
||||
spans `~ b*d^2/near` **metres** of eye depth at distance; #129 leaked
|
||||
door-shaped holes through hills. Memory:
|
||||
`feedback_ndc_constants_eye_space_meaning`.
|
||||
6. **NAMING TRAP:** in AC's codebase "shadow" means the per-cell **physics**
|
||||
registration list (`CPhysicsObj::add_shadows_to_cells`, `shadow_objects`) —
|
||||
nothing to do with lighting. Grepping "shadow" drowns in collision hits.
|
||||
7. Quality scaling for weak hardware: half/quarter-res bloom and rays, fewer
|
||||
cascades, lower shadow resolution. 4K pays ~4x the tier-1 pixel cost.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open questions carried forward
|
||||
|
||||
1. Does retail smooth terrain vertex normals? (§4) — decides parity-gap vs
|
||||
enhancement.
|
||||
2. Who calls `SmartBox::SetDetailTexturing`, and is landscape detail ever
|
||||
enabled in practice? (§2)
|
||||
3. What is the non-single-pass detail fallback in `landPolyDraw`? (§2)
|
||||
4. What uses the `BumpMap` / `DotProduct3` path, if not the detail textures?
|
||||
(§3)
|
||||
5. For #226: port retail's brightening blend **verbatim**, fix it to `dst=ZERO`
|
||||
so it roughens as intended and register the deviation, or expose both behind
|
||||
the setting? (§2)
|
||||
|
||||
## 8. Where this sits
|
||||
|
||||
M4 is the active milestone; the next planned implementation work is the
|
||||
**#268 + TS-8 stat-chain package**, and none of the above should displace it
|
||||
without the user's say-so. #226 and the terrain-normals question are **parity
|
||||
gaps** (schedulable now). The shader-pack tiers are **post-M7**, since
|
||||
rendering phases are frozen until the polish pass — but they are wanted, so the
|
||||
design should be captured rather than rediscovered.
|
||||
Loading…
Add table
Add a link
Reference in a new issue