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>
5.3 KiB
CSphere::intersects_sphere — swept-sphere-vs-sphere pseudocode
Date: 2026-06-24
Task: Task 2 — true sphere collision primitive (collision-inclusion phase)
Oracles consulted
-
Named-retail decomp
acclient_2013_pseudo_c.txt:CSphere::collides_with_sphere@0x005369E0— static overlap testCSphere::intersects_sphere(primary) @0x00537A80— the full 6-path dispatcherCSphere::intersects_sphere(Position variant) @0x00537FD0
-
ACE C# port
references/ACE/Source/ACE.Server/Physics/Sphere.cs:CollidesWithSphere(Vector3 otherSphere, float radsum)— static overlapFindTimeOfCollision(Vector3 movement, Vector3 spherePos, float radSum)— swept solveIntersectsSphere(Vector3 center, float radius, Transition transition, bool isCreature)— 6-path dispatcher
CSphere::collides_with_sphere (static overlap test)
Retail @ 0x005369E0:
collides_with_sphere(this, disp_vec3, radsum_float):
lenSq = disp_vec3.x² + disp_vec3.y² + disp_vec3.z²
if radsum² > lenSq: // i.e. lenSq < radsum²
return 1 (true — overlapping)
return 0 (false)
ACE equivalent: disp.LengthSquared() <= radsum * radsum
Note: retail uses > (strictly greater-than radsum²), ACE uses <=. These are the same predicate — the
retail FPU instruction emits "collides" when radsum² is NOT less than lenSq, which is lenSq <= radsum².
FindTimeOfCollision (swept quadratic, from ACE)
ACE Sphere.FindTimeOfCollision(Vector3 movement, Vector3 spherePos, float radSum):
Interprets "mover starts at origin, travels by movement; target is at spherePos relative to mover".
distSq = |movement|² // if < EPSILON: no sweep (degenerate), return -1
nonCollide = |spherePos|² - radSum² // if < EPSILON: already overlapping → no forward collision needed, return -1
similar = -dot(spherePos, movement) // projection of separation onto movement direction
disc = similar² - nonCollide * distSq // discriminant of quadratic
if disc < 0: return -1 // no real intersection
cDist = sqrt(disc)
if similar - cDist < 0:
return -(cDist + similar) / distSq
else:
return -(similar - cDist) / distSq
This returns a time in the range [0, 1] for the first contact. A return of -1 means no hit (miss or already overlapping). Values > 1 mean the sweep doesn't reach the target within the movement step.
SweptSphereHitsSphere — our primitive (pure function)
Wraps FindTimeOfCollision with a clean bool/out API for the narrow-phase dispatch:
SweptSphereHitsSphere(moverCenter, moverRadius, sweepDelta, targetCenter, targetRadius, out float t):
movement = sweepDelta // vector the mover travels
spherePos = targetCenter - moverCenter // target relative to mover's start
radSum = moverRadius + targetRadius
t = (float) FindTimeOfCollision(movement, spherePos, radSum)
return t > 0 && t <= 1
t is the parametric fraction of sweepDelta at which surfaces first touch.
t <= 0: target is behind or already overlapping (use static test separately).
t > 1: sweep misses (target too far in this step).
Retail dispatch order for Sphere objects
From CSphere::intersects_sphere @ 0x00537A80 — the same 6-path structure as for CylSpheres:
-
obstruction_ethereal || insert_type == PLACEMENT_INSERT:
Static overlap test only (collides_with_sphere). Return Collided or OK. -
step_down != 0:
Delegates tostep_sphere_down(for non-creature movers). -
check_walkable != 0:
Static overlap test. Return Collided or OK. -
collide == 0:
Sub-dispatch onobject_info.state & 3(Contact/OnWalkable):- Contact: step_sphere_up or slide_sphere
- PathClipped: collide_with_point
- Default: land_on_sphere or collide_with_point
-
collide != 0+isCreature:
Return OK (creatures don't block each other via sphere-sphere in this path). -
collide != 0+ not creature:
Full swept quadratic. Set contact plane, adjust check_pos.
For our narrow-phase dispatch in FindObjCollisionsInCell, the "narrow-phase Sphere branch"
maps directly to ACE's IntersectsSphere — which acdream already implements for Cylinder objects
via CylinderCollision. The sphere primitive just provides the swept check without the cylinder's
height clipping.
Acdream adaptation note
The SweptSphereHitsSphere primitive is PURE (no Transition state). The actual 6-path dispatch
(step-up, land-on, slide, etc.) is handled by the existing CylinderCollision infrastructure —
for Sphere-typed shadow entries we call through the same dispatcher after the overlap check,
using 3-D distance for the broad-phase (not XY-only cylinder distance).
The primitive's narrow phase: static overlap (CollidesWithSphere) is the gate; the swept
quadratic from FindTimeOfCollision resolves the time-of-contact for the walkable landing path.
For the initial ship (Task 2), we implement the static overlap test in the dispatch
(matching the obstruction_ethereal/check_walkable/Contact paths that don't use the swept
form), plus SweptSphereHitsSphere for the swept narrow-phase. The full 6-path wiring for
sphere objects mirrors the cylinder path already in CylinderCollision, extended to use 3-D
distance instead of XY-only.