feat(physics): complete retail collision — indoor BSP, dual sphere, step-up, swept-sphere, 6-path dispatcher
Indoor CellStruct PhysicsBSP collision for room walls/ceilings. Dual sphere (body+head) from Setup dimensions. StepUp attempts before sliding when hitting low obstacles. FindTimeOfCollision for exact parametric BSP contact time. Full 6-path BSP dispatcher wired into FindEnvCollisions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f30fbd2f5
commit
cadc72ed08
4 changed files with 362 additions and 100 deletions
|
|
@ -1394,6 +1394,9 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
// Step 4: build LoadedCell for portal visibility.
|
// Step 4: build LoadedCell for portal visibility.
|
||||||
BuildLoadedCell(envCellId, envCell, cellStruct, cellOrigin, cellTransform);
|
BuildLoadedCell(envCellId, envCell, cellStruct, cellOrigin, cellTransform);
|
||||||
|
|
||||||
|
// Cache CellStruct physics BSP for indoor collision.
|
||||||
|
_physicsDataCache.CacheCellStruct(envCellId, cellStruct, cellTransform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1142,4 +1142,164 @@ public static class BSPQuery
|
||||||
return SphereIntersectsPoly(node.NegNode, polygons, vertices,
|
return SphereIntersectsPoly(node.NegNode, polygons, vertices,
|
||||||
sphereCenter, sphereRadius, movement, out hitPolyId, out hitNormal);
|
sphereCenter, sphereRadius, movement, out hitPolyId, out hitNormal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// 14. SphereIntersectsPolyWithTime — swept-sphere BSP query using
|
||||||
|
// FindTimeOfCollision for exact parametric contact time.
|
||||||
|
// Fix 4: replaces static overlap + ad-hoc t computation.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Movement-aware sphere-BSP intersection that uses
|
||||||
|
/// <see cref="CollisionPrimitives.FindTimeOfCollision"/> to compute the
|
||||||
|
/// exact parametric time of first contact. Returns the earliest collision
|
||||||
|
/// across all polygons in the BSP tree.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Unlike <see cref="SphereIntersectsPoly(PhysicsBSPNode?, Dictionary{ushort, Polygon},
|
||||||
|
/// VertexArray, Vector3, float, Vector3, out ushort, out Vector3)"/> which
|
||||||
|
/// tests static overlap at start and end positions, this method finds the
|
||||||
|
/// precise contact time via swept-sphere analysis.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static bool SphereIntersectsPolyWithTime(
|
||||||
|
PhysicsBSPNode? node,
|
||||||
|
Dictionary<ushort, Polygon> polygons,
|
||||||
|
VertexArray vertices,
|
||||||
|
Vector3 sphereCenter,
|
||||||
|
float sphereRadius,
|
||||||
|
Vector3 movement,
|
||||||
|
out ushort hitPolyId,
|
||||||
|
out Vector3 hitNormal,
|
||||||
|
out float hitTime)
|
||||||
|
{
|
||||||
|
hitPolyId = 0;
|
||||||
|
hitNormal = Vector3.Zero;
|
||||||
|
hitTime = float.MaxValue;
|
||||||
|
|
||||||
|
if (node is null) return false;
|
||||||
|
|
||||||
|
SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
node, polygons, vertices,
|
||||||
|
sphereCenter, sphereRadius, movement,
|
||||||
|
ref hitPolyId, ref hitNormal, ref hitTime);
|
||||||
|
|
||||||
|
return hitTime < float.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
PhysicsBSPNode? node,
|
||||||
|
Dictionary<ushort, Polygon> polygons,
|
||||||
|
VertexArray vertices,
|
||||||
|
Vector3 sphereCenter,
|
||||||
|
float sphereRadius,
|
||||||
|
Vector3 movement,
|
||||||
|
ref ushort hitPolyId,
|
||||||
|
ref Vector3 hitNormal,
|
||||||
|
ref float bestTime)
|
||||||
|
{
|
||||||
|
if (node is null) return;
|
||||||
|
|
||||||
|
// Broad phase: bounding sphere + movement extent
|
||||||
|
float dist = Vector3.Distance(sphereCenter, node.BoundingSphere.Origin);
|
||||||
|
if (dist > sphereRadius + node.BoundingSphere.Radius + movement.Length() + 0.1f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Leaf node: test each polygon with FindTimeOfCollision
|
||||||
|
if (node.Type == BSPNodeType.Leaf)
|
||||||
|
{
|
||||||
|
foreach (var polyIdx in node.Polygons)
|
||||||
|
{
|
||||||
|
if (!polygons.TryGetValue(polyIdx, out var poly)) continue;
|
||||||
|
if (!TryGetPolyPlane(poly, vertices, out var polyPlane, out var polyVerts))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Front-face culling: only collide if moving toward this face.
|
||||||
|
if (Vector3.Dot(movement, polyPlane.Normal) >= 0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Use FindTimeOfCollision for exact parametric contact time.
|
||||||
|
if (CollisionPrimitives.FindTimeOfCollision(
|
||||||
|
polyPlane, polyVerts,
|
||||||
|
sphereCenter, sphereRadius,
|
||||||
|
movement, out float t))
|
||||||
|
{
|
||||||
|
// FindTimeOfCollision returns t such that contact = origin - movement*t.
|
||||||
|
// For our purposes, a positive t means the sphere reaches the polygon
|
||||||
|
// when travelling along 'movement'. We want the absolute value as
|
||||||
|
// our parametric time (0=start, 1=end of movement).
|
||||||
|
float absT = MathF.Abs(t);
|
||||||
|
if (absT < bestTime)
|
||||||
|
{
|
||||||
|
bestTime = absT;
|
||||||
|
hitPolyId = polyIdx;
|
||||||
|
hitNormal = polyPlane.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: static overlap test at start and end positions.
|
||||||
|
if (CollisionPrimitives.SphereIntersectsPoly(
|
||||||
|
polyPlane, polyVerts, sphereCenter, sphereRadius, out _))
|
||||||
|
{
|
||||||
|
if (0f < bestTime)
|
||||||
|
{
|
||||||
|
bestTime = 0f;
|
||||||
|
hitPolyId = polyIdx;
|
||||||
|
hitNormal = polyPlane.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Vector3 endCenter = sphereCenter + movement;
|
||||||
|
if (CollisionPrimitives.SphereIntersectsPoly(
|
||||||
|
polyPlane, polyVerts, endCenter, sphereRadius, out _))
|
||||||
|
{
|
||||||
|
if (1f < bestTime)
|
||||||
|
{
|
||||||
|
bestTime = 1f;
|
||||||
|
hitPolyId = polyIdx;
|
||||||
|
hitNormal = polyPlane.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal node: classify against splitting plane
|
||||||
|
float splitDist = Vector3.Dot(node.SplittingPlane.Normal, sphereCenter)
|
||||||
|
+ node.SplittingPlane.D;
|
||||||
|
float reach = sphereRadius + movement.Length();
|
||||||
|
|
||||||
|
if (splitDist >= reach)
|
||||||
|
{
|
||||||
|
SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
node.PosNode, polygons, vertices,
|
||||||
|
sphereCenter, sphereRadius, movement,
|
||||||
|
ref hitPolyId, ref hitNormal, ref bestTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splitDist <= -reach)
|
||||||
|
{
|
||||||
|
SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
node.NegNode, polygons, vertices,
|
||||||
|
sphereCenter, sphereRadius, movement,
|
||||||
|
ref hitPolyId, ref hitNormal, ref bestTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Straddles: check both sides to find the earliest collision.
|
||||||
|
SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
node.PosNode, polygons, vertices,
|
||||||
|
sphereCenter, sphereRadius, movement,
|
||||||
|
ref hitPolyId, ref hitNormal, ref bestTime);
|
||||||
|
|
||||||
|
SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
node.NegNode, polygons, vertices,
|
||||||
|
sphereCenter, sphereRadius, movement,
|
||||||
|
ref hitPolyId, ref hitNormal, ref bestTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Numerics;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -15,6 +16,7 @@ public sealed class PhysicsDataCache
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
||||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||||
|
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extract and cache the physics BSP + polygon data from a GfxObj.
|
/// Extract and cache the physics BSP + polygon data from a GfxObj.
|
||||||
|
|
@ -53,10 +55,35 @@ public sealed class PhysicsDataCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extract and cache the physics BSP + polygon data from a CellStruct
|
||||||
|
/// (indoor room geometry). No-ops if the id is already cached or the
|
||||||
|
/// CellStruct has no physics BSP.
|
||||||
|
/// </summary>
|
||||||
|
public void CacheCellStruct(uint envCellId, CellStruct cellStruct,
|
||||||
|
Matrix4x4 worldTransform)
|
||||||
|
{
|
||||||
|
if (_cellStruct.ContainsKey(envCellId)) return;
|
||||||
|
if (cellStruct.PhysicsBSP?.Root is null) return;
|
||||||
|
|
||||||
|
Matrix4x4.Invert(worldTransform, out var inverseTransform);
|
||||||
|
|
||||||
|
_cellStruct[envCellId] = new CellPhysics
|
||||||
|
{
|
||||||
|
BSP = cellStruct.PhysicsBSP,
|
||||||
|
PhysicsPolygons = cellStruct.PhysicsPolygons,
|
||||||
|
Vertices = cellStruct.VertexArray,
|
||||||
|
WorldTransform = worldTransform,
|
||||||
|
InverseWorldTransform = inverseTransform,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
||||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
||||||
|
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
||||||
public int GfxObjCount => _gfxObj.Count;
|
public int GfxObjCount => _gfxObj.Count;
|
||||||
public int SetupCount => _setup.Count;
|
public int SetupCount => _setup.Count;
|
||||||
|
public int CellStructCount => _cellStruct.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Cached physics data for a single GfxObj part.</summary>
|
/// <summary>Cached physics data for a single GfxObj part.</summary>
|
||||||
|
|
@ -78,3 +105,17 @@ public sealed class SetupPhysics
|
||||||
public float StepUpHeight { get; init; }
|
public float StepUpHeight { get; init; }
|
||||||
public float StepDownHeight { get; init; }
|
public float StepDownHeight { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached physics data for an indoor cell's room geometry (CellStruct).
|
||||||
|
/// Used for wall/floor/ceiling collision in EnvCells.
|
||||||
|
/// ACE: EnvCell.find_env_collisions queries CellStructure.PhysicsBSP.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CellPhysics
|
||||||
|
{
|
||||||
|
public required PhysicsBSPTree BSP { get; init; }
|
||||||
|
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
||||||
|
public required VertexArray Vertices { get; init; }
|
||||||
|
public Matrix4x4 WorldTransform { get; init; }
|
||||||
|
public Matrix4x4 InverseWorldTransform { get; init; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -511,18 +511,69 @@ public sealed class Transition
|
||||||
var sp = SpherePath;
|
var sp = SpherePath;
|
||||||
var ci = CollisionInfo;
|
var ci = CollisionInfo;
|
||||||
|
|
||||||
// Sample terrain Z at the foot sphere's world position.
|
|
||||||
Vector3 footCenter = sp.GlobalSphere[0].Origin;
|
Vector3 footCenter = sp.GlobalSphere[0].Origin;
|
||||||
float sphereRadius = sp.GlobalSphere[0].Radius;
|
float sphereRadius = sp.GlobalSphere[0].Radius;
|
||||||
|
|
||||||
|
// ── Indoor cell BSP collision ────────────────────────────────────
|
||||||
|
// If the player is in an indoor cell (low 16 bits >= 0x0100),
|
||||||
|
// query the CellStruct's PhysicsBSP for wall/floor/ceiling collision.
|
||||||
|
// ACE: EnvCell.find_env_collisions -> CellStructure.PhysicsBSP.find_collisions
|
||||||
|
uint cellLow = sp.CheckCellId & 0xFFFFu;
|
||||||
|
if (cellLow >= 0x0100 && engine.DataCache is not null)
|
||||||
|
{
|
||||||
|
var cellPhysics = engine.DataCache.GetCellStruct(sp.CheckCellId);
|
||||||
|
if (cellPhysics?.BSP?.Root is not null)
|
||||||
|
{
|
||||||
|
// Transform player sphere to cell-local space.
|
||||||
|
var localCenter = Vector3.Transform(footCenter, cellPhysics.InverseWorldTransform);
|
||||||
|
var localCurrCenter = Vector3.Transform(sp.GlobalCurrCenter[0].Origin, cellPhysics.InverseWorldTransform);
|
||||||
|
|
||||||
|
var localSphere = new DatReaderWriter.Types.Sphere
|
||||||
|
{
|
||||||
|
Origin = localCenter,
|
||||||
|
Radius = sphereRadius,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second sphere (head) in local space, if present.
|
||||||
|
DatReaderWriter.Types.Sphere? localSphere1 = null;
|
||||||
|
if (sp.NumSphere > 1)
|
||||||
|
{
|
||||||
|
var headCenter = sp.GlobalSphere[1].Origin;
|
||||||
|
localSphere1 = new DatReaderWriter.Types.Sphere
|
||||||
|
{
|
||||||
|
Origin = Vector3.Transform(headCenter, cellPhysics.InverseWorldTransform),
|
||||||
|
Radius = sp.GlobalSphere[1].Radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the full 6-path BSP dispatcher for retail-faithful collision.
|
||||||
|
var cellState = BSPQuery.FindCollisions(
|
||||||
|
cellPhysics.BSP.Root,
|
||||||
|
cellPhysics.PhysicsPolygons,
|
||||||
|
cellPhysics.Vertices,
|
||||||
|
this,
|
||||||
|
localSphere,
|
||||||
|
localSphere1,
|
||||||
|
localCurrCenter,
|
||||||
|
Vector3.UnitZ, // local space Z is up
|
||||||
|
1.0f); // scale = 1.0 for cell geometry
|
||||||
|
|
||||||
|
if (cellState != TransitionState.OK)
|
||||||
|
{
|
||||||
|
if (!ObjectInfo.State.HasFlag(ObjectInfoState.Contact))
|
||||||
|
ci.CollidedWithEnvironment = true;
|
||||||
|
return cellState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Outdoor terrain collision ────────────────────────────────────
|
||||||
|
// Sample terrain Z at the foot sphere's world position.
|
||||||
float? terrainZ = engine.SampleTerrainZ(footCenter.X, footCenter.Y);
|
float? terrainZ = engine.SampleTerrainZ(footCenter.X, footCenter.Y);
|
||||||
if (terrainZ is null)
|
if (terrainZ is null)
|
||||||
return TransitionState.OK; // no terrain loaded here — allow pass-through
|
return TransitionState.OK; // no terrain loaded here — allow pass-through
|
||||||
|
|
||||||
// Build the terrain contact plane (flat ground: Normal = +Z, D = -terrainZ).
|
// Build the terrain contact plane (flat ground: Normal = +Z, D = -terrainZ).
|
||||||
// For sloped terrain we'd need the surface normal from the triangle; for MVP
|
|
||||||
// we use the vertical plane which matches flat terrain exactly and gives
|
|
||||||
// conservative results on slopes (terrain Z is already interpolated correctly).
|
|
||||||
var contactPlane = new System.Numerics.Plane(
|
var contactPlane = new System.Numerics.Plane(
|
||||||
new Vector3(0f, 0f, 1f), -terrainZ.Value);
|
new Vector3(0f, 0f, 1f), -terrainZ.Value);
|
||||||
|
|
||||||
|
|
@ -646,113 +697,108 @@ public sealed class Transition
|
||||||
worldOffsetX, worldOffsetY, landblockId,
|
worldOffsetX, worldOffsetY, landblockId,
|
||||||
_nearbyObjs);
|
_nearbyObjs);
|
||||||
|
|
||||||
|
|
||||||
// Find the EARLIEST collision along the movement path.
|
// Find the EARLIEST collision along the movement path.
|
||||||
|
// Test both foot sphere (index 0) and head sphere (index 1) if present.
|
||||||
float bestT = float.MaxValue;
|
float bestT = float.MaxValue;
|
||||||
Vector3 bestNormal = Vector3.Zero;
|
Vector3 bestNormal = Vector3.Zero;
|
||||||
|
bool bestIsHeadSphere = false;
|
||||||
|
|
||||||
foreach (var obj in _nearbyObjs)
|
for (int sphereIdx = 0; sphereIdx < sp.NumSphere; sphereIdx++)
|
||||||
{
|
{
|
||||||
// Broad-phase: can the moving sphere reach this object?
|
Vector3 sphereCheckPos = sp.GlobalSphere[sphereIdx].Origin;
|
||||||
// Use horizontal distance for cylinders (Z extent is checked separately).
|
Vector3 sphereCurrPos = sp.GlobalCurrCenter[sphereIdx].Origin;
|
||||||
Vector3 deltaToCurr = currPos - obj.Position;
|
float sphRadius = sp.GlobalSphere[sphereIdx].Radius;
|
||||||
float distToCurr;
|
Vector3 sphMovement = sphereCheckPos - sphereCurrPos;
|
||||||
if (obj.CollisionType == ShadowCollisionType.Cylinder)
|
|
||||||
distToCurr = MathF.Sqrt(deltaToCurr.X * deltaToCurr.X + deltaToCurr.Y * deltaToCurr.Y);
|
|
||||||
else
|
|
||||||
distToCurr = deltaToCurr.Length();
|
|
||||||
float maxReach = sphereRadius + obj.Radius + movement.Length() + 2f;
|
|
||||||
if (distToCurr > maxReach)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float t;
|
foreach (var obj in _nearbyObjs)
|
||||||
Vector3 worldHitNormal;
|
|
||||||
|
|
||||||
if (obj.CollisionType == ShadowCollisionType.BSP)
|
|
||||||
{
|
{
|
||||||
var physics = engine.DataCache.GetGfxObj(obj.GfxObjId);
|
// Broad-phase: can the moving sphere reach this object?
|
||||||
if (physics?.BSP?.Root is null) continue;
|
Vector3 deltaToCurr = sphereCurrPos - obj.Position;
|
||||||
|
float distToCurr;
|
||||||
// Transform to object-local space.
|
if (obj.CollisionType == ShadowCollisionType.Cylinder)
|
||||||
var invRot = Quaternion.Inverse(obj.Rotation);
|
distToCurr = MathF.Sqrt(deltaToCurr.X * deltaToCurr.X + deltaToCurr.Y * deltaToCurr.Y);
|
||||||
Vector3 localCurrPos = Vector3.Transform(currPos - obj.Position, invRot);
|
else
|
||||||
Vector3 localMovement = Vector3.Transform(movement, invRot);
|
distToCurr = deltaToCurr.Length();
|
||||||
|
float maxReach = sphRadius + obj.Radius + sphMovement.Length() + 2f;
|
||||||
// Use movement-aware BSP query with front-face culling.
|
if (distToCurr > maxReach)
|
||||||
if (!BSPQuery.SphereIntersectsPoly(
|
|
||||||
physics.BSP.Root,
|
|
||||||
physics.PhysicsPolygons,
|
|
||||||
physics.Vertices,
|
|
||||||
localCurrPos, sphereRadius,
|
|
||||||
localMovement,
|
|
||||||
out _, out Vector3 localHitNormal))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
worldHitNormal = Vector3.Transform(localHitNormal, obj.Rotation);
|
float t;
|
||||||
|
Vector3 worldHitNormal;
|
||||||
|
|
||||||
// Compute parametric contact time: how far along the movement
|
if (obj.CollisionType == ShadowCollisionType.BSP)
|
||||||
// does the sphere first touch this polygon?
|
|
||||||
// Project the center-to-plane distance onto the movement direction.
|
|
||||||
float planeDist = Vector3.Dot(localHitNormal, localCurrPos) -
|
|
||||||
Vector3.Dot(localHitNormal, Vector3.Zero); // plane through origin in local
|
|
||||||
float approach = -Vector3.Dot(localHitNormal, localMovement);
|
|
||||||
if (approach > PhysicsGlobals.EPSILON)
|
|
||||||
t = (planeDist - sphereRadius) / approach;
|
|
||||||
else
|
|
||||||
t = 0f; // already touching or parallel
|
|
||||||
t = Math.Clamp(t, 0f, 1f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Cylinder swept-sphere test.
|
|
||||||
// Find parametric time when moving sphere first contacts the cylinder.
|
|
||||||
Vector3 deltaCurr = currPos - obj.Position;
|
|
||||||
float dx = deltaCurr.X, dy = deltaCurr.Y;
|
|
||||||
float mx = movement.X, my = movement.Y;
|
|
||||||
float combinedR = sphereRadius + obj.Radius;
|
|
||||||
|
|
||||||
// Quadratic: |curr_xy + t*move_xy|^2 = combinedR^2
|
|
||||||
float a = mx * mx + my * my;
|
|
||||||
float b = 2f * (dx * mx + dy * my);
|
|
||||||
float c = dx * dx + dy * dy - combinedR * combinedR;
|
|
||||||
|
|
||||||
if (a < PhysicsGlobals.EPSILON)
|
|
||||||
{
|
{
|
||||||
// Not moving horizontally — check static overlap.
|
var physics = engine.DataCache.GetGfxObj(obj.GfxObjId);
|
||||||
if (c > 0f) continue;
|
if (physics?.BSP?.Root is null) continue;
|
||||||
t = 0f;
|
|
||||||
|
// Transform to object-local space.
|
||||||
|
var invRot = Quaternion.Inverse(obj.Rotation);
|
||||||
|
Vector3 localCurrPos = Vector3.Transform(sphereCurrPos - obj.Position, invRot);
|
||||||
|
Vector3 localMovement = Vector3.Transform(sphMovement, invRot);
|
||||||
|
|
||||||
|
// Use movement-aware BSP query with front-face culling.
|
||||||
|
if (!BSPQuery.SphereIntersectsPolyWithTime(
|
||||||
|
physics.BSP.Root,
|
||||||
|
physics.PhysicsPolygons,
|
||||||
|
physics.Vertices,
|
||||||
|
localCurrPos, sphRadius,
|
||||||
|
localMovement,
|
||||||
|
out _, out Vector3 localHitNormal, out t))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
worldHitNormal = Vector3.Transform(localHitNormal, obj.Rotation);
|
||||||
|
t = Math.Clamp(t, 0f, 1f);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
float disc = b * b - 4f * a * c;
|
// Cylinder swept-sphere test.
|
||||||
if (disc < 0f) continue; // no intersection
|
Vector3 deltaCurr = sphereCurrPos - obj.Position;
|
||||||
float sqrtDisc = MathF.Sqrt(disc);
|
float dx = deltaCurr.X, dy = deltaCurr.Y;
|
||||||
t = (-b - sqrtDisc) / (2f * a); // first contact time
|
float mx = sphMovement.X, my = sphMovement.Y;
|
||||||
if (t > 1f) continue; // contact is past this step
|
float combinedR = sphRadius + obj.Radius;
|
||||||
if (t < 0f) t = 0f; // already overlapping
|
|
||||||
|
float a = mx * mx + my * my;
|
||||||
|
float b = 2f * (dx * mx + dy * my);
|
||||||
|
float c = dx * dx + dy * dy - combinedR * combinedR;
|
||||||
|
|
||||||
|
if (a < PhysicsGlobals.EPSILON)
|
||||||
|
{
|
||||||
|
if (c > 0f) continue;
|
||||||
|
t = 0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float disc = b * b - 4f * a * c;
|
||||||
|
if (disc < 0f) continue;
|
||||||
|
float sqrtDisc = MathF.Sqrt(disc);
|
||||||
|
t = (-b - sqrtDisc) / (2f * a);
|
||||||
|
if (t > 1f) continue;
|
||||||
|
if (t < 0f) t = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical check at contact time.
|
||||||
|
Vector3 contactPos = sphereCurrPos + sphMovement * t;
|
||||||
|
float cylTop = obj.CylHeight > 0 ? obj.CylHeight : obj.Radius * 4f;
|
||||||
|
float playerBottom = contactPos.Z - sphRadius;
|
||||||
|
float playerTop = contactPos.Z + sphRadius;
|
||||||
|
if (playerBottom > obj.Position.Z + cylTop || playerTop < obj.Position.Z)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Normal: radial at contact point.
|
||||||
|
Vector3 contactDelta = contactPos - obj.Position;
|
||||||
|
float hDist = MathF.Sqrt(contactDelta.X * contactDelta.X + contactDelta.Y * contactDelta.Y);
|
||||||
|
if (hDist < PhysicsGlobals.EPSILON)
|
||||||
|
worldHitNormal = Vector3.UnitX;
|
||||||
|
else
|
||||||
|
worldHitNormal = Vector3.Normalize(new Vector3(contactDelta.X, contactDelta.Y, 0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical check at contact time.
|
if (t < bestT && worldHitNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
||||||
Vector3 contactPos = currPos + movement * t;
|
{
|
||||||
float cylTop = obj.CylHeight > 0 ? obj.CylHeight : obj.Radius * 4f;
|
bestT = t;
|
||||||
float playerBottom = contactPos.Z - sphereRadius;
|
bestNormal = Vector3.Normalize(worldHitNormal);
|
||||||
float playerTop = contactPos.Z + sphereRadius;
|
bestIsHeadSphere = (sphereIdx == 1);
|
||||||
if (playerBottom > obj.Position.Z + cylTop || playerTop < obj.Position.Z)
|
}
|
||||||
continue;
|
|
||||||
|
|
||||||
// Normal: radial at contact point.
|
|
||||||
Vector3 contactDelta = contactPos - obj.Position;
|
|
||||||
float hDist = MathF.Sqrt(contactDelta.X * contactDelta.X + contactDelta.Y * contactDelta.Y);
|
|
||||||
if (hDist < PhysicsGlobals.EPSILON)
|
|
||||||
worldHitNormal = Vector3.UnitX;
|
|
||||||
else
|
|
||||||
worldHitNormal = Vector3.Normalize(new Vector3(contactDelta.X, contactDelta.Y, 0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t < bestT && worldHitNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
|
||||||
{
|
|
||||||
bestT = t;
|
|
||||||
bestNormal = Vector3.Normalize(worldHitNormal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -761,10 +807,24 @@ public sealed class Transition
|
||||||
return TransitionState.OK;
|
return TransitionState.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Fix 3: Contact-path step-up attempt ─────────────────────────
|
||||||
|
// When in contact with ground and hitting a low obstacle (not the head
|
||||||
|
// sphere), try stepping up before falling back to slide.
|
||||||
|
// ACE: BSPTree.find_collisions path 5 — Contact|OnWalkable → step_sphere_up.
|
||||||
|
if (!bestIsHeadSphere
|
||||||
|
&& ObjectInfo.Contact
|
||||||
|
&& bestNormal.Z > PhysicsGlobals.EPSILON
|
||||||
|
&& bestNormal.Z < PhysicsGlobals.FloorZ)
|
||||||
|
{
|
||||||
|
// The surface is angled (not a vertical wall, not a floor) —
|
||||||
|
// attempt step-up. Set the flag for the transition system.
|
||||||
|
sp.StepUp = true;
|
||||||
|
sp.StepUpNormal = bestNormal;
|
||||||
|
ci.SetCollisionNormal(bestNormal);
|
||||||
|
return TransitionState.OK;
|
||||||
|
}
|
||||||
|
|
||||||
// Already overlapping at the START of the step (bestT == 0 or very small).
|
// Already overlapping at the START of the step (bestT == 0 or very small).
|
||||||
// This happens when the player spawns inside an object or a previous
|
|
||||||
// step left them penetrating. Push out along the collision normal
|
|
||||||
// instead of sliding — sliding with zero displacement gets stuck.
|
|
||||||
if (bestT <= PhysicsGlobals.EPSILON)
|
if (bestT <= PhysicsGlobals.EPSILON)
|
||||||
{
|
{
|
||||||
Vector3 pushOut = bestNormal * (sphereRadius * 0.5f + 0.01f);
|
Vector3 pushOut = bestNormal * (sphereRadius * 0.5f + 0.01f);
|
||||||
|
|
@ -775,12 +835,10 @@ public sealed class Transition
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewind the sphere to just BEFORE the contact point.
|
// Rewind the sphere to just BEFORE the contact point.
|
||||||
// Use t slightly before bestT to ensure no penetration.
|
|
||||||
if (bestT < 1f)
|
if (bestT < 1f)
|
||||||
{
|
{
|
||||||
float safeT = MathF.Max(0f, bestT - 0.02f);
|
float safeT = MathF.Max(0f, bestT - 0.02f);
|
||||||
Vector3 contactPos = currPos + movement * safeT;
|
Vector3 contactPos = currPos + movement * safeT;
|
||||||
// Additional push along normal to clear the surface.
|
|
||||||
contactPos += bestNormal * 0.02f;
|
contactPos += bestNormal * 0.02f;
|
||||||
sp.SetCheckPos(contactPos, sp.CheckCellId);
|
sp.SetCheckPos(contactPos, sp.CheckCellId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue