tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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}