gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main llimphi-voxel/src/camera_rig.rs Rust · 211 líneas · 8.0 KB Historia Quién tocó qué crudo

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

1//! **Modos de cámara** atados a un sujeto + la **secuencia de nacimiento** del
2//! corto. Dos modos generales, reusables por cualquier juego/escena:
3//! - [`CamMode::Subject`] — *primera persona*: la cámara **es** el sujeto (ojo en su
4//!   cabeza, mira hacia su rumbo).
5//! - [`CamMode::Follow`] — *tercera persona*: detrás y arriba del sujeto, mirándolo.
6//!
7//! Y [`BirthSequence`], que guiona la apertura: la cámara **cae del cielo mirando
8//! abajo**, ve el huevo, y al aterrizar (nacer) **sale del sujeto** y se planta
9//! detrás — una transición suave `Subject → Follow`.
10
11use llimphi_3d::glam::Vec3;
12use llimphi_3d::Camera3d;
13
14use crate::actor::Actor;
15use crate::player::{forward_h, look_dir};
16use crate::potential::Egg;
17
18/// Suavizado Hermite `3t²−2t³` (deriva nula en los extremos).
19fn smoothstep(x: f32) -> f32 {
20    let x = x.clamp(0.0, 1.0);
21    x * x * (3.0 - 2.0 * x)
22}
23
24/// **Modo de cámara** relativo a un sujeto (su `pos` = centro de pies, y su
25/// `facing` = rumbo). [`camera`](Self::camera) produce la [`Camera3d`] del frame.
26#[derive(Debug, Clone, Copy)]
27pub enum CamMode {
28    /// Primera persona: el ojo está a `eye_height` sobre los pies del sujeto y mira
29    /// hacia su rumbo (con `pitch` de cabeceo). La cámara **es** el sujeto.
30    Subject { eye_height: f32, pitch: f32 },
31    /// Tercera persona: el ojo está `distance` detrás del sujeto y `height` arriba,
32    /// mirándolo a la altura del pecho.
33    Follow { distance: f32, height: f32 },
34}
35
36impl CamMode {
37    /// La cámara de este modo para un sujeto en `pos` (pies) mirando a `facing`.
38    pub fn camera(&self, pos: Vec3, facing: f32) -> Camera3d {
39        match *self {
40            CamMode::Subject { eye_height, pitch } => {
41                let eye = pos + Vec3::new(0.0, eye_height, 0.0);
42                let dir = look_dir(facing, pitch);
43                Camera3d { eye, target: eye + dir, ..Camera3d::default() }
44            }
45            CamMode::Follow { distance, height } => {
46                let look = pos + Vec3::new(0.0, height, 0.0);
47                let behind = forward_h(facing) * distance;
48                let eye = pos - behind + Vec3::new(0.0, height + 0.6, 0.0);
49                Camera3d { eye, target: look, ..Camera3d::default() }
50            }
51        }
52    }
53}
54
55/// Interpola dos cámaras (ojo, objetivo y FOV) con suavizado — la base de las
56/// **transiciones** entre modos (p.ej. salir del sujeto hacia atrás).
57pub fn cam_lerp(a: &Camera3d, b: &Camera3d, t: f32) -> Camera3d {
58    let s = smoothstep(t);
59    Camera3d {
60        eye: a.eye.lerp(b.eye, s),
61        target: a.target.lerp(b.target, s),
62        up: Vec3::Y,
63        fovy_rad: a.fovy_rad + (b.fovy_rad - a.fovy_rad) * s,
64        znear: a.znear,
65        zfar: a.zfar,
66    }
67}
68
69/// **Secuencia de nacimiento**: la cámara cae del cielo sobre el huevo mirando
70/// abajo; al aterrizar el huevo eclosiona y la cámara —que llegó *metida en el
71/// sujeto*— sale hacia atrás a tercera persona. Determinista por tiempo
72/// (reproducible cuadro a cuadro).
73#[derive(Debug, Clone, Copy)]
74pub struct BirthSequence {
75    /// El huevo (lleva pos, rumbo y el potencial que nace).
76    pub egg: Egg,
77    /// Altura desde la que cae la cámara.
78    pub sky_height: f32,
79    /// Instante del aterrizaje/nacimiento (seg).
80    pub t_land: f32,
81    /// Duración de la salida del sujeto hacia atrás (seg).
82    pub t_pull: f32,
83    /// Distancia/altura del plano de seguimiento final.
84    pub follow_distance: f32,
85    pub follow_height: f32,
86}
87
88impl BirthSequence {
89    /// Secuencia con tiempos por defecto (caída ~2.4 s, salida ~1.2 s).
90    pub fn new(egg: Egg) -> Self {
91        Self {
92            egg,
93            sky_height: 60.0,
94            t_land: 2.4,
95            t_pull: 1.2,
96            follow_distance: 3.5,
97            follow_height: 1.0,
98        }
99    }
100
101    /// Duración total de la secuencia (hasta un pequeño respiro tras la salida).
102    pub fn duration(&self) -> f32 {
103        self.t_land + self.t_pull + 1.0
104    }
105
106    /// El recién nacido (materializa el potencial del huevo).
107    pub fn newborn(&self) -> Actor {
108        self.egg.newborn()
109    }
110
111    /// Progreso de eclosión del huevo en `t`: arranca poco antes del aterrizaje y
112    /// completa poco después (el huevo se abre **mientras** la cámara aterriza).
113    pub fn hatch(&self, t: f32) -> f32 {
114        let start = self.t_land - 0.4;
115        let end = self.t_land + 0.3;
116        ((t - start) / (end - start)).clamp(0.0, 1.0)
117    }
118
119    /// Altura del ojo en primera persona = la cabeza del recién nacido.
120    fn eye_height(&self) -> f32 {
121        self.newborn().build.head_y
122    }
123
124    /// Cámara en primera persona parada en el sujeto (recién nacido).
125    fn subject_cam(&self) -> Camera3d {
126        CamMode::Subject { eye_height: self.eye_height(), pitch: 0.0 }
127            .camera(self.egg.pos, self.egg.facing)
128    }
129
130    /// Cámara de seguimiento detrás del sujeto.
131    fn follow_cam(&self) -> Camera3d {
132        CamMode::Follow { distance: self.follow_distance, height: self.follow_height }
133            .camera(self.egg.pos, self.egg.facing)
134    }
135
136    /// Cámara cayendo del cielo, mirando hacia abajo al huevo.
137    fn sky_cam(&self) -> Camera3d {
138        let eye = self.egg.pos + Vec3::new(0.0, self.sky_height, 0.01);
139        Camera3d { eye, target: self.egg.pos, ..Camera3d::default() }
140    }
141
142    /// La cámara en el instante `t`: **caída** (cielo→sujeto, mirando abajo→al
143    /// frente) hasta `t_land`; **salida** (sujeto→seguimiento) durante `t_pull`;
144    /// luego, seguimiento fijo.
145    pub fn camera(&self, t: f32) -> Camera3d {
146        let subject = self.subject_cam();
147        if t < self.t_land {
148            // Cae del cielo y, al final, queda calzada en el sujeto.
149            cam_lerp(&self.sky_cam(), &subject, (t / self.t_land.max(1e-3)).clamp(0.0, 1.0))
150        } else if t < self.t_land + self.t_pull {
151            // Sale del sujeto hacia atrás (3ª persona).
152            let u = (t - self.t_land) / self.t_pull.max(1e-3);
153            cam_lerp(&subject, &self.follow_cam(), u)
154        } else {
155            self.follow_cam()
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::actor::Age;
164    use crate::potential::Hatchling;
165
166    fn egg() -> Egg {
167        let mut e = Egg::new(Vec3::new(0.0, 0.0, 0.0), 1.2, Hatchling::human(Age::Baby));
168        e.facing = 0.0; // mira a +Z
169        e
170    }
171
172    #[test]
173    fn subject_mira_al_frente_desde_la_cabeza() {
174        let cam = CamMode::Subject { eye_height: 1.6, pitch: 0.0 }.camera(Vec3::ZERO, 0.0);
175        assert!((cam.eye.y - 1.6).abs() < 1e-5, "ojo en la cabeza");
176        // Mira hacia +Z (rumbo 0).
177        assert!((cam.target - cam.eye).z > 0.5, "mira al frente (+Z)");
178    }
179
180    #[test]
181    fn follow_esta_detras_del_sujeto() {
182        let cam = CamMode::Follow { distance: 4.0, height: 1.0 }.camera(Vec3::ZERO, 0.0);
183        // Rumbo +Z → "detrás" es −Z; el ojo debe estar en z negativo.
184        assert!(cam.eye.z < -1.0, "ojo detrás (−Z): {}", cam.eye.z);
185        assert!(cam.eye.y > 1.0, "ojo por encima");
186    }
187
188    #[test]
189    fn la_secuencia_cae_del_cielo_y_termina_atras() {
190        let seq = BirthSequence::new(egg());
191        let high = seq.camera(0.0);
192        assert!(high.eye.y > 40.0, "arranca alto en el cielo: {}", high.eye.y);
193        // Al final = plano de seguimiento (detrás del sujeto).
194        let end = seq.camera(seq.duration());
195        let follow = seq.camera(1e9);
196        assert!((end.eye - follow.eye).length() < 1e-3, "termina en seguimiento");
197        assert!(end.eye.z < 0.0, "el seguimiento final está detrás (−Z)");
198        // La eclosión arranca cerrada y termina abierta.
199        assert_eq!(seq.hatch(0.0), 0.0);
200        assert_eq!(seq.hatch(seq.duration()), 1.0);
201    }
202
203    #[test]
204    fn la_camara_es_continua_en_el_aterrizaje() {
205        // Sin saltos bruscos al pasar de caída a salida (mismo punto en t_land).
206        let seq = BirthSequence::new(egg());
207        let before = seq.camera(seq.t_land - 0.001);
208        let after = seq.camera(seq.t_land + 0.001);
209        assert!((before.eye - after.eye).length() < 0.2, "ojo continuo en el aterrizaje");
210    }
211}