tawasuyu

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-3d/src/scene.rs Rust · 187 líneas · 6.6 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! `Scene3d` — orquestador de una escena 3D **general**: compone, en un único
2//! pase con **depth buffer compartido**, el render volumétrico de voxels
3//! ([`VoxelRenderer`](crate::VoxelRenderer)) y mallas de triángulos
4//! ([`Renderer3d`](crate::Renderer3d)). Es el keystone que vuelve a `llimphi-3d`
5//! un motor 3D general y no "sólo voxels": voxels y mallas se **ocluyen
6//! correctamente entre sí** porque ambos escriben/testean el mismo depth.
7//!
8//! La firma de [`Scene3d::render`] es compatible con la closure de
9//! `View::gpu_paint_with` (más los renderers a componer): el `Scene3d` posee el
10//! depth y abre el pase; cada renderer aporta su `upload` (uniforms) + `draw`
11//! (en el pase ya abierto).
12
13use crate::camera::Camera3d;
14use crate::renderer::Renderer3d;
15use crate::voxel_renderer::VoxelRenderer;
16
17/// Formato del depth buffer de toda la escena 3D (debe coincidir entre el
18/// pipeline de voxels, el de mallas y la textura de depth).
19pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
20
21/// Depth attachment cacheado, recreado cuando cambia el tamaño del viewport.
22pub(crate) struct DepthBuffer {
23    pub view: wgpu::TextureView,
24    w: u32,
25    h: u32,
26}
27
28/// Asegura que `slot` tenga un depth buffer de `w×h` (lo recrea si cambió).
29pub(crate) fn ensure_depth(
30    slot: &mut Option<DepthBuffer>,
31    device: &wgpu::Device,
32    w: u32,
33    h: u32,
34) {
35    if matches!(slot, Some(d) if d.w == w && d.h == h) {
36        return;
37    }
38    let tex = device.create_texture(&wgpu::TextureDescriptor {
39        label: Some("llimphi-3d-depth"),
40        size: wgpu::Extent3d {
41            width: w,
42            height: h,
43            depth_or_array_layers: 1,
44        },
45        mip_level_count: 1,
46        sample_count: 1,
47        dimension: wgpu::TextureDimension::D2,
48        format: DEPTH_FORMAT,
49        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
50        view_formats: &[],
51    });
52    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
53    *slot = Some(DepthBuffer { view, w, h });
54}
55
56/// Escena 3D que comparte un depth buffer entre el pase de voxels y el de
57/// mallas. Sólo posee el depth; los renderers los aporta el llamador por
58/// referencia en cada frame (así la app conserva la propiedad y los muta).
59#[derive(Default)]
60pub struct Scene3d {
61    depth: Option<DepthBuffer>,
62}
63
64impl Scene3d {
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Compone la escena sobre `target` (textura intermedia del frame). Primero
70    /// ray-marchea los voxels (escriben color + profundidad), luego dibuja las
71    /// mallas (testean contra esa profundidad) — todo en un pase con el depth
72    /// compartido, limpiado a lejano (`1.0`) al abrirlo. El color se preserva
73    /// (`LoadOp::Load`) para no pisar la UI vello de abajo.
74    ///
75    /// Firma compatible con `View::gpu_paint_with` más los renderers a componer.
76    #[allow(clippy::too_many_arguments)]
77    pub fn render(
78        &mut self,
79        device: &wgpu::Device,
80        queue: &wgpu::Queue,
81        encoder: &mut wgpu::CommandEncoder,
82        target: &wgpu::TextureView,
83        (w, h): (u32, u32),
84        camera: &Camera3d,
85        voxel: Option<&VoxelRenderer>,
86        meshes: &[&Renderer3d],
87    ) {
88        // El caso por defecto: la escena ocupa todo el target.
89        self.render_in(
90            device,
91            queue,
92            encoder,
93            target,
94            (w, h),
95            (0.0, 0.0, w as f32, h as f32),
96            camera,
97            voxel,
98            meshes,
99        );
100    }
101
102    /// Como [`render`](Self::render) pero **confina** la escena a la sub-región
103    /// `rect = (x, y, w, h)` (en px del target, esquina sup-izq), vía
104    /// `set_viewport` + `set_scissor_rect`. Es lo que permite montar el 3D en un
105    /// **panel** de una UI (un canvas que no ocupa toda la ventana) sin pisar el
106    /// chrome alrededor: la pasada de ray-march/mallas pinta sólo dentro del rect,
107    /// con el aspect del rect (no el de la ventana). `target`/`viewport` siguen
108    /// siendo el frame completo (load-preserve del chrome ya rasterizado).
109    #[allow(clippy::too_many_arguments)]
110    pub fn render_in(
111        &mut self,
112        device: &wgpu::Device,
113        queue: &wgpu::Queue,
114        encoder: &mut wgpu::CommandEncoder,
115        target: &wgpu::TextureView,
116        (w, h): (u32, u32),
117        rect: (f32, f32, f32, f32),
118        camera: &Camera3d,
119        voxel: Option<&VoxelRenderer>,
120        meshes: &[&Renderer3d],
121    ) {
122        if w == 0 || h == 0 {
123            return;
124        }
125        let (rx, ry, rw, rh) = rect;
126        if rw < 1.0 || rh < 1.0 {
127            return;
128        }
129        // El aspect es el del rect (el viewport mapea NDC a esa sub-región).
130        let aspect = rw / rh;
131
132        // Subir uniforms antes de abrir el pase (queue.write_buffer se ordena
133        // antes del submit).
134        if let Some(v) = voxel {
135            v.upload(queue, aspect, camera);
136        }
137        for m in meshes {
138            m.upload(queue, aspect, camera);
139        }
140
141        ensure_depth(&mut self.depth, device, w, h);
142        let depth_view = &self.depth.as_ref().unwrap().view;
143
144        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
145            label: Some("llimphi-3d-scene-pass"),
146            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
147                view: target,
148                resolve_target: None,
149                depth_slice: None,
150                ops: wgpu::Operations {
151                    load: wgpu::LoadOp::Load,
152                    store: wgpu::StoreOp::Store,
153                },
154            })],
155            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
156                view: depth_view,
157                depth_ops: Some(wgpu::Operations {
158                    load: wgpu::LoadOp::Clear(1.0),
159                    store: wgpu::StoreOp::Store,
160                }),
161                stencil_ops: None,
162            }),
163            timestamp_writes: None,
164            occlusion_query_set: None,
165            multiview_mask: None,
166        });
167
168        // Viewport (mapeo NDC→rect) + scissor (recorte físico al rect, clampeado a
169        // los límites del attachment).
170        pass.set_viewport(rx, ry, rw, rh, 0.0, 1.0);
171        let sx = rx.max(0.0);
172        let sy = ry.max(0.0);
173        let sw = (rw.min(w as f32 - sx)).max(0.0) as u32;
174        let sh = (rh.min(h as f32 - sy)).max(0.0) as u32;
175        if sw == 0 || sh == 0 {
176            return;
177        }
178        pass.set_scissor_rect(sx as u32, sy as u32, sw, sh);
179
180        if let Some(v) = voxel {
181            v.draw(&mut pass);
182        }
183        for m in meshes {
184            m.draw(&mut pass);
185        }
186    }
187}