perf(streaming): cursor publication across frame budgets
This commit is contained in:
parent
bb16f74fd4
commit
98f1ac8934
32 changed files with 2215 additions and 823 deletions
|
|
@ -2,6 +2,7 @@ using System.Diagnostics;
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
|
|
@ -15,15 +16,45 @@ public sealed class LandblockPhysicsPublication
|
|||
internal LandblockPhysicsPublication(
|
||||
object owner,
|
||||
LandblockBuild build,
|
||||
Vector3 origin)
|
||||
Vector3 origin,
|
||||
uint currentCellId,
|
||||
BuildingInfo[] buildings,
|
||||
uint[] priorStaticOwnerIds)
|
||||
{
|
||||
Owner = owner;
|
||||
Build = build;
|
||||
Origin = origin;
|
||||
CurrentCellId = currentCellId;
|
||||
Buildings = buildings;
|
||||
PriorStaticOwnerIds = priorStaticOwnerIds;
|
||||
}
|
||||
|
||||
internal object Owner { get; }
|
||||
internal LandblockBuild Build { get; }
|
||||
internal uint CurrentCellId { get; }
|
||||
internal BuildingInfo[] Buildings { get; }
|
||||
internal uint[] PriorStaticOwnerIds { get; }
|
||||
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
|
||||
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
|
||||
internal int PreparationCursor { get; set; }
|
||||
internal bool PreparationCommitted { get; set; }
|
||||
internal bool PriorCacheRemoved { get; set; }
|
||||
internal TerrainSurface? TerrainSurface { get; set; }
|
||||
internal List<CellSurface> CellSurfaces { get; } = new();
|
||||
internal List<PortalPlane> PortalPlanes { get; } = new();
|
||||
internal uint CellCursor { get; set; }
|
||||
internal int BuildingCursor { get; set; }
|
||||
internal bool BaseCommitted { get; set; }
|
||||
internal int GfxCursor { get; set; }
|
||||
internal int PriorStaticCursor { get; set; }
|
||||
internal int StaticCursor { get; set; }
|
||||
internal int BspOwnerCount { get; set; }
|
||||
internal int CylinderOwnerCount { get; set; }
|
||||
internal int NoCollisionCount { get; set; }
|
||||
internal int SceneryTried { get; set; }
|
||||
internal uint[]? RefloodOwnerIds { get; set; }
|
||||
internal int RefloodCursor { get; set; }
|
||||
internal bool RefloodCommitted { get; set; }
|
||||
internal bool BeginCommitted { get; set; }
|
||||
internal bool CompletionCommitted { get; set; }
|
||||
|
||||
|
|
@ -142,6 +173,21 @@ public sealed class LandblockPhysicsPublisher
|
|||
/// </summary>
|
||||
public LandblockPhysicsPublication PreparePublication(
|
||||
LandblockRenderPublication renderPublication)
|
||||
{
|
||||
LandblockPhysicsPublication publication =
|
||||
CreatePublication(renderPublication);
|
||||
while (!AdvancePreparationOne(publication))
|
||||
{
|
||||
}
|
||||
return publication;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the immutable physics root without walking the entity suffix.
|
||||
/// The concrete pipeline advances that walk under its frame meter.
|
||||
/// </summary>
|
||||
internal LandblockPhysicsPublication CreatePublication(
|
||||
LandblockRenderPublication renderPublication)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(renderPublication);
|
||||
LandblockBuild build = renderPublication.Build;
|
||||
|
|
@ -149,10 +195,50 @@ public sealed class LandblockPhysicsPublisher
|
|||
if (!IsFinite(origin))
|
||||
throw new ArgumentOutOfRangeException(nameof(renderPublication));
|
||||
|
||||
PhysicsDatBundle datBundle =
|
||||
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
||||
?? Array.Empty<BuildingInfo>();
|
||||
return new LandblockPhysicsPublication(
|
||||
_receiptOwner,
|
||||
build,
|
||||
origin);
|
||||
origin,
|
||||
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
|
||||
buildings,
|
||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||
build.Landblock.LandblockId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances one entity's stable GfxObj-index preparation. No external
|
||||
/// physics state is mutated until this cursor and the static-validation
|
||||
/// cursor both complete.
|
||||
/// </summary>
|
||||
internal bool AdvancePreparationOne(
|
||||
LandblockPhysicsPublication publication)
|
||||
{
|
||||
ValidateReceipt(publication);
|
||||
if (publication.PreparationCommitted)
|
||||
return true;
|
||||
|
||||
IReadOnlyList<WorldEntity> entities =
|
||||
publication.Build.Landblock.Entities;
|
||||
if (publication.PreparationCursor < entities.Count)
|
||||
{
|
||||
WorldEntity entity = entities[publication.PreparationCursor];
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
{
|
||||
uint id = entity.MeshRefs[i].GfxObjId;
|
||||
if ((id & 0xFF000000u) == 0x01000000u)
|
||||
publication.GfxObjectIdSet.Add(id);
|
||||
}
|
||||
publication.PreparationCursor++;
|
||||
return false;
|
||||
}
|
||||
|
||||
publication.GfxObjectIds = publication.GfxObjectIdSet.ToArray();
|
||||
publication.PreparationCommitted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -160,10 +246,26 @@ public sealed class LandblockPhysicsPublisher
|
|||
/// retained receipt.
|
||||
/// </summary>
|
||||
public void BeginPublication(LandblockPhysicsPublication publication)
|
||||
{
|
||||
ValidateReceipt(publication);
|
||||
if (!publication.PreparationCommitted)
|
||||
throw new InvalidOperationException(
|
||||
"Physics publication cannot begin before preparation commits.");
|
||||
while (!AdvanceBeginOne(publication))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances one exact physics-prefix operation: prior-cache withdrawal,
|
||||
/// terrain construction, one EnvCell, one building, or the final canonical
|
||||
/// physics-engine replacement. All arrays and cursors live on the receipt.
|
||||
/// </summary>
|
||||
internal bool AdvanceBeginOne(LandblockPhysicsPublication publication)
|
||||
{
|
||||
ValidateReceipt(publication);
|
||||
if (publication.BeginCommitted)
|
||||
return;
|
||||
return true;
|
||||
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
LandblockBuild build = publication.Build;
|
||||
|
|
@ -171,157 +273,76 @@ public sealed class LandblockPhysicsPublisher
|
|||
LoadedLandblock landblock = build.Landblock;
|
||||
PhysicsDatBundle datBundle =
|
||||
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
uint currentCellId = _physicsDataCache.CellGraph.CurrCell?.Id ?? 0u;
|
||||
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
|
||||
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
|
||||
|
||||
// A publication is an exact replacement for this landblock prefix.
|
||||
// CacheCellStruct/CacheBuilding intentionally use retail first-wins
|
||||
// semantics within one pass, so retire the prior pass before adding
|
||||
// the replacement. Adjacent prefixes remain untouched.
|
||||
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
|
||||
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
||||
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
|
||||
landblock.LandblockId);
|
||||
|
||||
// TerrainInfo.Type occupies bits 2-6. The low byte retains those bits
|
||||
// plus Road (bits 0-1), which TerrainSurface masks independently.
|
||||
var terrainBytes = new byte[81];
|
||||
for (int i = 0; i < terrainBytes.Length; i++)
|
||||
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
|
||||
|
||||
var terrainSurface = new TerrainSurface(
|
||||
landblock.Heightmap.Height,
|
||||
_heightTable,
|
||||
landblockX,
|
||||
landblockY,
|
||||
terrainBytes);
|
||||
var cellSurfaces = new List<CellSurface>();
|
||||
var portalPlanes = new List<PortalPlane>();
|
||||
|
||||
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo = datBundle.Info;
|
||||
if (landblockInfo is not null && landblockInfo.NumCells > 0)
|
||||
|
||||
if (!publication.PriorCacheRemoved)
|
||||
{
|
||||
uint firstCellId =
|
||||
(landblock.LandblockId & 0xFFFF0000u) | 0x0100u;
|
||||
for (uint offset = 0; offset < landblockInfo.NumCells; offset++)
|
||||
// CacheCellStruct/CacheBuilding use first-wins semantics within one
|
||||
// publication, so the replacement pass starts with one exact
|
||||
// landblock-scoped withdrawal.
|
||||
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
|
||||
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
||||
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
|
||||
landblock.LandblockId);
|
||||
publication.PriorCacheRemoved = true;
|
||||
}
|
||||
else if (publication.TerrainSurface is null)
|
||||
{
|
||||
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
|
||||
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
|
||||
var terrainBytes = new byte[81];
|
||||
for (int i = 0; i < terrainBytes.Length; i++)
|
||||
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
|
||||
publication.TerrainSurface = new TerrainSurface(
|
||||
landblock.Heightmap.Height,
|
||||
_heightTable,
|
||||
landblockX,
|
||||
landblockY,
|
||||
terrainBytes);
|
||||
}
|
||||
else if (landblockInfo is not null
|
||||
&& publication.CellCursor < landblockInfo.NumCells)
|
||||
{
|
||||
PublishCell(publication, datBundle, publication.CellCursor);
|
||||
publication.CellCursor++;
|
||||
}
|
||||
else if (publication.BuildingCursor < publication.Buildings.Length)
|
||||
{
|
||||
PublishBuilding(
|
||||
landblock,
|
||||
datBundle,
|
||||
publication.TerrainSurface,
|
||||
origin,
|
||||
publication.Buildings[publication.BuildingCursor]);
|
||||
publication.BuildingCursor++;
|
||||
}
|
||||
else if (!publication.BaseCommitted)
|
||||
{
|
||||
_physicsEngine.AddLandblock(
|
||||
landblock.LandblockId,
|
||||
publication.TerrainSurface,
|
||||
publication.CellSurfaces,
|
||||
publication.PortalPlanes,
|
||||
origin.X,
|
||||
origin.Y);
|
||||
if ((publication.CurrentCellId & 0xFFFF0000u)
|
||||
== (landblock.LandblockId & 0xFFFF0000u))
|
||||
{
|
||||
uint envCellId = firstCellId + offset;
|
||||
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell))
|
||||
continue;
|
||||
if (envCell.EnvironmentId == 0)
|
||||
continue;
|
||||
if (!datBundle.Environments.TryGetValue(
|
||||
0x0D000000u | envCell.EnvironmentId,
|
||||
out var environment))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!environment.Cells.TryGetValue(
|
||||
envCell.CellStructure,
|
||||
out var cellStruct))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Quaternion rotation = envCell.Position.Orientation;
|
||||
Vector3 cellOriginWorld = envCell.Position.Origin + origin;
|
||||
Matrix4x4 physicsCellTransform =
|
||||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
|
||||
_physicsDataCache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
cellStruct,
|
||||
physicsCellTransform);
|
||||
|
||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||
cellStruct.VertexArray.Vertices.Count);
|
||||
foreach ((ushort vertexId, var vertex) in
|
||||
cellStruct.VertexArray.Vertices)
|
||||
{
|
||||
Vector3 worldPosition =
|
||||
Vector3.Transform(vertex.Origin, rotation)
|
||||
+ cellOriginWorld;
|
||||
worldVertices[(ushort)vertexId] = worldPosition;
|
||||
}
|
||||
|
||||
var polygonVertexIds = new List<List<short>>(
|
||||
cellStruct.PhysicsPolygons.Count);
|
||||
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
|
||||
{
|
||||
var vertexIds = new List<short>(polygon.VertexIds.Count);
|
||||
foreach (short vertexId in polygon.VertexIds)
|
||||
vertexIds.Add(vertexId);
|
||||
polygonVertexIds.Add(vertexIds);
|
||||
}
|
||||
cellSurfaces.Add(new CellSurface(
|
||||
envCellId,
|
||||
worldVertices,
|
||||
polygonVertexIds));
|
||||
|
||||
// CellPortal.PolygonId indexes rendering Polygons, not
|
||||
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
|
||||
foreach (var portal in envCell.CellPortals)
|
||||
{
|
||||
if (!cellStruct.Polygons.TryGetValue(
|
||||
portal.PolygonId,
|
||||
out var polygon)
|
||||
|| polygon.VertexIds.Count < 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var portalVertices = new Vector3[polygon.VertexIds.Count];
|
||||
bool allFound = true;
|
||||
for (int index = 0; index < polygon.VertexIds.Count; index++)
|
||||
{
|
||||
if (!worldVertices.TryGetValue(
|
||||
(ushort)polygon.VertexIds[index],
|
||||
out portalVertices[index]))
|
||||
{
|
||||
allFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allFound)
|
||||
continue;
|
||||
|
||||
portalPlanes.Add(PortalPlane.FromVertices(
|
||||
portalVertices.AsSpan(),
|
||||
portal.OtherCellId,
|
||||
envCellId & 0xFFFFu,
|
||||
(ushort)portal.Flags));
|
||||
}
|
||||
_physicsEngine.UpdatePlayerCurrCell(publication.CurrentCellId);
|
||||
}
|
||||
publication.BaseCommitted = true;
|
||||
}
|
||||
|
||||
int buildingCount = PublishBuildings(
|
||||
landblock,
|
||||
datBundle,
|
||||
landblockInfo,
|
||||
terrainSurface,
|
||||
origin);
|
||||
_physicsEngine.AddLandblock(
|
||||
landblock.LandblockId,
|
||||
terrainSurface,
|
||||
cellSurfaces,
|
||||
portalPlanes,
|
||||
origin.X,
|
||||
origin.Y);
|
||||
if ((currentCellId & 0xFFFF0000u)
|
||||
== (landblock.LandblockId & 0xFFFF0000u))
|
||||
else
|
||||
{
|
||||
_physicsEngine.UpdatePlayerCurrCell(currentCellId);
|
||||
_cellSurfaceCount += publication.CellSurfaces.Count;
|
||||
_portalPlaneCount += publication.PortalPlanes.Count;
|
||||
_buildingCount += publication.Buildings.Length;
|
||||
publication.BeginCommitted = true;
|
||||
_beginCount++;
|
||||
}
|
||||
|
||||
_cellSurfaceCount += cellSurfaces.Count;
|
||||
_portalPlaneCount += portalPlanes.Count;
|
||||
_buildingCount += buildingCount;
|
||||
publication.BeginCommitted = true;
|
||||
_beginCount++;
|
||||
_basePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||
return publication.BeginCommitted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -338,209 +359,93 @@ public sealed class LandblockPhysicsPublisher
|
|||
if (!publication.BeginCommitted)
|
||||
throw new InvalidOperationException(
|
||||
"Physics publication cannot complete before its prefix commits.");
|
||||
if (publication.CompletionCommitted)
|
||||
return;
|
||||
while (!AdvanceCompleteOne(publication, beforeStaticCollision))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
long completedStarted = Stopwatch.GetTimestamp();
|
||||
/// <summary>
|
||||
/// Advances one exact physics-suffix operation: one GfxObj cache entry,
|
||||
/// prior static withdrawal, one static light/collision entity, or reflood.
|
||||
/// </summary>
|
||||
internal bool AdvanceCompleteOne(
|
||||
LandblockPhysicsPublication publication,
|
||||
Action<WorldEntity>? beforeStaticCollision = null)
|
||||
{
|
||||
ValidateReceipt(publication);
|
||||
if (!publication.BeginCommitted)
|
||||
throw new InvalidOperationException(
|
||||
"Physics publication cannot complete before its prefix commits.");
|
||||
if (publication.CompletionCommitted)
|
||||
return true;
|
||||
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
LoadedLandblock landblock = publication.Build.Landblock;
|
||||
PhysicsDatBundle datBundle =
|
||||
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
|
||||
long cacheStarted = Stopwatch.GetTimestamp();
|
||||
foreach (WorldEntity entity in landblock.Entities)
|
||||
if (publication.GfxCursor < publication.GfxObjectIds.Length)
|
||||
{
|
||||
foreach (MeshRef meshRef in entity.MeshRefs)
|
||||
{
|
||||
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u)
|
||||
continue;
|
||||
if (datBundle.GfxObjs.TryGetValue(meshRef.GfxObjId, out var gfx))
|
||||
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
|
||||
}
|
||||
long cacheStarted = Stopwatch.GetTimestamp();
|
||||
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
||||
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
||||
_physicsDataCache.CacheGfxObj(gfxObjectId, gfx);
|
||||
publication.GfxCursor++;
|
||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||
}
|
||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||
|
||||
// The immutable entity list is an exact Near snapshot. Retire every
|
||||
// static owner created by the prior snapshot before registering this
|
||||
// one; RegisterMultiPart can replace matching IDs but cannot discover
|
||||
// owners omitted by a ForceReload/reapply. Logical-owner removal also
|
||||
// clears footprints flooded across an adjacent landblock seam.
|
||||
_physicsEngine.ShadowObjects.DeregisterStaticOwnersForLandblock(
|
||||
landblock.LandblockId);
|
||||
|
||||
int landblockBspCount = 0;
|
||||
int landblockCylinderCount = 0;
|
||||
int landblockNoCollisionCount = 0;
|
||||
int sceneryTried = 0;
|
||||
foreach (WorldEntity entity in landblock.Entities)
|
||||
else if (publication.PriorStaticCursor
|
||||
< publication.PriorStaticOwnerIds.Length)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
|
||||
publication.PriorStaticOwnerIds[
|
||||
publication.PriorStaticCursor],
|
||||
landblock.LandblockId);
|
||||
publication.PriorStaticCursor++;
|
||||
}
|
||||
else if (publication.StaticCursor < landblock.Entities.Count)
|
||||
{
|
||||
WorldEntity entity = landblock.Entities[publication.StaticCursor];
|
||||
beforeStaticCollision?.Invoke(entity);
|
||||
|
||||
// Retail building shells collide exclusively through the
|
||||
// per-landcell building channel. They never become ordinary
|
||||
// shadow objects, even when their source Setup has cylinders.
|
||||
if (entity.IsBuildingShell)
|
||||
continue;
|
||||
|
||||
int entityBspCount = 0;
|
||||
int entityCylinderCount = 0;
|
||||
uint sourcePrefix =
|
||||
entity.SourceGfxObjOrSetupId & 0xFF000000u;
|
||||
bool isOutdoorMesh =
|
||||
(entity.Id & 0x80000000u) != 0
|
||||
|| (entity.Id < 0x40000000u
|
||||
&& (sourcePrefix == 0x01000000u
|
||||
|| sourcePrefix == 0x02000000u));
|
||||
if (isOutdoorMesh)
|
||||
sceneryTried++;
|
||||
|
||||
IReadOnlyList<ShadowShape> bspShapes =
|
||||
ShadowShapeBuilder.FromLandblockBspParts(
|
||||
entity.MeshRefs,
|
||||
entity.IsBuildingShell,
|
||||
_physicsDataCache.GetGfxObj);
|
||||
entityBspCount = bspShapes.Count;
|
||||
if (entityBspCount > 0)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
bspShapes,
|
||||
0u,
|
||||
EntityCollisionFlags.None,
|
||||
publication.Origin.X,
|
||||
publication.Origin.Y,
|
||||
landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
LogMultipartRegistration(landblock, entity, bspShapes);
|
||||
}
|
||||
|
||||
SetupPhysics? setup =
|
||||
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is not null && entityBspCount == 0)
|
||||
{
|
||||
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
||||
var setupShapes = new List<ShadowShape>();
|
||||
for (int cylinderIndex = 0;
|
||||
cylinderIndex < setup.CylSpheres.Count;
|
||||
cylinderIndex++)
|
||||
{
|
||||
DatReaderWriter.Types.CylSphere cylinder =
|
||||
setup.CylSpheres[cylinderIndex];
|
||||
float radius = cylinder.Radius * scale;
|
||||
float baseHeight = cylinder.Height > 0f
|
||||
? cylinder.Height
|
||||
: cylinder.Radius * 4f;
|
||||
float height = baseHeight * scale;
|
||||
if (radius <= 0f)
|
||||
continue;
|
||||
|
||||
Vector3 localOffset = cylinder.Origin * scale;
|
||||
setupShapes.Add(new ShadowShape(
|
||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||
LocalPosition: localOffset,
|
||||
LocalRotation: Quaternion.Identity,
|
||||
Scale: scale,
|
||||
CollisionType: ShadowCollisionType.Cylinder,
|
||||
Radius: radius,
|
||||
CylHeight: height));
|
||||
}
|
||||
|
||||
if (setup.CylSpheres.Count == 0)
|
||||
{
|
||||
for (int sphereIndex = 0;
|
||||
sphereIndex < setup.Spheres.Count;
|
||||
sphereIndex++)
|
||||
{
|
||||
DatReaderWriter.Types.Sphere sphere =
|
||||
setup.Spheres[sphereIndex];
|
||||
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(
|
||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||
LocalPosition: localBaseOffset,
|
||||
LocalRotation: Quaternion.Identity,
|
||||
Scale: scale,
|
||||
CollisionType: ShadowCollisionType.Cylinder,
|
||||
Radius: radius,
|
||||
CylHeight: radius * 2f));
|
||||
}
|
||||
}
|
||||
|
||||
if (setup.CylSpheres.Count == 0
|
||||
&& setup.Spheres.Count == 0
|
||||
&& setup.Radius > 0f)
|
||||
{
|
||||
float radius = setup.Radius * scale;
|
||||
float height = (setup.Height > 0f
|
||||
? setup.Height
|
||||
: setup.Radius * 2f) * scale;
|
||||
setupShapes.Add(new ShadowShape(
|
||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||
LocalPosition: Vector3.Zero,
|
||||
LocalRotation: Quaternion.Identity,
|
||||
Scale: scale,
|
||||
CollisionType: ShadowCollisionType.Cylinder,
|
||||
Radius: radius,
|
||||
CylHeight: height));
|
||||
}
|
||||
|
||||
if (setupShapes.Count > 0)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
setupShapes,
|
||||
0u,
|
||||
EntityCollisionFlags.None,
|
||||
publication.Origin.X,
|
||||
publication.Origin.Y,
|
||||
landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
LogSetupRegistration(landblock, entity, setupShapes);
|
||||
entityCylinderCount = setupShapes.Count;
|
||||
}
|
||||
}
|
||||
|
||||
if (entityBspCount > 0)
|
||||
landblockBspCount++;
|
||||
if (entityCylinderCount > 0)
|
||||
landblockCylinderCount++;
|
||||
if (entityBspCount == 0 && entityCylinderCount == 0
|
||||
&& (sourcePrefix == 0x01000000u
|
||||
|| sourcePrefix == 0x02000000u))
|
||||
{
|
||||
landblockNoCollisionCount++;
|
||||
}
|
||||
PublishStaticEntity(publication, entity);
|
||||
publication.StaticCursor++;
|
||||
}
|
||||
|
||||
if (PhysicsDiagnostics.ProbeBuildingEnabled && sceneryTried > 0)
|
||||
else if (publication.RefloodOwnerIds is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"lb 0x{landblock.LandblockId:X8}: scenery tried={sceneryTried} " +
|
||||
$"(outdoorNone={landblockNoCollisionCount})");
|
||||
publication.RefloodOwnerIds =
|
||||
_physicsEngine.ShadowObjects
|
||||
.CaptureRefloodOwnersForLandblock(landblock.LandblockId);
|
||||
}
|
||||
else if (publication.RefloodCursor
|
||||
< publication.RefloodOwnerIds.Length)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.RefloodOwnerForLandblock(
|
||||
publication.RefloodOwnerIds[publication.RefloodCursor],
|
||||
landblock.LandblockId);
|
||||
publication.RefloodCursor++;
|
||||
}
|
||||
else if (!publication.RefloodCommitted)
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeBuildingEnabled
|
||||
&& publication.SceneryTried > 0)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"lb 0x{landblock.LandblockId:X8}: scenery tried={publication.SceneryTried} " +
|
||||
$"(outdoorNone={publication.NoCollisionCount})");
|
||||
}
|
||||
LogMissingSceneryBounds(landblock);
|
||||
_refloodCount++;
|
||||
publication.RefloodCommitted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_staticBspOwnerCount += publication.BspOwnerCount;
|
||||
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
|
||||
publication.CompletionCommitted = true;
|
||||
_completeCount++;
|
||||
}
|
||||
LogMissingSceneryBounds(landblock);
|
||||
|
||||
_physicsEngine.ShadowObjects.RefloodLandblock(landblock.LandblockId);
|
||||
_refloodCount++;
|
||||
|
||||
_staticBspOwnerCount += landblockBspCount;
|
||||
_staticCylinderOwnerCount += landblockCylinderCount;
|
||||
publication.CompletionCommitted = true;
|
||||
_completeCount++;
|
||||
_completePublishTicks +=
|
||||
Stopwatch.GetTimestamp() - completedStarted;
|
||||
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||
return publication.CompletionCommitted;
|
||||
}
|
||||
|
||||
public void DemoteToTerrain(uint landblockId)
|
||||
|
|
@ -555,54 +460,292 @@ public sealed class LandblockPhysicsPublisher
|
|||
_fullRemovalCount++;
|
||||
}
|
||||
|
||||
private int PublishBuildings(
|
||||
private void PublishCell(
|
||||
LandblockPhysicsPublication publication,
|
||||
PhysicsDatBundle datBundle,
|
||||
uint offset)
|
||||
{
|
||||
LoadedLandblock landblock = publication.Build.Landblock;
|
||||
uint envCellId =
|
||||
(landblock.LandblockId & 0xFFFF0000u) | (0x0100u + offset);
|
||||
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell)
|
||||
|| envCell.EnvironmentId == 0
|
||||
|| !datBundle.Environments.TryGetValue(
|
||||
0x0D000000u | envCell.EnvironmentId,
|
||||
out var environment)
|
||||
|| !environment.Cells.TryGetValue(
|
||||
envCell.CellStructure,
|
||||
out var cellStruct))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion rotation = envCell.Position.Orientation;
|
||||
Vector3 cellOriginWorld = envCell.Position.Origin + publication.Origin;
|
||||
Matrix4x4 physicsCellTransform =
|
||||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
|
||||
_physicsDataCache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
cellStruct,
|
||||
physicsCellTransform);
|
||||
|
||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||
cellStruct.VertexArray.Vertices.Count);
|
||||
foreach ((ushort vertexId, var vertex) in
|
||||
cellStruct.VertexArray.Vertices)
|
||||
{
|
||||
Vector3 worldPosition =
|
||||
Vector3.Transform(vertex.Origin, rotation)
|
||||
+ cellOriginWorld;
|
||||
worldVertices[(ushort)vertexId] = worldPosition;
|
||||
}
|
||||
|
||||
var polygonVertexIds = new List<List<short>>(
|
||||
cellStruct.PhysicsPolygons.Count);
|
||||
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
|
||||
{
|
||||
var vertexIds = new List<short>(polygon.VertexIds.Count);
|
||||
foreach (short vertexId in polygon.VertexIds)
|
||||
vertexIds.Add(vertexId);
|
||||
polygonVertexIds.Add(vertexIds);
|
||||
}
|
||||
|
||||
var portalPlanes = new List<PortalPlane>();
|
||||
// CellPortal.PolygonId indexes rendering Polygons, not
|
||||
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
|
||||
foreach (var portal in envCell.CellPortals)
|
||||
{
|
||||
if (!cellStruct.Polygons.TryGetValue(
|
||||
portal.PolygonId,
|
||||
out var polygon)
|
||||
|| polygon.VertexIds.Count < 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var portalVertices = new Vector3[polygon.VertexIds.Count];
|
||||
bool allFound = true;
|
||||
for (int index = 0; index < polygon.VertexIds.Count; index++)
|
||||
{
|
||||
if (!worldVertices.TryGetValue(
|
||||
(ushort)polygon.VertexIds[index],
|
||||
out portalVertices[index]))
|
||||
{
|
||||
allFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allFound)
|
||||
continue;
|
||||
|
||||
portalPlanes.Add(PortalPlane.FromVertices(
|
||||
portalVertices.AsSpan(),
|
||||
portal.OtherCellId,
|
||||
envCellId & 0xFFFFu,
|
||||
(ushort)portal.Flags));
|
||||
}
|
||||
|
||||
publication.CellSurfaces.Add(new CellSurface(
|
||||
envCellId,
|
||||
worldVertices,
|
||||
polygonVertexIds));
|
||||
publication.PortalPlanes.AddRange(portalPlanes);
|
||||
}
|
||||
|
||||
private void PublishBuilding(
|
||||
LoadedLandblock landblock,
|
||||
PhysicsDatBundle datBundle,
|
||||
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo,
|
||||
TerrainSurface terrainSurface,
|
||||
Vector3 origin)
|
||||
Vector3 origin,
|
||||
BuildingInfo building)
|
||||
{
|
||||
if (landblockInfo is null || landblockInfo.Buildings.Count == 0)
|
||||
return 0;
|
||||
|
||||
uint landblockPrefix = landblock.LandblockId & 0xFFFF0000u;
|
||||
foreach (var building in landblockInfo.Buildings)
|
||||
var portals = new List<BldPortalInfo>(building.Portals.Count);
|
||||
foreach (var portal in building.Portals)
|
||||
{
|
||||
var portals = new List<BldPortalInfo>(building.Portals.Count);
|
||||
foreach (var portal in building.Portals)
|
||||
{
|
||||
portals.Add(new BldPortalInfo(
|
||||
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
|
||||
otherPortalId: unchecked((short)portal.OtherPortalId),
|
||||
flags: (ushort)portal.Flags));
|
||||
}
|
||||
|
||||
Vector3 buildingOrigin = building.Frame.Origin + origin;
|
||||
Matrix4x4 buildingTransform =
|
||||
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(buildingOrigin);
|
||||
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
|
||||
building.Frame.Origin.X,
|
||||
building.Frame.Origin.Y);
|
||||
uint landcellId = landblockPrefix | landcellLow;
|
||||
|
||||
uint shellPartZero = building.ModelId;
|
||||
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
datBundle.Setups.TryGetValue(
|
||||
building.ModelId,
|
||||
out var setup);
|
||||
shellPartZero = setup is not null && setup.Parts.Count > 0
|
||||
? setup.Parts[0]
|
||||
: 0u;
|
||||
}
|
||||
_physicsDataCache.CacheBuilding(
|
||||
landcellId,
|
||||
portals,
|
||||
buildingTransform,
|
||||
modelId: shellPartZero);
|
||||
portals.Add(new BldPortalInfo(
|
||||
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
|
||||
otherPortalId: unchecked((short)portal.OtherPortalId),
|
||||
flags: (ushort)portal.Flags));
|
||||
}
|
||||
|
||||
Vector3 buildingOrigin = building.Frame.Origin + origin;
|
||||
Matrix4x4 buildingTransform =
|
||||
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(buildingOrigin);
|
||||
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
|
||||
building.Frame.Origin.X,
|
||||
building.Frame.Origin.Y);
|
||||
uint landcellId = landblockPrefix | landcellLow;
|
||||
|
||||
uint shellPartZero = building.ModelId;
|
||||
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
datBundle.Setups.TryGetValue(
|
||||
building.ModelId,
|
||||
out var setup);
|
||||
shellPartZero = setup is not null && setup.Parts.Count > 0
|
||||
? setup.Parts[0]
|
||||
: 0u;
|
||||
}
|
||||
_physicsDataCache.CacheBuilding(
|
||||
landcellId,
|
||||
portals,
|
||||
buildingTransform,
|
||||
modelId: shellPartZero);
|
||||
}
|
||||
|
||||
private void PublishStaticEntity(
|
||||
LandblockPhysicsPublication publication,
|
||||
WorldEntity entity)
|
||||
{
|
||||
LoadedLandblock landblock = publication.Build.Landblock;
|
||||
// Retail building shells collide exclusively through the per-landcell
|
||||
// building channel. They never become ordinary shadow objects.
|
||||
if (entity.IsBuildingShell)
|
||||
return;
|
||||
|
||||
int entityBspCount = 0;
|
||||
int entityCylinderCount = 0;
|
||||
uint sourcePrefix = entity.SourceGfxObjOrSetupId & 0xFF000000u;
|
||||
bool isOutdoorMesh =
|
||||
(entity.Id & 0x80000000u) != 0
|
||||
|| (entity.Id < 0x40000000u
|
||||
&& (sourcePrefix == 0x01000000u
|
||||
|| sourcePrefix == 0x02000000u));
|
||||
if (isOutdoorMesh)
|
||||
publication.SceneryTried++;
|
||||
|
||||
IReadOnlyList<ShadowShape> bspShapes =
|
||||
ShadowShapeBuilder.FromLandblockBspParts(
|
||||
entity.MeshRefs,
|
||||
entity.IsBuildingShell,
|
||||
_physicsDataCache.GetGfxObj);
|
||||
entityBspCount = bspShapes.Count;
|
||||
if (entityBspCount > 0)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
bspShapes,
|
||||
0u,
|
||||
EntityCollisionFlags.None,
|
||||
publication.Origin.X,
|
||||
publication.Origin.Y,
|
||||
landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
LogMultipartRegistration(landblock, entity, bspShapes);
|
||||
}
|
||||
|
||||
SetupPhysics? setup =
|
||||
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is not null && entityBspCount == 0)
|
||||
{
|
||||
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
||||
var setupShapes = new List<ShadowShape>();
|
||||
for (int cylinderIndex = 0;
|
||||
cylinderIndex < setup.CylSpheres.Count;
|
||||
cylinderIndex++)
|
||||
{
|
||||
CylSphere cylinder = setup.CylSpheres[cylinderIndex];
|
||||
float radius = cylinder.Radius * scale;
|
||||
float baseHeight = cylinder.Height > 0f
|
||||
? cylinder.Height
|
||||
: cylinder.Radius * 4f;
|
||||
float height = baseHeight * scale;
|
||||
if (radius <= 0f)
|
||||
continue;
|
||||
|
||||
Vector3 localOffset = cylinder.Origin * scale;
|
||||
setupShapes.Add(new ShadowShape(
|
||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||
LocalPosition: localOffset,
|
||||
LocalRotation: Quaternion.Identity,
|
||||
Scale: scale,
|
||||
CollisionType: ShadowCollisionType.Cylinder,
|
||||
Radius: radius,
|
||||
CylHeight: height));
|
||||
}
|
||||
|
||||
if (setup.CylSpheres.Count == 0)
|
||||
{
|
||||
for (int sphereIndex = 0;
|
||||
sphereIndex < setup.Spheres.Count;
|
||||
sphereIndex++)
|
||||
{
|
||||
Sphere sphere = setup.Spheres[sphereIndex];
|
||||
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(
|
||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||
LocalPosition: localBaseOffset,
|
||||
LocalRotation: Quaternion.Identity,
|
||||
Scale: scale,
|
||||
CollisionType: ShadowCollisionType.Cylinder,
|
||||
Radius: radius,
|
||||
CylHeight: radius * 2f));
|
||||
}
|
||||
}
|
||||
|
||||
if (setup.CylSpheres.Count == 0
|
||||
&& setup.Spheres.Count == 0
|
||||
&& setup.Radius > 0f)
|
||||
{
|
||||
float radius = setup.Radius * scale;
|
||||
float height = (setup.Height > 0f
|
||||
? setup.Height
|
||||
: setup.Radius * 2f) * scale;
|
||||
setupShapes.Add(new ShadowShape(
|
||||
GfxObjId: entity.SourceGfxObjOrSetupId,
|
||||
LocalPosition: Vector3.Zero,
|
||||
LocalRotation: Quaternion.Identity,
|
||||
Scale: scale,
|
||||
CollisionType: ShadowCollisionType.Cylinder,
|
||||
Radius: radius,
|
||||
CylHeight: height));
|
||||
}
|
||||
|
||||
if (setupShapes.Count > 0)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
setupShapes,
|
||||
0u,
|
||||
EntityCollisionFlags.None,
|
||||
publication.Origin.X,
|
||||
publication.Origin.Y,
|
||||
landblock.LandblockId,
|
||||
seedCellId: entity.ParentCellId ?? 0u,
|
||||
isStatic: true);
|
||||
LogSetupRegistration(landblock, entity, setupShapes);
|
||||
entityCylinderCount = setupShapes.Count;
|
||||
}
|
||||
}
|
||||
|
||||
if (entityBspCount > 0)
|
||||
publication.BspOwnerCount++;
|
||||
if (entityCylinderCount > 0)
|
||||
publication.CylinderOwnerCount++;
|
||||
if (entityBspCount == 0 && entityCylinderCount == 0
|
||||
&& (sourcePrefix == 0x01000000u
|
||||
|| sourcePrefix == 0x02000000u))
|
||||
{
|
||||
publication.NoCollisionCount++;
|
||||
}
|
||||
return landblockInfo.Buildings.Count;
|
||||
}
|
||||
|
||||
private static void LogSetupRegistration(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue