tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4
llimphi-3d/src/renderer.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
1//! `Renderer3d` — pipeline wgpu mínimo que dibuja geometría 3D indexada con
2//! test de profundidad sobre la textura intermedia del frame de Llimphi.
3//!
4//! La firma de [`Renderer3d::render`] es la que pide la closure de
5//! `View::gpu_paint_with` (`device, queue, encoder, target_view, (w, h)`), más
6//! la cámara — así un nodo 3D entra en el árbol `View<Msg>` sin tocar el
7//! runtime. Mantiene su **propio depth buffer** (recreado al cambiar de
8//! tamaño); el color se compone con `LoadOp::Load` para preservar la UI vello
9//! que ya está debajo.
10
11use glam::Mat4;
12
13use crate::camera::Camera3d;
14use crate::mesh::{cube, Vertex3d};
15use crate::scene::{ensure_depth, DepthBuffer, DEPTH_FORMAT};
16
17/// Renderer de **mallas** indexadas (por defecto un cubo) visto desde una
18/// [`Camera3d`]. Cachea pipeline, buffers de geometría, uniform y (para el
19/// camino standalone) un depth propio. En [`Scene3d`](crate::Scene3d) comparte
20/// el depth con el pase de voxels para ocluirse mutuamente.
21///
22/// `model` ubica la malla en el mundo (default identidad): `mvp = view_proj ·
23/// model`, así una misma malla se instancia/posiciona sin reconstruir buffers.
24pub struct Renderer3d {
25 pipeline: wgpu::RenderPipeline,
26 vbuf: wgpu::Buffer,
27 ibuf: wgpu::Buffer,
28 index_count: u32,
29 ubuf: wgpu::Buffer,
30 bind_group: wgpu::BindGroup,
31 model: Mat4,
32 depth: Option<DepthBuffer>,
33}
34
35impl Renderer3d {
36 /// Crea el renderer para un `color_format` dado (el de la textura
37 /// intermedia del frame — `Rgba8Unorm` en headless, el de la surface en
38 /// vivo). Arranca con el cubo de prueba cargado.
39 pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
40 let (verts, indices) = cube();
41 Self::with_mesh(device, color_format, &verts, &indices)
42 }
43
44 /// Igual que [`Self::new`] pero con una malla arbitraria.
45 pub fn with_mesh(
46 device: &wgpu::Device,
47 color_format: wgpu::TextureFormat,
48 verts: &[Vertex3d],
49 indices: &[u16],
50 ) -> Self {
51 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
52 label: Some("llimphi-3d-shader"),
53 source: wgpu::ShaderSource::Wgsl(WGSL.into()),
54 });
55
56 let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
57 label: Some("llimphi-3d-bgl"),
58 entries: &[wgpu::BindGroupLayoutEntry {
59 binding: 0,
60 visibility: wgpu::ShaderStages::VERTEX,
61 ty: wgpu::BindingType::Buffer {
62 ty: wgpu::BufferBindingType::Uniform,
63 has_dynamic_offset: false,
64 min_binding_size: None,
65 },
66 count: None,
67 }],
68 });
69
70 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
71 label: Some("llimphi-3d-pl"),
72 bind_group_layouts: &[&bind_layout],
73 push_constant_ranges: &[],
74 });
75
76 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
77 label: Some("llimphi-3d-pipeline"),
78 layout: Some(&pipeline_layout),
79 vertex: wgpu::VertexState {
80 module: &shader,
81 entry_point: Some("vs"),
82 compilation_options: Default::default(),
83 buffers: &[wgpu::VertexBufferLayout {
84 array_stride: Vertex3d::SIZE as u64,
85 step_mode: wgpu::VertexStepMode::Vertex,
86 attributes: &[
87 wgpu::VertexAttribute {
88 format: wgpu::VertexFormat::Float32x3,
89 offset: 0,
90 shader_location: 0,
91 },
92 wgpu::VertexAttribute {
93 format: wgpu::VertexFormat::Float32x3,
94 offset: 12,
95 shader_location: 1,
96 },
97 ],
98 }],
99 },
100 primitive: wgpu::PrimitiveState {
101 topology: wgpu::PrimitiveTopology::TriangleList,
102 strip_index_format: None,
103 front_face: wgpu::FrontFace::Ccw,
104 // M0 sin cull: el depth test ya resuelve la oclusión y nos
105 // ahorra bugs de winding al sumar mallas. El cull entra en M1+.
106 cull_mode: None,
107 unclipped_depth: false,
108 polygon_mode: wgpu::PolygonMode::Fill,
109 conservative: false,
110 },
111 depth_stencil: Some(wgpu::DepthStencilState {
112 format: DEPTH_FORMAT,
113 depth_write_enabled: true,
114 depth_compare: wgpu::CompareFunction::Less,
115 stencil: wgpu::StencilState::default(),
116 bias: wgpu::DepthBiasState::default(),
117 }),
118 multisample: wgpu::MultisampleState::default(),
119 fragment: Some(wgpu::FragmentState {
120 module: &shader,
121 entry_point: Some("fs"),
122 compilation_options: Default::default(),
123 targets: &[Some(wgpu::ColorTargetState {
124 format: color_format,
125 // Opaco: el cubo reemplaza el fondo vello donde lo cubre.
126 blend: None,
127 write_mask: wgpu::ColorWrites::ALL,
128 })],
129 }),
130 multiview: None,
131 cache: None,
132 });
133
134 // Geometría → buffers (idiom `to_ne_bytes`, sin bytemuck).
135 let mut vbytes = Vec::with_capacity(verts.len() * Vertex3d::SIZE);
136 for v in verts {
137 v.write_to(&mut vbytes);
138 }
139 let vbuf = create_buffer_init(device, "llimphi-3d-vbuf", wgpu::BufferUsages::VERTEX, &vbytes);
140
141 let mut ibytes = Vec::with_capacity(indices.len() * 2);
142 for &i in indices {
143 ibytes.extend_from_slice(&i.to_ne_bytes());
144 }
145 let ibuf = create_buffer_init(device, "llimphi-3d-ibuf", wgpu::BufferUsages::INDEX, &ibytes);
146
147 let ubuf = device.create_buffer(&wgpu::BufferDescriptor {
148 label: Some("llimphi-3d-ubuf"),
149 size: 64, // una mat4x4<f32>
150 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
151 mapped_at_creation: false,
152 });
153
154 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
155 label: Some("llimphi-3d-bg"),
156 layout: &bind_layout,
157 entries: &[wgpu::BindGroupEntry {
158 binding: 0,
159 resource: ubuf.as_entire_binding(),
160 }],
161 });
162
163 Self {
164 pipeline,
165 vbuf,
166 ibuf,
167 index_count: indices.len() as u32,
168 ubuf,
169 bind_group,
170 model: Mat4::IDENTITY,
171 depth: None,
172 }
173 }
174
175 /// Ubica la malla en el mundo (`mvp = view_proj · model`). Default identidad.
176 pub fn set_model(&mut self, model: Mat4) {
177 self.model = model;
178 }
179
180 /// Reemplaza la geometría (recrea los buffers de vértices/índices). Pensado
181 /// para mallas que cambian cada frame — p.ej. un **muñeco articulado** cuya
182 /// pose se rehornea en CPU (limbos rotados) y se vuelve a subir. Las mallas
183 /// son chicas (decenas-cientos de vértices), así que recrear los buffers por
184 /// frame es despreciable; el pipeline/uniform/bind-group se conservan.
185 pub fn set_geometry(&mut self, device: &wgpu::Device, verts: &[Vertex3d], indices: &[u16]) {
186 let mut vbytes = Vec::with_capacity(verts.len() * Vertex3d::SIZE);
187 for v in verts {
188 v.write_to(&mut vbytes);
189 }
190 self.vbuf =
191 create_buffer_init(device, "llimphi-3d-vbuf", wgpu::BufferUsages::VERTEX, &vbytes);
192
193 let mut ibytes = Vec::with_capacity(indices.len() * 2);
194 for &i in indices {
195 ibytes.extend_from_slice(&i.to_ne_bytes());
196 }
197 self.ibuf =
198 create_buffer_init(device, "llimphi-3d-ibuf", wgpu::BufferUsages::INDEX, &ibytes);
199 self.index_count = indices.len() as u32;
200 }
201
202 /// Sube el uniform del frame (`mvp = view_proj · model`). Lo llama
203 /// [`Self::render`] y [`Scene3d`](crate::Scene3d). `aspect` = w/h.
204 pub fn upload(&self, queue: &wgpu::Queue, aspect: f32, camera: &Camera3d) {
205 let mvp = camera.view_proj(aspect) * self.model;
206 // glam es column-major; el shader WGSL espera column-major → upload tal cual.
207 let mut ubytes = Vec::with_capacity(64);
208 for v in mvp.to_cols_array() {
209 ubytes.extend_from_slice(&v.to_ne_bytes());
210 }
211 queue.write_buffer(&self.ubuf, 0, &ubytes);
212 }
213
214 /// Dibuja la malla indexada en un pase **ya abierto** (color + depth). Lo usa
215 /// [`Scene3d`](crate::Scene3d) para compartir el pase con los voxels.
216 /// Requiere `upload` previo en el mismo frame.
217 pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
218 pass.set_pipeline(&self.pipeline);
219 pass.set_bind_group(0, &self.bind_group, &[]);
220 pass.set_vertex_buffer(0, self.vbuf.slice(..));
221 pass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint16);
222 pass.draw_indexed(0..self.index_count, 0, 0..1);
223 }
224
225 /// Dibuja la malla sola sobre `target` (camino standalone, depth propio).
226 /// Firma compatible con `View::gpu_paint_with`; color preservado
227 /// (`LoadOp::Load`), depth propio limpiado cada frame.
228 pub fn render(
229 &mut self,
230 device: &wgpu::Device,
231 queue: &wgpu::Queue,
232 encoder: &mut wgpu::CommandEncoder,
233 target: &wgpu::TextureView,
234 (w, h): (u32, u32),
235 camera: &Camera3d,
236 ) {
237 if w == 0 || h == 0 {
238 return;
239 }
240 self.upload(queue, w as f32 / h as f32, camera);
241 ensure_depth(&mut self.depth, device, w, h);
242 let depth_view = &self.depth.as_ref().unwrap().view;
243
244 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
245 label: Some("llimphi-3d-pass"),
246 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
247 view: target,
248 resolve_target: None,
249 depth_slice: None,
250 ops: wgpu::Operations {
251 load: wgpu::LoadOp::Load,
252 store: wgpu::StoreOp::Store,
253 },
254 })],
255 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
256 view: depth_view,
257 depth_ops: Some(wgpu::Operations {
258 load: wgpu::LoadOp::Clear(1.0),
259 store: wgpu::StoreOp::Store,
260 }),
261 stencil_ops: None,
262 }),
263 timestamp_writes: None,
264 occlusion_query_set: None,
265 });
266 self.draw(&mut pass);
267 }
268}
269
270/// Crea un buffer ya inicializado con `data` (sin `wgpu::util::DeviceExt`, para
271/// no arrastrar la feature `util`): `mapped_at_creation` + copia + `unmap`.
272fn create_buffer_init(
273 device: &wgpu::Device,
274 label: &str,
275 usage: wgpu::BufferUsages,
276 data: &[u8],
277) -> wgpu::Buffer {
278 let buf = device.create_buffer(&wgpu::BufferDescriptor {
279 label: Some(label),
280 size: data.len() as u64,
281 usage,
282 mapped_at_creation: true,
283 });
284 buf.slice(..).get_mapped_range_mut().copy_from_slice(data);
285 buf.unmap();
286 buf
287}
288
289const WGSL: &str = r#"
290struct Uniforms { mvp: mat4x4<f32> };
291@group(0) @binding(0) var<uniform> u: Uniforms;
292
293struct VIn {
294 @location(0) pos: vec3<f32>,
295 @location(1) color: vec3<f32>,
296};
297struct VOut {
298 @builtin(position) clip: vec4<f32>,
299 @location(0) color: vec3<f32>,
300};
301
302@vertex
303fn vs(in: VIn) -> VOut {
304 var out: VOut;
305 out.clip = u.mvp * vec4<f32>(in.pos, 1.0);
306 out.color = in.color;
307 return out;
308}
309
310@fragment
311fn fs(in: VOut) -> @location(0) vec4<f32> {
312 return vec4<f32>(in.color, 1.0);
313}
314"#;