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
194
src/AcDream.Core/World/SceneryGenerator.cs
Normal file
194
src/AcDream.Core/World/SceneryGenerator.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System.Numerics;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.World;
|
||||
|
||||
/// <summary>
|
||||
/// Procedural scenery placement for a landblock. AC encodes "sparse decoration"
|
||||
/// (trees, bushes, rocks, fences, small props) NOT as explicit Stab entries on
|
||||
/// the LandBlockInfo but as per-terrain-vertex Scene/ObjectDesc references in
|
||||
/// the Region dat, placed pseudo-randomly via deterministic LCG math keyed on
|
||||
/// the vertex's global cell coordinates. Without this generator, any landblock
|
||||
/// rendered from dats is missing all of its natural scenery.
|
||||
///
|
||||
/// Algorithm ported from ACViewer's Physics/Common/Landblock.get_land_scenes()
|
||||
/// which is itself a port of the original AC client's scenery walker. We
|
||||
/// deliberately skip the slope/road/building-overlap checks the original does;
|
||||
/// those prevent scenery from floating in roads or clipping buildings but
|
||||
/// require walkable-polygon lookups that we don't yet have. Accepting visual
|
||||
/// artifacts (trees inside roads, scenery clipping buildings) for a first pass
|
||||
/// and deferring the filters to a later phase.
|
||||
/// </summary>
|
||||
public static class SceneryGenerator
|
||||
{
|
||||
// AC landblock geometry — matches LandblockMesh.
|
||||
private const int VerticesPerSide = 9;
|
||||
private const float CellSize = 24.0f;
|
||||
private const float LandblockSize = 192.0f; // 8 cells * 24 units
|
||||
|
||||
public readonly record struct ScenerySpawn(
|
||||
uint ObjectId, // GfxObj or Setup id
|
||||
Vector3 LocalPosition, // landblock-local world units
|
||||
Quaternion Rotation,
|
||||
float Scale);
|
||||
|
||||
/// <summary>
|
||||
/// Generate all scenery entries for one landblock. Uses the bit-packed
|
||||
/// TerrainInfo Type (bits 2-6) and Scenery (bits 11-15) fields to index into
|
||||
/// Region.TerrainInfo.TerrainTypes[type].SceneTypes[scenery] → a SceneInfo
|
||||
/// index into Region.SceneInfo.SceneTypes[sceneInfo].Scenes. Each cell picks
|
||||
/// one scene via a pseudo-random hash of the cell's global coordinates, then
|
||||
/// iterates the scene's ObjectDesc entries with per-object frequency rolls.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ScenerySpawn> Generate(
|
||||
DatCollection dats,
|
||||
Region region,
|
||||
LandBlock block,
|
||||
uint landblockId)
|
||||
{
|
||||
var result = new List<ScenerySpawn>();
|
||||
|
||||
if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null)
|
||||
return result;
|
||||
|
||||
uint blockX = (landblockId >> 24) * 8; // 8 cells per landblock
|
||||
uint blockY = ((landblockId >> 16) & 0xFFu) * 8;
|
||||
|
||||
// The original iterates Terrain[0..80] — 81 vertices of a 9x9 grid.
|
||||
// The heightmap is packed x-major (Height[x*9+y]), so we match that here.
|
||||
for (int x = 0; x < VerticesPerSide; x++)
|
||||
{
|
||||
for (int y = 0; y < VerticesPerSide; y++)
|
||||
{
|
||||
int i = x * VerticesPerSide + y;
|
||||
ushort raw = block.Terrain[i];
|
||||
|
||||
uint terrainType = (uint)((raw >> 2) & 0x1F); // bits 2-6
|
||||
uint sceneType = (uint)((raw >> 11) & 0x1F); // bits 11-15
|
||||
|
||||
if (terrainType >= region.TerrainInfo.TerrainTypes.Count) continue;
|
||||
var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes;
|
||||
if (sceneType >= sceneTypeList.Count) continue;
|
||||
|
||||
uint sceneInfo = sceneTypeList[(int)sceneType];
|
||||
if (sceneInfo >= region.SceneInfo.SceneTypes.Count) continue;
|
||||
|
||||
var scenes = region.SceneInfo.SceneTypes[(int)sceneInfo].Scenes;
|
||||
if (scenes.Count == 0) continue;
|
||||
|
||||
uint cellX = (uint)x;
|
||||
uint cellY = (uint)y;
|
||||
uint globalCellX = cellX + blockX;
|
||||
uint globalCellY = cellY + blockY;
|
||||
|
||||
// Scene-selection hash: picks one scene from the terrain's scene list.
|
||||
uint cellMat = globalCellY * (712977289u * globalCellX + 1813693831u)
|
||||
- 1109124029u * globalCellX + 2139937281u;
|
||||
double offset = cellMat * 2.3283064e-10;
|
||||
int sceneIdx = (int)(scenes.Count * offset);
|
||||
if (sceneIdx >= scenes.Count || sceneIdx < 0) sceneIdx = 0;
|
||||
|
||||
uint sceneId = (uint)scenes[sceneIdx];
|
||||
var scene = dats.Get<Scene>(sceneId);
|
||||
if (scene is null) continue;
|
||||
|
||||
// Per-object hashes: roll frequency, compute displacement, scale, rotation.
|
||||
uint cellXMat = unchecked(0u - 1109124029u * globalCellX);
|
||||
uint cellYMat = 1813693831u * globalCellY;
|
||||
uint cellMat2 = 1360117743u * globalCellX * globalCellY + 1888038839u;
|
||||
|
||||
for (uint j = 0; j < scene.Objects.Count; j++)
|
||||
{
|
||||
var obj = scene.Objects[(int)j];
|
||||
if (obj.WeenieObj != 0) continue; // Weenie entries are dynamic spawns, not static scenery
|
||||
|
||||
double noise = unchecked((uint)(cellXMat + cellYMat - cellMat2 * (23399u + j))) * 2.3283064e-10;
|
||||
if (noise >= obj.Frequency) continue;
|
||||
|
||||
// Displacement: pseudo-random offset within the cell.
|
||||
var localPos = DisplaceObject(obj, globalCellX, globalCellY, j);
|
||||
|
||||
float lx = cellX * CellSize + localPos.X;
|
||||
float ly = cellY * CellSize + localPos.Y;
|
||||
|
||||
if (lx < 0 || ly < 0 || lx >= LandblockSize || ly >= LandblockSize)
|
||||
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
|
||||
|
||||
// Rotation
|
||||
Quaternion rotation = Quaternion.Identity;
|
||||
if (obj.MaxRotation > 0)
|
||||
{
|
||||
double rotNoise = unchecked((uint)(1813693831u * globalCellY
|
||||
- (j + 63127u) * (1360117743u * globalCellY * globalCellX + 1888038839u)
|
||||
- 1109124029u * globalCellX)) * 2.3283064e-10;
|
||||
float degrees = (float)(rotNoise * obj.MaxRotation);
|
||||
float radians = degrees * MathF.PI / 180f;
|
||||
rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, radians);
|
||||
}
|
||||
|
||||
// Scale
|
||||
float scale;
|
||||
if (obj.MinScale == obj.MaxScale)
|
||||
{
|
||||
scale = obj.MaxScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
double scaleNoise = unchecked((uint)(1813693831u * globalCellY
|
||||
- (j + 32593u) * (1360117743u * globalCellY * globalCellX + 1888038839u)
|
||||
- 1109124029u * globalCellX)) * 2.3283064e-10;
|
||||
scale = (float)(Math.Pow(obj.MaxScale / obj.MinScale, scaleNoise) * obj.MinScale);
|
||||
}
|
||||
if (scale <= 0) scale = 1f;
|
||||
|
||||
result.Add(new ScenerySpawn(
|
||||
ObjectId: obj.ObjectId,
|
||||
LocalPosition: new Vector3(lx, ly, lz),
|
||||
Rotation: rotation,
|
||||
Scale: scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pseudo-random displacement within a cell for a scenery object. Returns a
|
||||
/// Vector3 in local cell-offset space (the caller adds it to the cell corner
|
||||
/// to get landblock-local position).
|
||||
/// </summary>
|
||||
private static Vector3 DisplaceObject(ObjectDesc obj, uint ix, uint iy, uint iq)
|
||||
{
|
||||
float x, y;
|
||||
var baseLoc = obj.BaseLoc.Origin;
|
||||
|
||||
if (obj.DisplaceX <= 0)
|
||||
x = baseLoc.X;
|
||||
else
|
||||
x = (float)(unchecked((uint)(1813693831u * iy - (iq + 45773u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
|
||||
* 2.3283064e-10 * obj.DisplaceX + baseLoc.X);
|
||||
|
||||
if (obj.DisplaceY <= 0)
|
||||
y = baseLoc.Y;
|
||||
else
|
||||
y = (float)(unchecked((uint)(1813693831u * iy - (iq + 72719u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
|
||||
* 2.3283064e-10 * obj.DisplaceY + baseLoc.Y);
|
||||
|
||||
float z = baseLoc.Z;
|
||||
|
||||
double quadrant = unchecked((uint)(1813693831u * iy - ix * (1870387557u * iy + 1109124029u) - 402451965u)) * 2.3283064e-10;
|
||||
|
||||
if (quadrant >= 0.75) return new Vector3(y, -x, z);
|
||||
if (quadrant >= 0.5) return new Vector3(-x, -y, z);
|
||||
if (quadrant >= 0.25) return new Vector3(-y, x, z);
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue