gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-3d/src/hud.rs Rust · 307 siq'ikuna · 13.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! `Hud` — un pase **screen-space** mínimo: dibuja rectángulos de color plano
2//! (con alpha) directamente en NDC, *después* del pase 3D, sobre el mismo
3//! target. Es la pieza que faltaba para un **HUD / mira (crosshair)** en primera
4//! persona: el contenido vello del árbol Llimphi queda **debajo** del canvas GPU
5//! full-screen, así que cualquier overlay que deba ir *encima* del ray-march
6//! tiene que pintarse en GPU en la misma closure `gpu_paint_with`, y eso es
7//! justo lo que hace [`Hud::render`].
8//!
9//! Deliberadamente tonto: sin texturas, sin bind groups, sin depth. Geometría
10//! en CPU → un vertex buffer dinámico → un draw. Suficiente para miras, barras,
11//! marcos y **texto** ([`HudQuad::text`], fuente bitmap 5×7 = un quad por píxel
12//! encendido, sin salir del pipeline de quads).
13
14/// Un rectángulo de HUD en **pixels** (origen arriba-izquierda, como la
15/// pantalla), color RGBA lineal `0..1`.
16#[derive(Debug, Clone, Copy)]
17pub struct HudQuad {
18    pub x: f32,
19    pub y: f32,
20    pub w: f32,
21    pub h: f32,
22    pub color: [f32; 4],
23}
24
25impl HudQuad {
26    /// Una **mira (crosshair)** centrada en un viewport `(w, h)`: dos barras
27    /// (horizontal + vertical) de brazo `arm` y grosor `th` pixels.
28    pub fn crosshair(viewport: (u32, u32), arm: f32, th: f32, color: [f32; 4]) -> [HudQuad; 2] {
29        let cx = viewport.0 as f32 * 0.5;
30        let cy = viewport.1 as f32 * 0.5;
31        [
32            HudQuad { x: cx - arm, y: cy - th * 0.5, w: arm * 2.0, h: th, color },
33            HudQuad { x: cx - th * 0.5, y: cy - arm, w: th, h: arm * 2.0, color },
34        ]
35    }
36
37    /// Emite los quads de una cadena con la **fuente bitmap 5×7** embebida
38    /// ([`glyph`]): origen arriba-izquierda en `(x, y)` pixels, cada píxel de
39    /// glifo mide `px` pixels de lado y los caracteres avanzan `6·px` (5 de ancho
40    /// + 1 de espacio). Sólo ASCII; las minúsculas se dibujan en mayúscula y los
41    /// caracteres desconocidos quedan en blanco. Se mantiene dentro del pipeline
42    /// tonto del HUD (un quad por píxel encendido, sin texturas).
43    pub fn text(s: &str, x: f32, y: f32, px: f32, color: [f32; 4]) -> Vec<HudQuad> {
44        let mut out = Vec::new();
45        let mut cx = x;
46        for ch in s.chars() {
47            if ch != ' ' {
48                let g = glyph(ch);
49                for (r, row) in g.iter().enumerate() {
50                    for c in 0..5u32 {
51                        if row & (1 << (4 - c)) != 0 {
52                            out.push(HudQuad {
53                                x: cx + c as f32 * px,
54                                y: y + r as f32 * px,
55                                w: px,
56                                h: px,
57                                color,
58                            });
59                        }
60                    }
61                }
62            }
63            cx += 6.0 * px;
64        }
65        out
66    }
67
68    /// Ancho en pixels que ocuparía `s` con [`text`](Self::text) a tamaño `px`
69    /// (útil para dimensionar un panel de fondo antes de dibujar el texto).
70    pub fn text_width(s: &str, px: f32) -> f32 {
71        s.chars().count() as f32 * 6.0 * px
72    }
73}
74
75/// Mapa de un carácter a su bitmap **5×7**: 7 filas, cada `u8` con los 5 bits
76/// bajos = columnas de izquierda (bit 4) a derecha (bit 0). Cubre `0-9`, `A-Z`
77/// y puntuación común; lo desconocido devuelve un glifo en blanco. Las filas se
78/// escriben en binario para que la forma sea legible en el código.
79fn glyph(c: char) -> [u8; 7] {
80    match c.to_ascii_uppercase() {
81        '0' => [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
82        '1' => [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
83        '2' => [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
84        '3' => [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
85        '4' => [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
86        '5' => [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
87        '6' => [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
88        '7' => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
89        '8' => [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
90        '9' => [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
91        'A' => [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
92        'B' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
93        'C' => [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
94        'D' => [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],
95        'E' => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
96        'F' => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
97        'G' => [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110],
98        'H' => [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
99        'I' => [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
100        'J' => [0b00111, 0b00010, 0b00010, 0b00010, 0b10010, 0b10010, 0b01100],
101        'K' => [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
102        'L' => [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
103        'M' => [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
104        'N' => [0b10001, 0b11001, 0b10101, 0b10101, 0b10011, 0b10001, 0b10001],
105        'O' => [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
106        'P' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
107        'Q' => [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
108        'R' => [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
109        'S' => [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
110        'T' => [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
111        'U' => [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
112        'V' => [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
113        'W' => [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
114        'X' => [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
115        'Y' => [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
116        'Z' => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
117        ':' => [0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b00000, 0b00000],
118        '.' => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100],
119        ',' => [0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100, 0b01000],
120        '-' => [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000],
121        '+' => [0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000],
122        '/' => [0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000],
123        '(' => [0b00110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b00110],
124        ')' => [0b01100, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01100],
125        '%' => [0b11001, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b10011],
126        _ => [0; 7],
127    }
128}
129
130/// Tamaño de un vértice del HUD: `pos: vec2<f32>` + `color: vec4<f32>`.
131const VSIZE: usize = 2 * 4 + 4 * 4;
132
133/// Renderer de overlay screen-space. Cachea pipeline + un vertex buffer
134/// dinámico que crece según haga falta.
135pub struct Hud {
136    pipeline: wgpu::RenderPipeline,
137    vbuf: wgpu::Buffer,
138    cap: u64,
139}
140
141impl Hud {
142    /// Crea el HUD para el `color_format` del target (el de la intermedia del
143    /// frame). No toca depth: dibuja siempre encima.
144    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
145        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
146            label: Some("llimphi-3d-hud-shader"),
147            source: wgpu::ShaderSource::Wgsl(WGSL.into()),
148        });
149
150        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
151            label: Some("llimphi-3d-hud-pl"),
152            bind_group_layouts: &[],
153            immediate_size: 0,
154        });
155
156        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
157            label: Some("llimphi-3d-hud-pipeline"),
158            layout: Some(&pipeline_layout),
159            vertex: wgpu::VertexState {
160                module: &shader,
161                entry_point: Some("vs"),
162                compilation_options: Default::default(),
163                buffers: &[wgpu::VertexBufferLayout {
164                    array_stride: VSIZE as u64,
165                    step_mode: wgpu::VertexStepMode::Vertex,
166                    attributes: &[
167                        wgpu::VertexAttribute {
168                            format: wgpu::VertexFormat::Float32x2,
169                            offset: 0,
170                            shader_location: 0,
171                        },
172                        wgpu::VertexAttribute {
173                            format: wgpu::VertexFormat::Float32x4,
174                            offset: 8,
175                            shader_location: 1,
176                        },
177                    ],
178                }],
179            },
180            primitive: wgpu::PrimitiveState {
181                topology: wgpu::PrimitiveTopology::TriangleList,
182                ..Default::default()
183            },
184            // Sin depth: el HUD va siempre encima del 3D.
185            depth_stencil: None,
186            multisample: wgpu::MultisampleState::default(),
187            fragment: Some(wgpu::FragmentState {
188                module: &shader,
189                entry_point: Some("fs"),
190                compilation_options: Default::default(),
191                targets: &[Some(wgpu::ColorTargetState {
192                    format: color_format,
193                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
194                    write_mask: wgpu::ColorWrites::ALL,
195                })],
196            }),
197            multiview_mask: None,
198            cache: None,
199        });
200
201        let cap = (64 * 6 * VSIZE) as u64; // ~64 quads sin recrear
202        let vbuf = device.create_buffer(&wgpu::BufferDescriptor {
203            label: Some("llimphi-3d-hud-vbuf"),
204            size: cap,
205            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
206            mapped_at_creation: false,
207        });
208
209        Self { pipeline, vbuf, cap }
210    }
211
212    /// Dibuja `quads` sobre `target` (color `LoadOp::Load`, sin depth). Firma
213    /// compatible con la closure `gpu_paint_with`: llamar *después* del pase 3D.
214    pub fn render(
215        &mut self,
216        device: &wgpu::Device,
217        queue: &wgpu::Queue,
218        encoder: &mut wgpu::CommandEncoder,
219        target: &wgpu::TextureView,
220        (w, h): (u32, u32),
221        quads: &[HudQuad],
222    ) {
223        if w == 0 || h == 0 || quads.is_empty() {
224            return;
225        }
226
227        // Geometría en CPU: 2 triángulos (6 vértices) por quad, en NDC. El eje Y
228        // de pantalla va hacia abajo; NDC hacia arriba → `1 - 2·y/h`.
229        let (fw, fh) = (w as f32, h as f32);
230        let mut bytes = Vec::with_capacity(quads.len() * 6 * VSIZE);
231        let mut vert = |x_px: f32, y_px: f32, c: [f32; 4]| {
232            let ndc_x = x_px / fw * 2.0 - 1.0;
233            let ndc_y = 1.0 - y_px / fh * 2.0;
234            bytes.extend_from_slice(&ndc_x.to_ne_bytes());
235            bytes.extend_from_slice(&ndc_y.to_ne_bytes());
236            for ch in c {
237                bytes.extend_from_slice(&ch.to_ne_bytes());
238            }
239        };
240        for q in quads {
241            let (x0, y0, x1, y1) = (q.x, q.y, q.x + q.w, q.y + q.h);
242            vert(x0, y0, q.color);
243            vert(x1, y0, q.color);
244            vert(x1, y1, q.color);
245            vert(x0, y0, q.color);
246            vert(x1, y1, q.color);
247            vert(x0, y1, q.color);
248        }
249
250        // Crecer el buffer si hiciera falta (raro: la mira son 2 quads).
251        if bytes.len() as u64 > self.cap {
252            self.cap = (bytes.len() as u64).next_power_of_two();
253            self.vbuf = device.create_buffer(&wgpu::BufferDescriptor {
254                label: Some("llimphi-3d-hud-vbuf"),
255                size: self.cap,
256                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
257                mapped_at_creation: false,
258            });
259        }
260        queue.write_buffer(&self.vbuf, 0, &bytes);
261
262        let count = (quads.len() * 6) as u32;
263        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
264            label: Some("llimphi-3d-hud-pass"),
265            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
266                view: target,
267                resolve_target: None,
268                depth_slice: None,
269                ops: wgpu::Operations {
270                    load: wgpu::LoadOp::Load,
271                    store: wgpu::StoreOp::Store,
272                },
273            })],
274            depth_stencil_attachment: None,
275            timestamp_writes: None,
276            occlusion_query_set: None,
277            multiview_mask: None,
278        });
279        pass.set_pipeline(&self.pipeline);
280        pass.set_vertex_buffer(0, self.vbuf.slice(..bytes.len() as u64));
281        pass.draw(0..count, 0..1);
282    }
283}
284
285const WGSL: &str = r#"
286struct VIn {
287    @location(0) pos: vec2<f32>,
288    @location(1) color: vec4<f32>,
289};
290struct VOut {
291    @builtin(position) clip: vec4<f32>,
292    @location(0) color: vec4<f32>,
293};
294
295@vertex
296fn vs(in: VIn) -> VOut {
297    var out: VOut;
298    out.clip = vec4<f32>(in.pos, 0.0, 1.0);
299    out.color = in.color;
300    return out;
301}
302
303@fragment
304fn fs(in: VOut) -> @location(0) vec4<f32> {
305    return in.color;
306}
307"#;