Skip to main content

primase_core/
spot_frustum.rs

1//! A spot light's volume where the source's spot is a pyramid, not glTF's circular cone.
2use serde::Serialize;
3
4use crate::{Component, Entity, Geometry, JsonError, Light, LightKind, World};
5
6/// A spot light whose volume is a rectangular pyramid with a near plane: from its apex it opens by
7/// `horizontal_half_angle` about its local +X and `vertical_half_angle` about its local +Y (radians, from the beam),
8/// and it lights only past `near` along the beam (metres), to its [`Light::range`]. Attached to the same entity as a
9/// spot [`Light`], by a lowering whose source states such a spot (the source's angles and near plane, exactly). The
10/// light's own cone ([`LightKind::Spot`]) is then the portable approximation the lowering states (glTF's circle has
11/// one angle and no near plane); a consumer that does not read this component takes that cone.
12#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
13pub struct SpotFrustum {
14    pub horizontal_half_angle: f32,
15    pub vertical_half_angle: f32,
16    pub near: f32,
17}
18
19impl Component for SpotFrustum {
20    const NAME: &'static str = "core.spot_frustum";
21    const GEOMETRY: Geometry = Geometry::None;
22    fn to_json(&self) -> Result<Option<serde_json::Value>, JsonError> {
23        crate::json::to_value(self).map(Some)
24    }
25    /// Half-angles above 0 and below pi / 2, a finite non-negative near plane, on a spot light whose range, where it
26    /// states one, lies past the near plane.
27    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
28        let half = 0.0..std::f32::consts::FRAC_PI_2;
29        for (axis, angle) in [
30            ("horizontal", self.horizontal_half_angle),
31            ("vertical", self.vertical_half_angle),
32        ] {
33            if !(half.contains(&angle) && angle > 0.0) {
34                return Err(format!(
35                    "a spot frustum's {axis} half-angle {angle} not in (0, pi / 2)"
36                ));
37            }
38        }
39        if !(self.near.is_finite() && self.near >= 0.0) {
40            return Err(format!(
41                "a spot frustum's near plane {} not finite and non-negative",
42                self.near
43            ));
44        }
45        match w.get::<Light>(e) {
46            Some(Light {
47                kind: LightKind::Spot { .. },
48                range,
49                ..
50            }) => match range {
51                Some(r) if *r <= self.near => Err(format!(
52                    "a spot frustum's near plane {} not before the light's range {r}",
53                    self.near
54                )),
55                _ => Ok(()),
56            },
57            _ => Err("a spot frustum on an entity with no spot light".into()),
58        }
59    }
60}