tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definitions

⎇ e8f97b50cb24848ab7b1634a78c0f84ec468a762 crates/modules/gioser/gioser-shaders/src/lib.rs Rust · 711 lines · 27.1 KB History Who touched what raw

feat(gioser): luna con textura rica + terminador curvo, auras anchas, overlay nubes e8f97b50c · sergio ·

1//! Fuentes GLSL ES 3.00 para GioSer.
2//!
3//! Cada `const &str` es un shader completo listo para `gl.shaderSource()`.
4//! Convención: precision `highp float`, atributo `a_pos`, varying `v_*`,
5//! uniforms `u_*`.
6
7#![no_std]
8
9/// Vertex shader para quads en clip-space `[-1, 1]²`.
10pub const VS_FULLSCREEN: &str = "#version 300 es
11precision highp float;
12in vec2 a_pos;
13out vec2 v_clip;
14out vec2 v_uv;
15void main() {
16    v_clip = a_pos;
17    v_uv = a_pos * 0.5 + 0.5;
18    gl_Position = vec4(a_pos, 0.0, 1.0);
19}
20";
21
22/// Fragment del fondo cósmico: nubes FBM en 3 capas con drift visible,
23/// 3 estratos de estrellas con titilación independiente, viñeta radial,
24/// 4 meteoros procedurales con vida cíclica.
25pub const FS_COSMOS: &str = "#version 300 es
26precision highp float;
27in vec2 v_clip;
28in vec2 v_uv;
29out vec4 fragColor;
30uniform vec2  u_resolution;
31uniform float u_time;
32uniform vec2  u_parallax;
33uniform vec3  u_void;
34uniform vec3  u_nebula_a;
35uniform vec3  u_nebula_b;
36uniform vec3  u_stardust;
37
38float hash21(vec2 p) {
39    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
40}
41float hash11(float n) {
42    return fract(sin(n * 78.233) * 43758.5453);
43}
44float vnoise(vec2 p) {
45    vec2 i = floor(p);
46    vec2 f = fract(p);
47    f = f * f * (3.0 - 2.0 * f);
48    float a = hash21(i);
49    float b = hash21(i + vec2(1.0, 0.0));
50    float c = hash21(i + vec2(0.0, 1.0));
51    float d = hash21(i + vec2(1.0, 1.0));
52    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
53}
54float fbm(vec2 p) {
55    float v = 0.0;
56    float a = 0.55;
57    for (int i = 0; i < 5; i++) {
58        v += a * vnoise(p);
59        p = p * 2.07 + vec2(11.3, 7.7);
60        a *= 0.55;
61    }
62    return v;
63}
64
65float meteor(vec2 uv, float seed) {
66    float period = 6.5 + 4.0 * hash11(seed * 17.0);
67    float t_seeded = u_time + seed * 19.0;
68    float phase = mod(t_seeded, period);
69    float life = 1.6;
70    if (phase > life) return 0.0;
71    float t = phase / life;
72
73    float epoch = floor(t_seeded / period);
74    vec2 origin = vec2(
75        hash21(vec2(seed, epoch)) * 2.6 - 1.3,
76        0.55 + hash21(vec2(seed + 5.0, epoch)) * 0.55
77    );
78    vec2 dir = normalize(vec2(
79        hash21(vec2(seed + 1.0, epoch)) * 1.6 - 0.8,
80        -0.7 - hash21(vec2(seed + 2.0, epoch)) * 0.6
81    ));
82    vec2 head = origin + dir * t * 2.1;
83    vec2 tail = head - dir * 0.24;
84    vec2 pa = uv - tail;
85    vec2 ba = head - tail;
86    float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
87    float dist = length(pa - ba * h);
88    float perpGlow = exp(-dist * 420.0);
89    float trailFalloff = smoothstep(0.0, 1.0, h);
90    float headPulse = exp(-dist * 900.0);
91    float lifeFade = sin(t * 3.14159);
92    return (perpGlow * trailFalloff + headPulse * 1.4) * lifeFade;
93}
94
95void main() {
96    float aspect = u_resolution.x / max(u_resolution.y, 1.0);
97    vec2 uv = v_clip;
98    uv.x *= aspect;
99
100    vec2 d1 = vec2( u_time * 0.055,  u_time * 0.022) + u_parallax * 0.10;
101    vec2 d2 = vec2(-u_time * 0.085,  u_time * 0.058) + u_parallax * 0.22;
102    vec2 d3 = vec2( u_time * 0.130, -u_time * 0.095) + u_parallax * 0.40;
103    float n1 = fbm(uv * 0.85 + d1);
104    float n2 = fbm(uv * 2.05 + d2);
105    float n3 = fbm(uv * 4.40 + d3);
106
107    vec3 color = u_void;
108    color = mix(color, u_nebula_a, pow(n1, 1.5) * 0.80);
109    color = mix(color, u_nebula_b, pow(n2, 1.85) * 0.62);
110    color += u_nebula_a * pow(n3, 3.0) * 0.28;
111
112    float r = length(v_clip);
113    color *= 1.0 - smoothstep(0.55, 1.40, r) * 0.85;
114
115    {
116        vec2 sgrid = uv * 75.0;
117        vec2 sid = floor(sgrid);
118        float sh = hash21(sid);
119        float tw = 0.45 + 0.55 * sin(u_time * 2.6 + sh * 41.0);
120        float mask = smoothstep(0.9935, 0.999, sh);
121        color += u_stardust * mask * tw * 1.15;
122    }
123    {
124        vec2 sgrid = uv * 135.0 + vec2(7.0, 11.0);
125        vec2 sid = floor(sgrid);
126        float sh = hash21(sid);
127        float tw = 0.55 + 0.45 * sin(u_time * 1.1 + sh * 28.0);
128        float mask = smoothstep(0.987, 0.994, sh);
129        color += u_stardust * mask * tw * 0.75;
130    }
131    {
132        vec2 sgrid = uv * 260.0 + vec2(13.0, 3.0);
133        vec2 sid = floor(sgrid);
134        float sh = hash21(sid);
135        float tw = 0.7 + 0.3 * sin(u_time * 0.5 + sh * 15.0);
136        float mask = smoothstep(0.982, 0.989, sh);
137        color += u_stardust * mask * tw * 0.40;
138    }
139
140    float meteors = 0.0;
141    meteors += meteor(uv, 0.31);
142    meteors += meteor(uv, 1.73);
143    meteors += meteor(uv, 4.29);
144    meteors += meteor(uv, 7.11);
145    color += vec3(1.0, 0.94, 0.78) * meteors * 1.1;
146
147    fragColor = vec4(color, 1.0);
148}
149";
150
151/// Vertex de la chacana: aplica MVP y pasa la posición de mundo al fragment.
152pub const VS_CHACANA: &str = "#version 300 es
153precision highp float;
154in vec2 a_pos;
155out vec2 v_world;
156uniform mat4 u_mvp;
157void main() {
158    v_world = a_pos;
159    gl_Position = u_mvp * vec4(a_pos, 0.0, 1.0);
160}
161";
162
163/// Fragment de la chacana mística (estética dorada del logo GioSer):
164/// 1. **Sol detrás**: halo gauss + corona, visible SÓLO dentro de la superficie
165///    de la chacana (clip por SDF), apenas asomando por las junturas de los pasos.
166/// 2. **Doble outline**: dos líneas paralelas en dorado/ámbar — la chacana se
167///    siente "grabada" sobre el cielo.
168/// 3. **Interior**: niebla oscura translúcida con sutiles rayos radiales
169///    desde el centro (el sol los proyecta a través de la superficie).
170/// 4. **Aro doble exterior**: ring fino + ring grueso (este último con marcas
171///    cardinales de 3 puntos cada una, como en el logo).
172///
173/// Uniforms:
174///   `u_time`, `u_thickness` (s), `u_center_half` (c), `u_arm_extent` (L),
175///   `u_line_color` (gold rim), `u_rim_color` (gold rim oscuro),
176///   `u_sun_color`, `u_sun_pulse`, `u_dark_color` (interior fill).
177pub const FS_CHACANA: &str = "#version 300 es
178precision highp float;
179in vec2 v_world;
180out vec4 fragColor;
181uniform float u_time;
182uniform float u_thickness;
183uniform float u_center_half;
184uniform float u_arm_extent;
185uniform vec3  u_line_color;
186uniform vec3  u_rim_color;
187uniform vec3  u_sun_color;
188uniform vec3  u_dark_color;
189uniform vec3  u_aire_color;
190uniform vec3  u_fuego_color;
191uniform vec3  u_tierra_color;
192uniform vec3  u_agua_color;
193uniform vec3  u_zodiac[12];
194uniform float u_sun_pulse;
195// Ciclo del cuerpo central: 0=sol, 1=luna, 2=tierra. Se interpola entre
196// `u_body_a` y `u_body_b` con `u_body_blend ∈ [0, 1]`.
197uniform int   u_body_a;
198uniform int   u_body_b;
199uniform float u_body_blend;
200
201const float PI = 3.14159265;
202
203float hash21c(vec2 p) {
204    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
205}
206float hash11c(float n) {
207    return fract(sin(n * 78.233) * 43758.5453);
208}
209// Value noise + fbm para superficies (lunar craters, continentes terrestres).
210float vnoise_c(vec2 p) {
211    vec2 i = floor(p);
212    vec2 f = fract(p);
213    f = f * f * (3.0 - 2.0 * f);
214    float a = hash21c(i);
215    float b = hash21c(i + vec2(1.0, 0.0));
216    float c = hash21c(i + vec2(0.0, 1.0));
217    float d = hash21c(i + vec2(1.0, 1.0));
218    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
219}
220float fbm_c(vec2 p) {
221    float v = 0.0;
222    float a = 0.5;
223    for (int i = 0; i < 4; i++) {
224        v += a * vnoise_c(p);
225        p = p * 2.03 + vec2(1.7, 9.2);
226        a *= 0.5;
227    }
228    return v;
229}
230
231float sdBox(vec2 p, vec2 b) {
232    vec2 d = abs(p) - b;
233    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
234}
235
236// ===== CUERPO CENTRAL: Sol / Luna / Tierra =====
237//
238// Cada uno renderea dentro del cuadrado central de la chacana con su
239// propia personalidad realista. El loop temporal (cambio entre cuerpos
240// + transiciones graduales) lo decide el host vía u_body_a/b/blend.
241
242vec3 render_sun(vec2 p, float r, float pulse) {
243    // Núcleo brillante + corona difusa con pulso.
244    float coreR = u_thickness * 0.42;
245    float core  = exp(-(r * r) / (2.0 * coreR * coreR));
246    float halR  = u_center_half * 0.70;
247    float halo  = exp(-(r * r) / (2.0 * halR * halR));
248    // Superficie boiling (plasma) muy sutil.
249    float plasma = fbm_c(p * 18.0 + vec2(u_time * 0.15, u_time * 0.10)) * 0.20;
250    vec3 base = u_sun_color * (core * (1.20 + 0.25 * pulse) + halo * (0.55 + 0.15 * pulse));
251    return base + u_sun_color * core * plasma * 0.6;
252}
253
254vec3 render_moon(vec2 p, float r, float time) {
255    float moonR = u_thickness * 1.40;
256    if (r > moonR * 1.20) return vec3(0.0);
257
258    // Normales de la esfera proyectadas en pantalla (sólo la cara visible).
259    float nx = p.x / moonR;
260    float ny = p.y / moonR;
261    float n2 = nx * nx + ny * ny;
262    float nz = sqrt(max(1.0 - n2, 0.0));
263    float disk = 1.0 - smoothstep(moonR * 0.88, moonR * 1.00, r);
264
265    // Limb darkening realista (regolito lunar dispersa más al borde).
266    float limb_factor = pow(max(nz, 0.0), 0.45);
267
268    // === SUPERFICIE: 4 capas + crater rings ===
269    // Maria — mares oscuros grandes (Mare Imbrium, Tranquillitatis, etc.)
270    float maria_n = fbm_c(p * 4.5 + vec2(2.3, 1.1));
271    float maria = smoothstep(0.42, 0.60, maria_n);
272    // Cráteres grandes (radio mid).
273    float craters_mid = fbm_c(p * 13.0 + vec2(8.5, 3.2));
274    // Cráteres chicos.
275    float craters_small = fbm_c(p * 28.0 + vec2(17.1, 5.8));
276    // Detalle fino y micro (granularidad de polvo).
277    float fine = fbm_c(p * 55.0 + vec2(7.3, 11.4));
278    float micro = fbm_c(p * 110.0);
279    // Rims (bordes elevados de cráteres) — picos donde el fbm cruza 0.5.
280    float ring_mid = pow(abs(craters_mid - 0.5) * 2.0, 3.5);
281    float ring_small = pow(abs(craters_small - 0.5) * 2.0, 5.0);
282
283    float albedo = 0.80;
284    albedo += (craters_mid - 0.5) * 0.32;
285    albedo += (craters_small - 0.5) * 0.22;
286    albedo += (fine - 0.5) * 0.20;
287    albedo += (micro - 0.5) * 0.10;
288    albedo += ring_mid * 0.22;
289    albedo += ring_small * 0.16;
290    albedo -= maria * 0.50;
291    albedo = clamp(albedo, 0.10, 1.15);
292
293    // === FASE LUNAR CURVA ===
294    // Ciclo lineal — un mes lunar comprimido en ~40 s. Avanza 0→1
295    // pasando por new(0) → first-q(0.25) → full(0.5) → last-q(0.75) → new(1).
296    float phase = fract(time / 40.0);
297    float phi = phase * 2.0 * PI;
298    // Dirección del sol relativa al observador.
299    //   phi=0   → sun_dir = (0, 0, -1) ⇒ atrás de la luna (new moon).
300    //   phi=π/2 → sun_dir = (+1, 0, 0) ⇒ a la derecha (first quarter).
301    //   phi=π   → sun_dir = (0, 0, +1) ⇒ frente al observador (full moon).
302    // Como `dot(normal, sun_dir) = nx*sin(phi) - nz*cos(phi)`, el terminador
303    // resulta ser una elipse en la pantalla — curva como en la luna real.
304    float lit_value = nx * sin(phi) - nz * cos(phi);
305    float lit = smoothstep(-0.035, 0.035, lit_value);
306
307    // Color superficie (gris ligeramente azulado).
308    vec3 surface = vec3(0.86, 0.88, 0.94) * albedo;
309
310    // Halo cercano al limb iluminado — luz dispersada en el regolito.
311    float outer_glow = smoothstep(moonR * 1.15, moonR * 0.95, r) - disk;
312    vec3 glow = vec3(0.55, 0.70, 0.95) * max(outer_glow, 0.0) * lit * 0.55;
313
314    return surface * disk * lit * limb_factor + glow;
315}
316
317vec3 render_earth(vec2 p, float r, float time) {
318    float earthR = u_thickness * 1.35;
319    float disk = 1.0 - smoothstep(earthR * 0.86, earthR * 1.00, r);
320    float limb_factor = sqrt(max(1.0 - (r * r) / (earthR * earthR), 0.0));
321    // Rotación lenta: continentes drift horizontalmente.
322    float rot = time * 0.08;
323    vec2 rp = p + vec2(rot, 0.0);
324    // Continentes con fbm orgánico.
325    float landmass = fbm_c(rp * 7.5);
326    float is_land = smoothstep(0.50, 0.56, landmass);
327    vec3 ocean = vec3(0.08, 0.28, 0.52);
328    vec3 land  = vec3(0.30, 0.52, 0.24);
329    vec3 land_high = vec3(0.55, 0.45, 0.28); // montañas / desiertos
330    land = mix(land, land_high, smoothstep(0.60, 0.78, landmass));
331    vec3 surface = mix(ocean, land, is_land);
332    // Casquetes polares.
333    float ny = abs(p.y / max(earthR, 1e-4));
334    float polar = smoothstep(0.70, 0.94, ny);
335    surface = mix(surface, vec3(0.96, 0.97, 1.0), polar * 0.85);
336    // Nubes flotando en otra capa rotando algo distinto.
337    float clouds = fbm_c(rp * 6.0 + vec2(rot * 0.3, 0.0)) * 0.7;
338    float cloud_mask = smoothstep(0.55, 0.75, clouds);
339    surface = mix(surface, vec3(0.95, 0.96, 0.99), cloud_mask * 0.40);
340    // Día / noche: hemisferio iluminado.
341    float lit = smoothstep(-0.45, 0.55, p.x / max(earthR, 1e-4) + 0.15);
342    surface *= 0.30 + 0.70 * lit;
343    // Atmósfera azul en el limb (Rayleigh scattering simplificado).
344    float atm_inner = smoothstep(earthR * 1.10, earthR * 0.95, r);
345    float atm = max(atm_inner - disk, 0.0);
346    return surface * disk * limb_factor + vec3(0.30, 0.55, 0.95) * atm * 0.55;
347}
348
349vec3 render_body(int kind, vec2 p, float r, float time, float pulse) {
350    if (kind == 0) return render_sun(p, r, pulse);
351    if (kind == 1) return render_moon(p, r, time);
352    return render_earth(p, r, time);
353}
354
355// ===== AURA ELEMENTAL (NUBE ANCHA POR CARDINAL) =====
356// Se suma a las partículas puntuales: una cobertura ancha del cuadrante
357// del cardinal correspondiente, con personalidad por elemento.
358vec3 element_cloud(vec2 p, vec2 tip, vec2 outward, vec3 color, float time, int kind) {
359    vec2 perp = vec2(-outward.y, outward.x);
360    // Centro de la nube: bien adentro del cuadrante (más allá del tip).
361    vec2 cloud_center = tip + outward * 0.28;
362    vec2 to_p = p - cloud_center;
363    float along = dot(to_p, outward);
364    float perp_d = dot(to_p, perp);
365    // Anisotropía: el aura cubre TODO el cuadrante del cardinal. Sigmas
366    // grandes → se solapan en las esquinas (NE/NW/SE/SW) y crean mezclas.
367    float sigma_along = 0.62;
368    float sigma_perp  = 0.62;
369    float base = exp(-(along * along) / (2.0 * sigma_along * sigma_along)
370                     -(perp_d * perp_d) / (2.0 * sigma_perp * sigma_perp));
371    // Textura noise animada por elemento.
372    vec2 noise_uv = (p - tip) * (3.5 + float(kind) * 0.4)
373                  + vec2(time * (0.20 + float(kind) * 0.05), time * 0.10);
374    float n = fbm_c(noise_uv);
375    float modulation;
376    if (kind == 0) {
377        // AIRE: corrientes suaves que se mueven horizontalmente.
378        modulation = 0.55 + 0.45 * (n * 0.7 + sin(time * 0.6 + perp_d * 4.0) * 0.3);
379    } else if (kind == 1) {
380        // FUEGO: lengüetazos que parpadean rápido.
381        modulation = (0.40 + 0.60 * n) * (0.7 + 0.3 * sin(time * 3.2 + along * 8.0));
382    } else if (kind == 2) {
383        // TIERRA: densidad sólida con variación lenta.
384        modulation = 0.65 + 0.35 * fbm_c(p * 4.0 + vec2(time * 0.05, 0.0));
385    } else {
386        // AGUA: ondulaciones grandes que viajan hacia afuera.
387        modulation = 0.50 + 0.50 * sin(time * 0.9 - along * 5.0 + n * 4.0);
388    }
389    return color * base * max(modulation, 0.0) * 0.26;
390}
391
392// Chacana de 2 escalones (mística clásica de Tiwanaku).
393float sdChacana(vec2 p, float s, float c) {
394    float d = sdBox(p, vec2(c, c));
395    float hd = 0.5 * s;
396    float mid1 = c + 0.5 * s;
397    float hw1 = 2.0 * s;
398    d = min(d, sdBox(p - vec2(0.0,  mid1), vec2(hw1, hd)));
399    d = min(d, sdBox(p - vec2(0.0, -mid1), vec2(hw1, hd)));
400    d = min(d, sdBox(p - vec2( mid1, 0.0), vec2(hd, hw1)));
401    d = min(d, sdBox(p - vec2(-mid1, 0.0), vec2(hd, hw1)));
402    float mid2 = c + 1.5 * s;
403    float hw2 = 1.0 * s;
404    d = min(d, sdBox(p - vec2(0.0,  mid2), vec2(hw2, hd)));
405    d = min(d, sdBox(p - vec2(0.0, -mid2), vec2(hw2, hd)));
406    d = min(d, sdBox(p - vec2( mid2, 0.0), vec2(hd, hw2)));
407    d = min(d, sdBox(p - vec2(-mid2, 0.0), vec2(hd, hw2)));
408    return d;
409}
410
411// Emisor de partículas por tip cardinal. Cada elemento tiene su propio
412// patrón de velocidad para sentirse vivo:
413//   AIRE  → drift hacia afuera con sway lateral (viento)
414//   FUEGO → asciende erráticamente con flicker amplio
415//   TIERRA→ cae con gravedad y rebote sutil
416//   AGUA  → ondula descendiendo (gotas que se deslizan)
417//
418// `element_kind`: 0=AIRE, 1=FUEGO, 2=TIERRA, 3=AGUA.
419// `outward`: dirección unitaria desde el centro hacia el tip.
420vec3 element_particles(vec2 p, vec2 tip, vec2 outward, vec3 color, int kind, float seed_base) {
421    vec3 accum = vec3(0.0);
422    vec2 perp = vec2(-outward.y, outward.x);
423    // 4 partículas por tip — suficiente densidad sin saturar el costo del frag.
424    for (int k = 0; k < 4; k++) {
425        float seed = seed_base + float(k) * 1.31;
426        float life = 1.5 + hash11c(seed * 11.0) * 0.7;
427        float t_seeded = u_time + seed * 9.3;
428        float phase = mod(t_seeded, life);
429        float ph = phase / life; // 0..1
430
431        // Random offsets por época (cuando el ciclo reinicia).
432        float epoch = floor(t_seeded / life);
433        vec2 jitter = vec2(
434            hash21c(vec2(seed, epoch)) - 0.5,
435            hash21c(vec2(epoch, seed)) - 0.5
436        );
437
438        // Velocidad por elemento — distinto carácter visual.
439        vec2 vel;
440        float sway = sin(u_time * 4.0 + seed * 7.3);
441        if (kind == 0) {
442            // AIRE: drift hacia afuera + sway perpendicular notable.
443            vel = outward * 0.14 + perp * sway * 0.10;
444        } else if (kind == 1) {
445            // FUEGO: rise erratic — siempre con componente +Y (hacia arriba en el mundo),
446            // independiente del tip → flamas suben.
447            float erratic = sin(u_time * 6.0 + seed * 11.0) * 0.06;
448            vel = outward * 0.10 + vec2(erratic, 0.18 + 0.04 * sway);
449        } else if (kind == 2) {
450            // TIERRA: cae — outward más componente -Y.
451            vel = outward * 0.05 + vec2(0.03 * sway, -0.16);
452        } else {
453            // AGUA: drift outward con descenso y ondulación.
454            float wave = sin(u_time * 3.2 + seed * 8.7) * 0.07;
455            vel = outward * 0.12 + vec2(wave, -0.08);
456        }
457
458        vec2 pos = tip + vel * phase + jitter * 0.04;
459
460        // Brillo gauss + envelope sinusoidal en la vida.
461        float bright = sin(ph * PI);
462        float dist = length(p - pos);
463        float size = 0.014 + 0.006 * (kind == 1 ? sway : 0.0); // fuego pulsa
464        float glow = exp(-(dist * dist) / (2.0 * size * size));
465        accum += color * glow * bright;
466    }
467    return accum;
468}
469
470// 3 puntos pequeños en cada uno de los 4 cardinales sobre el aro grueso.
471float cardinal_dots(vec2 p, float ringR, float dotSize) {
472    float r = length(p);
473    float ang = atan(p.y, p.x);
474    // Acercamiento al aro (gauss tight en r=ringR).
475    float on_ring = exp(-((r - ringR) * (r - ringR)) / (2.0 * dotSize * dotSize));
476    float dots = 0.0;
477    // 4 cardinales en ángulos 0, π/2, π, -π/2.
478    for (int i = 0; i < 4; i++) {
479        float base = float(i) * (PI * 0.5);
480        // 3 puntos por cardinal, offset angular pequeño.
481        for (int j = -1; j <= 1; j++) {
482            float a = base + float(j) * 0.075;
483            float da = ang - a;
484            da = da - 2.0 * PI * floor((da + PI) / (2.0 * PI));
485            dots += exp(-(da * da) / (2.0 * 0.012 * 0.012));
486        }
487    }
488    return on_ring * dots;
489}
490
491void main() {
492    vec2 p = v_world;
493    float d = sdChacana(p, u_thickness, u_center_half);
494    float r = length(p);
495
496    // === CUERPO CENTRAL — SOL / LUNA / TIERRA ===
497    // Sólo se computa dentro de la superficie de la chacana (perf + estética).
498    float inside = 1.0 - smoothstep(-0.004, 0.004, d);
499    vec3 central = vec3(0.0);
500    if (inside > 0.001) {
501        central = render_body(u_body_a, p, r, u_time, u_sun_pulse);
502        if (u_body_blend > 0.001) {
503            vec3 next_body = render_body(u_body_b, p, r, u_time, u_sun_pulse);
504            central = mix(central, next_body, u_body_blend);
505        }
506    }
507
508    // Rayos radiales sutiles desde el centro (el cuerpo los proyecta a
509    // través de la superficie). El multiplicador disminuye cuando la luna
510    // o la tierra están activas — el sol es el que más irradia.
511    float ang = atan(p.y, p.x);
512    float radial_mult = (u_body_a == 0) ? 1.0 : 0.35;
513    if (u_body_blend > 0.001) {
514        float next_mult = (u_body_b == 0) ? 1.0 : 0.35;
515        radial_mult = mix(radial_mult, next_mult, u_body_blend);
516    }
517    float radial = pow(abs(cos(ang * 4.0 + sin(u_time * 0.3) * 0.2)), 8.0)
518                 * smoothstep(0.0, u_center_half * 0.8, r)
519                 * (1.0 - smoothstep(u_center_half * 0.85, u_center_half * 1.2, r))
520                 * 0.30 * radial_mult;
521
522    // === DOBLE OUTLINE ===
523    // Línea interior (sobre la SDF=0).
524    float lineW1 = 0.0085;
525    float line_in = exp(-(d * d) / (2.0 * lineW1 * lineW1));
526    // Línea exterior paralela, offset 0.018 hacia afuera.
527    float dOff = d - 0.020;
528    float lineW2 = 0.005;
529    float line_out = exp(-(dOff * dOff) / (2.0 * lineW2 * lineW2));
530    float line = line_in * 1.0 + line_out * 0.65;
531
532    // Glow exterior leve.
533    float glow = exp(-max(d, 0.0) * 14.0) * 0.30;
534
535    // === AROS EXTERIORES ===
536    float ringR_main = u_arm_extent * 1.45;
537    float ringD_main = abs(r - ringR_main);
538    float ring_main = exp(-(ringD_main * ringD_main) / (2.0 * 0.005 * 0.005));
539
540    float ringR_inner = u_arm_extent * 1.30;
541    float ringD_inner = abs(r - ringR_inner);
542    float ring_inner = exp(-(ringD_inner * ringD_inner) / (2.0 * 0.003 * 0.003)) * 0.40;
543
544    // 4 grupos de 3 puntos cardinales sobre el aro principal.
545    float dots = cardinal_dots(p, ringR_main, 0.008) * 1.10;
546
547    // === PARTÍCULAS POR ELEMENTO ===
548    // Cada tip emite partículas con la personalidad del elemento.
549    float L = u_arm_extent;
550    vec3 particles = vec3(0.0);
551    particles += element_particles(p, vec2(0.0,  L), vec2(0.0,  1.0), u_aire_color,   0, 0.31);
552    particles += element_particles(p, vec2( L, 0.0), vec2( 1.0, 0.0), u_fuego_color,  1, 1.73);
553    particles += element_particles(p, vec2(0.0, -L), vec2(0.0, -1.0), u_tierra_color, 2, 3.11);
554    particles += element_particles(p, vec2(-L, 0.0), vec2(-1.0, 0.0), u_agua_color,   3, 5.97);
555
556    // === AURA ELEMENTAL ANCHA ===
557    // Una nube wide por cardinal — ocupa el cuadrante entero, no sólo la
558    // punta. Las partículas puntuales aportan detalle agudo encima.
559    vec3 clouds = vec3(0.0);
560    clouds += element_cloud(p, vec2(0.0,  L), vec2(0.0,  1.0), u_aire_color,   u_time, 0);
561    clouds += element_cloud(p, vec2( L, 0.0), vec2( 1.0, 0.0), u_fuego_color,  u_time, 1);
562    clouds += element_cloud(p, vec2(0.0, -L), vec2(0.0, -1.0), u_tierra_color, u_time, 2);
563    clouds += element_cloud(p, vec2(-L, 0.0), vec2(-1.0, 0.0), u_agua_color,   u_time, 3);
564
565    // === TRAZOS ZODIACALES ===
566    // 12 líneas radiales muy sutiles entre la chacana y el aro principal,
567    // una por signo, con sus colores significativos (Aries=fuego rojo,
568    // Tauro=tierra verde, Géminis=aire amarillo, Cáncer=agua plata, ...).
569    // Aries arranca en el norte y giran en sentido horario (rueda zodiacal
570    // clásica).
571    vec3 zodiac = vec3(0.0);
572    {
573        float seg = 2.0 * PI / 12.0;
574        // delta = ángulo medido desde el norte, en sentido horario, en [0, 2π).
575        float delta = (PI * 0.5) - ang;
576        delta = mod(delta + 8.0 * PI, 2.0 * PI);
577        // Índice del signo más cercano.
578        float k_round = mod(floor(delta / seg + 0.5), 12.0);
579        int k = int(k_round);
580        // Distancia angular al centro del segmento de ese signo.
581        float center_delta = k_round * seg;
582        float ang_diff = delta - center_delta;
583        // Wrap a (-π, π].
584        if (ang_diff >  PI) ang_diff -= 2.0 * PI;
585        if (ang_diff < -PI) ang_diff += 2.0 * PI;
586        float ang_dist = abs(ang_diff);
587
588        // Línea fina, gaussiana.
589        float lineW = 0.0042;
590        float line = exp(-(ang_dist * ang_dist) / (2.0 * lineW * lineW));
591
592        // Banda radial: arranca un poco fuera de la punta de la chacana y
593        // termina antes del aro principal.
594        float r_inner = u_arm_extent * 1.05;
595        float r_outer = ringR_main * 0.96;
596        float band = smoothstep(r_inner, r_inner + 0.035, r)
597                   * (1.0 - smoothstep(r_outer - 0.035, r_outer, r));
598
599        zodiac = u_zodiac[k] * line * band;
600    }
601
602    // === COMPOSICIÓN ===
603    vec3 col = vec3(0.0);
604    // Cuerpo central (sol / luna / tierra) clipeado al interior de la chacana.
605    col += central * inside * 1.55;
606    col += u_line_color * radial * inside * 0.6;
607    // Niebla oscura translúcida en el interior para profundidad.
608    col += u_dark_color * inside * 0.20;
609    // Auras anchas de los elementos (debajo de los aros, sin clip al interior).
610    col += clouds;
611    // Líneas y aros.
612    col += u_line_color * line * 1.70;
613    col += u_line_color * glow * 0.95;
614    col += u_line_color * ring_main * 1.45;
615    col += u_rim_color  * ring_inner * 1.05;
616    col += u_line_color * dots * 1.85;
617    col += particles * 1.25;
618    col += zodiac * 0.55; // muy sutil — apenas visible.
619
620    float zodiac_lum = zodiac.r + zodiac.g + zodiac.b;
621    float cloud_lum = clouds.r + clouds.g + clouds.b;
622    float central_lum = central.r + central.g + central.b;
623    float alpha = clamp(
624        central_lum * inside * 0.7 + line + glow + ring_main + ring_inner
625            + dots + inside * 0.12
626            + (particles.r + particles.g + particles.b) * 0.5
627            + cloud_lum * 0.45
628            + zodiac_lum * 0.3,
629        0.0, 1.0);
630    fragColor = vec4(col, alpha);
631}
632";
633
634/// Capa overlay de nubes apenas visible, dibujada DESPUÉS de la chacana
635/// con `blend = SRC_ALPHA, ONE_MINUS_SRC_ALPHA` (compositing normal).
636/// Dos capas FBM en parallax distinto del fondo, alpha máximo ~0.10.
637/// Da sensación de niebla / cirros pasando por delante de la escena.
638///
639/// Uniforms: `u_resolution`, `u_time`, `u_parallax`.
640pub const FS_OVERLAY_CLOUDS: &str = "#version 300 es
641precision highp float;
642in vec2 v_clip;
643in vec2 v_uv;
644out vec4 fragColor;
645uniform vec2  u_resolution;
646uniform float u_time;
647uniform vec2  u_parallax;
648
649float hash21o(vec2 p) {
650    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
651}
652float vnoise_o(vec2 p) {
653    vec2 i = floor(p);
654    vec2 f = fract(p);
655    f = f * f * (3.0 - 2.0 * f);
656    float a = hash21o(i);
657    float b = hash21o(i + vec2(1.0, 0.0));
658    float c = hash21o(i + vec2(0.0, 1.0));
659    float d = hash21o(i + vec2(1.0, 1.0));
660    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
661}
662float fbm_o(vec2 p) {
663    float v = 0.0;
664    float a = 0.55;
665    for (int i = 0; i < 4; i++) {
666        v += a * vnoise_o(p);
667        p = p * 2.10 + vec2(3.1, 9.4);
668        a *= 0.55;
669    }
670    return v;
671}
672
673void main() {
674    float aspect = u_resolution.x / max(u_resolution.y, 1.0);
675    vec2 uv = v_clip;
676    uv.x *= aspect;
677
678    // Parallax inverso (las nubes 'delante' se mueven al revés que las del
679    // fondo) → percepción de capa más cercana.
680    vec2 drift1 = vec2( u_time * 0.020,  u_time * 0.007) - u_parallax * 0.05;
681    vec2 drift2 = vec2(-u_time * 0.028,  u_time * 0.013) - u_parallax * 0.09;
682
683    // Escalas grandes (~0.55 y 1.30) = cúmulos amplios, no granulado.
684    float n1 = fbm_o(uv * 0.55 + drift1);
685    float n2 = fbm_o(uv * 1.30 + drift2);
686
687    // Densidad: sólo las crestas del noise se vuelven nube. Mucha del
688    // viewport queda transparente.
689    float dens = smoothstep(0.55, 0.88, n1) * 0.65
690               + smoothstep(0.50, 0.82, n2) * 0.35;
691
692    // Color levemente azul-blanco; con baja densidad tira a gris.
693    vec3 cloud_color = mix(vec3(0.55, 0.62, 0.74), vec3(0.90, 0.93, 1.00), dens);
694
695    // Alpha bajísimo: 0.10 máximo (apenas visible, como pidieron).
696    float alpha = dens * 0.10;
697    fragColor = vec4(cloud_color, alpha);
698}
699";
700
701/// Geometría del quad fullscreen: dos triángulos en clip-space.
702pub const FULLSCREEN_QUAD: [f32; 12] = [
703    -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
704];
705
706/// Quad ligeramente mayor que la chacana + aros + glow.
707pub fn chacana_quad(arm_extent: f32) -> [f32; 12] {
708    // Aro principal vive a 1.45 * arm_extent; sumamos margen para el glow.
709    let e = arm_extent * 1.70;
710    [-e, -e, e, -e, e, e, -e, -e, e, e, -e, e]
711}