docs(issues): file #48 — subset of tree species hover above terrain

A few specific scenery GfxObjs render with their trunk base ~0.5-1.5m
above the terrain mesh while the vast majority sit flush. Per-GfxObj-
id ⇒ deterministic across instances. Player Z snap is unaffected.
Side-by-side with retail confirmed the same species place flush there.

Filed with three competing hypotheses: per-GfxObj origin convention
(some tree meshes authored with origin at bbox-center vs trunk-base),
physics-vs-bilinear terrain Z mismatch on NE↔SW-cut cells, or the
same DIDDegrade close-detail story as #47 applied to scenery.

Detailed cut-and-paste handoff for the next agent at
docs/research/2026-05-06-issue-48-handoff.md — covers the diagnostic
dump that disambiguates the hypotheses with one log line per
offending tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-06 17:18:26 +02:00
parent 50da2bb81d
commit 0d3b85dd69
2 changed files with 405 additions and 0 deletions

View file

@ -46,6 +46,108 @@ Copy this block when adding a new issue:
# Active issues
## #48 — A few specific scenery trees hover above terrain (per-GfxObj Z misplacement)
**Status:** OPEN
**Severity:** LOW (cosmetic; ~3 trees per landblock, easy to ignore but obvious once spotted)
**Filed:** 2026-05-06
**Component:** rendering / scenery placement / terrain Z sampling
**Description:** In outdoor landblocks, a small subset of tree
scenery instances render visibly **floating above the terrain**
(trunk base ~0.51.5 m above the ground line). The vast majority
of scenery (other tree species, bushes, rocks) sits flush. The bug
is **per-GfxObj-id**: the same handful of species float wherever
they spawn; other species at the same (x, y) cell sit correctly.
Side-by-side with retail in the same area: retail places the same
species flush. User-confirmed via screenshot pair 2026-05-06.
The user noted this is the only thing left wrong with terrain
rendering (canopy density / shape were *not* the issue — those
match retail when looked at carefully). The bug is purely vertical
offset on a few species.
**Investigation 2026-05-06:**
[`SceneryGenerator.cs:204`](src/AcDream.Core/World/SceneryGenerator.cs:204)
returns `LocalPosition.Z = obj.BaseLoc.Origin.Z` (just the
ObjectDesc's BaseLoc Z offset, no terrain). [`GameWindow.cs:4642`](src/AcDream.App/Rendering/GameWindow.cs:4642)
adds the terrain ground Z:
```csharp
float groundZ = _physicsEngine.SampleTerrainZ(worldPx, worldPy)
?? SampleTerrainZ(lb.Heightmap, _heightTable, localX, localY);
float finalZ = groundZ + spawn.LocalPosition.Z;
```
Both samplers claim to use the AC2D split-direction terrain mesh
formula. Player feet land flush, so player Z sampling is correct;
scenery for most species is also flush; only specific GfxObjs
float.
**Three competing hypotheses (need one diagnostic to disambiguate):**
1. **Per-GfxObj origin convention.** Most AC tree GfxObjs are
authored with local origin at the trunk base (mesh vertices
have `Z >= 0` measured up from the origin). A few species
may be authored with origin at bbox-center or visual top —
for those, `finalZ = groundZ + BaseLoc.Z` plants the *center*
at ground and the visible trunk floats by half its height.
Per-GfxObj-id ⇒ deterministic across instances ⇒ fits the
"same 3 species everywhere" pattern.
2. **Physics-sampler vs bilinear-fallback Z mismatch on
NE↔SW-cut cells.** The physics path uses the AC2D
split-direction formula. The bilinear-fallback at
`GameWindow.cs:4643` uses naive bilinear over heightmap
corners — wrong on cells whose visible triangle slopes
the *other* way. If physics hasn't registered a landblock
yet when scenery hydrates (timing race), affected scenery
uses the bilinear sampler and lands on a different Z than
the visible terrain. Player Z is fine because player movement
always goes through the physics sampler.
3. **Same close-degrade story as #47, applied to scenery.** Some
tree GfxObjs have `DIDDegrade` tables; slot 0 (close-detail)
and the base-LOD-3 mesh may have different mesh-local origins.
We currently draw the base GfxObj id directly for scenery (the
close-degrade resolver is scoped to humanoid setups only).
Retail draws slot 0 for nearby trees. If slot-0 has origin at
trunk-base while base-LOD-3 has origin at bbox-center, those
species float by exactly the offset between the two origins.
**Cheapest first move:** add a one-shot scenery placement dump
gated by `ACDREAM_DUMP_SCENERY_Z=1` that logs, per spawn:
```
[scenery-z] gfxObj=0xXXXXXXXX setupOrGfx=… worldPos=(x,y,z)
BaseLoc.Z=… groundZ=… meshZRange=[zMin..zMax]
hasDIDDegrade=true/false degrades[0]=0xXX
```
User identifies one floating tree → grep that GfxObj id in the
log → look at meshZRange and `hasDIDDegrade`. That tells us
hypothesis 1 (zMin > 0 by the float amount), hypothesis 2 (matching
species correctly placed elsewhere → timing race), or hypothesis 3
(`hasDIDDegrade=true` and slot 0 mesh has different zMin). One log
sample answers the question.
**Files:**
- [`src/AcDream.Core/World/SceneryGenerator.cs:204`](src/AcDream.Core/World/SceneryGenerator.cs:204) — BaseLoc.Z passthrough
- [`src/AcDream.App/Rendering/GameWindow.cs:4632-4655`](src/AcDream.App/Rendering/GameWindow.cs:4632) — groundZ resolution + finalZ assembly
- [`src/AcDream.Core/Physics/TerrainSurface.cs`](src/AcDream.Core/Physics/TerrainSurface.cs) — physics sampler (AC2D split-direction formula)
- `SampleTerrainZ` (private, in GameWindow.cs) — bilinear fallback
- [`src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs`](src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs) — close-degrade resolver if hypothesis 3 confirmed; would need scenery-scope expansion (drop the `IsIssue47HumanoidSetup` gate or add a scenery-aware variant)
**Acceptance:** All scenery species rest flush on the visible
terrain mesh in side-by-side outdoor screenshots vs retail. No
regression on the species that already render correctly.
**Handoff:** [docs/research/2026-05-06-issue-48-handoff.md](docs/research/2026-05-06-issue-48-handoff.md)
---
## #39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer)
**Status:** OPEN