From b15589c72d666640b69f4485761983c007af4828 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Wed, 26 Aug 2026 05:26:49 -0700 Subject: [PATCH] [Patch] Fix octree raycast fallback dropping inside-origin hits under tight maxDistance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OctreeSystem.query's own maxDistance cull compares against its sorted ray-AABB distance, which for an inside-origin box is the far EXIT distance, not the true hit distance. A short raycast whose origin sits inside a large registered AABB would get that box dropped before PhysicsQuery's narrow phase ever recomputed the correct entry distance (0, per the documented inside-the-box contract) — reporting a miss instead of the real hit. The query is now asked for the unbounded fallback distance instead of the caller's maxDistance; the narrow phase already enforces the real cap correctly, against the recomputed entry distance. For outside-origin candidates (where the broad-phase distance already equals the true hit distance) this changes nothing but which phase does the filtering. Adds a regression test with a tight maxDistance smaller than the containing box's exit distance; verified it fails against the previous code and passes now. --- .../UntoldEngine/Physics/PhysicsQuery.swift | 10 +++++++- .../UntoldEngineTests/PhysicsQueryTests.swift | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Sources/UntoldEngine/Physics/PhysicsQuery.swift b/Sources/UntoldEngine/Physics/PhysicsQuery.swift index f0d8954a..093e9c55 100644 --- a/Sources/UntoldEngine/Physics/PhysicsQuery.swift +++ b/Sources/UntoldEngine/Physics/PhysicsQuery.swift @@ -73,11 +73,19 @@ public enum PhysicsQuery { // farther candidates even though its reported hit distance is 0 — so // inside-origin candidates are always examined and the scan never // breaks out of the sorted order early. + // + // The query itself is asked for the unbounded distance, not the + // caller's maxDistance: its own internal cull compares maxDistance + // against that same entry-or-exit value, so an inside-origin box + // whose EXIT point lies beyond a tight maxDistance would otherwise be + // dropped before the narrow phase ever sees it — even though its true + // hit distance (0, at the ray origin) is well within range. The real + // cap is enforced below, against the recomputed entry distance. var best: PhysicsRayHit? for (entity, sortedDistance) in OctreeSystem.shared.query( rayOrigin: ray.origin, rayDirection: direction, - maxDistance: maxDistance + maxDistance: fallbackUnboundedDistance ) { guard passesFilter(entity, filter), let bounds = OctreeSystem.shared.getBounds(for: entity) diff --git a/Tests/UntoldEngineTests/PhysicsQueryTests.swift b/Tests/UntoldEngineTests/PhysicsQueryTests.swift index d67fc946..8f8599d1 100644 --- a/Tests/UntoldEngineTests/PhysicsQueryTests.swift +++ b/Tests/UntoldEngineTests/PhysicsQueryTests.swift @@ -189,6 +189,29 @@ final class PhysicsQueryTests: XCTestCase { XCTAssertEqual(hit.normal, -direction) } + func testInsideBoxHitSurvivesTightMaxDistance() throws { + // Regression: the broad-phase cull inside OctreeSystem.query compares + // maxDistance against the sorted distance, which for an inside-origin + // box is its far EXIT distance, not the true hit distance (0). Passing + // the caller's maxDistance straight through to that query would drop + // this box before the narrow phase ever saw it, even though the real + // hit (distance 0, at the ray origin) is well inside a tight budget. + let bigBox = makeObstacle( + center: simd_float3(0, 0, -50), + halfExtents: simd_float3(50, 50, 100) + ) + + let direction = simd_float3(0, 0, -1) + let hit = try XCTUnwrap(PhysicsQuery.raycast( + PhysicsRay(origin: .zero, direction: direction, maxDistance: 5.0) + )) + + XCTAssertEqual(hit.entity, bigBox) + XCTAssertEqual(hit.distance, 0.0) + XCTAssertEqual(hit.position, .zero) + XCTAssertEqual(hit.normal, -direction) + } + func testFallbackMissReturnsNil() { _ = makeObstacle(center: simd_float3(0.0, 10.0, -5.0))