tawasuyu

tawasuyu/llimphi

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

Rust 123Markdown 81TOML 75Shell 12086 Sutichaykuna

⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4 llimphi-3d/src/renderer.rs Rust · 314 siq'ikuna · 11.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

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"#;