tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-voxel-render/src/escenario.rs Rust · 283 siq'ikuna · 11.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! El **escenario**: una escena voxel montada y lista para dar cuadros.
2//!
3//! Es el bucle que estaba adentro de `export_scene` del studio, sacado a
4//! donde lo pueda usar cualquiera. La forma importa más que el contenido:
5//!
6//! ```text
7//!   preparar(project, scene)   ── una vez: mundo, reparto, ventana
8//!        │
9//!        └─ cuadro(t)          ── por cuadro: posar, encuadrar, componer
10//! ```
11//!
12//! Esa separación es lo que vuelve viable filmar: generar el terreno cuesta
13//! bastante más que dibujarlo, y adentro del bucle se pagaría una vez por
14//! cuadro. Lo único que `cuadro` hace caro es **regenerar la ventana cuando el
15//! reparto cruza un paso**, que es justo lo que permite que una escena viva en
16//! un mundo infinito en vez de una caja de 128³.
17//!
18//! No escribe archivos ni muxea: compone sobre la textura que le den. Quien
19//! filma decide la resolución, el aspecto y qué hace con el resultado.
20
21use llimphi_3d::glam::Vec3;
22use llimphi_3d::Camera3d;
23use llimphi_hal::wgpu;
24use llimphi_voxel::{
25    reparto_mixto, window_origin_for_cast, world_dim, ActorScript, CharSpec, EnCuadro, Enjambre,
26    MundoRender, Project, SceneSpec, PREVIEW_DIM_XZ,
27};
28
29use crate::preview::WorldPreview;
30
31/// El **cielo** con el que hay que limpiar el cuadro antes de componer. El
32/// ray-march pinta su propia atmósfera sobre los rayos que no pegan, pero el
33/// clear evita un borde frío si la niebla afloja.
34pub const CIELO: [u8; 4] = [150, 186, 224, 255];
35
36/// **La luz de una escena**: sol, cielo y bruma.
37///
38/// Vive acá y no en el `SceneSpec` porque no es de la escena: es de **quien
39/// filma**. La misma escena rodada por dos marcas distintas tiene que salir con
40/// dos luces distintas, o las dos marcas se leen como un solo canal. En `ende`
41/// esto sale del sello.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct Ambiente {
44    /// Dirección **hacia** el sol, sin normalizar.
45    pub sol: [f32; 3],
46    pub cenit: [u8; 3],
47    pub horizonte: [u8; 3],
48    /// Densidad de bruma como numerador: la real es esto sobre el ancho de la
49    /// ventana.
50    pub bruma: f32,
51    /// Haces de sol cruzando la bruma, `0..1`.
52    pub haces: f32,
53}
54
55/// Una escena montada sobre la GPU, lista para entregar cuadros.
56pub struct Escenario {
57    preview: WorldPreview,
58    mundo: MundoRender,
59    escena: SceneSpec,
60    guiones: Vec<ActorScript>,
61    reparto: Vec<CharSpec>,
62    dim: [u32; 3],
63    /// La luz de quien filma. `None` = la del estudio.
64    ambiente: Option<Ambiente>,
65    /// La materia desprendida que anda por el aire (los sucesos «desmorona» la
66    /// alimentan; ver [`llimphi_voxel::disparar_sucesos`]).
67    ceniza: Enjambre,
68    /// Instante del cuadro anterior: el intervalo `(t_previo, t]` es lo que decide
69    /// qué sucesos se disparan en ESTE cuadro, y por qué se disparan una sola vez.
70    t_previo: Option<f32>,
71}
72
73impl Escenario {
74    /// Monta la escena `escena` de `proyecto` con el tamaño de mundo por
75    /// defecto.
76    pub fn preparar(
77        device: &wgpu::Device,
78        queue: &wgpu::Queue,
79        proyecto: &Project,
80        escena: &SceneSpec,
81    ) -> Result<Escenario, String> {
82        Escenario::preparar_con(device, queue, proyecto, escena, world_dim(PREVIEW_DIM_XZ))
83    }
84
85    /// Igual, eligiendo el tamaño de la ventana voxel. Más grande se ve más
86    /// lejos y cuesta más; el ray-march es por píxel, así que lo que pesa es
87    /// la profundidad del volumen, no su ancho en pantalla.
88    pub fn preparar_con(
89        device: &wgpu::Device,
90        queue: &wgpu::Queue,
91        proyecto: &Project,
92        escena: &SceneSpec,
93        dim: [u32; 3],
94    ) -> Result<Escenario, String> {
95        let mundo = proyecto
96            .render_mundo(escena.mundo)
97            .ok_or("la escena no tiene un mundo de fondo válido")?;
98        let preview = WorldPreview::build(device, queue, &mundo, dim, 1, None);
99        let guiones = escena.scripts();
100        let reparto = escena
101            .actors
102            .iter()
103            .map(|a| proyecto.character_or_default(a.character))
104            .collect();
105        Ok(Escenario {
106            preview,
107            mundo,
108            escena: escena.clone(),
109            guiones,
110            reparto,
111            dim,
112            ambiente: None,
113            ceniza: Enjambre::ceniza(0x1CE),
114            t_previo: None,
115        })
116    }
117
118    /// Duración de la escena, en segundos de su propio guion.
119    pub fn duracion(&self) -> f32 {
120        self.escena.duration
121    }
122
123    /// Fija la luz con que se filma. Sin esto se usa la del estudio, que es la
124    /// misma para todo el mundo.
125    pub fn set_ambiente(&mut self, a: Ambiente) {
126        self.ambiente = Some(a);
127    }
128
129    /// Fija el **temblor de cámara en mano**. Es la perilla que hace que un
130    /// plano filmado y un trazo dibujado se lean como la misma persona, así
131    /// que la decide quien filma y no la escena.
132    pub fn set_temblor(&mut self, amt: f32) {
133        self.escena.handheld = amt.max(0.0);
134    }
135
136    /// El color con que hay que limpiar el cuadro antes de componer: el
137    /// horizonte del ambiente, o el cielo del estudio.
138    pub fn cielo(&self) -> [u8; 4] {
139        match self.ambiente {
140            Some(a) => [a.horizonte[0], a.horizonte[1], a.horizonte[2], 255],
141            None => CIELO,
142        }
143    }
144
145    /// Los instantes que merecen un acento musical: cortes de cámara y gestos.
146    pub fn acentos(&self) -> Vec<f32> {
147        self.escena.beat_times()
148    }
149
150    /// Compone el cuadro del segundo `t` sobre `target`.
151    ///
152    /// `rect` es el sub-rect del viewport donde va la escena (para montarla en
153    /// un panel); para un cuadro entero, `(0, 0, w, h)`.
154    ///
155    /// **Coordenadas**: el shader del voxel espera la cámara y las posiciones
156    /// en espacio CENTRADO (`mundo = grilla − dim/2`), porque hace
157    /// `ro = cam_eye + dim/2`. `ground_at_world` devuelve coords de ventana, así
158    /// que hay que restarle `half` — si no, en los planos cercanos el terreno
159    /// queda fuera de cuadro y el reparto parece flotar en el cielo. Costó dos
160    /// re-renders descubrirlo la primera vez; queda escrito.
161    #[allow(clippy::too_many_arguments)]
162    pub fn cuadro(
163        &mut self,
164        device: &wgpu::Device,
165        queue: &wgpu::Queue,
166        encoder: &mut wgpu::CommandEncoder,
167        target: &wgpu::TextureView,
168        viewport: (u32, u32),
169        rect: (f32, f32, f32, f32),
170        t: f32,
171    ) {
172        // Mundo infinito: la ventana voxel sigue al reparto. Sólo se regenera
173        // al cruzar un paso, así que por cuadro esto es casi gratis.
174        let origen = window_origin_for_cast(&self.guiones, t, self.dim);
175        self.preview
176            .ensure_window(device, queue, &self.mundo, 1, origen);
177
178        // La luz de quien filma, DESPUÉS de la ventana (que al regenerarse
179        // repone la atmósfera del estudio) y ANTES del fundido de apertura,
180        // que si existe tiene que ganar.
181        if let Some(a) = self.ambiente {
182            self.preview.set_lighting(
183                a.sol,
184                llimphi_3d::Atmosphere {
185                    sky_zenith: a.cenit,
186                    sky_horizon: a.horizonte,
187                    fog_density: a.bruma / self.dim[0] as f32,
188                    god_rays: a.haces,
189                },
190            );
191        }
192
193        // La atmósfera de apertura (bruma + fundido desde negro), si la escena
194        // la declara. Va antes de componer: sólo toca uniforms.
195        if let Some(atm) = self.escena.atmosfera.as_ref() {
196            self.preview.set_atmosfera_escena(atm, t);
197        }
198
199        let half = Vec3::new(self.dim[0] as f32, self.dim[1] as f32, self.dim[2] as f32) * 0.5;
200
201        // Lo que la escena dicta que se desmorona: los sucesos que caen entre el
202        // cuadro anterior y éste desprenden materia, y lo que ya volaba avanza.
203        // Va antes de encuadrar porque cambia el mundo que la cámara va a ver.
204        let t0 = self.t_previo.unwrap_or(t - 1e-4);
205        let sucesos = self.escena.sucesos.clone();
206        self.preview.desmoronar(queue, &sucesos, t0, t, &mut self.ceniza);
207        if !self.ceniza.is_empty() {
208            self.ceniza.paso((t - t0).max(0.0));
209        }
210        self.t_previo = Some(t);
211
212        let mut poses = Vec::with_capacity(self.guiones.len());
213        let mut centroide = Vec3::ZERO;
214        for (guion, quien) in self.guiones.iter().zip(&self.reparto) {
215            // Tiempo cuantizado a la tasa propia del actor: el Héroe a 12 fps
216            // se mueve y posa en doses; los demás, fluidos. Es un sello de
217            // animación, no una optimización.
218            let at = guion.quantize(t);
219            let s = guion.sample(at);
220            let pos = self.preview.ground_at_world(s.gx as i32, s.gz as i32) - half;
221            centroide += pos;
222            poses.push((pos, s, quien, at));
223        }
224        let mirar = if poses.is_empty() {
225            Vec3::new(0.0, self.dim[1] as f32 * (0.32 - 0.5), 0.0)
226        } else {
227            centroide / poses.len() as f32 + Vec3::new(0.0, 1.0, 0.0)
228        };
229        let distancia = 6.0 + poses.len() as f32 * 1.2;
230        let camara = self.escena.camera_at(mirar, distancia, t);
231
232        let mut mallas = Vec::with_capacity(poses.len());
233        let mut en_cuadro: Vec<EnCuadro> = Vec::new();
234        for (pos, s, quien, at) in &poses {
235            if quien.cuerpo.is_none() {
236                // Humanoide → ser SDF (toon + cara por decals), en coords de
237                // grilla: el ray-marcher los traza.
238                en_cuadro.push(EnCuadro {
239                    spec: quien,
240                    pos: *pos + half,
241                    yaw: s.facing,
242                    clip: s.clip,
243                    fase: *at,
244                });
245            } else {
246                // Rig → malla de su andar.
247                mallas.push(quien.to_meta(*pos, s.facing, s.clip, *at, Some(camara.eye)));
248            }
249        }
250        // Por el REPARTO y no crudo: el uniform tiene lugar para unas pocas
251        // primitivas, así que los de cerca se llevan la cara entera y los del
252        // fondo la pierden. Pasarle la lista cruda haría que los que sobran se
253        // caigan del cuadro sin aviso. Y al que ni degradado entra, el reparto
254        // MIXTO lo baja a malla de cajas en vez de borrarlo del plano.
255        let rep = reparto_mixto(&en_cuadro, camara.eye + half, true, half);
256        mallas.extend(rep.mallas);
257        let seres = rep.sers;
258        self.preview.set_motas(&self.ceniza, camara.eye + half);
259
260        self.preview.render_scene(
261            device, queue, encoder, target, viewport, rect, &camara, &mallas, seres,
262        );
263    }
264
265    /// La cámara de la escena en `t` — para que el caller pueda encuadrar algo
266    /// más (un cartel anclado al mundo, por ejemplo) sin re-derivarla.
267    pub fn camara_en(&self, t: f32) -> Camera3d {
268        let half = Vec3::new(self.dim[0] as f32, self.dim[1] as f32, self.dim[2] as f32) * 0.5;
269        let mut centroide = Vec3::ZERO;
270        let mut n = 0.0f32;
271        for guion in &self.guiones {
272            let s = guion.sample(guion.quantize(t));
273            centroide += self.preview.ground_at_world(s.gx as i32, s.gz as i32) - half;
274            n += 1.0;
275        }
276        let mirar = if n == 0.0 {
277            Vec3::new(0.0, self.dim[1] as f32 * (0.32 - 0.5), 0.0)
278        } else {
279            centroide / n + Vec3::new(0.0, 1.0, 0.0)
280        };
281        self.escena.camera_at(mirar, 6.0 + n * 1.2, t)
282    }
283}