Math
Spatial primitives for 3D viewport operations: axis-aligned bounding boxes, view-frustum culling, and ray-casting. These types provide geometric utilities for scene bounds, frustum intersection tests, and per-body visibility culling (issue #42). Ray-casting support for picking is documented on the Picking reference page — see Ray for screen-to-world ray construction.
Topics
BoundingBox
BoundingBox is an axis-aligned bounding box (AABB) value type defined by minimum and maximum corners. It supports union operations, diagonal measurement, and transformation by 4×4 matrices. When combined with Frustum, it enables per-body frustum culling to skip out-of-view geometry at render time.
BoundingBox.init(min:max:)
Creates a bounding box from minimum and maximum corners.
public init(min: SIMD3<Float>, max: SIMD3<Float>)
- Parameters:
min— lower corner;max— upper corner. - Example:
let box = BoundingBox( min: SIMD3(0, 0, 0), max: SIMD3(10, 10, 10) )
Corners
min
The minimum corner of the box.
public var min: SIMD3<Float>
- Example:
print(box.min) // e.g., SIMD3(0, 0, 0)
max
The maximum corner of the box.
public var max: SIMD3<Float>
- Example:
print(box.max) // e.g., SIMD3(10, 10, 10)
Derived Properties
center
Center point of the box.
public var center: SIMD3<Float> { get }
Computed as (min + max) * 0.5.
- Example:
let midpoint = box.center // SIMD3(5, 5, 5)
size
Size along each axis.
public var size: SIMD3<Float> { get }
Computed as max - min.
- Example:
let dimensions = box.size // SIMD3(10, 10, 10)
diagonalLength
Length of the space diagonal.
public var diagonalLength: Float { get }
Computed as simd_length(size).
- Example:
let radius = box.diagonalLength * 0.5 // half-diagonal for sphere fit
Operations
union(_:)
Returns the smallest box enclosing both self and other.
public func union(_ other: BoundingBox) -> BoundingBox
Useful for combining scene bounds or merging multiple body AABBs.
- Parameters:
other— the box to union with. - Returns: A new
BoundingBoxwithmin= component-wise min andmax= component-wise max. - Example:
let box1 = BoundingBox(min: SIMD3(0, 0, 0), max: SIMD3(5, 5, 5)) let box2 = BoundingBox(min: SIMD3(3, 3, 3), max: SIMD3(8, 8, 8)) let combined = box1.union(box2) // combined.min = (0, 0, 0), combined.max = (8, 8, 8)
transformed(by:)
The world-space AABB enclosing this box after applying a 4×4 transformation.
public func transformed(by transform: simd_float4x4) -> BoundingBox
Transforms all eight corners and takes their extent. Returns self unchanged for an identity transform (the common case) without work.
- Parameters:
transform— a column-major 4×4 transformation matrix (translation, rotation, scale). - Returns: A new
BoundingBoxwith transformed extent. - Example:
var tx = matrix_identity_float4x4 tx.columns.3 = SIMD4(10, 0, 0, 1) // translate by (10, 0, 0) let movedBox = box.transformed(by: tx) // movedBox.min ≈ SIMD3(10, 0, 0), movedBox.max ≈ SIMD3(20, 10, 10)
Frustum
Frustum represents the six planes of a view-projection frustum, extracted from a view-projection matrix. Each plane is stored with its normal pointing inward; a point is inside the view when it satisfies all six half-space tests. Frustums are used for per-body visibility culling: if a body’s AABB lies entirely outside any frustum plane, it can be skipped during rendering (issue #42).
Plane storage uses the Gribb–Hartmann method with Metal’s [0, 1] clip-space depth convention (near plane = row 2, not row 3 + row 2 as in traditional OpenGL).
Frustum.init(viewProjection:)
Extracts the six frustum planes from a view-projection matrix.
public init(viewProjection m: simd_float4x4)
Decomposes the matrix into six half-space planes (left, right, bottom, top, near, far) and normalizes each normal vector. The normals point inward so that a world point p is inside the plane when dot(normal, p) + d >= 0.
- Parameters:
m— a column-major 4×4 view-projection matrix (typicallyprojectionMatrix * viewMatrixfrom a camera). - Example:
let camera = Camera() camera.eye = SIMD3(0, -100, 50) camera.center = SIMD3(0, 0, 0) let viewProj = camera.projectionMatrix * camera.viewMatrix let frustum = Frustum(viewProjection: viewProj)
planes
The six normalized frustum planes.
public let planes: [SIMD4<Float>]
Each element is (a, b, c, d) representing the half-space ax + by + cz + d >= 0. The planes are stored in order: left, right, bottom, top, near, far.
- Example:
let frustum = Frustum(viewProjection: viewProj) for (i, plane) in frustum.planes.enumerated() { let normal = SIMD3<Float>(plane.x, plane.y, plane.z) print("Plane \(i) normal: \(normal), d: \(plane.w)") }
Intersection Tests
intersects(_:)
Tests whether an axis-aligned bounding box intersects the view frustum.
public func intersects(_ box: BoundingBox) -> Bool
Returns false only when box lies entirely outside at least one plane (i.e. it is safe to cull). Returns true for a box that straddles any plane (conservative: may include false positives, but never false negatives).
Uses the “p-vertex” technique: for each plane, selects the AABB corner furthest along the plane normal, tests it against the half-space, and returns false immediately if it’s outside any plane.
- Parameters:
box— the axis-aligned bounding box to test. - Returns:
trueif the box intersects or is inside the frustum;falseif entirely outside (safe to cull). - Example:
let viewProj = camera.projectionMatrix * camera.viewMatrix let frustum = Frustum(viewProjection: viewProj) for body in bodies { if frustum.intersects(body.boundingBox) { // Render the body renderBody(body) } else { // Skip this body — it's completely off-screen } }