//! Window ▸ VR Comp Editor: our own panel over the VR environments Composition ▸ VR ▸ Create VR //! Environment builds (an equirectangular output comp ← a 3:2 cube map ← six face comps, each //! with a 90° camera). It lists the environments and edits the 360 view's camera orientation: //! the six face cameras turn together, so the whole equirectangular output rotates (pan / tilt / //! roll the viewer's starting direction) without touching the scene. //! //! The view orientation is not stored separately: it is read back from the Front camera //! (`camera = view × face base`), so editing a camera by hand shows up here too. use effectcraft_geom::{Mat4, vec3}; use effectcraft_project::{GroupKind, ItemId, LayerId, LayerSource, Project}; use serde_json::{Value, json}; use super::app_more::{CELLS_3X2, FACES, ae, grouped, orientation_for_axes}; use super::{CommandSpec, always, bad}; use crate::{EngineError, Result, Session, cmd, query}; /// A VR environment: its output comp, cube map and the face cameras in [`FACES`] order. pub struct VrEnv { pub output: ItemId, pub cube: ItemId, pub faces: [(ItemId, LayerId); 6], } /// The VR environments of a project. pub fn environments(p: &Project) -> Vec { let mut out = vec![]; for (oid, oc) in p.comps() { for l in &oc.layers { let LayerSource::Comp { item: cube } = l.source else { continue }; let vr = l.effects().is_some_and(|fx| fx.groups().any(|g| matches!(&g.kind, GroupKind::Effect { effect } if effect == "ec.vr.converter"))); if !vr { continue; } let Some(cc) = p.comp(cube) else { continue }; let size = (cc.width / 3).max(1) as f64; let mut faces: [Option<(ItemId, LayerId)>; 6] = [None; 6]; for fl in &cc.layers { let LayerSource::Comp { item: face } = fl.source else { continue }; let pos = fl.props.prop("transform/position").map(|x| x.value.components()).unwrap_or_default(); let (Some(x), Some(y)) = (pos.first(), pos.get(1)) else { continue }; let cell = ((x / size).floor().max(0.0) as u32, (y / size).floor().max(0.0) as u32); let Some(i) = CELLS_3X2.iter().position(|c| *c == cell) else { continue }; let cam = p.comp(face).and_then(|fc| fc.layers.iter().find(|x| x.is_camera())).map(|x| x.id); if let Some(cam) = cam { faces[i] = Some((face, cam)); } } if faces.iter().all(Option::is_some) { out.push(VrEnv { output: *oid, cube, faces: faces.map(|f| f.unwrap_or((ItemId(0), LayerId(0)))) }); } } } out } fn base(i: usize) -> [f64; 3] { let (_, fw, r, u) = FACES[i]; orientation_for_axes(ae(r), ae([-u[0], -u[1], -u[2]]), ae(fw)) } fn orient(o: [f64; 3]) -> Mat4 { Mat4::orientation(vec3(o[0], o[1], o[2])) } fn transpose(m: Mat4) -> Mat4 { let mut t = Mat4::IDENTITY; for r in 0..3 { for c in 0..3 { t.0[r][c] = m.0[c][r]; } } t } fn norm(a: f64) -> f64 { let r = a.rem_euclid(360.0); let r = if (r - 360.0).abs() < 1e-7 { 0.0 } else { r }; (r * 1e6).round() / 1e6 } /// Orientation angles (X, Y, Z degrees, applied X then Y then Z) of a rotation matrix. pub fn euler(m: Mat4) -> [f64; 3] { let m = m.0; let y = (-m[2][0]).clamp(-1.0, 1.0).asin(); let (x, z) = if y.cos().abs() > 1e-6 { (m[2][1].atan2(m[2][2]), m[1][0].atan2(m[0][0])) } else { ((-m[1][2]).atan2(m[1][1]), 0.0) }; [norm(x.to_degrees()), norm(y.to_degrees()), norm(z.to_degrees())] } /// The current view orientation of an environment (from its Front camera). fn view_of(p: &Project, env: &VrEnv) -> [f64; 3] { let (fc, cam) = env.faces[4]; let o = p.comp(fc).and_then(|c| c.layer(cam)).and_then(|l| l.props.prop("transform/orientation")).map(|x| x.value.components()).unwrap_or_default(); let o = [o.first().copied().unwrap_or(0.0), o.get(1).copied().unwrap_or(0.0), o.get(2).copied().unwrap_or(0.0)]; euler(orient(o) * transpose(orient(base(4)))) } fn env_json(p: &Project, e: &VrEnv) -> Value { let name = |i: ItemId| p.item(i).map(|x| x.name.clone()).unwrap_or_default(); json!({ "output": e.output.0, "name": name(e.output), "cubeMap": e.cube.0, "faces": e.faces.iter().enumerate().map(|(i, (c, cam))| json!({"face": FACES[i].0, "comp": c.0, "camera": cam.0})).collect::>(), "view": view_of(p, e), }) } fn list(s: &mut Session, _: &Value) -> Result { Ok(json!(environments(&s.project).iter().map(|e| env_json(&s.project, e)).collect::>())) } /// The environment `comp` (its output, cube map or a face comp), else the active comp's, else /// the only one. fn env_p(s: &Session, p: &Value, cmd: &str) -> Result { let envs = environments(&s.project); let want = p.get("comp").and_then(Value::as_u64).map(ItemId).or(s.state.active_comp); let hit = |e: &VrEnv, c: ItemId| e.output == c || e.cube == c || e.faces.iter().any(|f| f.0 == c); let i = want.and_then(|c| envs.iter().position(|e| hit(e, c))).or((envs.len() == 1).then_some(0)); let i = i.ok_or_else(|| { bad(cmd, if envs.is_empty() { "no VR environment (Composition ▸ VR ▸ Create VR Environment…)" } else { "give `comp`: a VR environment's output comp" }) })?; envs.into_iter().nth(i).ok_or(EngineError::NoComp) } fn set_view(s: &mut Session, p: &Value) -> Result { const C: &str = "comp.vr.setView"; let env = env_p(s, p, C)?; let mut o = view_of(&s.project, &env); match p.get("orientation") { Some(Value::Array(a)) => { for (k, v) in a.iter().take(3).enumerate() { o[k] = v.as_f64().ok_or_else(|| bad(C, "orientation: [x, y, z] degrees"))?; } } Some(_) => return Err(bad(C, "orientation: [x, y, z] degrees")), None => {} } // Pan (Y), tilt (X) and roll (Z) by name. for (k, i) in [("tilt", 0), ("pan", 1), ("roll", 2)] { if let Some(v) = p.get(k).and_then(Value::as_f64) { o[i] = v; } } let v = orient(o); grouped(s, "VR View Orientation", |s| { for (i, (fc, cam)) in env.faces.iter().enumerate() { let e = euler(v * orient(base(i))); s.execute("prop.set", json!({"comp": fc.0, "layer": cam.0, "path": "transform/orientation", "value": e}))?; } Ok(()) })?; Ok(json!({"output": env.output.0, "view": view_of(&s.project, &env)})) } pub fn specs() -> Vec { vec![ query!("comp.vr.environments", "VR Environments", "{} → [{output, name, cubeMap, faces: [{face, comp, camera}], view: [x, y, z]}]", list), cmd!( "comp.vr.setView", "VR View Orientation", [], None, "{comp? (an environment's output / cube map / face comp), orientation?: [x, y, z] degrees, pan?, tilt?, roll?} → turns the six face cameras together", always, set_view ), ] }