feat(headless): hydrate isolated collision worlds
This commit is contained in:
parent
9569dadb57
commit
b6547ff38c
20 changed files with 1960 additions and 101 deletions
|
|
@ -4,6 +4,7 @@ using AcDream.Core.Meshing;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
|
||||
|
||||
namespace AcDream.Content;
|
||||
|
|
@ -16,10 +17,16 @@ namespace AcDream.Content;
|
|||
/// </summary>
|
||||
public static class LandblockPhysicsContentBuilder
|
||||
{
|
||||
public readonly record struct StaticCollisionPublication(
|
||||
int BspOwnerCount,
|
||||
int SetupOwnerCount,
|
||||
int NoCollisionCount);
|
||||
|
||||
public static IReadOnlyList<WorldEntity> HydrateStaticEntities(
|
||||
IDatReaderWriter dats,
|
||||
LoadedLandblock source,
|
||||
Vector3 worldOffset)
|
||||
Vector3 worldOffset,
|
||||
bool includeVisualBounds = true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
|
@ -35,13 +42,12 @@ public static class LandblockPhysicsContentBuilder
|
|||
GfxObj? gfx = dats.Get<GfxObj>(sourceId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
(Vector3 Min, Vector3 Max)? partBounds =
|
||||
GfxObjBounds.Get(gfx);
|
||||
if (partBounds is not null)
|
||||
if (includeVisualBounds
|
||||
&& GfxObjBounds.Get(gfx) is { } partBounds)
|
||||
{
|
||||
bounds.Add(
|
||||
Matrix4x4.Identity,
|
||||
partBounds.Value);
|
||||
partBounds);
|
||||
}
|
||||
meshRefs.Add(new MeshRef(
|
||||
sourceId,
|
||||
|
|
@ -59,13 +65,12 @@ public static class LandblockPhysicsContentBuilder
|
|||
dats.Get<GfxObj>(meshRef.GfxObjId);
|
||||
if (gfx is null)
|
||||
continue;
|
||||
(Vector3 Min, Vector3 Max)? partBounds =
|
||||
GfxObjBounds.Get(gfx);
|
||||
if (partBounds is not null)
|
||||
if (includeVisualBounds
|
||||
&& GfxObjBounds.Get(gfx) is { } partBounds)
|
||||
{
|
||||
bounds.Add(
|
||||
meshRef.PartTransform,
|
||||
partBounds.Value);
|
||||
partBounds);
|
||||
}
|
||||
meshRefs.Add(meshRef);
|
||||
}
|
||||
|
|
@ -94,6 +99,151 @@ public static class LandblockPhysicsContentBuilder
|
|||
return hydrated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hydrates retail's deterministic Region/Scene-generated outdoor
|
||||
/// scenery into the same immutable entity shape consumed by graphical
|
||||
/// and direct hosts. The supplied offset is the host's current
|
||||
/// landblock-relative world origin; DAT placement remains local.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<WorldEntity> HydrateProceduralScenery(
|
||||
IDatReaderWriter dats,
|
||||
LoadedLandblock source,
|
||||
Vector3 worldOffset,
|
||||
ReadOnlySpan<float> heightTable,
|
||||
bool includeVisualBounds = true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
if (heightTable.Length < 256)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retail terrain height table must contain at least 256 entries.",
|
||||
nameof(heightTable));
|
||||
}
|
||||
|
||||
Region? region = dats.Get<Region>(0x13000000u);
|
||||
if (region is null)
|
||||
return [];
|
||||
|
||||
HashSet<int>? buildingCells = null;
|
||||
LandBlockInfo? info = dats.Get<LandBlockInfo>(
|
||||
(source.LandblockId & 0xFFFF0000u) | 0xFFFEu);
|
||||
if (info is not null)
|
||||
{
|
||||
buildingCells = [];
|
||||
foreach (BuildingInfo building in info.Buildings)
|
||||
{
|
||||
int cellX = Math.Clamp(
|
||||
(int)(building.Frame.Origin.X / 24f),
|
||||
0,
|
||||
8);
|
||||
int cellY = Math.Clamp(
|
||||
(int)(building.Frame.Origin.Y / 24f),
|
||||
0,
|
||||
8);
|
||||
buildingCells.Add(cellX * 9 + cellY);
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<SceneryGenerator.ScenerySpawn> spawns =
|
||||
SceneryGenerator.Generate(
|
||||
dats,
|
||||
region,
|
||||
source.Heightmap,
|
||||
source.LandblockId,
|
||||
buildingCells);
|
||||
if (spawns.Count == 0)
|
||||
return [];
|
||||
|
||||
var entities = new List<WorldEntity>(spawns.Count);
|
||||
uint landblockX =
|
||||
(source.LandblockId >> 24) & 0xFFu;
|
||||
uint landblockY =
|
||||
(source.LandblockId >> 16) & 0xFFu;
|
||||
uint sceneryCounter = 0u;
|
||||
foreach (SceneryGenerator.ScenerySpawn spawn in spawns)
|
||||
{
|
||||
var meshRefs = new List<MeshRef>();
|
||||
var bounds = new LocalBoundsAccumulator();
|
||||
Matrix4x4 scale =
|
||||
Matrix4x4.CreateScale(spawn.Scale);
|
||||
if ((spawn.ObjectId & 0xFF000000u) == 0x01000000u)
|
||||
{
|
||||
GfxObj? gfx = dats.Get<GfxObj>(spawn.ObjectId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
if (includeVisualBounds
|
||||
&& GfxObjBounds.Get(gfx) is { } partBounds)
|
||||
{
|
||||
bounds.Add(scale, partBounds);
|
||||
}
|
||||
meshRefs.Add(new MeshRef(spawn.ObjectId, scale));
|
||||
}
|
||||
}
|
||||
else if ((spawn.ObjectId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
Setup? setup = dats.Get<Setup>(spawn.ObjectId);
|
||||
if (setup is not null)
|
||||
{
|
||||
foreach (MeshRef meshRef in SetupMesh.Flatten(setup))
|
||||
{
|
||||
GfxObj? gfx =
|
||||
dats.Get<GfxObj>(meshRef.GfxObjId);
|
||||
if (gfx is null)
|
||||
continue;
|
||||
Matrix4x4 partTransform =
|
||||
meshRef.PartTransform * scale;
|
||||
if (includeVisualBounds
|
||||
&& GfxObjBounds.Get(gfx) is { } partBounds)
|
||||
{
|
||||
bounds.Add(partTransform, partBounds);
|
||||
}
|
||||
meshRefs.Add(new MeshRef(
|
||||
meshRef.GfxObjId,
|
||||
partTransform));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meshRefs.Count == 0)
|
||||
continue;
|
||||
|
||||
float localX = spawn.LocalPosition.X;
|
||||
float localY = spawn.LocalPosition.Y;
|
||||
float groundZ = TerrainSurface.SampleZFromHeightmap(
|
||||
source.Heightmap.Height,
|
||||
heightTable,
|
||||
landblockX,
|
||||
landblockY,
|
||||
localX,
|
||||
localY);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = ProceduralSceneryIdAllocator.Allocate(
|
||||
landblockX,
|
||||
landblockY,
|
||||
ref sceneryCounter),
|
||||
SourceGfxObjOrSetupId = spawn.ObjectId,
|
||||
Position = new Vector3(
|
||||
localX,
|
||||
localY,
|
||||
groundZ + spawn.LocalPosition.Z)
|
||||
+ worldOffset,
|
||||
Rotation = spawn.Rotation,
|
||||
MeshRefs = meshRefs,
|
||||
Scale = spawn.Scale,
|
||||
EffectCellId =
|
||||
TerrainSurface.ComputeOutdoorCellId(
|
||||
source.LandblockId,
|
||||
localX,
|
||||
localY),
|
||||
};
|
||||
if (bounds.TryGet(out Vector3 min, out Vector3 max))
|
||||
entity.SetLocalBounds(min, max);
|
||||
entities.Add(entity);
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
public static PhysicsDatBundle BuildDatBundle(
|
||||
IDatReaderWriter dats,
|
||||
uint landblockId,
|
||||
|
|
@ -247,6 +397,353 @@ public static class LandblockPhysicsContentBuilder
|
|||
envCellIds);
|
||||
}
|
||||
|
||||
public static TerrainSurface BuildTerrainSurface(
|
||||
LoadedLandblock landblock,
|
||||
ReadOnlySpan<float> heightTable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(landblock);
|
||||
if (heightTable.Length < 256)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retail terrain height table must contain at least 256 entries.",
|
||||
nameof(heightTable));
|
||||
}
|
||||
|
||||
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
|
||||
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
|
||||
var terrainBytes = new byte[81];
|
||||
for (int index = 0; index < terrainBytes.Length; index++)
|
||||
{
|
||||
terrainBytes[index] =
|
||||
(byte)(ushort)landblock.Heightmap.Terrain[index];
|
||||
}
|
||||
return new TerrainSurface(
|
||||
landblock.Heightmap.Height,
|
||||
heightTable,
|
||||
landblockX,
|
||||
landblockY,
|
||||
terrainBytes);
|
||||
}
|
||||
|
||||
public static void PublishPreparedCells(
|
||||
PhysicsDataCache cache,
|
||||
LoadedLandblock landblock,
|
||||
LandblockCollisionBuild collisions,
|
||||
Vector3 origin,
|
||||
ICollection<CellSurface> cellSurfaces,
|
||||
ICollection<PortalPlane> portalPlanes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cache);
|
||||
ArgumentNullException.ThrowIfNull(landblock);
|
||||
ArgumentNullException.ThrowIfNull(collisions);
|
||||
ArgumentNullException.ThrowIfNull(cellSurfaces);
|
||||
ArgumentNullException.ThrowIfNull(portalPlanes);
|
||||
|
||||
PhysicsDatBundle dats =
|
||||
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
uint count = dats.Info?.NumCells ?? 0u;
|
||||
for (uint offset = 0; offset < count; offset++)
|
||||
{
|
||||
uint envCellId =
|
||||
(landblock.LandblockId & 0xFFFF0000u)
|
||||
| (0x0100u + offset);
|
||||
if (!dats.EnvCells.TryGetValue(
|
||||
envCellId,
|
||||
out EnvCell? envCell)
|
||||
|| !collisions.CellStructures.TryGetValue(
|
||||
envCellId,
|
||||
out FlatCellStructureCollisionAsset? structure)
|
||||
|| !collisions.EnvCells.TryGetValue(
|
||||
envCellId,
|
||||
out FlatEnvCellTopology? topology))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Quaternion rotation = envCell.Position.Orientation;
|
||||
Vector3 cellOriginWorld =
|
||||
envCell.Position.Origin + origin;
|
||||
Matrix4x4 transform =
|
||||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
cache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
transform,
|
||||
structure,
|
||||
topology);
|
||||
|
||||
FlatPolygonTable portalPolygons =
|
||||
structure.PortalPolygons;
|
||||
for (int portalIndex = 0;
|
||||
portalIndex < topology.Portals.Length;
|
||||
portalIndex++)
|
||||
{
|
||||
FlatEnvCellPortal portal =
|
||||
topology.Portals[portalIndex];
|
||||
if ((uint)portal.PolygonIndex
|
||||
>= (uint)portalPolygons.Polygons.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FlatCollisionPolygon polygon =
|
||||
portalPolygons.Polygons[portal.PolygonIndex];
|
||||
if (polygon.VertexRange.Count < 3)
|
||||
continue;
|
||||
|
||||
var vertices =
|
||||
new Vector3[polygon.VertexRange.Count];
|
||||
for (int index = 0; index < vertices.Length; index++)
|
||||
{
|
||||
Vector3 local = portalPolygons.Vertices[
|
||||
polygon.VertexRange.Start + index];
|
||||
vertices[index] =
|
||||
Vector3.Transform(local, rotation)
|
||||
+ cellOriginWorld;
|
||||
}
|
||||
portalPlanes.Add(PortalPlane.FromVertices(
|
||||
vertices.AsSpan(),
|
||||
portal.OtherCellId,
|
||||
envCellId & 0xFFFFu,
|
||||
portal.Flags));
|
||||
}
|
||||
|
||||
cellSurfaces.Add(new CellSurface(
|
||||
envCellId,
|
||||
structure.PhysicsBsp.PolygonTable,
|
||||
rotation,
|
||||
cellOriginWorld));
|
||||
}
|
||||
}
|
||||
|
||||
public static void CacheBuildings(
|
||||
PhysicsDataCache cache,
|
||||
LoadedLandblock landblock,
|
||||
TerrainSurface terrain,
|
||||
Vector3 origin)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cache);
|
||||
ArgumentNullException.ThrowIfNull(landblock);
|
||||
ArgumentNullException.ThrowIfNull(terrain);
|
||||
PhysicsDatBundle dats =
|
||||
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
if (dats.Info is not { } info)
|
||||
return;
|
||||
|
||||
uint prefix = landblock.LandblockId & 0xFFFF0000u;
|
||||
foreach (BuildingInfo building in info.Buildings)
|
||||
{
|
||||
var portals =
|
||||
new List<BldPortalInfo>(building.Portals.Count);
|
||||
foreach (var portal in building.Portals)
|
||||
{
|
||||
portals.Add(new BldPortalInfo(
|
||||
prefix | (uint)portal.OtherCellId,
|
||||
unchecked((short)portal.OtherPortalId),
|
||||
(ushort)portal.Flags));
|
||||
}
|
||||
|
||||
Vector3 buildingOrigin = building.Frame.Origin + origin;
|
||||
Matrix4x4 transform =
|
||||
Matrix4x4.CreateFromQuaternion(
|
||||
building.Frame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(buildingOrigin);
|
||||
uint landcellId = prefix
|
||||
| terrain.ComputeOutdoorCellId(
|
||||
building.Frame.Origin.X,
|
||||
building.Frame.Origin.Y);
|
||||
uint shellPartZero = building.ModelId;
|
||||
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
dats.Setups.TryGetValue(
|
||||
building.ModelId,
|
||||
out Setup? setup);
|
||||
shellPartZero =
|
||||
setup is not null && setup.Parts.Count > 0
|
||||
? setup.Parts[0]
|
||||
: 0u;
|
||||
}
|
||||
cache.CacheBuilding(
|
||||
landcellId,
|
||||
portals,
|
||||
transform,
|
||||
shellPartZero);
|
||||
}
|
||||
}
|
||||
|
||||
public static void CachePreparedObjects(
|
||||
PhysicsDataCache cache,
|
||||
LandblockCollisionBuild collisions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cache);
|
||||
ArgumentNullException.ThrowIfNull(collisions);
|
||||
foreach ((uint id, FlatGfxObjCollisionAsset asset) in
|
||||
collisions.GfxObjs)
|
||||
{
|
||||
cache.CacheGfxObj(id, asset);
|
||||
}
|
||||
foreach ((uint id, FlatSetupCollision setup) in
|
||||
collisions.Setups)
|
||||
{
|
||||
cache.CacheSetup(id, setup);
|
||||
}
|
||||
}
|
||||
|
||||
public static StaticCollisionPublication PublishStaticCollision(
|
||||
PhysicsEngine engine,
|
||||
PhysicsDataCache cache,
|
||||
LoadedLandblock landblock,
|
||||
LandblockCollisionBuild collisions,
|
||||
Vector3 origin)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(engine);
|
||||
ArgumentNullException.ThrowIfNull(cache);
|
||||
ArgumentNullException.ThrowIfNull(landblock);
|
||||
ArgumentNullException.ThrowIfNull(collisions);
|
||||
|
||||
int bspOwners = 0;
|
||||
int setupOwners = 0;
|
||||
int noCollision = 0;
|
||||
foreach (WorldEntity entity in landblock.Entities)
|
||||
{
|
||||
if (entity.IsBuildingShell)
|
||||
continue;
|
||||
|
||||
IReadOnlyList<ShadowShape> bspShapes =
|
||||
ShadowShapeBuilder.FromLandblockBspParts(
|
||||
entity.MeshRefs,
|
||||
entity.IsBuildingShell,
|
||||
cache.GetGfxObj);
|
||||
if (bspShapes.Count > 0)
|
||||
{
|
||||
engine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
bspShapes,
|
||||
0u,
|
||||
EntityCollisionFlags.None,
|
||||
worldOffsetX: origin.X,
|
||||
worldOffsetY: origin.Y,
|
||||
landblockId: landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
bspOwners++;
|
||||
continue;
|
||||
}
|
||||
|
||||
FlatSetupCollision? setup =
|
||||
cache.GetFlatSetup(
|
||||
entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null)
|
||||
{
|
||||
noCollision++;
|
||||
continue;
|
||||
}
|
||||
|
||||
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
||||
var setupShapes = new List<ShadowShape>();
|
||||
for (int index = 0;
|
||||
index < setup.Cylinders.Length;
|
||||
index++)
|
||||
{
|
||||
FlatCollisionCylinder cylinder =
|
||||
setup.Cylinders[index];
|
||||
float radius = cylinder.Radius * scale;
|
||||
float height = (cylinder.Height > 0f
|
||||
? cylinder.Height
|
||||
: cylinder.Radius * 4f) * scale;
|
||||
if (radius <= 0f)
|
||||
continue;
|
||||
setupShapes.Add(new ShadowShape(
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
cylinder.Origin * scale,
|
||||
Quaternion.Identity,
|
||||
scale,
|
||||
ShadowCollisionType.Cylinder,
|
||||
radius,
|
||||
height));
|
||||
}
|
||||
|
||||
if (setup.Cylinders.Length == 0)
|
||||
{
|
||||
for (int index = 0;
|
||||
index < setup.Spheres.Length;
|
||||
index++)
|
||||
{
|
||||
FlatCollisionSphere sphere = setup.Spheres[index];
|
||||
if (sphere.Radius <= 0f)
|
||||
continue;
|
||||
float radius = sphere.Radius * scale;
|
||||
Vector3 localOffset = sphere.Origin * scale;
|
||||
Vector3 localBaseOffset = localOffset
|
||||
+ Vector3.Transform(
|
||||
-Vector3.UnitZ * radius,
|
||||
Quaternion.Inverse(entity.Rotation));
|
||||
setupShapes.Add(new ShadowShape(
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
localBaseOffset,
|
||||
Quaternion.Identity,
|
||||
scale,
|
||||
ShadowCollisionType.Cylinder,
|
||||
radius,
|
||||
radius * 2f));
|
||||
}
|
||||
}
|
||||
|
||||
if (setup.Cylinders.Length == 0
|
||||
&& setup.Spheres.Length == 0
|
||||
&& setup.Radius > 0f)
|
||||
{
|
||||
float radius = setup.Radius * scale;
|
||||
float height = (setup.Height > 0f
|
||||
? setup.Height
|
||||
: setup.Radius * 2f) * scale;
|
||||
setupShapes.Add(new ShadowShape(
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity,
|
||||
scale,
|
||||
ShadowCollisionType.Cylinder,
|
||||
radius,
|
||||
height));
|
||||
}
|
||||
|
||||
if (setupShapes.Count == 0)
|
||||
{
|
||||
noCollision++;
|
||||
continue;
|
||||
}
|
||||
engine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
setupShapes,
|
||||
0u,
|
||||
EntityCollisionFlags.None,
|
||||
worldOffsetX: origin.X,
|
||||
worldOffsetY: origin.Y,
|
||||
landblockId: landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
setupOwners++;
|
||||
}
|
||||
|
||||
uint[] reflood =
|
||||
engine.ShadowObjects.CaptureRefloodOwnersForLandblock(
|
||||
landblock.LandblockId);
|
||||
foreach (uint ownerId in reflood)
|
||||
{
|
||||
engine.ShadowObjects.RefloodOwnerForLandblock(
|
||||
ownerId,
|
||||
landblock.LandblockId);
|
||||
}
|
||||
return new StaticCollisionPublication(
|
||||
bspOwners,
|
||||
setupOwners,
|
||||
noCollision);
|
||||
}
|
||||
|
||||
private static T Require<T>(
|
||||
PreparedCollisionReadResult<T> result,
|
||||
string kind,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue