class_name SphereSweep extends RefCounted
## Shared sphere sweep utility. Wraps cast_motion + get_rest_info with
## Overgrowth-style sliding sphere refinement for accurate surface normals.
## Caches SphereShape3D by radius to avoid per-call allocations.
static var _shape_cache: Dictionary = {} ## radius → SphereShape3D
static var _params: PhysicsShapeQueryParameters3D = null
## Sweep a sphere from `from` to `to`. Returns { "point", "normal", "fraction" } or empty.
static func sweep(space: PhysicsDirectSpaceState3D, from: Vector3, to: Vector3,
radius: float, exclude: Array[RID] = [], collision_mask: int = 0xFFFFFFFF) -> Dictionary:
var motion: Vector3 = to - from
# Cache shapes by radius (snapped to 3 decimals to avoid unbounded growth)
var key: float = snappedf(radius, 0.001)
var shape: SphereShape3D
if _shape_cache.has(key):
shape = _shape_cache[key]
else:
shape = SphereShape3D.new()
shape.radius = key
_shape_cache[key] = shape
# Reuse params object
if _params == null:
_params = PhysicsShapeQueryParameters3D.new()
_params.shape = shape
_params.transform = Transform3D(Basis.IDENTITY, from)
_params.motion = motion
_params.collision_mask = collision_mask
if not exclude.is_empty():
_params.exclude = exclude
else:
_params.exclude = []
var fractions: PackedFloat32Array = space.cast_motion(_params)
if fractions.is_empty() or fractions[1] >= 1.0:
return {}
var contact_t: float = fractions[1]
var contact_pos: Vector3 = from + motion * contact_t
_params.transform = Transform3D(Basis.IDENTITY, contact_pos)
var rest: Dictionary = space.get_rest_info(_params)
if rest.is_empty():
return {
"point": contact_pos - motion.normalized() * radius,
"normal": -motion.normalized(),
"fraction": contact_t
}
var hit_point: Vector3 = rest["point"]
var hit_normal: Vector3 = rest["normal"]
# Sliding sphere refinement for accurate surface normal
var slide_pos: Vector3 = hit_point + hit_normal * radius
_params.transform = Transform3D(Basis.IDENTITY, slide_pos)
var slide_rest: Dictionary = space.get_rest_info(_params)
if not slide_rest.is_empty():
hit_normal = slide_rest["normal"]
return {
"point": hit_point,
"normal": hit_normal,
"fraction": contact_t
}