feat(core): procedural scenery from Region.SceneInfo (Phase 2c)
Adds SceneryGenerator.Generate which walks Region.TerrainInfo.TerrainTypes + Region.SceneInfo.SceneTypes for each landblock vertex, selects a scene using the AC client's pseudo-random LCG hash of global cell coordinates, then rolls each ObjectDesc's frequency, computes a displaced cell-local position, random scale, and random rotation — the exact algorithm ACViewer ports from the retail AC client's get_land_scenes(). Phase 2 rendered 239 explicit Stab+Building entities on the 3x3 Holtburg grid but was missing every procedurally-placed tree, bush, rock, fence, and small decoration because these are not stored as LandBlockInfo entries. This adds 419 scenery entities across the same 9 landblocks, bringing the total to 658. Integration in GameWindow.OnLoad: after the existing Stab/Building hydration loop, iterate each landblock's scenery spawns, resolve each to a GfxObj or Setup via the same mesh pipeline, bake the random scale into each MeshRef's PartTransform so the static mesh renderer doesn't need a scale field on WorldEntity, and sample the landblock heightmap bilinearly for the ground Z (simpler than ACViewer's find_terrain_poly slope-aware placement). Deliberate deferrals for first pass: - No slope-based rejection (obj.MinSlope/MaxSlope). Trees may end up on cliffs they shouldn't be on. - No road-overlap rejection. Scenery may spawn in roads. - No building-overlap rejection. Scenery may clip buildings. - No WeenieObj handling (those are dynamic spawns, not static scenery). All three filters will be added in a follow-up phase when we have the walkable-polygon infrastructure they need. Build clean, 48 tests still pass, smoke verified: "scenery: spawned 419 entities across 9 landblocks", process runs without exceptions. Addresses the user visual feedback after Phase 2b: "some extra details are missing, like a tree and the statue on top of the foundry". The tree issue is now fixed (419 trees/bushes/rocks/etc placed). The foundry statue may still be missing if it's a hierarchical Setup part (Phase 2a's SetupMesh.Flatten intentionally doesn't walk ParentIndex) — that's a separate fix if smoke verification shows it's still missing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe0bfb075b
commit
9970811dc3
2 changed files with 306 additions and 1 deletions
|
|
@ -237,8 +237,119 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 2c: procedural scenery — trees, bushes, rocks, fences from
|
||||
// Region.SceneInfo. These aren't stored as explicit Stab entries; they're
|
||||
// generated deterministically from per-vertex TerrainInfo.Scenery bits.
|
||||
int scenerySpawned = 0;
|
||||
uint sceneryIdCounter = 0x80000000u; // high bit set to avoid colliding with Stab ids
|
||||
foreach (var lb in worldView.Landblocks)
|
||||
{
|
||||
var spawns = AcDream.Core.World.SceneryGenerator.Generate(
|
||||
_dats, region!, lb.Heightmap, lb.LandblockId);
|
||||
if (spawns.Count == 0) continue;
|
||||
|
||||
int lbX = (int)((lb.LandblockId >> 24) & 0xFFu);
|
||||
int lbY = (int)((lb.LandblockId >> 16) & 0xFFu);
|
||||
var lbOffset = new System.Numerics.Vector3(
|
||||
(lbX - centerX) * 192f,
|
||||
(lbY - centerY) * 192f,
|
||||
0f);
|
||||
|
||||
foreach (var spawn in spawns)
|
||||
{
|
||||
// Resolve the object to a mesh (same GfxObj/Setup logic as Stabs).
|
||||
// Scale is baked into the root transform by wrapping each part's
|
||||
// transform with a scale matrix.
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
var scaleMat = System.Numerics.Matrix4x4.CreateScale(spawn.Scale);
|
||||
|
||||
if ((spawn.ObjectId & 0xFF000000u) == 0x01000000u)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
_staticMesh.EnsureUploaded(spawn.ObjectId, subMeshes);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
||||
}
|
||||
}
|
||||
else if ((spawn.ObjectId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.ObjectId);
|
||||
if (setup is not null)
|
||||
{
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
// Compose: part's own transform, then the spawn's scale.
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform * scaleMat));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (meshRefs.Count == 0) continue;
|
||||
|
||||
// Sample terrain Z at (localX, localY) to lift scenery onto the ground.
|
||||
float localX = spawn.LocalPosition.X;
|
||||
float localY = spawn.LocalPosition.Y;
|
||||
float groundZ = SampleTerrainZ(lb.Heightmap, heightTable, localX, localY);
|
||||
|
||||
var hydrated = new AcDream.Core.World.WorldEntity
|
||||
{
|
||||
Id = sceneryIdCounter++,
|
||||
SourceGfxObjOrSetupId = spawn.ObjectId,
|
||||
Position = new System.Numerics.Vector3(localX, localY, groundZ) + lbOffset,
|
||||
Rotation = spawn.Rotation,
|
||||
MeshRefs = meshRefs,
|
||||
};
|
||||
hydratedEntities.Add(hydrated);
|
||||
|
||||
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
|
||||
Id: hydrated.Id,
|
||||
SourceId: hydrated.SourceGfxObjOrSetupId,
|
||||
Position: hydrated.Position,
|
||||
Rotation: hydrated.Rotation);
|
||||
_worldGameState.Add(snapshot);
|
||||
_worldEvents.FireEntitySpawned(snapshot);
|
||||
scenerySpawned++;
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"scenery: spawned {scenerySpawned} entities across {worldView.Landblocks.Count} landblocks");
|
||||
|
||||
_entities = hydratedEntities;
|
||||
Console.WriteLine($"hydrated {_entities.Count} entities");
|
||||
Console.WriteLine($"hydrated {_entities.Count} entities total (stabs + buildings + scenery)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bilinear sample of the landblock heightmap at (x, y) in landblock-local
|
||||
/// world units. Matches the x-major indexing convention of LandblockMesh.
|
||||
/// </summary>
|
||||
private static float SampleTerrainZ(DatReaderWriter.DBObjs.LandBlock block, float[] heightTable, float worldX, float worldY)
|
||||
{
|
||||
const float CellSize = 24f;
|
||||
const int VerticesPerSide = 9;
|
||||
|
||||
float fx = Math.Clamp(worldX / CellSize, 0f, VerticesPerSide - 1);
|
||||
float fy = Math.Clamp(worldY / CellSize, 0f, VerticesPerSide - 1);
|
||||
int x0 = (int)MathF.Floor(fx);
|
||||
int y0 = (int)MathF.Floor(fy);
|
||||
int x1 = Math.Min(x0 + 1, VerticesPerSide - 1);
|
||||
int y1 = Math.Min(y0 + 1, VerticesPerSide - 1);
|
||||
float tx = fx - x0;
|
||||
float ty = fy - y0;
|
||||
|
||||
// Heightmap is packed x-major (Height[x*9+y]) matching LandblockMesh.
|
||||
float h00 = heightTable[block.Height[x0 * 9 + y0]];
|
||||
float h10 = heightTable[block.Height[x1 * 9 + y0]];
|
||||
float h01 = heightTable[block.Height[x0 * 9 + y1]];
|
||||
float h11 = heightTable[block.Height[x1 * 9 + y1]];
|
||||
float hx0 = h00 * (1 - tx) + h10 * tx;
|
||||
float hx1 = h01 * (1 - tx) + h11 * tx;
|
||||
return hx0 * (1 - ty) + hx1 * ty;
|
||||
}
|
||||
|
||||
private void OnUpdate(double dt)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue