tawasuyu

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-voxel/src/sdf_kit.rs Rust · 394 siq'ikuna · 12.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! **Kit de ensamble SDF** — los ladrillos comunes con los que se escriben los seres
2//! analíticos ([`llimphi_3d::SdfSer`]): esfera, cápsula/cono, elipsoide, decals, y las
3//! operaciones de conjunto (trasladar, escalar, arquear una cadena).
4//!
5//! Existe porque [`ser_sdf`](crate::ser_sdf) nació con estos constructores como
6//! closures privadas dentro de una función de 400 líneas, y el bestiario
7//! ([`bicho_sdf`](crate::bicho_sdf)) y la vegetación ([`flora_sdf`](crate::flora_sdf))
8//! los necesitaban igual. Son cinco funciones triviales: lo que aportan no es
9//! abstracción sino **un solo lugar donde están las convenciones** — el orden de la
10//! lista importa (el `smin` acumula contra lo anterior), el `blend` se mide en las
11//! mismas unidades que el mundo, y los decals van SIEMPRE después de la geometría.
12//!
13//! Convención de espacio, igual que el ser humanoide: **base en `y = 0`**, centrado
14//! en X/Z, **mirando `+Z`**. El caller lo posa con
15//! [`SdfSer::posado`](llimphi_3d::SdfSer::posado).
16
17use llimphi_3d::glam::Vec3;
18use llimphi_3d::{SdfKind, SdfPrim};
19
20/// Color desde un hexadecimal `0xRRGGBB`, componente a componente en `[0,1]`.
21///
22/// **Sin corrección de gamma a propósito**: los colores del ser humanoide se
23/// eligieron a ojo mirando el render (`[0.90, 0.72, 0.58]` de piel), así que la
24/// paleta del bestiario se escribe en la MISMA escala. Convertir sRGB→lineal acá
25/// dejaría a los animales oscuros al lado de la gente.
26#[inline]
27pub fn rgb(hex: u32) -> [f32; 3] {
28    [
29        ((hex >> 16) & 0xFF) as f32 / 255.0,
30        ((hex >> 8) & 0xFF) as f32 / 255.0,
31        (hex & 0xFF) as f32 / 255.0,
32    ]
33}
34
35/// Mezcla lineal de dos colores (`t = 0` → `a`).
36#[inline]
37pub fn mezcla(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
38    let t = t.clamp(0.0, 1.0);
39    [
40        a[0] + (b[0] - a[0]) * t,
41        a[1] + (b[1] - a[1]) * t,
42        a[2] + (b[2] - a[2]) * t,
43    ]
44}
45
46/// Aclara (`k > 1`) u oscurece (`k < 1`) un color, con tope en 1.
47#[inline]
48pub fn tono(c: [f32; 3], k: f32) -> [f32; 3] {
49    [
50        (c[0] * k).clamp(0.0, 1.0),
51        (c[1] * k).clamp(0.0, 1.0),
52        (c[2] * k).clamp(0.0, 1.0),
53    ]
54}
55
56#[inline]
57pub(crate) fn v3(v: Vec3) -> [f32; 3] {
58    [v.x, v.y, v.z]
59}
60
61/// Esfera de radio `r` en `c`.
62#[inline]
63pub fn esfera(c: Vec3, r: f32, color: [f32; 3], blend: f32) -> SdfPrim {
64    SdfPrim {
65        kind: SdfKind::Esfera,
66        a: v3(c),
67        ra: r,
68        b: v3(c),
69        rb: 0.0,
70        color,
71        blend,
72    }
73}
74
75/// Cápsula/cono redondeado del segmento `a→b`, con radio `ra` en `a` y `rb` en `b`.
76/// Es el ladrillo de todo lo alargado: patas, cuellos, troncos, frondas, colas.
77#[inline]
78pub fn capsula(a: Vec3, ra: f32, b: Vec3, rb: f32, color: [f32; 3], blend: f32) -> SdfPrim {
79    SdfPrim {
80        kind: SdfKind::Capsula,
81        a: v3(a),
82        ra,
83        b: v3(b),
84        rb,
85        color,
86        blend,
87    }
88}
89
90/// Elipsoide centrado en `c` de **semiejes** `r` (no un segundo punto: el campo `b`
91/// del elipsoide son los semiejes, y trasladarlo es el error clásico — deja discos
92/// gigantes; ver `LECCIONES` del ser SDF).
93#[inline]
94pub fn elipsoide(c: Vec3, r: Vec3, color: [f32; 3], blend: f32) -> SdfPrim {
95    SdfPrim {
96        kind: SdfKind::Elipsoide,
97        a: v3(c),
98        ra: 0.0,
99        b: v3(r),
100        rb: 0.0,
101        color,
102        blend,
103    }
104}
105
106/// Decal esférico: **pinta** color sobre la superficie ya trazada, sin geometría.
107/// Manchas, rayas, ojos, pupilas, flores en la copa.
108#[inline]
109pub fn decal(c: Vec3, r: f32, color: [f32; 3]) -> SdfPrim {
110    SdfPrim {
111        kind: SdfKind::DecalEsfera,
112        a: v3(c),
113        ra: r,
114        b: v3(c),
115        rb: 0.0,
116        color,
117        blend: 0.1,
118    }
119}
120
121/// Decal capsular: pinta una franja a lo largo del segmento `a→b` (rayas de cebra,
122/// líneas de la boca, bandas de la abeja).
123#[inline]
124pub fn decal_linea(a: Vec3, b: Vec3, r: f32, color: [f32; 3]) -> SdfPrim {
125    SdfPrim {
126        kind: SdfKind::DecalCapsula,
127        a: v3(a),
128        ra: r,
129        b: v3(b),
130        rb: r,
131        color,
132        blend: 0.1,
133    }
134}
135
136/// Rota `v` alrededor de X (plano Y-Z; adelante = `+Z`).
137#[inline]
138pub fn rot_x(v: Vec3, a: f32) -> Vec3 {
139    let (s, c) = a.sin_cos();
140    Vec3::new(v.x, v.y * c - v.z * s, v.y * s + v.z * c)
141}
142
143/// Rota `v` alrededor de Y (rumbo).
144#[inline]
145pub fn rot_y(v: Vec3, a: f32) -> Vec3 {
146    let (s, c) = a.sin_cos();
147    Vec3::new(v.x * c + v.z * s, v.y, -v.x * s + v.z * c)
148}
149
150/// Rota `v` alrededor de Z (alabeo lateral).
151#[inline]
152pub fn rot_z(v: Vec3, a: f32) -> Vec3 {
153    let (s, c) = a.sin_cos();
154    Vec3::new(v.x * c - v.y * s, v.x * s + v.y * c, v.z)
155}
156
157/// **Escala no uniforme** todo un ensamble ya armado, alrededor del origen local.
158/// Es el truco del *aplastar y estirar* del ser humanoide, generalizado: en vez de
159/// propagar un factor por veinte fórmulas de proporción, se deforma la lista.
160///
161/// El elipsoide se deforma EXACTO (sus semiejes viven en `b`); los radios de esfera
162/// y cápsula son isótropos y se llevan la media de los tres factores — a las
163/// amplitudes de un rebote o un achatamiento de estilo, eso no se ve.
164pub fn escalar(prims: &mut [SdfPrim], k: Vec3) {
165    let km = (k.x + k.y + k.z) / 3.0;
166    for p in prims.iter_mut() {
167        p.a = [p.a[0] * k.x, p.a[1] * k.y, p.a[2] * k.z];
168        p.b = [p.b[0] * k.x, p.b[1] * k.y, p.b[2] * k.z];
169        p.ra *= km;
170        p.rb *= km;
171        p.blend *= km;
172    }
173}
174
175/// Traslada un ensamble ya armado. Ojo: el elipsoide **no** se traslada por `b`
176/// (son semiejes), sólo por `a`.
177pub fn trasladar(prims: &mut [SdfPrim], d: Vec3) {
178    for p in prims.iter_mut() {
179        p.a = [p.a[0] + d.x, p.a[1] + d.y, p.a[2] + d.z];
180        if p.kind != SdfKind::Elipsoide {
181            p.b = [p.b[0] + d.x, p.b[1] + d.y, p.b[2] + d.z];
182        }
183    }
184}
185
186/// **Cadena de cápsulas** por una polilínea, con radio interpolado de `r0` a `r1`.
187/// El ladrillo de todo lo que serpentea: la serpiente, la cola, el tronco curvo de
188/// una palmera, la liana. `blend` generoso = un solo cuerpo continuo.
189pub fn cadena(
190    pts: &[Vec3],
191    r0: f32,
192    r1: f32,
193    color: [f32; 3],
194    blend: f32,
195    out: &mut Vec<SdfPrim>,
196) {
197    if pts.len() < 2 {
198        return;
199    }
200    let n = (pts.len() - 1) as f32;
201    for (i, w) in pts.windows(2).enumerate() {
202        let t0 = i as f32 / n;
203        let t1 = (i + 1) as f32 / n;
204        out.push(capsula(
205            w[0],
206            r0 + (r1 - r0) * t0,
207            w[1],
208            r0 + (r1 - r0) * t1,
209            color,
210            blend,
211        ));
212    }
213}
214
215/// **Un ojo** en `c` mirando hacia `dir`: domo blanco (geometría, para que sobreviva
216/// el escorzo 3/4) + pupila y chispa como decals pegadas al domo.
217///
218/// Es el mismo diseño que la cara del ser humanoide, extraído para que el bestiario
219/// no invente otro ojo: dos técnicas de ojo en la misma escena se notan.
220/// `esclera = None` = ojo de animal sin blanco visible (la mayoría): sólo la pupila
221/// oscura sobre el pelaje, que es lo que se ve de verdad en un ciervo o un pájaro.
222pub fn ojo(
223    c: Vec3,
224    dir: Vec3,
225    r: f32,
226    esclera: Option<[f32; 3]>,
227    pupila: [f32; 3],
228    chispa: bool,
229    out: &mut Vec<SdfPrim>,
230) {
231    let d = dir.normalize_or_zero();
232    let d = if d.length_squared() < 0.5 { Vec3::Z } else { d };
233    match esclera {
234        Some(blanco) => {
235            out.push(esfera(c, r, blanco, r * 0.25));
236            out.push(decal(c + d * (r * 0.99), r * 0.48, pupila));
237            if chispa {
238                out.push(decal(
239                    c + d * (r * 0.99) + Vec3::new(0.0, r * 0.26, 0.0) + d * (r * 0.10),
240                    r * 0.20,
241                    [0.98, 0.985, 0.99],
242                ));
243            }
244        }
245        None => {
246            // Globo oscuro como GEOMETRÍA: en un hocico peludo un decal plano se
247            // pierde con la luz rasante; la esferita capta su propio brillo.
248            out.push(esfera(c, r, pupila, r * 0.3));
249            if chispa {
250                out.push(decal(
251                    c + d * (r * 0.92) + Vec3::new(0.0, r * 0.30, 0.0),
252                    r * 0.30,
253                    [0.97, 0.98, 0.99],
254                ));
255            }
256        }
257    }
258}
259
260/// **Extensión vertical** de un ensamble (mínimo y máximo de `y`), contando sólo la
261/// GEOMETRÍA: los decals pintan sobre lo ya trazado y no agrandan a nadie — si
262/// contaran, una mancha del patrón correría la altura del animal.
263pub fn extension_y(prims: &[SdfPrim]) -> (f32, f32) {
264    let mut lo = f32::INFINITY;
265    let mut hi = f32::NEG_INFINITY;
266    for p in prims {
267        let (a, b) = match p.kind {
268            SdfKind::Elipsoide => (p.a[1] - p.b[1], p.a[1] + p.b[1]),
269            SdfKind::Esfera => (p.a[1] - p.ra, p.a[1] + p.ra),
270            SdfKind::Capsula => (
271                (p.a[1] - p.ra).min(p.b[1] - p.rb),
272                (p.a[1] + p.ra).max(p.b[1] + p.rb),
273            ),
274            _ => continue,
275        };
276        lo = lo.min(a);
277        hi = hi.max(b);
278    }
279    if lo > hi {
280        (0.0, 0.0)
281    } else {
282        (lo, hi)
283    }
284}
285
286/// **Asienta y escala**: baja el ensamble hasta apoyar en `y = 0` y lo escala
287/// uniforme para que mida `alto`.
288///
289/// Es lo que deja escribir cada receta del catálogo en las unidades que le quedan
290/// cómodas (el cuadrúpedo mide su mundo en alturas de lomo, el ave en alturas
291/// totales) sin que nadie tenga que hacer la cuenta: la jirafa y la hormiga salen de
292/// la misma tabla. Devuelve el factor aplicado.
293pub fn asentar(prims: &mut Vec<SdfPrim>, alto: f32) -> f32 {
294    let (lo, hi) = extension_y(prims);
295    let h = hi - lo;
296    if h <= 1e-6 {
297        return 1.0;
298    }
299    trasladar(prims, Vec3::new(0.0, -lo, 0.0));
300    let k = alto / h;
301    escalar(prims, Vec3::splat(k));
302    k
303}
304
305/// Ruido hash determinista `[0,1)` a partir de dos enteros — la variedad por
306/// semilla del catálogo (qué tan torcido sale este árbol, de qué color esta remera)
307/// sin arrastrar un generador aleatorio por toda la API.
308#[inline]
309pub fn hash01(a: u32, b: u32) -> f32 {
310    let mut h = a.wrapping_mul(0x9E37_79B9) ^ b.wrapping_mul(0x85EB_CA77);
311    h ^= h >> 15;
312    h = h.wrapping_mul(0x2C1B_3C6D);
313    h ^= h >> 13;
314    (h & 0x00FF_FFFF) as f32 / 16_777_216.0
315}
316
317/// Igual que [`hash01`] pero centrado en `[-1, 1)`.
318#[inline]
319pub fn hash11(a: u32, b: u32) -> f32 {
320    hash01(a, b) * 2.0 - 1.0
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn rgb_mapea_el_hexadecimal() {
329        assert_eq!(rgb(0xFFFFFF), [1.0, 1.0, 1.0]);
330        assert_eq!(rgb(0x000000), [0.0, 0.0, 0.0]);
331        let c = rgb(0x804020);
332        assert!((c[0] - 0.502).abs() < 0.01 && (c[1] - 0.251).abs() < 0.01);
333    }
334
335    /// El elipsoide se traslada por `a` y NUNCA por `b` (sus semiejes): el error que
336    /// dejaba discos gigantes cuando nació el ser SDF.
337    #[test]
338    fn trasladar_no_toca_los_semiejes_del_elipsoide() {
339        let mut p = vec![elipsoide(Vec3::ZERO, Vec3::new(1.0, 2.0, 3.0), [1.0; 3], 0.1)];
340        trasladar(&mut p, Vec3::new(10.0, 0.0, 0.0));
341        assert_eq!(p[0].a, [10.0, 0.0, 0.0]);
342        assert_eq!(p[0].b, [1.0, 2.0, 3.0], "los semiejes no se mueven");
343    }
344
345    #[test]
346    fn escalar_deforma_el_elipsoide_por_eje() {
347        let mut p = vec![elipsoide(Vec3::new(0.0, 1.0, 0.0), Vec3::ONE, [1.0; 3], 0.1)];
348        escalar(&mut p, Vec3::new(2.0, 0.5, 1.0));
349        assert_eq!(p[0].a, [0.0, 0.5, 0.0]);
350        assert_eq!(p[0].b, [2.0, 0.5, 1.0]);
351    }
352
353    #[test]
354    fn cadena_hila_los_tramos_con_radio_interpolado() {
355        let pts = [Vec3::ZERO, Vec3::Y, Vec3::new(0.0, 2.0, 0.0)];
356        let mut out = Vec::new();
357        cadena(&pts, 1.0, 0.0, [1.0; 3], 0.1, &mut out);
358        assert_eq!(out.len(), 2);
359        assert_eq!(out[0].ra, 1.0);
360        assert!((out[0].rb - 0.5).abs() < 1e-6, "mitad del recorrido = mitad del radio");
361        assert_eq!(out[1].rb, 0.0);
362    }
363
364    #[test]
365    fn asentar_apoya_en_cero_y_da_la_altura_pedida() {
366        let mut p = vec![
367            esfera(Vec3::new(0.0, 5.0, 0.0), 1.0, [1.0; 3], 0.1),
368            capsula(Vec3::new(0.0, 2.0, 0.0), 0.5, Vec3::new(0.0, 4.0, 0.0), 0.5, [1.0; 3], 0.1),
369        ];
370        let k = asentar(&mut p, 3.0);
371        let (lo, hi) = extension_y(&p);
372        assert!(lo.abs() < 1e-5, "apoya en 0, no en {lo}");
373        assert!((hi - 3.0).abs() < 1e-5, "mide 3, no {hi}");
374        assert!((k - 3.0 / 4.5).abs() < 1e-5, "factor {k}");
375    }
376
377    /// Un decal no puede agrandar al ser: pinta sobre lo que ya está trazado.
378    #[test]
379    fn los_decals_no_cuentan_para_la_altura() {
380        let base = vec![esfera(Vec3::new(0.0, 1.0, 0.0), 1.0, [1.0; 3], 0.1)];
381        let mut con = base.clone();
382        con.push(decal(Vec3::new(0.0, 9.0, 0.0), 0.5, [1.0; 3]));
383        assert_eq!(extension_y(&base), extension_y(&con));
384    }
385
386    #[test]
387    fn el_hash_es_determinista_y_reparte() {
388        assert_eq!(hash01(3, 7), hash01(3, 7));
389        assert_ne!(hash01(3, 7), hash01(3, 8));
390        let n = 400;
391        let media: f32 = (0..n).map(|i| hash01(i, 1)).sum::<f32>() / n as f32;
392        assert!((media - 0.5).abs() < 0.06, "media {media} lejos de 0.5");
393    }
394}