Setup.Spheres were previously coerced to short cylinders (CylHeight=2*r), which is geometrically wrong: a cylinder has flat caps; a sphere does not. This ported CSphere::intersects_sphere (0x00537A80) so sphere-typed shadow entries are tested as spheres — 3-D distance, no height clamping. Changes: - ShadowObjectRegistry.cs: added ShadowCollisionType.Sphere (enum value 2). The BuildFloodSpheres anyCyl dedup at :232 is unaffected: only Cylinder sets anyCyl=true; Sphere shapes fall through to the BSP-fallback path (anyCyl=false → included), which is correct. - ShadowShapeBuilder.cs: FromSetup now emits ShadowCollisionType.Sphere (CylHeight=0) for Setup.Spheres instead of a short Cylinder. - CollisionPrimitives.cs: added SweptSphereHitsSphere — quadratic swept solve ported from ACE Sphere.cs::FindTimeOfCollision, which is a C# port of retail's CSphere::intersects_sphere @ 0x00537A80. Sign convention confirmed against the decomp: retail negates the root to produce a forward t ∈ (0,1]. - TransitionTypes.cs: added Sphere narrow-phase branch between BSP and Cylinder in FindObjCollisionsInCell; uses 3-D distance for overlap (not XY-only). Added SphereCollision() method implementing the 3-D wall-slide response. Updated diagnostic logging at :2734 to cover Sphere. - Updated ShadowShapeBuilderTests for new Sphere type assertion. - New SphereIntersectsSphereConformanceTests: 9 geometrically-anchored cases (head-on, tangent, perpendicular-miss, lateral-near-miss, sweep-away, beyond-step, degenerate-zero-sweep, already-overlapping, vertical-sweep). Retail oracle: CSphere::intersects_sphere @ 0x00537A80 (named-retail); ACE Sphere.cs::FindTimeOfCollision (C# port, cross-confirmed). Build: 0 errors, 10 warnings (pre-existing). Tests: 1576 pass / 0 fail / 2 skip (1578 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
827 lines
33 KiB
C#
827 lines
33 KiB
C#
using System;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.Core.Physics;
|
||
|
||
/// <summary>
|
||
/// Pure-static collision primitive routines ported from the retail acclient.exe.
|
||
///
|
||
/// <para>
|
||
/// Each method corresponds to a specific decompiled function from
|
||
/// <c>chunk_00530000.c</c>. Addresses are noted in the per-method XML doc so
|
||
/// they can be cross-checked against the ghidra listing.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// "Polygon" in this context is an AC convex face stored in BSP leaves.
|
||
/// The polygon is described by an array of <see cref="Vector3"/> vertices
|
||
/// (wound counter-clockwise when viewed from the normal side) together with a
|
||
/// precomputed <see cref="Plane"/>. The plane normal is the outward-facing
|
||
/// surface normal; <c>Plane.D</c> is the signed distance from the origin
|
||
/// (positive-D convention: <c>dot(N,p) + D == 0</c> on the surface).
|
||
/// </para>
|
||
/// </summary>
|
||
public static class CollisionPrimitives
|
||
{
|
||
// -----------------------------------------------------------------------
|
||
// Global-equivalent constants (pulled from retail binary DAT addresses)
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Floating-point epsilon used for "near-zero" ray-direction checks
|
||
/// (<c>_DAT_007ca628</c> in the retail binary, ≈ 1×10⁻⁴).
|
||
/// </summary>
|
||
public const float Epsilon = 1e-4f;
|
||
|
||
/// <summary>
|
||
/// Relaxed epsilon for the squared-distance comparisons
|
||
/// (<c>_DAT_007ca5d8</c> in the retail binary, ≈ 1×10⁻⁸).
|
||
/// </summary>
|
||
public const float EpsilonSq = 1e-8f;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 1. SphereIntersectsRay — FUN_005384e0
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Tests whether a ray intersects a sphere and, if so, returns the
|
||
/// parametric time <paramref name="t"/> of the nearest intersection.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_005384e0 @ 0x005384E0</c>.
|
||
/// The sphere is represented by its centre <paramref name="sphereCenter"/>
|
||
/// and <paramref name="sphereRadius"/>. The ray is defined by a start
|
||
/// point <paramref name="rayOrigin"/> and direction vector
|
||
/// <paramref name="rayDir"/> (need not be unit length; <paramref name="t"/>
|
||
/// is in terms of that direction's length).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Returns <see langword="false"/> when the origin is <em>inside</em> the
|
||
/// sphere (AC treats that as non-colliding, matching the retail binary).
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="sphereCenter">Centre of the sphere.</param>
|
||
/// <param name="sphereRadius">Radius of the sphere.</param>
|
||
/// <param name="rayOrigin">Start point of the ray (param_1[0..2]).</param>
|
||
/// <param name="rayDir">Direction of the ray (param_2[3..5]).</param>
|
||
/// <param name="t">
|
||
/// On success: parametric intersection time ≥ 0.
|
||
/// Undefined on failure.
|
||
/// </param>
|
||
/// <returns><see langword="true"/> if the ray hits the sphere.</returns>
|
||
public static bool SphereIntersectsRay(
|
||
Vector3 sphereCenter, float sphereRadius,
|
||
Vector3 rayOrigin, Vector3 rayDir,
|
||
out double t)
|
||
{
|
||
t = 0.0;
|
||
|
||
// delta = rayOrigin − sphereCenter
|
||
float dx = rayOrigin.X - sphereCenter.X;
|
||
float dy = rayOrigin.Y - sphereCenter.Y;
|
||
float dz = rayOrigin.Z - sphereCenter.Z;
|
||
|
||
// c = |delta|² − r² (positive ⟹ origin is outside sphere)
|
||
float c = dx * dx + dy * dy + dz * dz - sphereRadius * sphereRadius;
|
||
if (c <= 0f)
|
||
return false; // origin is inside — not considered an intersection
|
||
|
||
// a = |rayDir|²
|
||
float a = rayDir.X * rayDir.X + rayDir.Y * rayDir.Y + rayDir.Z * rayDir.Z;
|
||
if (a < EpsilonSq)
|
||
return false; // degenerate ray
|
||
|
||
// b = −dot(delta, rayDir)
|
||
float b = -(dx * rayDir.X + dy * rayDir.Y + dz * rayDir.Z);
|
||
|
||
// discriminant = b² − c·a
|
||
float disc = b * b - c * a;
|
||
if (disc < 0f)
|
||
return false; // no real intersection
|
||
|
||
float sqrtDisc = MathF.Sqrt(disc);
|
||
|
||
// retail: if b < sqrtDisc return (b+sqrtDisc)/a else (b−sqrtDisc)/a
|
||
if (b < sqrtDisc)
|
||
t = (b + sqrtDisc) / a;
|
||
else
|
||
t = (b - sqrtDisc) / a;
|
||
|
||
return true;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 2. ray_plane_intersect — FUN_00539060
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Finds the parametric time at which a ray intersects a plane, returning
|
||
/// the result in <paramref name="t"/>.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_00539060 @ 0x00539060</c>.
|
||
/// The plane is stored as four floats: normal (XYZ) and D.
|
||
/// The ray is stored as six floats: origin (XYZ) then direction (XYZ).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Returns <see langword="false"/> when the ray is parallel to the plane
|
||
/// (dot(dir, normal) ≈ 0) or when the intersection is behind the ray
|
||
/// origin (<c>t < 0</c>).
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="plane">
|
||
/// The plane to test. <c>plane.D</c> is the signed offset such that
|
||
/// <c>dot(N,p) + D == 0</c> on the plane surface.
|
||
/// </param>
|
||
/// <param name="rayOrigin">Start point of the ray.</param>
|
||
/// <param name="rayDir">Direction of the ray (need not be normalised).</param>
|
||
/// <param name="t">Parametric intersection distance (≥ 0 on success).</param>
|
||
/// <returns><see langword="true"/> if the ray hits the plane.</returns>
|
||
public static bool RayPlaneIntersect(
|
||
Plane plane,
|
||
Vector3 rayOrigin, Vector3 rayDir,
|
||
out double t)
|
||
{
|
||
t = 0.0;
|
||
|
||
// denom = dot(rayDir, plane.Normal)
|
||
float denom = Vector3.Dot(rayDir, plane.Normal);
|
||
if (MathF.Abs(denom) < Epsilon)
|
||
return false; // ray is (nearly) parallel to plane
|
||
|
||
// numerator = −(dot(rayOrigin, plane.Normal) + plane.D) / denom
|
||
// Note: retail uses a negated constant (_DAT_0079cc48 = −1.0) then
|
||
// multiplies, which is equivalent to flipping the sign of
|
||
// (dot(origin,N) + D).
|
||
float num = -(Vector3.Dot(rayOrigin, plane.Normal) + plane.D);
|
||
float tVal = num / denom;
|
||
|
||
t = tVal;
|
||
return tVal >= 0f;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 3. calc_normal — FUN_00539110
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Computes the face normal and plane-distance for an N-gon described by
|
||
/// an ordered list of vertex positions, storing the result in
|
||
/// <paramref name="normal"/> and <paramref name="planeD"/>.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_00539110 @ 0x00539110</c>.
|
||
/// The algorithm accumulates a Newell-style cross-product sum across all
|
||
/// (fan) triangle pairs, then normalises to obtain the unit normal. The
|
||
/// plane-distance is the average dot-product of all vertices with the
|
||
/// normal, negated — matching how AC stores <c>Plane.D</c>.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="vertices">Polygon vertices in order (≥ 3 required).</param>
|
||
/// <param name="normal">Computed unit normal (zero-vector if degenerate).</param>
|
||
/// <param name="planeD">
|
||
/// Plane constant D such that <c>dot(N,p) + D == 0</c> on the surface.
|
||
/// </param>
|
||
public static void CalcNormal(
|
||
ReadOnlySpan<Vector3> vertices,
|
||
out Vector3 normal,
|
||
out float planeD)
|
||
{
|
||
normal = Vector3.Zero;
|
||
planeD = 0f;
|
||
|
||
int n = vertices.Length;
|
||
if (n < 3)
|
||
return;
|
||
|
||
// Accumulate cross-product contributions (Newell method)
|
||
float accX = 0f, accY = 0f, accZ = 0f;
|
||
var v0 = vertices[0];
|
||
for (int i = 1; i < n - 1; i++)
|
||
{
|
||
var vi = vertices[i];
|
||
var vi1 = vertices[i + 1];
|
||
|
||
// edge a = vi − v0, edge b = vi1 − v0
|
||
float ax = vi.X - v0.X, ay = vi.Y - v0.Y, az = vi.Z - v0.Z;
|
||
float bx = vi1.X - v0.X, by = vi1.Y - v0.Y, bz = vi1.Z - v0.Z;
|
||
|
||
accX += ay * bz - az * by;
|
||
accY += az * bx - ax * bz;
|
||
accZ += ax * by - ay * bx;
|
||
}
|
||
|
||
float len = MathF.Sqrt(accX * accX + accY * accY + accZ * accZ);
|
||
if (len < EpsilonSq)
|
||
return;
|
||
|
||
float invLen = 1f / len;
|
||
normal = new Vector3(accX * invLen, accY * invLen, accZ * invLen);
|
||
|
||
// planeD = −(average dot(normal, vertex))
|
||
float dotSum = 0f;
|
||
for (int i = 0; i < n; i++)
|
||
dotSum += Vector3.Dot(normal, vertices[i]);
|
||
|
||
planeD = -(dotSum / n);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 4. sphere_intersects_poly — FUN_00539500
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Returns <see langword="true"/> when a sphere overlaps (or just touches)
|
||
/// a convex polygon on the positive-normal side.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_00539500 @ 0x00539500</c>.
|
||
/// On success the contact point projected onto the polygon plane is written
|
||
/// into <paramref name="contactPoint"/>.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The algorithm:
|
||
/// <list type="number">
|
||
/// <item>Project sphere centre onto the polygon plane; if the signed
|
||
/// distance exceeds the radius the sphere cannot touch.</item>
|
||
/// <item>Walk each directed edge; if the projected contact lies outside
|
||
/// an edge check whether it is within the sphere's "inflated" radius —
|
||
/// returning immediately when an edge vertex is inside the sphere.</item>
|
||
/// </list>
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="polyPlane">Plane of the polygon (normal + D).</param>
|
||
/// <param name="vertices">Polygon vertices in winding order.</param>
|
||
/// <param name="sphereCenter">Centre of the sphere.</param>
|
||
/// <param name="sphereRadius">Radius of the sphere.</param>
|
||
/// <param name="contactPoint">
|
||
/// Projected contact point on the polygon plane (valid when result is
|
||
/// <see langword="true"/>).
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when the sphere touches the polygon face.
|
||
/// </returns>
|
||
public static bool SphereIntersectsPoly(
|
||
Plane polyPlane,
|
||
ReadOnlySpan<Vector3> vertices,
|
||
Vector3 sphereCenter, float sphereRadius,
|
||
out Vector3 contactPoint)
|
||
{
|
||
contactPoint = Vector3.Zero;
|
||
|
||
// Signed distance from sphere centre to plane
|
||
float dist = Vector3.Dot(polyPlane.Normal, sphereCenter) + polyPlane.D;
|
||
float rad = sphereRadius - Epsilon;
|
||
|
||
if (MathF.Abs(dist) > rad)
|
||
return false;
|
||
|
||
// Project sphere centre onto the plane
|
||
contactPoint = sphereCenter - polyPlane.Normal * dist;
|
||
|
||
float radSq = rad * rad - dist * dist; // available slack² for edge tests
|
||
|
||
int numVerts = vertices.Length;
|
||
if (numVerts == 0)
|
||
return true;
|
||
|
||
bool inside = true; // tracks whether contact point is unambiguously inside all edges
|
||
int prevIdx = numVerts - 1;
|
||
|
||
for (int i = 0; i < numVerts; i++)
|
||
{
|
||
var v0 = vertices[prevIdx];
|
||
var v1 = vertices[i];
|
||
prevIdx = i;
|
||
|
||
// edge vector and cross-product with plane normal (perpendicular-to-edge in the plane)
|
||
var edge = v1 - v0;
|
||
var disp = contactPoint - v0;
|
||
|
||
// edgePerp = edge × normal projected into plane ≈ cross(normal, edge) negated
|
||
var edgePerp = new Vector3(
|
||
edge.Z * polyPlane.Normal.Y - edge.Y * polyPlane.Normal.Z,
|
||
edge.X * polyPlane.Normal.Z - edge.Z * polyPlane.Normal.X,
|
||
edge.Y * polyPlane.Normal.X - edge.X * polyPlane.Normal.Y);
|
||
|
||
float dp = Vector3.Dot(disp, edgePerp);
|
||
|
||
if (dp < 0f)
|
||
{
|
||
// Contact point is outside this edge
|
||
float edgePerpLenSq = edgePerp.LengthSquared();
|
||
if (edgePerpLenSq * radSq < dp * dp)
|
||
return false; // too far outside to be reached by sphere radius
|
||
|
||
// Check if closest edge vertex is within sphere
|
||
float dispEdge = Vector3.Dot(disp, edge);
|
||
float edgeLenSq = edge.LengthSquared();
|
||
if (dispEdge >= 0f && dispEdge < edgeLenSq)
|
||
return true; // closest point on edge is inside sphere
|
||
|
||
inside = false;
|
||
}
|
||
|
||
// Vertex distance check
|
||
if (disp.LengthSquared() <= radSq)
|
||
return true;
|
||
}
|
||
|
||
return inside;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 5. find_time_of_collision — FUN_00539ba0
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Finds the parametric time at which a moving sphere (radius embedded in
|
||
/// <paramref name="sphere"/>[3]) first intersects a convex polygon.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_00539ba0 @ 0x00539BA0</c>.
|
||
/// The sphere travels from <paramref name="sphere"/>[0..2] in direction
|
||
/// <paramref name="rayDir"/>[0..2]. The radius is <paramref name="sphere"/>[3].
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Internally:
|
||
/// <list type="number">
|
||
/// <item>Compute the time <c>t</c> at which the sphere's centre reaches
|
||
/// the offset plane (plane pushed out by radius).</item>
|
||
/// <item>Walk each directed edge to verify the intersection point
|
||
/// actually lies inside the polygon (with sphere-radius tolerance at
|
||
/// edges).</item>
|
||
/// </list>
|
||
/// Returns <see langword="false"/> when the ray is parallel to the plane,
|
||
/// when the polygon has zero vertices, or when the intersection is behind
|
||
/// the ray.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="polyPlane">Plane of the polygon.</param>
|
||
/// <param name="vertices">Polygon vertices in winding order.</param>
|
||
/// <param name="sphereOrigin">Start position of the sphere centre.</param>
|
||
/// <param name="sphereRadius">Radius of the sphere.</param>
|
||
/// <param name="rayDir">Normalised movement direction of the sphere.</param>
|
||
/// <param name="t">
|
||
/// Parametric collision time on success. Sign convention matches the
|
||
/// retail binary: <c>t = (dot(origin,N) + D) / dot(dir,N)</c>. When
|
||
/// the ray is approaching the surface from above (dir·N < 0) the
|
||
/// returned t is negative; apply as <c>contact = origin − dir*t</c>.
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when a collision is found.
|
||
/// </returns>
|
||
public static bool FindTimeOfCollision(
|
||
Plane polyPlane,
|
||
ReadOnlySpan<Vector3> vertices,
|
||
Vector3 sphereOrigin, float sphereRadius,
|
||
Vector3 rayDir,
|
||
out float t)
|
||
{
|
||
t = 0f;
|
||
|
||
// dot(rayDir, planeNormal) — denominator
|
||
float denom = Vector3.Dot(rayDir, polyPlane.Normal);
|
||
if (MathF.Abs(denom) < Epsilon)
|
||
return false;
|
||
|
||
int numVerts = vertices.Length;
|
||
|
||
// t = (dot(origin, N) + D) / dot(dir, N)
|
||
// Note: sign is such that contact = origin − dir*t lands on the plane.
|
||
float num = Vector3.Dot(sphereOrigin, polyPlane.Normal) + polyPlane.D;
|
||
t = num / denom;
|
||
|
||
// Pre-compute the contact centre (constant per-call)
|
||
var contact = sphereOrigin - rayDir * t;
|
||
|
||
float radSq = sphereRadius * sphereRadius;
|
||
|
||
bool inside = true;
|
||
int prevIdx = numVerts - 1;
|
||
|
||
for (int i = 0; i < numVerts; i++)
|
||
{
|
||
var v0 = vertices[prevIdx];
|
||
var v1 = vertices[i];
|
||
prevIdx = i;
|
||
|
||
var edge = v1 - v0;
|
||
var dispFromV0 = contact - v0;
|
||
|
||
// edgePerp = cross(N, edge) — inward-facing perpendicular to edge in the polygon plane
|
||
var edgePerp = new Vector3(
|
||
edge.Z * polyPlane.Normal.Y - edge.Y * polyPlane.Normal.Z,
|
||
edge.X * polyPlane.Normal.Z - edge.Z * polyPlane.Normal.X,
|
||
edge.Y * polyPlane.Normal.X - edge.X * polyPlane.Normal.Y);
|
||
|
||
float dp = Vector3.Dot(dispFromV0, edgePerp);
|
||
|
||
if (dp < 0f)
|
||
{
|
||
float edgePerpLenSq = edgePerp.LengthSquared();
|
||
if (edgePerpLenSq * radSq < dp * dp)
|
||
return false;
|
||
|
||
float dispEdge = Vector3.Dot(dispFromV0, edge);
|
||
float edgeLenSq = edge.LengthSquared();
|
||
if (dispEdge >= 0f && dispEdge < edgeLenSq)
|
||
return true;
|
||
|
||
inside = false;
|
||
}
|
||
|
||
float dispLenSq = dispFromV0.LengthSquared();
|
||
if (dispLenSq < radSq)
|
||
return true;
|
||
}
|
||
|
||
// Retail returns the value held in the "inside" tracking variable (1 or 0 pointer)
|
||
return inside;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 6. hits_walkable — FUN_0053a230
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Returns <see langword="true"/> when a sphere touches a walkable polygon
|
||
/// and the movement direction has a positive component along the polygon
|
||
/// normal (i.e. the sphere is approaching from below / from the correct
|
||
/// side).
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_0053a230 @ 0x0053A230</c>.
|
||
/// Delegates to <see cref="SphereIntersectsPoly"/>; the "walkable" check
|
||
/// additionally requires that <c>dot(polyNormal, movementDir) ≥ 0</c>.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="polyPlane">Plane of the polygon.</param>
|
||
/// <param name="vertices">Polygon vertices.</param>
|
||
/// <param name="sphereCenter">Centre of the sphere.</param>
|
||
/// <param name="sphereRadius">Radius of the sphere.</param>
|
||
/// <param name="movementDir">Normalised movement direction of the sphere.</param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when the polygon is hit and is walkable from
|
||
/// the movement direction.
|
||
/// </returns>
|
||
public static bool HitsWalkable(
|
||
Plane polyPlane,
|
||
ReadOnlySpan<Vector3> vertices,
|
||
Vector3 sphereCenter, float sphereRadius,
|
||
Vector3 movementDir)
|
||
{
|
||
// The retail function checks sphere_intersects_solid (FUN_00539750)
|
||
// which is the two-sided version of sphere_intersects_poly. For our
|
||
// purposes (physics/walkable check) the single-sided version is
|
||
// sufficient. The walkable side-check is: dot(normal, movementDir) > 0.
|
||
if (Vector3.Dot(polyPlane.Normal, movementDir) < 0f)
|
||
return false;
|
||
|
||
return SphereIntersectsPoly(polyPlane, vertices, sphereCenter, sphereRadius, out _);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 7. find_walkable_collision — FUN_0053a040
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Finds a walkable surface collision for a moving sphere and, on hit,
|
||
/// writes the outward-facing edge normal of the first crossed edge into
|
||
/// <paramref name="edgeNormal"/>.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_0053a040 @ 0x0053A040</c>.
|
||
/// Unlike <see cref="SphereIntersectsPoly"/> (which returns the face
|
||
/// contact point), this variant is interested in <em>which edge</em> was
|
||
/// crossed during movement — useful for computing the slide direction.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The algorithm:
|
||
/// <list type="number">
|
||
/// <item>Verify the movement direction has a non-zero component along
|
||
/// the plane normal.</item>
|
||
/// <item>Compute the ray-plane intersection time and project the contact
|
||
/// centre onto the plane.</item>
|
||
/// <item>Walk each edge; if the contact lies outside an edge, compute
|
||
/// and normalise the edge perpendicular and return it.</item>
|
||
/// </list>
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="polyPlane">Plane of the polygon.</param>
|
||
/// <param name="vertices">Polygon vertices.</param>
|
||
/// <param name="sphereOrigin">Sphere start position.</param>
|
||
/// <param name="movementDir">Sphere movement direction.</param>
|
||
/// <param name="edgeNormal">
|
||
/// Outward-facing (normalised) edge normal on a crossed edge.
|
||
/// Populated only when the method returns <see langword="true"/>.
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when a crossed edge was found (sphere would
|
||
/// slide off the polygon).
|
||
/// </returns>
|
||
public static bool FindWalkableCollision(
|
||
Plane polyPlane,
|
||
ReadOnlySpan<Vector3> vertices,
|
||
Vector3 sphereOrigin,
|
||
Vector3 movementDir,
|
||
out Vector3 edgeNormal)
|
||
{
|
||
edgeNormal = Vector3.Zero;
|
||
|
||
float denom = Vector3.Dot(polyPlane.Normal, movementDir);
|
||
if (MathF.Abs(denom) < Epsilon)
|
||
return false;
|
||
|
||
int numVerts = vertices.Length;
|
||
|
||
// Ray-plane time
|
||
float t = (Vector3.Dot(polyPlane.Normal, sphereOrigin) + polyPlane.D) / denom;
|
||
|
||
// Contact centre projected onto polygon plane
|
||
var contact = sphereOrigin - movementDir * t;
|
||
|
||
int prevIdx = numVerts - 1;
|
||
|
||
for (int i = 0; i < numVerts; i++)
|
||
{
|
||
var v0 = vertices[prevIdx];
|
||
var v1 = vertices[i];
|
||
prevIdx = i;
|
||
|
||
var edge = v1 - v0;
|
||
var disp = contact - v0;
|
||
|
||
// edgePerp = cross(edge, normal) projected [= normal × edge in retail]
|
||
var nx = polyPlane.Normal.X;
|
||
var ny = polyPlane.Normal.Y;
|
||
var nz = polyPlane.Normal.Z;
|
||
|
||
var epX = edge.Z * ny - edge.Y * nz;
|
||
var epY = edge.X * nz - edge.Z * nx;
|
||
var epZ = edge.Y * nx - edge.X * ny;
|
||
|
||
float dp = disp.X * epX + disp.Y * epY + disp.Z * epZ;
|
||
|
||
if (dp < 0f)
|
||
{
|
||
// Contact point is outside this edge — this is the crossed edge
|
||
var raw = new Vector3(epX, epY, epZ);
|
||
float len = raw.Length();
|
||
if (len < EpsilonSq)
|
||
return false;
|
||
|
||
edgeNormal = raw / len;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 8. slide_sphere — FUN_00538eb0
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Computes the parametric distance a sphere must travel to reach a plane
|
||
/// when sliding along it, taking into account the sphere's radius.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_00538eb0 @ 0x00538EB0</c>.
|
||
/// Returns a positive value when the sphere should move toward the plane,
|
||
/// a negative value when it is moving away, or
|
||
/// <see cref="float.MaxValue"/> / <c>0f</c> for degenerate cases.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The retail code uses struct offsets into a "Plane" at +0x20 (normal
|
||
/// XYZ) and +0x2C (D). A second struct at +0x0C holds the sphere radius.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="plane">The plane to slide against (normal + D).</param>
|
||
/// <param name="sphereRadius">Radius of the sphere.</param>
|
||
/// <param name="sphereCenter">Current sphere centre.</param>
|
||
/// <param name="movementDir">Desired movement direction (unit vector).</param>
|
||
/// <returns>
|
||
/// Parametric distance along <paramref name="movementDir"/> to the plane
|
||
/// surface, accounting for sphere radius.
|
||
/// Returns <see cref="float.MaxValue"/> when the sphere is already flush
|
||
/// with the plane; returns <c>0f</c> when the movement is perpendicular.
|
||
/// </returns>
|
||
public static float SlideSphere(
|
||
Plane plane, float sphereRadius,
|
||
Vector3 sphereCenter, Vector3 movementDir)
|
||
{
|
||
// Signed distance from sphere centre to plane
|
||
float dist = Vector3.Dot(sphereCenter, plane.Normal) + plane.D;
|
||
|
||
if (MathF.Abs(dist) < sphereRadius)
|
||
return float.MaxValue; // already touching — no slide needed
|
||
|
||
float denom = Vector3.Dot(movementDir, plane.Normal);
|
||
if (MathF.Abs(denom) < Epsilon)
|
||
return 0f; // movement is parallel to plane
|
||
|
||
// offset = ±radius (sign chosen by which side the sphere is on)
|
||
float offset = dist <= 0f ? -sphereRadius : sphereRadius;
|
||
|
||
return (offset - dist) / denom;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 8b. SweptSphereHitsSphere — CSphere::intersects_sphere narrow-phase
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Returns <see langword="true"/> when a moving sphere first intersects a
|
||
/// stationary sphere within the movement step, and the parametric contact
|
||
/// time <paramref name="t"/> is in (0, 1].
|
||
///
|
||
/// <para>
|
||
/// Ported from <c>CSphere::FindTimeOfCollision</c> in
|
||
/// <c>ACE.Server/Physics/Sphere.cs</c>, which is a line-for-line C# port
|
||
/// of retail's <c>CSphere::intersects_sphere @ 0x00537A80</c> (the
|
||
/// "collide ≠ 0, not creature" branch at <c>0x00537B8C</c>).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The retail quadratic (from the decomp):
|
||
/// <list type="bullet">
|
||
/// <item><c>distSq = |movement|²</c> — squared length of sweep vector.</item>
|
||
/// <item><c>gap = |spherePos|² − radSum²</c> — positive when centers
|
||
/// are separated, negative when already overlapping.</item>
|
||
/// <item><c>similar = −dot(spherePos, movement)</c> — projection of the
|
||
/// separation onto the movement direction.</item>
|
||
/// <item><c>disc = similar² − gap·distSq</c> — discriminant.</item>
|
||
/// <item>Pick the earlier root, normalise by <c>distSq</c>.</item>
|
||
/// </list>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Returns <see langword="false"/> when the spheres are already overlapping
|
||
/// (<c>gap < ε</c>), the discriminant is negative (miss), the movement
|
||
/// is degenerate, or the contact time is outside (0, 1].
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="moverCenter">
|
||
/// World-space centre of the moving sphere at the START of the step.
|
||
/// </param>
|
||
/// <param name="moverRadius">Radius of the moving sphere.</param>
|
||
/// <param name="sweepDelta">
|
||
/// Movement vector: <c>checkPos − currCenter</c>.
|
||
/// </param>
|
||
/// <param name="targetCenter">
|
||
/// World-space centre of the stationary target sphere.
|
||
/// </param>
|
||
/// <param name="targetRadius">Radius of the target sphere.</param>
|
||
/// <param name="t">
|
||
/// On success: parametric fraction of <paramref name="sweepDelta"/> at
|
||
/// which the sphere surfaces first touch (in (0, 1]).
|
||
/// Undefined on failure.
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when the mover hits the target within this step.
|
||
/// </returns>
|
||
public static bool SweptSphereHitsSphere(
|
||
Vector3 moverCenter, float moverRadius,
|
||
Vector3 sweepDelta,
|
||
Vector3 targetCenter, float targetRadius,
|
||
out float t)
|
||
{
|
||
t = 0f;
|
||
|
||
// movement = sweepDelta (mover travels from moverCenter by this vector)
|
||
// spherePos = targetCenter − moverCenter (target relative to mover start)
|
||
// radSum = combined radius for first-surface-contact
|
||
float radSum = moverRadius + targetRadius;
|
||
|
||
float mx = sweepDelta.X, my = sweepDelta.Y, mz = sweepDelta.Z;
|
||
float distSq = mx * mx + my * my + mz * mz;
|
||
if (distSq < EpsilonSq)
|
||
return false; // degenerate sweep (stationary mover)
|
||
|
||
float sx = targetCenter.X - moverCenter.X;
|
||
float sy = targetCenter.Y - moverCenter.Y;
|
||
float sz = targetCenter.Z - moverCenter.Z;
|
||
|
||
// gap = |spherePos|² − radSum²
|
||
// Positive → centers are separated (the common case).
|
||
// Negative → already overlapping → treat as no forward collision (retail returns -1).
|
||
float gap = sx * sx + sy * sy + sz * sz - radSum * radSum;
|
||
if (gap < EpsilonSq)
|
||
return false; // already overlapping — use static test separately
|
||
|
||
// similar = −dot(spherePos, movement)
|
||
// Positive when the sphere is in FRONT of us (moving toward it).
|
||
float similar = -(sx * mx + sy * my + sz * mz);
|
||
|
||
// discriminant = similar² − gap · distSq
|
||
float disc = similar * similar - gap * distSq;
|
||
if (disc < 0f)
|
||
return false; // ray misses the combined-radius sphere entirely
|
||
|
||
float cDist = MathF.Sqrt(disc);
|
||
|
||
// Pick the nearer root. ACE mirrors retail (Sphere.cs::FindTimeOfCollision):
|
||
// if (similar − cDist < 0) → return −1 × (cDist + similar) / distSq
|
||
// else → return −1 × (similar − cDist) / distSq
|
||
// The −1 negation converts from ACE's "closest-approach" parameterisation
|
||
// back to a forward t ∈ (0,1] (positive = hit ahead of mover).
|
||
float root = (similar - cDist < 0f) ? -(cDist + similar) : -(similar - cDist);
|
||
|
||
// Normalise to [0, 1] scale
|
||
t = root / distSq;
|
||
|
||
// t ≤ 0: contact is behind / at the start (already handled by gap check).
|
||
// t > 1: contact is beyond this movement step — miss.
|
||
return t > 0f && t <= 1f;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 9. land_on_sphere — FUN_00538f50
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Steps a sphere "down" onto a plane surface, updating its centre in
|
||
/// place when the landing is valid.
|
||
///
|
||
/// <para>
|
||
/// Decompiled from <c>FUN_00538f50 @ 0x00538F50</c>.
|
||
/// The sphere tries to land on the plane by moving
|
||
/// <c>−radius·normal</c> (toward the surface). The move is accepted only
|
||
/// when:
|
||
/// <list type="bullet">
|
||
/// <item>The movement direction has a non-zero component along the normal
|
||
/// (sphere is not travelling parallel to the surface).</item>
|
||
/// <item>The resulting interpolation factor lies in the valid range
|
||
/// <c>[−0.1, walkInterp)</c>.</item>
|
||
/// </list>
|
||
/// When accepted, <paramref name="sphereCenter"/> is modified and
|
||
/// <paramref name="walkInterp"/> is updated.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="plane">Surface plane (normal + D).</param>
|
||
/// <param name="sphereRadius">Radius of the sphere.</param>
|
||
/// <param name="sphereCenter">
|
||
/// Sphere centre — updated in place when landing succeeds.
|
||
/// </param>
|
||
/// <param name="movementDir">
|
||
/// Current movement direction (updated in place).
|
||
/// </param>
|
||
/// <param name="walkInterp">
|
||
/// Walk interpolation factor. Updated to the new interp value when
|
||
/// landing is accepted.
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when the sphere successfully landed on the
|
||
/// plane.
|
||
/// </returns>
|
||
public static bool LandOnSphere(
|
||
Plane plane, float sphereRadius,
|
||
ref Vector3 sphereCenter, ref Vector3 movementDir,
|
||
ref float walkInterp)
|
||
{
|
||
// Signed distance from sphere centre to plane
|
||
float distToPlane = Vector3.Dot(sphereCenter, plane.Normal) + plane.D;
|
||
|
||
float denom = Vector3.Dot(movementDir, plane.Normal);
|
||
|
||
// Retail logic (FUN_00538f50):
|
||
// if denom > +Eps → moving away from surface → use (-r - dist) / denom
|
||
// if denom in [−Eps, +Eps] → parallel, return 0
|
||
// if denom < −Eps → moving toward surface → use (dist - r) / denom
|
||
float tLand;
|
||
if (denom > Epsilon)
|
||
{
|
||
// Moving away from surface (along positive normal direction)
|
||
tLand = (-sphereRadius - distToPlane) / denom;
|
||
}
|
||
else if (denom >= -Epsilon)
|
||
{
|
||
// Parallel to plane
|
||
return false;
|
||
}
|
||
else
|
||
{
|
||
// Moving toward surface (against positive normal direction)
|
||
tLand = (distToPlane - sphereRadius) / denom;
|
||
}
|
||
|
||
// Retail check: newInterp must be in [−0.5, walkInterp)
|
||
// (_DAT_007ca630 ≈ −0.5 from usage context in FUN_00538f50)
|
||
float newInterp = (1f - tLand) * walkInterp;
|
||
if (newInterp >= walkInterp || newInterp < -0.5f)
|
||
return false;
|
||
|
||
// Apply the landing
|
||
sphereCenter -= movementDir * tLand;
|
||
walkInterp = newInterp;
|
||
return true;
|
||
}
|
||
}
|