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>
130 lines
5.3 KiB
Markdown
130 lines
5.3 KiB
Markdown
# CSphere::intersects_sphere — swept-sphere-vs-sphere pseudocode
|
|
|
|
**Date:** 2026-06-24
|
|
**Task:** Task 2 — true sphere collision primitive (collision-inclusion phase)
|
|
|
|
---
|
|
|
|
## Oracles consulted
|
|
|
|
1. **Named-retail decomp** `acclient_2013_pseudo_c.txt`:
|
|
- `CSphere::collides_with_sphere` @ `0x005369E0` — static overlap test
|
|
- `CSphere::intersects_sphere` (primary) @ `0x00537A80` — the full 6-path dispatcher
|
|
- `CSphere::intersects_sphere` (Position variant) @ `0x00537FD0`
|
|
|
|
2. **ACE C# port** `references/ACE/Source/ACE.Server/Physics/Sphere.cs`:
|
|
- `CollidesWithSphere(Vector3 otherSphere, float radsum)` — static overlap
|
|
- `FindTimeOfCollision(Vector3 movement, Vector3 spherePos, float radSum)` — swept solve
|
|
- `IntersectsSphere(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:
|
|
|
|
1. `obstruction_ethereal || insert_type == PLACEMENT_INSERT`:
|
|
Static overlap test only (`collides_with_sphere`). Return Collided or OK.
|
|
|
|
2. `step_down != 0`:
|
|
Delegates to `step_sphere_down` (for non-creature movers).
|
|
|
|
3. `check_walkable != 0`:
|
|
Static overlap test. Return Collided or OK.
|
|
|
|
4. `collide == 0`:
|
|
Sub-dispatch on `object_info.state & 3` (Contact/OnWalkable):
|
|
- Contact: step_sphere_up or slide_sphere
|
|
- PathClipped: collide_with_point
|
|
- Default: land_on_sphere or collide_with_point
|
|
|
|
5. `collide != 0` + `isCreature`:
|
|
Return OK (creatures don't block each other via sphere-sphere in this path).
|
|
|
|
6. `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.
|