From 9d4967a4616653dbcdb23b34c39dd8d6d9923700 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 12 Apr 2026 22:52:08 +0200 Subject: [PATCH] =?UTF-8?q?fix(core):=20ACME=20cross-check=20fixes=20?= =?UTF-8?q?=E2=80=94=20normals,=20placement,=20scenery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes from the ACME StaticObjectManager cross-reference: 1. GfxObjMesh: normalize vertex normals (1d). Dat normals may not be unit-length; without normalization, lighting is wrong per-vertex. 2. SetupMesh: add third-fallback placement frame (2a). If neither Resting nor Default exists, use the first available frame from PlacementFrames. Matches ACME's GetDefaultPlacementFrame. 3. SceneryGenerator: building cell exclusion (4d). Compute which terrain vertices have buildings (from LandBlockInfo.Objects + Buildings), skip scenery spawns in those cells. Prevents trees from spawning inside building footprints. 4. SceneryGenerator: slope filter (4e). Compute terrain normal Z at each displaced position and check against ObjectDesc.MinSlope / MaxSlope bounds. Prevents trees from spawning on cliff faces. Also confirmed 4f (scenery Z=0) is NOT a bug — GameWindow's hydrator lifts scenery to terrain Z at line 1213. The Z=0 in SceneryGenerator is a placeholder correctly overridden at render time. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 24 ++++++++++++++++- src/AcDream.Core/Meshing/GfxObjMesh.cs | 2 +- src/AcDream.Core/Meshing/SetupMesh.cs | 11 ++++++++ src/AcDream.Core/World/SceneryGenerator.cs | 31 ++++++++++++++++++---- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 1d6452a..283497f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1154,8 +1154,30 @@ public sealed class GameWindow : IDisposable var region = _dats.Get(0x13000000u); if (region is null) return result; + // Build a set of terrain vertex indices that have buildings on them, + // so the scenery generator can skip those cells (ACME conformance fix 4d). + HashSet? buildingCells = null; + var lbInfo = _dats.Get( + (lb.LandblockId & 0xFFFF0000u) | 0xFFFEu); + if (lbInfo is not null) + { + buildingCells = new HashSet(); + foreach (var stab in lbInfo.Objects) + { + int cx = Math.Clamp((int)(stab.Frame.Origin.X / 24f), 0, 8); + int cy = Math.Clamp((int)(stab.Frame.Origin.Y / 24f), 0, 8); + buildingCells.Add(cx * 9 + cy); + } + foreach (var bldg in lbInfo.Buildings) + { + int cx = Math.Clamp((int)(bldg.Frame.Origin.X / 24f), 0, 8); + int cy = Math.Clamp((int)(bldg.Frame.Origin.Y / 24f), 0, 8); + buildingCells.Add(cx * 9 + cy); + } + } + var spawns = AcDream.Core.World.SceneryGenerator.Generate( - _dats, region, lb.Heightmap, lb.LandblockId); + _dats, region, lb.Heightmap, lb.LandblockId, buildingCells, _heightTable); if (spawns.Count == 0) return result; var lbOffset = new System.Numerics.Vector3( diff --git a/src/AcDream.Core/Meshing/GfxObjMesh.cs b/src/AcDream.Core/Meshing/GfxObjMesh.cs index b2a24c1..fea8a52 100644 --- a/src/AcDream.Core/Meshing/GfxObjMesh.cs +++ b/src/AcDream.Core/Meshing/GfxObjMesh.cs @@ -139,7 +139,7 @@ public static class GfxObjMesh // culling disabled the shader still samples this normal // for the diffuse term so getting it right matters // regardless of backface state. - var normal = isNeg ? -sw.Normal : sw.Normal; + var normal = System.Numerics.Vector3.Normalize(isNeg ? -sw.Normal : sw.Normal); var key = (posIdx, uvIdx, isNeg); if (!bucket.Dedupe.TryGetValue(key, out var outIdx)) diff --git a/src/AcDream.Core/Meshing/SetupMesh.cs b/src/AcDream.Core/Meshing/SetupMesh.cs index 26470b4..74ad9bb 100644 --- a/src/AcDream.Core/Meshing/SetupMesh.cs +++ b/src/AcDream.Core/Meshing/SetupMesh.cs @@ -37,6 +37,17 @@ public static class SetupMesh defaultAnim = resting; if (defaultAnim is null && setup.PlacementFrames.TryGetValue(Placement.Default, out var af)) defaultAnim = af; + // Last resort: use the first available placement frame (matches ACME's + // StaticObjectManager.GetDefaultPlacementFrame third fallback). Handles + // rare Setups that define only an unusual placement frame key. + if (defaultAnim is null) + { + foreach (var kvp in setup.PlacementFrames) + { + defaultAnim = kvp.Value; + break; + } + } var result = new List(setup.Parts.Count); for (int i = 0; i < setup.Parts.Count; i++) diff --git a/src/AcDream.Core/World/SceneryGenerator.cs b/src/AcDream.Core/World/SceneryGenerator.cs index 5758025..93ebdfe 100644 --- a/src/AcDream.Core/World/SceneryGenerator.cs +++ b/src/AcDream.Core/World/SceneryGenerator.cs @@ -46,7 +46,9 @@ public static class SceneryGenerator DatCollection dats, Region region, LandBlock block, - uint landblockId) + uint landblockId, + HashSet? buildingCells = null, + float[]? heightTable = null) { var result = new List(); @@ -74,6 +76,10 @@ public static class SceneryGenerator // check in get_land_scenes(). Roads should not have trees/rocks. if (IsRoadVertex(raw)) continue; + // Skip cells that contain buildings (ACME conformance fix 4d). + // Building footprints shouldn't have scenery spawning inside them. + if (buildingCells is not null && buildingCells.Contains(i)) continue; + if (terrainType >= region.TerrainInfo.TerrainTypes.Count) continue; var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes; if (sceneType >= sceneTypeList.Count) continue; @@ -133,10 +139,25 @@ public static class SceneryGenerator if (IsRoadVertex(nearRaw)) continue; } - // Z at the cell corner from the heightmap. Skipping slope-based - // Z placement (ACViewer uses find_terrain_poly which we don't have) - // — accept that some scenery will float or clip. - float lz = 0f; // will be lifted to ground at render time via landblock heightmap + // Slope filter (ACME conformance fix 4e): compute terrain normal + // Z-component at the displaced position and check against the + // object's MinSlope/MaxSlope bounds. + if (heightTable is not null && (obj.MinSlope > 0f || obj.MaxSlope < 1f)) + { + int sx = Math.Clamp((int)(lx / CellSize), 0, VerticesPerSide - 2); + int sy = Math.Clamp((int)(ly / CellSize), 0, VerticesPerSide - 2); + int sxR = sx + 1; + int syU = sy + 1; + float h00 = heightTable[block.Height[sx * VerticesPerSide + sy]]; + float h10 = heightTable[block.Height[sxR * VerticesPerSide + sy]]; + float h01 = heightTable[block.Height[sx * VerticesPerSide + syU]]; + float dx = (h10 - h00) / CellSize; + float dy = (h01 - h00) / CellSize; + float nz = 1f / MathF.Sqrt(dx * dx + dy * dy + 1f); // normal Z component + if (nz < obj.MinSlope || nz > obj.MaxSlope) continue; + } + + float lz = 0f; // lifted to ground at render time via landblock heightmap // Rotation Quaternion rotation = Quaternion.Identity;