tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! Demo headless del **rig**: las cuatro morfologías preset (humanoide, cuadrúpedo,
2//! ave, serpiente) en fila, en plena caminata (andar procedural), a un PNG.
3//!
4//! `cargo run -p llimphi-voxel --example rig_demo --release` → `/tmp/rig.png`
5
6use std::fs::File;
7use std::io::BufWriter;
8
9use llimphi_3d::glam::{Mat4, Vec3};
10use llimphi_3d::{Camera3d, Renderer3d, Scene3d};
11use llimphi_hal::{wgpu, Hal};
12use llimphi_raster::peniko::Color;
13use llimphi_raster::{vello, Renderer};
14use llimphi_voxel::{Andar, Rig};
15
16const W: u32 = 1100;
17const H: u32 = 520;
18const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
19
20fn main() {
21 let phase: f32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(1.1);
22
23 let hal = pollster::block_on(Hal::new(None)).expect("hal");
24 let mut renderer = Renderer::new(&hal).expect("renderer");
25
26 // Las cuatro criaturas, con paletas distinguibles (piel, cuerpo, patas).
27 let palettes = [
28 ([0.90, 0.72, 0.58], [0.82, 0.30, 0.28], [0.20, 0.22, 0.34]), // humanoide
29 ([0.78, 0.62, 0.42], [0.55, 0.42, 0.30], [0.40, 0.30, 0.22]), // cuadrúpedo
30 ([0.95, 0.80, 0.30], [0.30, 0.55, 0.85], [0.85, 0.55, 0.20]), // ave
31 ([0.55, 0.80, 0.40], [0.30, 0.62, 0.34], [0.30, 0.62, 0.34]), // serpiente
32 ];
33 let rigs = Rig::presets();
34
35 // Un Renderer3d por criatura, ubicado en su columna (separadas en X).
36 let mut actors: Vec<Renderer3d> = Vec::new();
37 let spacing = 2.6_f32;
38 let x0 = -(rigs.len() as f32 - 1.0) * 0.5 * spacing;
39 for (k, rig) in rigs.iter().enumerate() {
40 let (piel, cuerpo, patas) = palettes[k];
41 let andar = Andar::caminar(rig);
42 let pose = andar.pose(phase);
43 let (v, i) = rig.mesh(&pose, piel, cuerpo, patas);
44 let mut r = Renderer3d::new(&hal.device, FMT);
45 r.set_geometry(&hal.device, &v, &i);
46 r.set_model(Mat4::from_translation(Vec3::new(x0 + k as f32 * spacing, 0.0, 0.0)));
47 actors.push(r);
48 }
49
50 // Cámara: mira al centro de la fila, a la altura del pecho, en 3/4.
51 let camera = Camera3d::orbit(
52 Vec3::new(0.0, 0.7, 0.0),
53 28_f32.to_radians(),
54 18_f32.to_radians(),
55 7.5,
56 );
57
58 let mut scene = Scene3d::new();
59 let target = make_target(&hal);
60 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
61 let sky = Color::from_rgba8(150, 186, 224, 255);
62 renderer.render_to_view(&hal, &vello::Scene::new(), &view, W, H, sky).expect("clear");
63
64 let mut enc = hal
65 .device
66 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("rig") });
67 let refs: Vec<&Renderer3d> = actors.iter().collect();
68 scene.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), &camera, None, &refs);
69 hal.queue.submit(std::iter::once(enc.finish()));
70 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
71
72 let pixels = readback(&hal, &target);
73 write_png(&pixels, "/tmp/rig.png");
74 eprintln!("escrito /tmp/rig.png (humanoide · cuadrúpedo · ave · serpiente, fase {phase})");
75}
76
77fn make_target(hal: &Hal) -> wgpu::Texture {
78 hal.device.create_texture(&wgpu::TextureDescriptor {
79 label: Some("rig-target"),
80 size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
81 mip_level_count: 1,
82 sample_count: 1,
83 dimension: wgpu::TextureDimension::D2,
84 format: FMT,
85 usage: wgpu::TextureUsages::STORAGE_BINDING
86 | wgpu::TextureUsages::TEXTURE_BINDING
87 | wgpu::TextureUsages::RENDER_ATTACHMENT
88 | wgpu::TextureUsages::COPY_SRC,
89 view_formats: &[],
90 })
91}
92
93fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
94 let unpadded = (W * 4) as usize;
95 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
96 let padded = unpadded.div_ceil(align) * align;
97 let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
98 label: Some("readback"),
99 size: (padded * H as usize) as u64,
100 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
101 mapped_at_creation: false,
102 });
103 let mut enc = hal
104 .device
105 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
106 enc.copy_texture_to_buffer(
107 wgpu::TexelCopyTextureInfo {
108 texture: target,
109 mip_level: 0,
110 origin: wgpu::Origin3d::ZERO,
111 aspect: wgpu::TextureAspect::All,
112 },
113 wgpu::TexelCopyBufferInfo {
114 buffer: &buf,
115 layout: wgpu::TexelCopyBufferLayout {
116 offset: 0,
117 bytes_per_row: Some(padded as u32),
118 rows_per_image: Some(H),
119 },
120 },
121 wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
122 );
123 hal.queue.submit(std::iter::once(enc.finish()));
124 let slice = buf.slice(..);
125 let (tx, rx) = std::sync::mpsc::channel();
126 slice.map_async(wgpu::MapMode::Read, move |r| {
127 let _ = tx.send(r);
128 });
129 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
130 rx.recv().unwrap().unwrap();
131 let data = slice.get_mapped_range();
132 let mut pixels = Vec::with_capacity((W * H * 4) as usize);
133 for row in 0..H as usize {
134 let s = row * padded;
135 pixels.extend_from_slice(&data[s..s + unpadded]);
136 }
137 drop(data);
138 buf.unmap();
139 pixels
140}
141
142fn write_png(pixels: &[u8], path: &str) {
143 let file = File::create(path).expect("png");
144 let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
145 enc.set_color(png::ColorType::Rgba);
146 enc.set_depth(png::BitDepth::Eight);
147 let mut wtr = enc.write_header().unwrap();
148 wtr.write_image_data(pixels).unwrap();
149}