checkpoint(render): preserve pre-overhaul investigation state
This commit is contained in:
parent
e880860291
commit
b3b7d922f1
45 changed files with 3168 additions and 619 deletions
|
|
@ -1002,11 +1002,6 @@ public static class CellTransit
|
|||
/// docs/research/2026-08-07-ap159-pseudocode.md for the derivation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="worldParts">Per-part world-placed authored boxes — the
|
||||
/// outdoor extent walk's input.</param>
|
||||
/// <param name="worldPartSpheres">Per-part world-placed BSP root spheres —
|
||||
/// the indoor residual's and the building bridge's input. Same parts, same
|
||||
/// order, from the same <see cref="ShadowPartGeometry"/> values.</param>
|
||||
public static IReadOnlyList<uint> BuildShadowCellSetFromParts(
|
||||
PhysicsDataCache cache,
|
||||
uint seedCellId,
|
||||
|
|
@ -1107,27 +1102,17 @@ public static class CellTransit
|
|||
}
|
||||
}
|
||||
|
||||
// Static prune (do_not_load_cells, 0x0052b66e) — indoor-seeded ONLY.
|
||||
// The outdoor rectangle is deliberately unpruned: pruning it would
|
||||
// re-create #334 in a new form.
|
||||
if (isStatic && seedLow >= 0x0100u)
|
||||
{
|
||||
var seedCell = cache.GetCellStruct(seedCellId);
|
||||
if (seedCell is not null)
|
||||
{
|
||||
var keep = new List<uint>(candidates.Count);
|
||||
foreach (uint id in candidates.OrderedIds)
|
||||
{
|
||||
if (id == seedCellId || seedCell.VisibleCellIds.Contains(id))
|
||||
keep.Add(id);
|
||||
}
|
||||
if (keep.Count != candidates.Count)
|
||||
{
|
||||
candidates.Clear();
|
||||
foreach (uint id in keep) candidates.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do NOT apply CObjCell::find_cell_list's do_not_load_cells prune
|
||||
// (0x0052b66e) here. That code belongs to the sphere/cylsphere entry
|
||||
// point at 0x0052b4e0. The part-array route used here is retail
|
||||
// CPhysicsObj::find_bbox_cell_list @0x00510fc0: it walks the growing
|
||||
// CELLARRAY through CPartArray::calc_cross_cells_static and returns
|
||||
// it directly. calc_cross_cells_static does set do_not_load_cells=1,
|
||||
// but the box transit consumes that only as a no-load policy while
|
||||
// resolving neighbouring cells; it does not delete the already-added
|
||||
// exterior cells. Pruning here removed a static Setup's legitimate
|
||||
// outdoor shadow membership after an exit portal crossing (the
|
||||
// cathedral ramp 0x020009A2: collision remained, render vanished).
|
||||
|
||||
return candidates.OrderedIds;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,6 +360,39 @@ public sealed class ShadowObjectRegistry
|
|||
private PhysicsDataCache? _fallback;
|
||||
private PhysicsDataCache FloodCache => DataCache ?? _fallbackCache;
|
||||
|
||||
/// <summary>
|
||||
/// Computes retail's static PartArray render-cell membership without
|
||||
/// publishing collision rows. Static visual parts and collision shapes
|
||||
/// share the same <c>find_bbox_cell_list</c> portal walk, but retail keeps
|
||||
/// them in separate cell lists (<c>shadow_part_list</c> versus
|
||||
/// <c>shadow_object_list</c>). Decorative meshes therefore need this path
|
||||
/// even when <see cref="GetOwnerCells"/> is empty.
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> ComputeStaticRenderCells(
|
||||
uint seedCellId,
|
||||
Vector3 entityWorldPosition,
|
||||
Quaternion entityWorldRotation,
|
||||
IReadOnlyList<ShadowShape> visualParts)
|
||||
{
|
||||
if (seedCellId == 0u || visualParts.Count == 0)
|
||||
return Array.Empty<uint>();
|
||||
|
||||
List<ShadowPartBox> boxes = BuildFloodPartBoxes(
|
||||
entityWorldPosition,
|
||||
entityWorldRotation,
|
||||
visualParts);
|
||||
List<DatReaderWriter.Types.Sphere> spheres = BuildBspPartSpheres(
|
||||
entityWorldPosition,
|
||||
entityWorldRotation,
|
||||
visualParts);
|
||||
return CellTransit.BuildShadowCellSetFromParts(
|
||||
FloodCache,
|
||||
seedCellId,
|
||||
boxes,
|
||||
spheres,
|
||||
isStatic: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a single-shape entity. <paramref name="seedCellId"/> is the
|
||||
/// entity's <c>m_position.objcell_id</c> — the flood seed. Pass 0 to
|
||||
|
|
|
|||
|
|
@ -342,6 +342,93 @@ public static class ShadowShapeBuilder
|
|||
return shapes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves every visual part of a static object into the paired sphere +
|
||||
/// vertex-box geometry used by retail's render-shadow cell registration.
|
||||
/// Unlike <see cref="FromLandblockBspParts"/>, this deliberately includes
|
||||
/// decorative GfxObjs with no physics BSP: <c>CEnvCell::init_static_objects</c>
|
||||
/// creates a real <c>CPhysicsObj</c> for every static and
|
||||
/// <c>CPartArray::AddPartsShadow</c> registers every visual part in each
|
||||
/// crossed cell even when that part cannot collide.
|
||||
///
|
||||
/// <para>The returned values are geometry carriers only. Callers must not
|
||||
/// publish them to the collision registry.</para>
|
||||
/// </summary>
|
||||
public static List<ShadowShape> FromStaticRenderParts(
|
||||
IReadOnlyList<MeshRef> meshRefs,
|
||||
Func<uint, GfxObjPhysics?> getGfxObj,
|
||||
Func<uint, GfxObjVisualBounds?> getVisualBounds,
|
||||
out bool hasPhysicsBsp)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(meshRefs);
|
||||
ArgumentNullException.ThrowIfNull(getGfxObj);
|
||||
ArgumentNullException.ThrowIfNull(getVisualBounds);
|
||||
|
||||
hasPhysicsBsp = false;
|
||||
var parts = new List<ShadowShape>(meshRefs.Count);
|
||||
foreach (MeshRef meshRef in meshRefs)
|
||||
{
|
||||
GfxObjPhysics? physics = getGfxObj(meshRef.GfxObjId);
|
||||
bool partHasPhysicsBsp = physics?.FlatPhysicsBsp is { RootIndex: >= 0 }
|
||||
|| physics?.BSP?.Root is not null;
|
||||
hasPhysicsBsp |= partHasPhysicsBsp;
|
||||
|
||||
FlatGfxObjVisualBounds? flatBounds = physics?.VisualBounds;
|
||||
if (flatBounds is null && getVisualBounds(meshRef.GfxObjId) is { } visual)
|
||||
{
|
||||
flatBounds = new FlatGfxObjVisualBounds(
|
||||
visual.Min,
|
||||
visual.Max,
|
||||
visual.Center,
|
||||
visual.Radius,
|
||||
visual.HalfExtents);
|
||||
}
|
||||
if (flatBounds is not { } bounds)
|
||||
continue;
|
||||
|
||||
if (!Matrix4x4.Decompose(
|
||||
meshRef.PartTransform,
|
||||
out Vector3 partScaleVector,
|
||||
out Quaternion partRotation,
|
||||
out Vector3 partPosition))
|
||||
{
|
||||
partScaleVector = Vector3.One;
|
||||
partRotation = Quaternion.Identity;
|
||||
partPosition = meshRef.PartTransform.Translation;
|
||||
}
|
||||
|
||||
float partScale = partScaleVector.X > 0f ? partScaleVector.X : 1f;
|
||||
FlatCollisionSphere sphere;
|
||||
if (partHasPhysicsBsp)
|
||||
{
|
||||
FlatPhysicsBsp? flat = physics!.FlatPhysicsBsp;
|
||||
sphere = flat is { RootIndex: >= 0 }
|
||||
? flat.Nodes[flat.RootIndex].BoundingSphere
|
||||
: new FlatCollisionSphere(
|
||||
physics.BoundingSphere?.Origin ?? bounds.Center,
|
||||
physics.BoundingSphere?.Radius ?? bounds.Radius);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retail falls back from physics_sphere to the GfxObj drawing
|
||||
// sphere. The prepared collision catalog retains the exact
|
||||
// visual AABB, not that drawing sphere; its circumsphere is a
|
||||
// conservative cheap reject while the exact box below remains
|
||||
// the admitting test.
|
||||
sphere = new FlatCollisionSphere(bounds.Center, bounds.Radius);
|
||||
}
|
||||
|
||||
parts.Add(ShadowShape.Bsp(
|
||||
meshRef.GfxObjId,
|
||||
partPosition,
|
||||
partRotation,
|
||||
partScale,
|
||||
ShadowPartGeometry.Create(sphere, bounds)));
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The collision identity of part <paramref name="index"/>: the installed
|
||||
/// <c>AnimPartChanged</c> replacement when one was supplied, else the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue