tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}