gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-3d/src/mesh.rs Rust · 123 líneas · 4.7 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! Geometría de mallas: el vértice 3D ([`Vertex3d`]), un cubo de prueba
2//! ([`cube`]) y un compositor de cajas transformadas ([`push_cube`]) para armar
3//! mallas multi-caja en CPU — p.ej. un **muñeco articulado** (cabeza/torso/
4//! miembros como cajas rotadas en sus articulaciones).
5//!
6//! Sigue el idiom de `llimphi-raster::gpu` (subir a GPU vía `to_ne_bytes`, sin
7//! `bytemuck`) para no agregar una dependencia nueva al workspace.
8
9use glam::{Mat4, Vec3};
10
11/// Vértice 3D: posición en mundo + color RGB lineal.
12#[derive(Debug, Clone, Copy)]
13pub struct Vertex3d {
14    pub pos: [f32; 3],
15    pub color: [f32; 3],
16}
17
18impl Vertex3d {
19    /// Tamaño en bytes de un vértice empaquetado (`6 × f32`).
20    pub const SIZE: usize = 6 * 4;
21
22    /// Vuelca este vértice al buffer en orden `pos.xyz, color.rgb` (native
23    /// endian, como hace `GpuBatch`).
24    pub fn write_to(&self, out: &mut Vec<u8>) {
25        for v in self.pos {
26            out.extend_from_slice(&v.to_ne_bytes());
27        }
28        for v in self.color {
29            out.extend_from_slice(&v.to_ne_bytes());
30        }
31    }
32}
33
34/// Las 8 esquinas del cubo unitario centrado en el origen (lado 1, `-0.5..0.5`).
35const CUBE_CORNERS: [[f32; 3]; 8] = [
36    [-0.5, -0.5, -0.5],
37    [0.5, -0.5, -0.5],
38    [0.5, 0.5, -0.5],
39    [-0.5, 0.5, -0.5],
40    [-0.5, -0.5, 0.5],
41    [0.5, -0.5, 0.5],
42    [0.5, 0.5, 0.5],
43    [-0.5, 0.5, 0.5],
44];
45
46/// Los 36 índices (12 triángulos) del cubo, winding CCW visto desde afuera.
47#[rustfmt::skip]
48pub const CUBE_INDICES: [u16; 36] = [
49    0, 2, 1, 0, 3, 2, // -Z (atrás)
50    4, 5, 6, 4, 6, 7, // +Z (frente)
51    0, 4, 7, 0, 7, 3, // -X (izquierda)
52    1, 2, 6, 1, 6, 5, // +X (derecha)
53    0, 1, 5, 0, 5, 4, // -Y (abajo)
54    3, 7, 6, 3, 6, 2, // +Y (arriba)
55];
56
57/// Cubo unitario centrado en el origen (lado 1, de `-0.5` a `0.5`). 8 vértices
58/// coloreados por su posición (`color = pos + 0.5`) → un degradé que deja ver
59/// las tres caras visibles distintas. 36 índices (12 triángulos), winding CCW.
60pub fn cube() -> (Vec<Vertex3d>, Vec<u16>) {
61    let verts = CUBE_CORNERS
62        .iter()
63        .map(|&[x, y, z]| Vertex3d {
64            pos: [x, y, z],
65            color: [x + 0.5, y + 0.5, z + 0.5],
66        })
67        .collect();
68    (verts, CUBE_INDICES.to_vec())
69}
70
71/// Genera una **esfera UV** unitaria (radio 1, centro en el origen): posiciones
72/// (que también sirven de normales, por ser unitarias) + índices de triángulos
73/// (winding CCW visto desde afuera). `stacks` = anillos de latitud, `slices` =
74/// gajos de longitud. El caller arma los [`Vertex3d`] coloreando cada posición
75/// (p.ej. por latitud/longitud y un sombreado horneado): el `Vertex3d` sólo
76/// lleva color (no normal), así que para una esfera lisa conviene hornear el
77/// sombreado en el color (Gouraud) en vez de depender de la normal plana del
78/// shader de mallas.
79pub fn uv_sphere(stacks: u32, slices: u32) -> (Vec<[f32; 3]>, Vec<u16>) {
80    let stacks = stacks.max(2);
81    let slices = slices.max(3);
82    let mut pos: Vec<[f32; 3]> = Vec::with_capacity(((stacks + 1) * (slices + 1)) as usize);
83    for i in 0..=stacks {
84        let v = i as f32 / stacks as f32;
85        let phi = v * std::f32::consts::PI; // 0 = polo norte, π = polo sur
86        let (sp, cp) = phi.sin_cos();
87        for j in 0..=slices {
88            let u = j as f32 / slices as f32;
89            let theta = u * std::f32::consts::TAU;
90            let (st, ct) = theta.sin_cos();
91            // +Y arriba (polo norte); x,z en el plano ecuatorial.
92            pos.push([sp * ct, cp, sp * st]);
93        }
94    }
95    let mut idx: Vec<u16> = Vec::with_capacity((stacks * slices * 6) as usize);
96    let row = slices + 1;
97    for i in 0..stacks {
98        for j in 0..slices {
99            let a = (i * row + j) as u16;
100            let b = (i * row + j + 1) as u16;
101            let c = ((i + 1) * row + j) as u16;
102            let d = ((i + 1) * row + j + 1) as u16;
103            idx.extend_from_slice(&[a, c, b, b, c, d]);
104        }
105    }
106    (pos, idx)
107}
108
109/// Apila un cubo transformado por `m` (mapea el cubo unitario `[-0.5,0.5]³` a su
110/// caja en mundo) con color plano `color`, en `verts`/`indices`. Es el ladrillo
111/// para componer mallas multi-caja en CPU: cada llamada agrega 8 vértices + 36
112/// índices con la base reubicada. Para un miembro articulado, `m` suele ser
113/// `T(articulación) · R(ángulo) · T(0,-largo/2,0) · S(tamaño)`.
114pub fn push_cube(verts: &mut Vec<Vertex3d>, indices: &mut Vec<u16>, m: Mat4, color: [f32; 3]) {
115    let base = verts.len() as u16;
116    for c in CUBE_CORNERS {
117        let p = m.transform_point3(Vec3::from_array(c));
118        verts.push(Vertex3d { pos: p.to_array(), color });
119    }
120    for i in CUBE_INDICES {
121        indices.push(base + i);
122    }
123}