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
+306
View File
@@ -0,0 +1,306 @@
//! `Hud` — un pase **screen-space** mínimo: dibuja rectángulos de color plano
//! (con alpha) directamente en NDC, *después* del pase 3D, sobre el mismo
//! target. Es la pieza que faltaba para un **HUD / mira (crosshair)** en primera
//! persona: el contenido vello del árbol Llimphi queda **debajo** del canvas GPU
//! full-screen, así que cualquier overlay que deba ir *encima* del ray-march
//! tiene que pintarse en GPU en la misma closure `gpu_paint_with`, y eso es
//! justo lo que hace [`Hud::render`].
//!
//! Deliberadamente tonto: sin texturas, sin bind groups, sin depth. Geometría
//! en CPU → un vertex buffer dinámico → un draw. Suficiente para miras, barras,
//! marcos y **texto** ([`HudQuad::text`], fuente bitmap 5×7 = un quad por píxel
//! encendido, sin salir del pipeline de quads).
/// Un rectángulo de HUD en **pixels** (origen arriba-izquierda, como la
/// pantalla), color RGBA lineal `0..1`.
#[derive(Debug, Clone, Copy)]
pub struct HudQuad {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub color: [f32; 4],
}
impl HudQuad {
/// Una **mira (crosshair)** centrada en un viewport `(w, h)`: dos barras
/// (horizontal + vertical) de brazo `arm` y grosor `th` pixels.
pub fn crosshair(viewport: (u32, u32), arm: f32, th: f32, color: [f32; 4]) -> [HudQuad; 2] {
let cx = viewport.0 as f32 * 0.5;
let cy = viewport.1 as f32 * 0.5;
[
HudQuad { x: cx - arm, y: cy - th * 0.5, w: arm * 2.0, h: th, color },
HudQuad { x: cx - th * 0.5, y: cy - arm, w: th, h: arm * 2.0, color },
]
}
/// Emite los quads de una cadena con la **fuente bitmap 5×7** embebida
/// ([`glyph`]): origen arriba-izquierda en `(x, y)` pixels, cada píxel de
/// glifo mide `px` pixels de lado y los caracteres avanzan `6·px` (5 de ancho
/// + 1 de espacio). Sólo ASCII; las minúsculas se dibujan en mayúscula y los
/// caracteres desconocidos quedan en blanco. Se mantiene dentro del pipeline
/// tonto del HUD (un quad por píxel encendido, sin texturas).
pub fn text(s: &str, x: f32, y: f32, px: f32, color: [f32; 4]) -> Vec<HudQuad> {
let mut out = Vec::new();
let mut cx = x;
for ch in s.chars() {
if ch != ' ' {
let g = glyph(ch);
for (r, row) in g.iter().enumerate() {
for c in 0..5u32 {
if row & (1 << (4 - c)) != 0 {
out.push(HudQuad {
x: cx + c as f32 * px,
y: y + r as f32 * px,
w: px,
h: px,
color,
});
}
}
}
}
cx += 6.0 * px;
}
out
}
/// Ancho en pixels que ocuparía `s` con [`text`](Self::text) a tamaño `px`
/// (útil para dimensionar un panel de fondo antes de dibujar el texto).
pub fn text_width(s: &str, px: f32) -> f32 {
s.chars().count() as f32 * 6.0 * px
}
}
/// Mapa de un carácter a su bitmap **5×7**: 7 filas, cada `u8` con los 5 bits
/// bajos = columnas de izquierda (bit 4) a derecha (bit 0). Cubre `0-9`, `A-Z`
/// y puntuación común; lo desconocido devuelve un glifo en blanco. Las filas se
/// escriben en binario para que la forma sea legible en el código.
fn glyph(c: char) -> [u8; 7] {
match c.to_ascii_uppercase() {
'0' => [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
'1' => [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
'2' => [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
'3' => [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
'4' => [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
'5' => [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
'6' => [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
'7' => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
'8' => [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
'9' => [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
'A' => [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
'B' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
'C' => [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
'D' => [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],
'E' => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
'F' => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
'G' => [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110],
'H' => [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
'I' => [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
'J' => [0b00111, 0b00010, 0b00010, 0b00010, 0b10010, 0b10010, 0b01100],
'K' => [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
'L' => [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
'M' => [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
'N' => [0b10001, 0b11001, 0b10101, 0b10101, 0b10011, 0b10001, 0b10001],
'O' => [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
'P' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
'Q' => [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
'R' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
'S' => [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
'T' => [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
'U' => [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
'V' => [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
'W' => [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
'X' => [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
'Y' => [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
'Z' => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
':' => [0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b00000, 0b00000],
'.' => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100],
',' => [0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100, 0b01000],
'-' => [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000],
'+' => [0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000],
'/' => [0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000],
'(' => [0b00110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b00110],
')' => [0b01100, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01100],
'%' => [0b11001, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b10011],
_ => [0; 7],
}
}
/// Tamaño de un vértice del HUD: `pos: vec2<f32>` + `color: vec4<f32>`.
const VSIZE: usize = 2 * 4 + 4 * 4;
/// Renderer de overlay screen-space. Cachea pipeline + un vertex buffer
/// dinámico que crece según haga falta.
pub struct Hud {
pipeline: wgpu::RenderPipeline,
vbuf: wgpu::Buffer,
cap: u64,
}
impl Hud {
/// Crea el HUD para el `color_format` del target (el de la intermedia del
/// frame). No toca depth: dibuja siempre encima.
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("llimphi-3d-hud-shader"),
source: wgpu::ShaderSource::Wgsl(WGSL.into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("llimphi-3d-hud-pl"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("llimphi-3d-hud-pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[wgpu::VertexBufferLayout {
array_stride: VSIZE as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 8,
shader_location: 1,
},
],
}],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
// Sin depth: el HUD va siempre encima del 3D.
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
cache: None,
});
let cap = (64 * 6 * VSIZE) as u64; // ~64 quads sin recrear
let vbuf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("llimphi-3d-hud-vbuf"),
size: cap,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self { pipeline, vbuf, cap }
}
/// Dibuja `quads` sobre `target` (color `LoadOp::Load`, sin depth). Firma
/// compatible con la closure `gpu_paint_with`: llamar *después* del pase 3D.
pub fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
(w, h): (u32, u32),
quads: &[HudQuad],
) {
if w == 0 || h == 0 || quads.is_empty() {
return;
}
// Geometría en CPU: 2 triángulos (6 vértices) por quad, en NDC. El eje Y
// de pantalla va hacia abajo; NDC hacia arriba → `1 - 2·y/h`.
let (fw, fh) = (w as f32, h as f32);
let mut bytes = Vec::with_capacity(quads.len() * 6 * VSIZE);
let mut vert = |x_px: f32, y_px: f32, c: [f32; 4]| {
let ndc_x = x_px / fw * 2.0 - 1.0;
let ndc_y = 1.0 - y_px / fh * 2.0;
bytes.extend_from_slice(&ndc_x.to_ne_bytes());
bytes.extend_from_slice(&ndc_y.to_ne_bytes());
for ch in c {
bytes.extend_from_slice(&ch.to_ne_bytes());
}
};
for q in quads {
let (x0, y0, x1, y1) = (q.x, q.y, q.x + q.w, q.y + q.h);
vert(x0, y0, q.color);
vert(x1, y0, q.color);
vert(x1, y1, q.color);
vert(x0, y0, q.color);
vert(x1, y1, q.color);
vert(x0, y1, q.color);
}
// Crecer el buffer si hiciera falta (raro: la mira son 2 quads).
if bytes.len() as u64 > self.cap {
self.cap = (bytes.len() as u64).next_power_of_two();
self.vbuf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("llimphi-3d-hud-vbuf"),
size: self.cap,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
queue.write_buffer(&self.vbuf, 0, &bytes);
let count = (quads.len() * 6) as u32;
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-3d-hud-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: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_vertex_buffer(0, self.vbuf.slice(..bytes.len() as u64));
pass.draw(0..count, 0..1);
}
}
const WGSL: &str = r#"
struct VIn {
@location(0) pos: vec2<f32>,
@location(1) color: vec4<f32>,
};
struct VOut {
@builtin(position) clip: vec4<f32>,
@location(0) color: vec4<f32>,
};
@vertex
fn vs(in: VIn) -> VOut {
var out: VOut;
out.clip = vec4<f32>(in.pos, 0.0, 1.0);
out.color = in.color;
return out;
}
@fragment
fn fs(in: VOut) -> @location(0) vec4<f32> {
return in.color;
}
"#;