using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; namespace AcDream.Core.Selection; /// /// Indoor walking Phase 1 (2026-05-19). Pure ray-vs-cell-BSP-polygon /// occlusion test. Given a ray and a set of /// (currently-loaded EnvCells with resolved polygon planes), returns /// the nearest world-space t along the ray that hits any cell /// polygon — or if the ray clears /// all cells. /// /// /// Used by to filter entities that sit /// behind a wall from the camera's POV (issue #86). Möller-Trumbore /// ray-triangle intersection; one test per triangle. Cells are /// transformed via their /// so the ray runs in cell-local space and the resolved-polygon /// vertices don't need re-transformation per query. /// /// /// /// No BSP traversal — iterates every polygon in every cell. Cell count /// in a Holtburg-radius-4 streaming window is ~80 cells × ~50 polys /// each = ~4K triangles. Möller-Trumbore is ~40 ns per triangle on /// modern hardware; one Pick call is well under 1 ms. /// /// public static class CellBspRayOccluder { /// /// Returns the nearest positive t such that /// origin + t * direction intersects a polygon in any cell. /// Returns if no cell polygon /// is intersected. /// /// Need not be normalized; returned t /// scales with direction length the same as a parametric ray. public static float NearestWallT( Vector3 origin, Vector3 direction, IEnumerable loadedCells) { if (loadedCells is null) return float.PositiveInfinity; float bestT = float.PositiveInfinity; foreach (var cell in loadedCells) { if (cell?.Resolved is null) continue; // Bring the ray into cell-local space ONCE per cell. var localOrigin = Vector3.Transform(origin, cell.InverseWorldTransform); var localDirection = Vector3.TransformNormal(direction, cell.InverseWorldTransform); foreach (var (_, poly) in cell.Resolved) { // Triangulate the (possibly polygonal) face into a fan. int n = poly.NumPoints; if (n < 3 || poly.Vertices is null || poly.Vertices.Length < n) continue; for (int i = 1; i < n - 1; i++) { if (TryRayTriangle( localOrigin, localDirection, poly.Vertices[0], poly.Vertices[i], poly.Vertices[i + 1], out var t) && t < bestT) { bestT = t; } } } } return bestT; } /// /// Möller-Trumbore ray-triangle intersection. Returns true with /// t in if the ray hits the triangle /// at a positive distance. /// private static bool TryRayTriangle( Vector3 origin, Vector3 direction, Vector3 v0, Vector3 v1, Vector3 v2, out float t) { const float Epsilon = 1e-7f; var edge1 = v1 - v0; var edge2 = v2 - v0; var pvec = Vector3.Cross(direction, edge2); float det = Vector3.Dot(edge1, pvec); // No two-sided handling here — picker should be permissive so // a wall blocks regardless of which side the camera is on. if (det > -Epsilon && det < Epsilon) { t = 0f; return false; } float invDet = 1f / det; var tvec = origin - v0; float u = Vector3.Dot(tvec, pvec) * invDet; if (u < 0f || u > 1f) { t = 0f; return false; } var qvec = Vector3.Cross(tvec, edge1); float v = Vector3.Dot(direction, qvec) * invDet; if (v < 0f || u + v > 1f) { t = 0f; return false; } t = Vector3.Dot(edge2, qvec) * invDet; return t > Epsilon; } }