using System.Numerics; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.Core.Physics; /// /// BSP tree traversal for sphere-polygon collision detection. /// /// /// Ported from decompiled FUN_00539270 (chunk_00539000.c), cross-referenced /// against ACE's BSPNode.sphere_intersects_poly() in /// Source/ACE.Server/Physics/BSP/BSPNode.cs. /// /// /// /// The algorithm is a recursive descent through the BSP tree: /// /// Broad phase: discard the subtree if the sphere cannot reach the /// node's bounding sphere. /// Leaf: test each polygon using the existing retail-ported /// . /// Internal: classify the sphere against the splitting plane and /// recurse into the positive half, the negative half, or both when the /// sphere straddles the plane. /// /// /// public static class BSPQuery { /// /// Test if a sphere intersects any polygon in the physics BSP tree. /// Returns on the first hit, populating /// and . /// /// /// Ported from FUN_00539270; cross-ref ACE BSPNode.sphere_intersects_poly. /// /// /// Current BSP node (null-safe). /// Physics polygon dictionary from the GfxObj. /// Vertex array from the GfxObj. /// Sphere centre in object-local space. /// Sphere radius. /// Polygon id of the first intersecting polygon (0 on miss). /// Outward normal at the hit point (zero on miss). /// if any polygon is hit. public static bool SphereIntersectsPoly( PhysicsBSPNode? node, Dictionary polygons, VertexArray vertices, Vector3 sphereCenter, float sphereRadius, out ushort hitPolyId, out Vector3 hitNormal) { hitPolyId = 0; hitNormal = Vector3.Zero; if (node is null) return false; // ---------------------------------------------------------------- // Broad phase: reject the whole subtree when the sphere cannot // reach the node's bounding sphere. Both Leaf and internal nodes // carry a BoundingSphere in the retail format. // ---------------------------------------------------------------- { float dist = Vector3.Distance(sphereCenter, node.BoundingSphere.Origin); if (dist > sphereRadius + node.BoundingSphere.Radius + CollisionPrimitives.Epsilon) return false; } // ---------------------------------------------------------------- // Leaf node: test each referenced polygon against the sphere using // the retail-ported CollisionPrimitives.SphereIntersectsPoly. // ---------------------------------------------------------------- if (node.Type == BSPNodeType.Leaf) { foreach (var polyIdx in node.Polygons) { if (!polygons.TryGetValue(polyIdx, out var poly)) continue; if (poly.VertexIds.Count < 3) continue; // Gather polygon vertices from the vertex array. var polyVerts = new Vector3[poly.VertexIds.Count]; bool allFound = true; for (int i = 0; i < poly.VertexIds.Count; i++) { ushort vid = (ushort)poly.VertexIds[i]; if (vertices.Vertices.TryGetValue(vid, out var sv)) polyVerts[i] = sv.Origin; else { allFound = false; break; } } if (!allFound) continue; // Compute the polygon plane using the retail CalcNormal port. CollisionPrimitives.CalcNormal(polyVerts, out var normal, out float planeD); if (normal.LengthSquared() < CollisionPrimitives.EpsilonSq) continue; var polyPlane = new Plane(normal, planeD); if (CollisionPrimitives.SphereIntersectsPoly( polyPlane, polyVerts, sphereCenter, sphereRadius, out _)) { hitPolyId = polyIdx; hitNormal = normal; return true; } } return false; } // ---------------------------------------------------------------- // Internal node: classify sphere against splitting plane and // recurse into the positive side, negative side, or both. // // System.Numerics.Plane convention: dot(N, p) + D = 0 on the // surface, so signed distance = dot(N, center) + D. // FUN_00539270 uses the same sign convention. // ---------------------------------------------------------------- float splitDist = Vector3.Dot(node.SplittingPlane.Normal, sphereCenter) + node.SplittingPlane.D; float reach = sphereRadius - CollisionPrimitives.Epsilon; if (splitDist >= reach) { // Sphere entirely on the positive side. return SphereIntersectsPoly( node.PosNode, polygons, vertices, sphereCenter, sphereRadius, out hitPolyId, out hitNormal); } if (splitDist <= -reach) { // Sphere entirely on the negative side. return SphereIntersectsPoly( node.NegNode, polygons, vertices, sphereCenter, sphereRadius, out hitPolyId, out hitNormal); } // Sphere straddles the plane — check both sides, return on first hit. if (SphereIntersectsPoly(node.PosNode, polygons, vertices, sphereCenter, sphereRadius, out hitPolyId, out hitNormal)) return true; return SphereIntersectsPoly(node.NegNode, polygons, vertices, sphereCenter, sphereRadius, out hitPolyId, out hitNormal); } }