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:
@@ -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;
|
||||
}
|
||||
"#;
|
||||
Reference in New Issue
Block a user