fix(core): ACME cross-check fixes — normals, placement, scenery
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) <noreply@anthropic.com>
This commit is contained in:
parent
05749f52e0
commit
9d4967a461
4 changed files with 61 additions and 7 deletions
|
|
@ -1154,8 +1154,30 @@ public sealed class GameWindow : IDisposable
|
||||||
var region = _dats.Get<DatReaderWriter.DBObjs.Region>(0x13000000u);
|
var region = _dats.Get<DatReaderWriter.DBObjs.Region>(0x13000000u);
|
||||||
if (region is null) return result;
|
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<int>? buildingCells = null;
|
||||||
|
var lbInfo = _dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
|
||||||
|
(lb.LandblockId & 0xFFFF0000u) | 0xFFFEu);
|
||||||
|
if (lbInfo is not null)
|
||||||
|
{
|
||||||
|
buildingCells = new HashSet<int>();
|
||||||
|
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(
|
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;
|
if (spawns.Count == 0) return result;
|
||||||
|
|
||||||
var lbOffset = new System.Numerics.Vector3(
|
var lbOffset = new System.Numerics.Vector3(
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ public static class GfxObjMesh
|
||||||
// culling disabled the shader still samples this normal
|
// culling disabled the shader still samples this normal
|
||||||
// for the diffuse term so getting it right matters
|
// for the diffuse term so getting it right matters
|
||||||
// regardless of backface state.
|
// 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);
|
var key = (posIdx, uvIdx, isNeg);
|
||||||
if (!bucket.Dedupe.TryGetValue(key, out var outIdx))
|
if (!bucket.Dedupe.TryGetValue(key, out var outIdx))
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,17 @@ public static class SetupMesh
|
||||||
defaultAnim = resting;
|
defaultAnim = resting;
|
||||||
if (defaultAnim is null && setup.PlacementFrames.TryGetValue(Placement.Default, out var af))
|
if (defaultAnim is null && setup.PlacementFrames.TryGetValue(Placement.Default, out var af))
|
||||||
defaultAnim = 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<MeshRef>(setup.Parts.Count);
|
var result = new List<MeshRef>(setup.Parts.Count);
|
||||||
for (int i = 0; i < setup.Parts.Count; i++)
|
for (int i = 0; i < setup.Parts.Count; i++)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ public static class SceneryGenerator
|
||||||
DatCollection dats,
|
DatCollection dats,
|
||||||
Region region,
|
Region region,
|
||||||
LandBlock block,
|
LandBlock block,
|
||||||
uint landblockId)
|
uint landblockId,
|
||||||
|
HashSet<int>? buildingCells = null,
|
||||||
|
float[]? heightTable = null)
|
||||||
{
|
{
|
||||||
var result = new List<ScenerySpawn>();
|
var result = new List<ScenerySpawn>();
|
||||||
|
|
||||||
|
|
@ -74,6 +76,10 @@ public static class SceneryGenerator
|
||||||
// check in get_land_scenes(). Roads should not have trees/rocks.
|
// check in get_land_scenes(). Roads should not have trees/rocks.
|
||||||
if (IsRoadVertex(raw)) continue;
|
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;
|
if (terrainType >= region.TerrainInfo.TerrainTypes.Count) continue;
|
||||||
var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes;
|
var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes;
|
||||||
if (sceneType >= sceneTypeList.Count) continue;
|
if (sceneType >= sceneTypeList.Count) continue;
|
||||||
|
|
@ -133,10 +139,25 @@ public static class SceneryGenerator
|
||||||
if (IsRoadVertex(nearRaw)) continue;
|
if (IsRoadVertex(nearRaw)) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Z at the cell corner from the heightmap. Skipping slope-based
|
// Slope filter (ACME conformance fix 4e): compute terrain normal
|
||||||
// Z placement (ACViewer uses find_terrain_poly which we don't have)
|
// Z-component at the displaced position and check against the
|
||||||
// — accept that some scenery will float or clip.
|
// object's MinSlope/MaxSlope bounds.
|
||||||
float lz = 0f; // will be lifted to ground at render time via landblock heightmap
|
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
|
// Rotation
|
||||||
Quaternion rotation = Quaternion.Identity;
|
Quaternion rotation = Quaternion.Identity;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue