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

Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la
estructura vieja de eventloop) y suma el 3D:

- bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 /
  accesskit_winit 0.33 / vello_hybrid 0.0.9.
- nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido,
  montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel
  (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox).
- README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif).
- excluido modules/allichay (arrastra deps fuera del alcance del front-door).
- cargo check --workspace: verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-06-18 14:40:00 +00:00
parent e74800d9da
commit ccab39f140
202 changed files with 44034 additions and 1811 deletions
+186
View File
@@ -0,0 +1,186 @@
//! `Scene3d` — orquestador de una escena 3D **general**: compone, en un único
//! pase con **depth buffer compartido**, el render volumétrico de voxels
//! ([`VoxelRenderer`](crate::VoxelRenderer)) y mallas de triángulos
//! ([`Renderer3d`](crate::Renderer3d)). Es el keystone que vuelve a `llimphi-3d`
//! un motor 3D general y no "sólo voxels": voxels y mallas se **ocluyen
//! correctamente entre sí** porque ambos escriben/testean el mismo depth.
//!
//! La firma de [`Scene3d::render`] es compatible con la closure de
//! `View::gpu_paint_with` (más los renderers a componer): el `Scene3d` posee el
//! depth y abre el pase; cada renderer aporta su `upload` (uniforms) + `draw`
//! (en el pase ya abierto).
use crate::camera::Camera3d;
use crate::renderer::Renderer3d;
use crate::voxel_renderer::VoxelRenderer;
/// Formato del depth buffer de toda la escena 3D (debe coincidir entre el
/// pipeline de voxels, el de mallas y la textura de depth).
pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
/// Depth attachment cacheado, recreado cuando cambia el tamaño del viewport.
pub(crate) struct DepthBuffer {
pub view: wgpu::TextureView,
w: u32,
h: u32,
}
/// Asegura que `slot` tenga un depth buffer de `w×h` (lo recrea si cambió).
pub(crate) fn ensure_depth(
slot: &mut Option<DepthBuffer>,
device: &wgpu::Device,
w: u32,
h: u32,
) {
if matches!(slot, Some(d) if d.w == w && d.h == h) {
return;
}
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("llimphi-3d-depth"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
*slot = Some(DepthBuffer { view, w, h });
}
/// Escena 3D que comparte un depth buffer entre el pase de voxels y el de
/// mallas. Sólo posee el depth; los renderers los aporta el llamador por
/// referencia en cada frame (así la app conserva la propiedad y los muta).
#[derive(Default)]
pub struct Scene3d {
depth: Option<DepthBuffer>,
}
impl Scene3d {
pub fn new() -> Self {
Self::default()
}
/// Compone la escena sobre `target` (textura intermedia del frame). Primero
/// ray-marchea los voxels (escriben color + profundidad), luego dibuja las
/// mallas (testean contra esa profundidad) — todo en un pase con el depth
/// compartido, limpiado a lejano (`1.0`) al abrirlo. El color se preserva
/// (`LoadOp::Load`) para no pisar la UI vello de abajo.
///
/// Firma compatible con `View::gpu_paint_with` más los renderers a componer.
#[allow(clippy::too_many_arguments)]
pub fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
(w, h): (u32, u32),
camera: &Camera3d,
voxel: Option<&VoxelRenderer>,
meshes: &[&Renderer3d],
) {
// El caso por defecto: la escena ocupa todo el target.
self.render_in(
device,
queue,
encoder,
target,
(w, h),
(0.0, 0.0, w as f32, h as f32),
camera,
voxel,
meshes,
);
}
/// Como [`render`](Self::render) pero **confina** la escena a la sub-región
/// `rect = (x, y, w, h)` (en px del target, esquina sup-izq), vía
/// `set_viewport` + `set_scissor_rect`. Es lo que permite montar el 3D en un
/// **panel** de una UI (un canvas que no ocupa toda la ventana) sin pisar el
/// chrome alrededor: la pasada de ray-march/mallas pinta sólo dentro del rect,
/// con el aspect del rect (no el de la ventana). `target`/`viewport` siguen
/// siendo el frame completo (load-preserve del chrome ya rasterizado).
#[allow(clippy::too_many_arguments)]
pub fn render_in(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
(w, h): (u32, u32),
rect: (f32, f32, f32, f32),
camera: &Camera3d,
voxel: Option<&VoxelRenderer>,
meshes: &[&Renderer3d],
) {
if w == 0 || h == 0 {
return;
}
let (rx, ry, rw, rh) = rect;
if rw < 1.0 || rh < 1.0 {
return;
}
// El aspect es el del rect (el viewport mapea NDC a esa sub-región).
let aspect = rw / rh;
// Subir uniforms antes de abrir el pase (queue.write_buffer se ordena
// antes del submit).
if let Some(v) = voxel {
v.upload(queue, aspect, camera);
}
for m in meshes {
m.upload(queue, aspect, camera);
}
ensure_depth(&mut self.depth, device, w, h);
let depth_view = &self.depth.as_ref().unwrap().view;
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-3d-scene-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
// Viewport (mapeo NDC→rect) + scissor (recorte físico al rect, clampeado a
// los límites del attachment).
pass.set_viewport(rx, ry, rw, rh, 0.0, 1.0);
let sx = rx.max(0.0);
let sy = ry.max(0.0);
let sw = (rw.min(w as f32 - sx)).max(0.0) as u32;
let sh = (rh.min(h as f32 - sy)).max(0.0) as u32;
if sw == 0 || sh == 0 {
return;
}
pass.set_scissor_rect(sx as u32, sy as u32, sw, sh);
if let Some(v) = voxel {
v.draw(&mut pass);
}
for m in meshes {
m.draw(&mut pass);
}
}
}