Source code for simvx.graphics.scene.frustum

"""View frustum culling utilities."""


from __future__ import annotations

import logging

import numpy as np

log = logging.getLogger(__name__)

__all__ = ["Frustum"]


[docs] class Frustum: """View frustum defined by 6 planes for culling.""" def __init__(self): self.planes = np.zeros((6, 4), dtype=np.float32)
[docs] def extract_from_matrix(self, vp: np.ndarray) -> None: """Extract frustum planes from view-projection matrix. Planes: left, right, bottom, top, near, far. """ self.planes[0] = vp[3] + vp[0] # Left self.planes[1] = vp[3] - vp[0] # Right self.planes[2] = vp[3] + vp[1] # Bottom self.planes[3] = vp[3] - vp[1] # Top self.planes[4] = vp[3] + vp[2] # Near self.planes[5] = vp[3] - vp[2] # Far # Normalize planes norms = np.linalg.norm(self.planes[:, :3], axis=1, keepdims=True) self.planes /= norms
[docs] def test_sphere(self, center: np.ndarray, radius: float) -> bool: """Test if sphere intersects or is inside the frustum.""" dists = self.planes[:, :3] @ center + self.planes[:, 3] return bool(np.all(dists >= -radius))
[docs] def test_aabb(self, min_bounds: np.ndarray, max_bounds: np.ndarray) -> bool: """Test if AABB intersects the frustum.""" for i in range(6): normal = self.planes[i, :3] # Positive vertex: furthest in plane's positive direction p = np.where(normal >= 0, max_bounds, min_bounds) if np.dot(normal, p) + self.planes[i, 3] < 0: return False return True
[docs] def cull_spheres( self, centers: np.ndarray, radii: np.ndarray, ) -> np.ndarray: """Vectorized frustum cull for many bounding spheres. Args: centers: (N, 3) array of sphere centers. radii: (N,) array of radii. Returns: boolean mask (N,) — True = visible. """ # (6, N) = (6, 3) @ (3, N) + (6, 1) dists = self.planes[:, :3] @ centers.T + self.planes[:, 3:4] # Visible if all planes pass: dist >= -radius return np.all(dists >= -radii[np.newaxis, :], axis=0)