Skip to main content

sim_lib_view_spatial/
reproject.rs

1//! Viture stereo reprojection as the one bespoke glasses adapter.
2
3use std::rc::Rc;
4
5use sim_kernel::{Error, Expr, Result};
6use sim_lib_scene::{Anchor, AnchorSpace, Transform3};
7use sim_lib_view_device::{DeviceProfile, EncodedScene, GlassesClass, LocalAdapter, glasses_class};
8use sim_value::{access, build};
9
10use crate::PoseView;
11
12/// Stereo reprojector for the Viture-rich glasses path.
13///
14/// This is the one bespoke [`LocalAdapter`] in the glasses instance because it
15/// turns one content-rate `scene/spatial` packet into two device-rate eye views.
16/// Halo and other one-card tiers use the shared DEVICE_3 glance adapter instead.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct Reprojector {
19    /// Maximum prediction lead applied during this adapter step.
20    pub max_predict_ms: u64,
21}
22
23impl Reprojector {
24    /// Builds a reprojector with a prediction clamp.
25    pub fn new(max_predict_ms: u64) -> Self {
26        Self { max_predict_ms }
27    }
28}
29
30impl LocalAdapter for Reprojector {
31    type State = PoseView;
32
33    fn adapt(
34        &self,
35        scene: &EncodedScene,
36        state: &Self::State,
37        profile: &DeviceProfile,
38    ) -> Result<Rc<Expr>> {
39        if glasses_class(profile) != Some(GlassesClass::Stereo6Dof) {
40            return Ok(scene.shared());
41        }
42        expect_scene_kind(scene.expr(), "spatial")?;
43        let left = project_eye(scene.expr(), state, self.max_predict_ms, Eye::Left)?;
44        let right = project_eye(scene.expr(), state, self.max_predict_ms, Eye::Right)?;
45        Ok(Rc::new(stereo_scene(
46            left,
47            right,
48            state,
49            self.max_predict_ms,
50        )))
51    }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55enum Eye {
56    Left,
57    Right,
58}
59
60impl Eye {
61    fn name(self) -> &'static str {
62        match self {
63            Self::Left => "left",
64            Self::Right => "right",
65        }
66    }
67
68    fn sign(self) -> f64 {
69        match self {
70            Self::Left => -1.0,
71            Self::Right => 1.0,
72        }
73    }
74}
75
76fn project_eye(scene: &Expr, state: &PoseView, max_predict_ms: u64, eye: Eye) -> Result<Expr> {
77    let children = spatial_children(scene)?;
78    let projected = children
79        .iter()
80        .filter_map(|child| project_child(child, state, max_predict_ms, eye).transpose())
81        .collect::<Result<Vec<_>>>()?;
82    Ok(sim_lib_scene::data_map(vec![
83        ("eye", build::sym(eye.name())),
84        ("children", build::list(projected)),
85    ]))
86}
87
88fn project_child(
89    child: &Expr,
90    state: &PoseView,
91    max_predict_ms: u64,
92    eye: Eye,
93) -> Result<Option<Expr>> {
94    if scene_kind(child).as_deref() != Some("panel") {
95        return Ok(Some(child.clone()));
96    }
97    let anchor = Anchor::from_expr(access::required(child, "anchor", "scene/panel")?)?;
98    let transform = Transform3::from_expr(access::required(child, "transform", "scene/panel")?)?;
99    let projected = project_transform(&anchor, &transform, state, max_predict_ms, eye);
100    if !visible_in_frustum(anchor.space, &projected) {
101        return Ok(None);
102    }
103    Ok(Some(projected_panel(child, &anchor, projected, eye)))
104}
105
106fn projected_panel(source: &Expr, anchor: &Anchor, transform: Transform3, eye: Eye) -> Expr {
107    let id = access::field_str(source, "id").unwrap_or("panel");
108    let body = access::field(source, "body").cloned().unwrap_or(Expr::Nil);
109    sim_lib_scene::node(
110        "panel",
111        vec![
112            ("id", build::text(format!("{id}:{}", eye.name()))),
113            ("source-panel", build::text(id)),
114            ("eye", build::sym(eye.name())),
115            ("anchor-rule", build::sym(anchor_rule(anchor.space))),
116            ("body", body),
117            ("anchor", anchor.to_expr()),
118            ("transform", transform.to_expr()),
119        ],
120    )
121}
122
123fn project_transform(
124    anchor: &Anchor,
125    transform: &Transform3,
126    state: &PoseView,
127    max_predict_ms: u64,
128    eye: Eye,
129) -> Transform3 {
130    let mut out = transform.clone();
131    let eye_offset = eye.sign() * state.inter_eye_m / 2.0;
132    match anchor.space {
133        AnchorSpace::Head => {
134            out.translate_m[0] += eye_offset;
135        }
136        AnchorSpace::World => {
137            out.translate_m[0] -= state.translation_m[0];
138            out.translate_m[1] -= state.translation_m[1];
139            out.translate_m[2] -= state.translation_m[2];
140            out.translate_m[0] += state.clamped_yaw_rad(max_predict_ms).sin() * depth(&out);
141            out.translate_m[0] += eye_offset;
142        }
143        AnchorSpace::Screen => {}
144        AnchorSpace::Body | AnchorSpace::Device => {
145            out.translate_m[0] += eye_offset * 0.5;
146        }
147    }
148    out
149}
150
151fn visible_in_frustum(space: AnchorSpace, transform: &Transform3) -> bool {
152    if matches!(space, AnchorSpace::Head | AnchorSpace::Screen) {
153        return true;
154    }
155    let z = transform.translate_m[2];
156    if z > 0.05 {
157        return false;
158    }
159    let distance = depth(transform);
160    let max_x = distance * (52.0_f64.to_radians() / 2.0).tan();
161    transform.translate_m[0].abs() <= max_x
162}
163
164fn depth(transform: &Transform3) -> f64 {
165    transform.translate_m[2].abs().max(1.0)
166}
167
168fn anchor_rule(space: AnchorSpace) -> &'static str {
169    match space {
170        AnchorSpace::Head => "head-locked",
171        AnchorSpace::World => "world-locked",
172        AnchorSpace::Screen => "screen-locked",
173        AnchorSpace::Body => "body-relative",
174        AnchorSpace::Device => "device-relative",
175    }
176}
177
178fn stereo_scene(left: Expr, right: Expr, state: &PoseView, max_predict_ms: u64) -> Expr {
179    sim_lib_scene::node(
180        "stereo",
181        vec![
182            ("left-eye", left),
183            ("right-eye", right),
184            ("sample-seq", build::uint(state.sample_seq)),
185            (
186                "predict-ms",
187                build::uint(state.clamped_predict_ms(max_predict_ms)),
188            ),
189            ("age-ms", build::uint(state.age_ms)),
190        ],
191    )
192}
193
194fn spatial_children(scene: &Expr) -> Result<&[Expr]> {
195    match access::required(scene, "children", "scene/spatial")? {
196        Expr::List(children) => Ok(children),
197        _ => Err(Error::HostError(
198            "scene/spatial children must be a list".to_owned(),
199        )),
200    }
201}
202
203fn expect_scene_kind(scene: &Expr, expected: &str) -> Result<()> {
204    match scene_kind(scene).as_deref() {
205        Some(kind) if kind == expected => Ok(()),
206        _ => Err(Error::HostError(format!("expected scene/{expected}"))),
207    }
208}
209
210fn scene_kind(expr: &Expr) -> Option<String> {
211    let kind = sim_lib_scene::node_kind(expr)?;
212    (kind.namespace.as_deref() == Some(sim_lib_scene::SCENE_NAMESPACE))
213        .then(|| kind.name.to_string())
214}