1# llimphi-voxel-app — máquina de cine voxel (machinima)
2
3App showcase del motor voxel 3D de Llimphi, convertida en una **máquina de cine
4voxel**: generar escenas controladas, mover personajes y **filmar una película**
5— sin pantalla, de forma determinista, a un video con sonido.
6
7No es un clon de juego: es la prueba de que el motor (`llimphi-3d`) + la capa de
8dinámica (`llimphi-voxel`) alcanzan para *dirigir* una escena. El diseño y el
9porqué están en `01_yachay/dominium/MOTOR-VOXEL.md` §12; este README es la **guía
10práctica**: cómo correrla y cómo escribir tu propia película.
11
12---
13
14## Los modos
15
16```bash
17cargo run -p llimphi-voxel-app --release -- --film # filma el guion → /tmp/voxel_film.mkv (AV1+Opus, con audio)
18cargo run -p llimphi-voxel-app --release -- --born # corto "nace en el desierto" → /tmp/voxel_born.mkv (AV1+Opus)
19cargo run -p llimphi-voxel-app --release -- --poses # catálogo de clips de actor → /tmp/actor_clips.png
20cargo run -p llimphi-voxel-app --release -- --vox # importa un .vox y lo renderiza → /tmp/vox_import.png
21cargo run -p llimphi-voxel-app --release # ventana interactiva (orbitar / explorar en 1ª persona)
22mpv /tmp/voxel_film.mkv # ver la película
23```
24
25El modo **`--born`** filma la *secuencia de nacimiento*: un mundo de **desierto
26llano** (`World::build_desert`, vía `WorldRecipe::desert` aplanada), un **huevo**
27(`Egg`) posado sobre la arena, y la cámara guionada por `BirthSequence` que **cae
28del cielo**, ve el huevo, **aterriza en el punto de vista** del recién nacido y
29**sale a tercera persona** mientras el huevo eclosiona y el niño emerge hacia un
30costado del cascarón. Banda sonora con el acento en el instante del nacimiento.
31Une las cuatro piezas del corto (creador de mundos · personaje por edad · huevo/
32eclosión · modos de cámara) en un pipeline punta a punta, determinista y sin
33pantalla.
34
35**Resolución de salida** (aplica a `--film`, `--born` y `--vox`): por defecto 960×540; subila con
36un flag extra `--720` / `--1080` / `--4k` — p.ej.
37`… --film --1080` rinde 1920×1080. En 4K el supersampling baja a 1× (a resolución
38nativa el ray-march ya queda nítido); el resto rinde a 2× y baja a la final.
39
40Todo el render de cine es **headless** (no necesita display): se rinde cada
41cuadro a una textura, se baja por supersampling a PNG, y `ffmpeg` los junta.
42
43---
44
45## Arquitectura en capas
46
47```
48 takiy-core/synth ─ banda sonora (Score → WAV)
49 │
50 foreign-vox ── .vox ─────┤
51 foreign-av ── ffmpeg ───┤
52 ▼
53 app (este crate) ── screenplay() + soundtrack() + golem_model()
54 │ Sequence (dirección) · Actor (reparto) · World (escenario)
55 ▼
56 llimphi-voxel ── Actor/Clip · director (ActorScript/Shot/Sequence) · terrain · vox import
57 ▼
58 llimphi-3d ── Scene3d · Camera3d · CameraTrack · VoxelRenderer · Renderer3d · Hud
59 ▼
60 wgpu (Vulkan/Metal/DX12/GL/WebGPU)
61```
62
63El motor (`llimphi-3d`) es **general** y no sabe de cine; la dirección vive en
64`llimphi-voxel::director` (data) y el **guion concreto** en la app (`film.rs`).
65
66---
67
68## Escribir una película (el guion)
69
70Una película es un [`Sequence`]: un reparto de **guiones de actor**
71(`ActorScript`) + una lista de **planos de cámara** (`Shot`) + la duración. Todo
72es determinista por tiempo: `t` fija el estado de la escena.
73
74### 1. El guion de cada actor — `ActorScript`
75
76Keyframes `(t, posición de grilla, clip?, rumbo?)`. La posición interpola; el
77clip y el rumbo son automáticos si no se fijan (camina si se mueve, mira hacia
78donde va).
79
80```rust
81use llimphi_voxel::{ActorScript, ActorKey, Clip};
82use std::f32::consts::{FRAC_PI_2, PI};
83
84let script = ActorScript::new(vec![
85 ActorKey::at(0.0, 34.0, 50.0), // arranca aquí (en grilla)
86 ActorKey::at(2.6, 50.0, 50.0).facing(FRAC_PI_2), // camina hasta aquí mirando +X
87 ActorKey::at(3.0, 50.0, 50.0).play(Clip::Wave).facing(PI), // gira a cámara y saluda
88 ActorKey::at(5.6, 50.0, 50.0).play(Clip::Wave).facing(PI), // sostiene hasta el final
89]);
90// script.sample(t) → ActorSample { gx, gz, facing, clip }
91```
92
93- **`.facing(yaw)`** en dos keys consecutivas → el actor **gira suave** entre
94 ambas (interpolación por el ángulo más corto). En una sola → mantiene ese rumbo.
95- **`.play(clip)`** fuerza un clip (saludar, festejar…); sin él, el director
96 elige `Walk` si el segmento se mueve o `Idle` si está quieto.
97- Posición en **grilla** (`gx, gz`): la app la posa sobre el relieve con
98 `World::ground_at` cada cuadro.
99
100### 2. Los planos de cámara — `Shot` + `CameraTrack`
101
102Una `CameraTrack` interpola poses `(t, eye, target, fov)` con suavizado; un
103`Shot` la ancla a un instante. Varios planos = **cortes duros** (cambia de plano
104sin interpolar entre ellos).
105
106```rust
107use llimphi_3d::{CameraTrack, CamKey, glam::Vec3};
108use llimphi_voxel::{Sequence, Shot};
109
110let focus = Vec3::new(0.0, 8.0, 0.0); // a dónde mira (mundo, centrado en el origen)
111
112let establishing = CameraTrack::new(vec![
113 CamKey::look(0.0, focus + Vec3::new(-6.0, 9.0, -30.0), focus, 52.0), // grúa lejos
114 CamKey::look(2.8, focus + Vec3::new(-2.0, 3.5, -17.0), focus, 44.0), // entra
115]);
116let closeup = CameraTrack::new(vec![
117 CamKey::look(0.0, focus + Vec3::new(0.0, 2.6, -11.0), focus, 40.0), // plano corto
118 CamKey::look(2.8, focus + Vec3::new(1.4, 2.1, -9.0), focus, 37.0), // empuja
119]);
120
121let seq = Sequence::new(
122 vec![script], // reparto
123 vec![Shot::new(0.0, establishing), Shot::new(2.8, closeup)], // corte a los 2.8 s
124 5.6, // duración
125);
126// seq.camera(t) → Camera3d · seq.frames(fps) → nº de cuadros · seq.cast_centroid(t)
127```
128
129Las `CamKey` se construyen **después** de conocer el terreno (la `Y` del foco
130sale de `World::ground_at`), por eso el guion se arma en `screenplay()` con el
131mundo ya construido.
132
133### 3. Reproducir — el bucle de `--film`
134
135```rust
136for f in 0..seq.frames(FPS) {
137 let t = f as f32 / FPS as f32;
138 for (actor, script) in cast.iter_mut().zip(&seq.actors) {
139 let s = script.sample(t);
140 actor.pos = world.ground_at(s.gx as u32, s.gz as u32); // posar sobre el relieve
141 actor.facing = s.facing;
142 actor.set_clip(s.clip); // dispara el cross-fade si cambió de clip
143 actor.advance(dt); // avanza la animación
144 }
145 // … subir las mallas de los actores, render con seq.camera(t), volcar PNG …
146}
147```
148
149**El guion canónico vive en `src/film.rs::screenplay()`** — cópialo y editalo
150para tu propia escena.
151
152---
153
154## El reparto — `Actor` y la librería de clips
155
156Un [`Actor`] es un muñeco de cajas articuladas (cabeza/torso/2 brazos/2 piernas)
157que produce una **malla** por cuadro; la app la sube a un `Renderer3d` y la
158compone con el terreno en `Scene3d` (oclusión correcta).
159
160```rust
161use llimphi_voxel::{Actor, Clip};
162use llimphi_3d::glam::Vec3;
163
164let mut a = Actor::new(Vec3::ZERO, 0.0)
165 .with_colors([0.88,0.70,0.56], [0.82,0.28,0.26], [0.18,0.20,0.28]); // piel, remera, pantalón
166a.set_clip(Clip::Run); // cambia con cross-fade suave (no corta)
167a.advance(dt); // avanza la fase a la cadencia del clip
168let (verts, idx) = a.mesh(); // malla local de la pose actual
169let model = a.model(); // matriz de ubicación (pos + rumbo)
170```
171
172Clips disponibles: `Idle`, `Walk`, `Run`, `Wave`, `Point`, `Cheer`.
173
174### Agregar un clip nuevo
175
176Un clip es una **función `fase → Pose`** (los ángulos de todas las
177articulaciones). Para sumar una animación, edita `llimphi-voxel/src/actor.rs`:
178
1791. Agrega la variante a `enum Clip`.
1802. Dale una cadencia en `Clip::cadence`.
1813. Devuelve su `Pose` en `Clip::pose` (mezclando `leg_l/r`, `arm_l/r`,
182 `arm_l/r_out` —apertura lateral—, `head_pitch`, `bob`, `lean`).
183
184No se toca el render: `Actor::mesh` hornea cualquier `Pose`. Verificalo con
185`--poses` (vuelca una fila etiquetada con todos los clips).
186
187---
188
189## Importar assets diseñados — `.vox` (MagicaVoxel)
190
191Diseñás un set o personaje en MagicaVoxel y lo metés a la escena:
192
193```rust
194// Un archivo .vox → VoxelGrid del motor (remapea z-arriba → y-arriba):
195let grid = llimphi_voxel::load_grid("mi_modelo.vox")?;
196
197// O componer un set metiendo piezas en un grid existente:
198let model = foreign_vox::parse(&std::fs::read("pieza.vox")?)?[0].clone();
199llimphi_voxel::stamp(&mut grid, &model, [origin_x, origin_y, origin_z]);
200```
201
202`foreign_vox::{parse, write}` también permite **exportar** una escena voxel a
203`.vox` para editarla afuera. El modo `--vox` genera un golem programático
204(`golem_model()`), lo escribe a `.vox` y lo reimporta — el camino real de carga.
205
206> Nota: si el `.vox` no trae chunk `RGBA`, se usa una paleta de fallback (rampa
207> HSV). Los exportes de MagicaVoxel siempre incluyen `RGBA`, así que entran bien.
208
209---
210
211## La banda sonora — `takiy`
212
213`src/soundtrack.rs` compone un `Score` con `takiy-core` (progresión, pads, bajo,
214melodía) y lo sintetiza a WAV con `takiy-synth`:
215
216```rust
217use takiy_core::{Score, Track, ScoreNote, Pitch, Chord, ChordQuality, PitchClass};
218use takiy_synth::{OscRenderer, Renderer, Waveform, Adsr, write_wav};
219
220let mut score = Score::new(86.0); // bpm
221let mut lead = Track::new("lead");
222lead.add(ScoreNote::new(Pitch::from_midi(72).unwrap(), 0.0, 1.0, 78)); // (pitch, start, dur, vel)
223score.add_track(lead);
224
225let buf = OscRenderer { sample_rate: 44_100, waveform: Waveform::Triangle, envelope: Adsr::DEFAULT }
226 .render(&score);
227write_wav(&buf, "/tmp/banda.wav")?;
228```
229
230`--film` muxea ese WAV al video con `foreign_av::encode_frames(pattern, fps, crf,
231Some(audio_path), out)` → pista **Opus**, recortada a la duración del video
232(`-shortest`).
233
234---
235
236## El render (cómo se ve bien sin pantalla)
237
238- **Headless**: cada cuadro se rinde a una textura intermedia y se lee de vuelta
239 a PNG (`crate::write_png_downsampled`); `--film` arma la secuencia
240 `frame_%04d.png` y la pasa a `foreign_av::encode_frames` (AV1 + Opus).
241- **Supersampling (SSAA)**: `--film`/`--vox` renderizan a **2×** y bajan
242 promediando bloques 2×2 → antialias de los bordes duros del ray-march.
243- **Luz con color**: el sol se tiñe por su elevación (cálido al ras → blanco en
244 lo alto) y el ambiente por el color de cielo (rebote frío). Se controla el
245 *mood* moviendo `VoxelRenderer::sun_dir` y `Atmosphere::{sky_zenith, sky_horizon}`.
246- **HUD/texto**: `llimphi_3d::{Hud, HudQuad}` pinta overlays screen-space (mira,
247 texto con fuente bitmap 5×7) — lo usa `--poses` para las etiquetas.
248
249---
250
251## Gotchas (aprendidos filmando)
252
253- **Encuadre del trío**: si los actores están muy separados en Z y la cámara
254 cerca, el del carril cercano se come el lente. Apretá la separación (±2.5) o
255 aleja la cámara (≥9 unidades).
256- **Tierra firme y plana**: el terreno es procedural; `find_flat_strip` (en
257 `film.rs`) elige un llano sobre el nivel del mar para que no caminen sobre el
258 agua ni los hunda el relieve.
259- **El monumento**: el escenario trae un cubo-malla flotante de demo; apagalo con
260 `World::show_monument(false)` cuando filmas personajes.
261- **`sun_dir` y caras a cámara**: el sol ilumina las caras que lo miran; si tu
262 cámara mira la cara opuesta al sol, esa cara queda sólo con ambiente. Orientá
263 `sun_dir` hacia la cámara para un *hero shot*.
264- **`ffmpeg`**: hace falta en el PATH con `libsvtav1` + `libopus`. Sin él, los
265 PNG quedan igual en `/tmp/voxel_film/` y se avisa cómo muxear a mano.
266- **Determinismo**: todo es función de `t` (y de semillas LCG). El mismo guion
267 produce el mismo video — ideal para iterar.
268
269---
270
271## Mapa de archivos
272
273| Archivo | Qué tiene |
274|---|---|
275| `src/main.rs` | flags (`--film/--born/--poses/--vox`), bucle interactivo, `write_png[_downsampled]` |
276| `src/film.rs` | `screenplay()` (el guion), `--film`/`--born`/`--poses`/`--vox`, `golem_model()`, SSAA |
277| `src/world.rs` | escenario: terreno + atmósfera + monumento + manada; `build_desert`, `render_with`, `ground_at` |
278| `src/soundtrack.rs` | la banda sonora (takiy) |
279| `llimphi-voxel/src/{actor,director,vox}.rs` | reparto, timeline, import `.vox` (reusables) |
280| `llimphi-3d/src/{scene,camera,cinema,voxel_renderer,renderer,hud}.rs` | el motor 3D general |
281
282[`Actor`]: ../llimphi-voxel/src/actor.rs
283[`Sequence`]: ../llimphi-voxel/src/director.rs
README.md
llimphi-voxel-app — máquina de cine voxel (machinima)
App showcase del motor voxel 3D de Llimphi, convertida en una máquina de cine
voxel: generar escenas controladas, mover personajes y filmar una película
— sin pantalla, de forma determinista, a un video con sonido.
No es un clon de juego: es la prueba de que el motor (llimphi-3d) + la capa de
dinámica (llimphi-voxel) alcanzan para dirigir una escena. El diseño y el
porqué están en 01_yachay/dominium/MOTOR-VOXEL.md §12; este README es la guía
práctica: cómo correrla y cómo escribir tu propia película.
Los modos
cargo run -p llimphi-voxel-app --release -- --film # filma el guion → /tmp/voxel_film.mkv (AV1+Opus, con audio)
cargo run -p llimphi-voxel-app --release -- --born # corto "nace en el desierto" → /tmp/voxel_born.mkv (AV1+Opus)
cargo run -p llimphi-voxel-app --release -- --poses # catálogo de clips de actor → /tmp/actor_clips.png
cargo run -p llimphi-voxel-app --release -- --vox # importa un .vox y lo renderiza → /tmp/vox_import.png
cargo run -p llimphi-voxel-app --release # ventana interactiva (orbitar / explorar en 1ª persona)
mpv /tmp/voxel_film.mkv # ver la película
El modo --born filma la secuencia de nacimiento: un mundo de desierto
llano (World::build_desert, vía WorldRecipe::desert aplanada), un huevo
(Egg) posado sobre la arena, y la cámara guionada por BirthSequence que cae
del cielo, ve el huevo, aterriza en el punto de vista del recién nacido y
sale a tercera persona mientras el huevo eclosiona y el niño emerge hacia un
costado del cascarón. Banda sonora con el acento en el instante del nacimiento.
Une las cuatro piezas del corto (creador de mundos · personaje por edad · huevo/
eclosión · modos de cámara) en un pipeline punta a punta, determinista y sin
pantalla.
Resolución de salida (aplica a --film, --born y --vox): por defecto 960×540; subila con
un flag extra --720 / --1080 / --4k — p.ej.
… --film --1080 rinde 1920×1080. En 4K el supersampling baja a 1× (a resolución
nativa el ray-march ya queda nítido); el resto rinde a 2× y baja a la final.
Todo el render de cine es headless (no necesita display): se rinde cada
cuadro a una textura, se baja por supersampling a PNG, y ffmpeg los junta.
Arquitectura en capas
takiy-core/synth ─ banda sonora (Score → WAV)
│
foreign-vox ── .vox ─────┤
foreign-av ── ffmpeg ───┤
▼
app (este crate) ── screenplay() + soundtrack() + golem_model()
│ Sequence (dirección) · Actor (reparto) · World (escenario)
▼
llimphi-voxel ── Actor/Clip · director (ActorScript/Shot/Sequence) · terrain · vox import
▼
llimphi-3d ── Scene3d · Camera3d · CameraTrack · VoxelRenderer · Renderer3d · Hud
▼
wgpu (Vulkan/Metal/DX12/GL/WebGPU)
El motor (llimphi-3d) es general y no sabe de cine; la dirección vive en
llimphi-voxel::director (data) y el guion concreto en la app (film.rs).
Escribir una película (el guion)
Una película es un Sequence: un reparto de guiones de actor
(ActorScript) + una lista de planos de cámara (Shot) + la duración. Todo
es determinista por tiempo: t fija el estado de la escena.
1. El guion de cada actor — ActorScript
Keyframes (t, posición de grilla, clip?, rumbo?). La posición interpola; el
clip y el rumbo son automáticos si no se fijan (camina si se mueve, mira hacia
donde va).
use llimphi_voxel::{ActorScript, ActorKey, Clip};
use std::f32::consts::{FRAC_PI_2, PI};
let script = ActorScript::new(vec![
ActorKey::at(0.0, 34.0, 50.0), // arranca aquí (en grilla)
ActorKey::at(2.6, 50.0, 50.0).facing(FRAC_PI_2), // camina hasta aquí mirando +X
ActorKey::at(3.0, 50.0, 50.0).play(Clip::Wave).facing(PI), // gira a cámara y saluda
ActorKey::at(5.6, 50.0, 50.0).play(Clip::Wave).facing(PI), // sostiene hasta el final
]);
// script.sample(t) → ActorSample { gx, gz, facing, clip }
.facing(yaw) en dos keys consecutivas → el actor gira suave entre
ambas (interpolación por el ángulo más corto). En una sola → mantiene ese rumbo.
.play(clip) fuerza un clip (saludar, festejar…); sin él, el director
elige Walk si el segmento se mueve o Idle si está quieto.
- Posición en grilla (
gx, gz): la app la posa sobre el relieve con
World::ground_at cada cuadro.
2. Los planos de cámara — Shot + CameraTrack
Una CameraTrack interpola poses (t, eye, target, fov) con suavizado; un
Shot la ancla a un instante. Varios planos = cortes duros (cambia de plano
sin interpolar entre ellos).
use llimphi_3d::{CameraTrack, CamKey, glam::Vec3};
use llimphi_voxel::{Sequence, Shot};
let focus = Vec3::new(0.0, 8.0, 0.0); // a dónde mira (mundo, centrado en el origen)
let establishing = CameraTrack::new(vec![
CamKey::look(0.0, focus + Vec3::new(-6.0, 9.0, -30.0), focus, 52.0), // grúa lejos
CamKey::look(2.8, focus + Vec3::new(-2.0, 3.5, -17.0), focus, 44.0), // entra
]);
let closeup = CameraTrack::new(vec![
CamKey::look(0.0, focus + Vec3::new(0.0, 2.6, -11.0), focus, 40.0), // plano corto
CamKey::look(2.8, focus + Vec3::new(1.4, 2.1, -9.0), focus, 37.0), // empuja
]);
let seq = Sequence::new(
vec![script], // reparto
vec![Shot::new(0.0, establishing), Shot::new(2.8, closeup)], // corte a los 2.8 s
5.6, // duración
);
// seq.camera(t) → Camera3d · seq.frames(fps) → nº de cuadros · seq.cast_centroid(t)
Las CamKey se construyen después de conocer el terreno (la Y del foco
sale de World::ground_at), por eso el guion se arma en screenplay() con el
mundo ya construido.
3. Reproducir — el bucle de --film
for f in 0..seq.frames(FPS) {
let t = f as f32 / FPS as f32;
for (actor, script) in cast.iter_mut().zip(&seq.actors) {
let s = script.sample(t);
actor.pos = world.ground_at(s.gx as u32, s.gz as u32); // posar sobre el relieve
actor.facing = s.facing;
actor.set_clip(s.clip); // dispara el cross-fade si cambió de clip
actor.advance(dt); // avanza la animación
}
// … subir las mallas de los actores, render con seq.camera(t), volcar PNG …
}
El guion canónico vive en src/film.rs::screenplay() — cópialo y editalo
para tu propia escena.
El reparto — Actor y la librería de clips
Un Actor es un muñeco de cajas articuladas (cabeza/torso/2 brazos/2 piernas)
que produce una malla por cuadro; la app la sube a un Renderer3d y la
compone con el terreno en Scene3d (oclusión correcta).
use llimphi_voxel::{Actor, Clip};
use llimphi_3d::glam::Vec3;
let mut a = Actor::new(Vec3::ZERO, 0.0)
.with_colors([0.88,0.70,0.56], [0.82,0.28,0.26], [0.18,0.20,0.28]); // piel, remera, pantalón
a.set_clip(Clip::Run); // cambia con cross-fade suave (no corta)
a.advance(dt); // avanza la fase a la cadencia del clip
let (verts, idx) = a.mesh(); // malla local de la pose actual
let model = a.model(); // matriz de ubicación (pos + rumbo)
Clips disponibles: Idle, Walk, Run, Wave, Point, Cheer.
Agregar un clip nuevo
Un clip es una función fase → Pose (los ángulos de todas las
articulaciones). Para sumar una animación, edita llimphi-voxel/src/actor.rs:
- Agrega la variante a
enum Clip.
- Dale una cadencia en
Clip::cadence.
- Devuelve su
Pose en Clip::pose (mezclando leg_l/r, arm_l/r,
arm_l/r_out —apertura lateral—, head_pitch, bob, lean).
No se toca el render: Actor::mesh hornea cualquier Pose. Verificalo con
--poses (vuelca una fila etiquetada con todos los clips).
Importar assets diseñados — .vox (MagicaVoxel)
Diseñás un set o personaje en MagicaVoxel y lo metés a la escena:
// Un archivo .vox → VoxelGrid del motor (remapea z-arriba → y-arriba):
let grid = llimphi_voxel::load_grid("mi_modelo.vox")?;
// O componer un set metiendo piezas en un grid existente:
let model = foreign_vox::parse(&std::fs::read("pieza.vox")?)?[0].clone();
llimphi_voxel::stamp(&mut grid, &model, [origin_x, origin_y, origin_z]);
foreign_vox::{parse, write} también permite exportar una escena voxel a
.vox para editarla afuera. El modo --vox genera un golem programático
(golem_model()), lo escribe a .vox y lo reimporta — el camino real de carga.
Nota: si el .vox no trae chunk RGBA, se usa una paleta de fallback (rampa
HSV). Los exportes de MagicaVoxel siempre incluyen RGBA, así que entran bien.
La banda sonora — takiy
src/soundtrack.rs compone un Score con takiy-core (progresión, pads, bajo,
melodía) y lo sintetiza a WAV con takiy-synth:
use takiy_core::{Score, Track, ScoreNote, Pitch, Chord, ChordQuality, PitchClass};
use takiy_synth::{OscRenderer, Renderer, Waveform, Adsr, write_wav};
let mut score = Score::new(86.0); // bpm
let mut lead = Track::new("lead");
lead.add(ScoreNote::new(Pitch::from_midi(72).unwrap(), 0.0, 1.0, 78)); // (pitch, start, dur, vel)
score.add_track(lead);
let buf = OscRenderer { sample_rate: 44_100, waveform: Waveform::Triangle, envelope: Adsr::DEFAULT }
.render(&score);
write_wav(&buf, "/tmp/banda.wav")?;
--film muxea ese WAV al video con foreign_av::encode_frames(pattern, fps, crf, Some(audio_path), out) → pista Opus, recortada a la duración del video
(-shortest).
El render (cómo se ve bien sin pantalla)
- Headless: cada cuadro se rinde a una textura intermedia y se lee de vuelta
a PNG (
crate::write_png_downsampled); --film arma la secuencia
frame_%04d.png y la pasa a foreign_av::encode_frames (AV1 + Opus).
- Supersampling (SSAA):
--film/--vox renderizan a 2× y bajan
promediando bloques 2×2 → antialias de los bordes duros del ray-march.
- Luz con color: el sol se tiñe por su elevación (cálido al ras → blanco en
lo alto) y el ambiente por el color de cielo (rebote frío). Se controla el
mood moviendo
VoxelRenderer::sun_dir y Atmosphere::{sky_zenith, sky_horizon}.
- HUD/texto:
llimphi_3d::{Hud, HudQuad} pinta overlays screen-space (mira,
texto con fuente bitmap 5×7) — lo usa --poses para las etiquetas.
Gotchas (aprendidos filmando)
- Encuadre del trío: si los actores están muy separados en Z y la cámara
cerca, el del carril cercano se come el lente. Apretá la separación (±2.5) o
aleja la cámara (≥9 unidades).
- Tierra firme y plana: el terreno es procedural;
find_flat_strip (en
film.rs) elige un llano sobre el nivel del mar para que no caminen sobre el
agua ni los hunda el relieve.
- El monumento: el escenario trae un cubo-malla flotante de demo; apagalo con
World::show_monument(false) cuando filmas personajes.
sun_dir y caras a cámara: el sol ilumina las caras que lo miran; si tu
cámara mira la cara opuesta al sol, esa cara queda sólo con ambiente. Orientá
sun_dir hacia la cámara para un hero shot.
ffmpeg: hace falta en el PATH con libsvtav1 + libopus. Sin él, los
PNG quedan igual en /tmp/voxel_film/ y se avisa cómo muxear a mano.
- Determinismo: todo es función de
t (y de semillas LCG). El mismo guion
produce el mismo video — ideal para iterar.
Mapa de archivos
| Archivo | Qué tiene |
src/main.rs | flags (--film/--born/--poses/--vox), bucle interactivo, write_png[_downsampled] |
src/film.rs | screenplay() (el guion), --film/--born/--poses/--vox, golem_model(), SSAA |
src/world.rs | escenario: terreno + atmósfera + monumento + manada; build_desert, render_with, ground_at |
src/soundtrack.rs | la banda sonora (takiy) |
llimphi-voxel/src/{actor,director,vox}.rs | reparto, timeline, import .vox (reusables) |
llimphi-3d/src/{scene,camera,cinema,voxel_renderer,renderer,hud}.rs | el motor 3D general |