feat(gioser): luna con textura rica + terminador curvo, auras anchas, overlay nubes
Luna (FS_CHACANA::render_moon):
- Normales esféricas reales: nx=p.x/R, ny=p.y/R, nz=sqrt(1-nx²-ny²).
- Terminador CURVO: dot(normal, sun_dir) donde sun_dir gira en el plano
X-Z según la fase. Resultado: la frontera luz/sombra es una elipse
proyectada en pantalla, como en la luna real (no una vertical recta).
- Fase lineal: phi = fract(t/40) * 2π cicla new→first-q→full→last-q→new
en ~40s.
- Limb darkening realista: pow(nz, 0.45) — bordes más oscuros que el
centro (el regolito lunar dispersa).
- 6 capas de textura:
maria_n (escala 4.5) → mares oscuros (smoothstep 0.42..0.60)
craters_mid (escala 13) → cráteres grandes
craters_small (escala 28) → cráteres chicos
fine (escala 55) → granularidad del terreno
micro (escala 110) → polvo
ring_mid + ring_small → crests via pow(abs(n-0.5)*2, k) → bordes
elevados de cráteres
Albedo final: 0.80 + craters±0.32 + small±0.22 + fine±0.20 + micro±0.10
+ rings (+0.22, +0.16) - maria 0.50, clamp [0.10, 1.15].
Auras elementales (FS_CHACANA::element_cloud):
- sigma_along 0.42 → 0.62 (más reach hacia afuera)
- sigma_perp 0.34 → 0.62 (mucho más ancho perpendicular)
- cloud_center offset 0.22 → 0.28 (más lejos del centro)
- multiplier 0.28 → 0.26 (compensa intensidad por la mayor cobertura)
- Resultado: las nubes elementales se solapan en las esquinas NE/NW/SE/SW
y mezclan colores. El cuadrante entero respira el color del cardinal.
Overlay clouds (FS_OVERLAY_CLOUDS — nuevo shader):
- Tercer pase tras chacana, fullscreen quad.
- blend = SRC_ALPHA / ONE_MINUS_SRC_ALPHA (compositing normal, no aditivo)
→ las nubes COMPONEN sobre la escena en lugar de sumar luz.
- Dos capas FBM (escalas 0.55 y 1.30) con parallax inverso del mouse
(-0.05 y -0.09) — se sienten "delante" del cosmos.
- Drift más lento que las nubes del cosmos (0.020 vs 0.055), para que se
perciban como otra capa atmosférica.
- smoothstep(0.55..0.88, 0.50..0.82) → sólo crestas se vuelven nube;
mucho del viewport queda transparente.
- Alpha máximo 0.10 — "apenas visible" como pidió el diseño.
- Color mix gris→blanco-azul según densidad local.
Renderer (gioser-canvas-web):
- Nuevo Program overlay_prog con uniforms u_resolution/u_time/u_parallax.
- render() ahora hace 3 pases: cosmos → chacana → overlay clouds.
Workspace verde.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@ use gioser_geom::ChacanaSpec;
|
|||||||
use gioser_palette::{cosmos, Rgb};
|
use gioser_palette::{cosmos, Rgb};
|
||||||
use gioser_physics::{SpringDamper1, SpringDamper2};
|
use gioser_physics::{SpringDamper1, SpringDamper2};
|
||||||
use gioser_shaders::{
|
use gioser_shaders::{
|
||||||
chacana_quad, FS_CHACANA, FS_COSMOS, FULLSCREEN_QUAD, VS_CHACANA, VS_FULLSCREEN,
|
chacana_quad, FS_CHACANA, FS_COSMOS, FS_OVERLAY_CLOUDS, FULLSCREEN_QUAD, VS_CHACANA,
|
||||||
|
VS_FULLSCREEN,
|
||||||
};
|
};
|
||||||
use glam::{Mat4, Vec3, Vec4};
|
use glam::{Mat4, Vec3, Vec4};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -80,6 +81,7 @@ pub struct Renderer {
|
|||||||
gl: GL,
|
gl: GL,
|
||||||
cosmos_prog: Program,
|
cosmos_prog: Program,
|
||||||
chacana_prog: Program,
|
chacana_prog: Program,
|
||||||
|
overlay_prog: Program,
|
||||||
cosmos_vao: WebGlVertexArrayObject,
|
cosmos_vao: WebGlVertexArrayObject,
|
||||||
chacana_vao: WebGlVertexArrayObject,
|
chacana_vao: WebGlVertexArrayObject,
|
||||||
chacana_quad_count: i32,
|
chacana_quad_count: i32,
|
||||||
@@ -244,6 +246,14 @@ impl Renderer {
|
|||||||
)
|
)
|
||||||
.map_err(JsValue::from)?;
|
.map_err(JsValue::from)?;
|
||||||
|
|
||||||
|
let overlay_prog = Program::new(
|
||||||
|
&gl,
|
||||||
|
VS_FULLSCREEN,
|
||||||
|
FS_OVERLAY_CLOUDS,
|
||||||
|
&["u_resolution", "u_time", "u_parallax"],
|
||||||
|
)
|
||||||
|
.map_err(JsValue::from)?;
|
||||||
|
|
||||||
let (cosmos_vao, _) = upload_quad(&gl, &FULLSCREEN_QUAD, 0).map_err(JsValue::from)?;
|
let (cosmos_vao, _) = upload_quad(&gl, &FULLSCREEN_QUAD, 0).map_err(JsValue::from)?;
|
||||||
let chacana_quad_verts = chacana_quad(chacana.arm_extent());
|
let chacana_quad_verts = chacana_quad(chacana.arm_extent());
|
||||||
let (chacana_vao, chacana_quad_count) =
|
let (chacana_vao, chacana_quad_count) =
|
||||||
@@ -258,6 +268,7 @@ impl Renderer {
|
|||||||
gl,
|
gl,
|
||||||
cosmos_prog,
|
cosmos_prog,
|
||||||
chacana_prog,
|
chacana_prog,
|
||||||
|
overlay_prog,
|
||||||
cosmos_vao,
|
cosmos_vao,
|
||||||
chacana_vao,
|
chacana_vao,
|
||||||
chacana_quad_count,
|
chacana_quad_count,
|
||||||
@@ -500,6 +511,23 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
gl.bind_vertex_array(Some(&self.chacana_vao));
|
gl.bind_vertex_array(Some(&self.chacana_vao));
|
||||||
gl.draw_arrays(GL::TRIANGLES, 0, self.chacana_quad_count);
|
gl.draw_arrays(GL::TRIANGLES, 0, self.chacana_quad_count);
|
||||||
|
|
||||||
|
// ---- Capa overlay de nubes (apenas visible, encima de todo) ----
|
||||||
|
// blend normal alpha — las nubes COMPONEN sobre la escena, no suman luz.
|
||||||
|
gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA);
|
||||||
|
gl.use_program(Some(&self.overlay_prog.program));
|
||||||
|
if let Some(u) = self.overlay_prog.u("u_resolution") {
|
||||||
|
gl.uniform2f(Some(u), self.viewport.0 as f32, self.viewport.1 as f32);
|
||||||
|
}
|
||||||
|
if let Some(u) = self.overlay_prog.u("u_time") {
|
||||||
|
gl.uniform1f(Some(u), t);
|
||||||
|
}
|
||||||
|
if let Some(u) = self.overlay_prog.u("u_parallax") {
|
||||||
|
gl.uniform2f(Some(u), self.mouse.0, self.mouse.1);
|
||||||
|
}
|
||||||
|
gl.bind_vertex_array(Some(&self.cosmos_vao));
|
||||||
|
gl.draw_arrays(GL::TRIANGLES, 0, 6);
|
||||||
|
|
||||||
gl.bind_vertex_array(None);
|
gl.bind_vertex_array(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,24 +252,65 @@ vec3 render_sun(vec2 p, float r, float pulse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vec3 render_moon(vec2 p, float r, float time) {
|
vec3 render_moon(vec2 p, float r, float time) {
|
||||||
float moonR = u_thickness * 1.35;
|
float moonR = u_thickness * 1.40;
|
||||||
// Disco con borde suave.
|
if (r > moonR * 1.20) return vec3(0.0);
|
||||||
float disk = 1.0 - smoothstep(moonR * 0.86, moonR * 1.00, r);
|
|
||||||
// Limb darkening: el borde más oscuro que el centro.
|
// Normales de la esfera proyectadas en pantalla (sólo la cara visible).
|
||||||
float limb_factor = sqrt(max(1.0 - (r * r) / (moonR * moonR), 0.0));
|
float nx = p.x / moonR;
|
||||||
// Cráteres y mares lunares vía fbm.
|
float ny = p.y / moonR;
|
||||||
float craters = fbm_c(p * 22.0) * 0.45 + fbm_c(p * 48.0) * 0.20;
|
float n2 = nx * nx + ny * ny;
|
||||||
// Fase: terminator se mueve de izquierda a derecha lentamente.
|
float nz = sqrt(max(1.0 - n2, 0.0));
|
||||||
// 1 ciclo lunar visible cada ~38 s. sin(t*0.165) recorre full→new→full.
|
float disk = 1.0 - smoothstep(moonR * 0.88, moonR * 1.00, r);
|
||||||
float phase = sin(time * 0.165) * 0.5 + 0.5; // 0..1
|
|
||||||
float terminator_x = mix(-moonR * 1.1, moonR * 1.1, phase);
|
// Limb darkening realista (regolito lunar dispersa más al borde).
|
||||||
float nx = p.x;
|
float limb_factor = pow(max(nz, 0.0), 0.45);
|
||||||
float lit = smoothstep(terminator_x - moonR * 0.12, terminator_x + moonR * 0.05, nx);
|
|
||||||
// Color superficie lunar (gris-azulado).
|
// === SUPERFICIE: 4 capas + crater rings ===
|
||||||
vec3 surface = vec3(0.86, 0.88, 0.94) * (0.70 + craters * 0.55);
|
// Maria — mares oscuros grandes (Mare Imbrium, Tranquillitatis, etc.)
|
||||||
// Halo cercano al limb iluminado (luz dispersada).
|
float maria_n = fbm_c(p * 4.5 + vec2(2.3, 1.1));
|
||||||
float outer_glow = smoothstep(moonR * 1.18, moonR * 0.94, r) - disk;
|
float maria = smoothstep(0.42, 0.60, maria_n);
|
||||||
vec3 glow = vec3(0.55, 0.70, 0.95) * max(outer_glow, 0.0) * lit * 0.50;
|
// Cráteres grandes (radio mid).
|
||||||
|
float craters_mid = fbm_c(p * 13.0 + vec2(8.5, 3.2));
|
||||||
|
// Cráteres chicos.
|
||||||
|
float craters_small = fbm_c(p * 28.0 + vec2(17.1, 5.8));
|
||||||
|
// Detalle fino y micro (granularidad de polvo).
|
||||||
|
float fine = fbm_c(p * 55.0 + vec2(7.3, 11.4));
|
||||||
|
float micro = fbm_c(p * 110.0);
|
||||||
|
// Rims (bordes elevados de cráteres) — picos donde el fbm cruza 0.5.
|
||||||
|
float ring_mid = pow(abs(craters_mid - 0.5) * 2.0, 3.5);
|
||||||
|
float ring_small = pow(abs(craters_small - 0.5) * 2.0, 5.0);
|
||||||
|
|
||||||
|
float albedo = 0.80;
|
||||||
|
albedo += (craters_mid - 0.5) * 0.32;
|
||||||
|
albedo += (craters_small - 0.5) * 0.22;
|
||||||
|
albedo += (fine - 0.5) * 0.20;
|
||||||
|
albedo += (micro - 0.5) * 0.10;
|
||||||
|
albedo += ring_mid * 0.22;
|
||||||
|
albedo += ring_small * 0.16;
|
||||||
|
albedo -= maria * 0.50;
|
||||||
|
albedo = clamp(albedo, 0.10, 1.15);
|
||||||
|
|
||||||
|
// === FASE LUNAR CURVA ===
|
||||||
|
// Ciclo lineal — un mes lunar comprimido en ~40 s. Avanza 0→1
|
||||||
|
// pasando por new(0) → first-q(0.25) → full(0.5) → last-q(0.75) → new(1).
|
||||||
|
float phase = fract(time / 40.0);
|
||||||
|
float phi = phase * 2.0 * PI;
|
||||||
|
// Dirección del sol relativa al observador.
|
||||||
|
// phi=0 → sun_dir = (0, 0, -1) ⇒ atrás de la luna (new moon).
|
||||||
|
// phi=π/2 → sun_dir = (+1, 0, 0) ⇒ a la derecha (first quarter).
|
||||||
|
// phi=π → sun_dir = (0, 0, +1) ⇒ frente al observador (full moon).
|
||||||
|
// Como `dot(normal, sun_dir) = nx*sin(phi) - nz*cos(phi)`, el terminador
|
||||||
|
// resulta ser una elipse en la pantalla — curva como en la luna real.
|
||||||
|
float lit_value = nx * sin(phi) - nz * cos(phi);
|
||||||
|
float lit = smoothstep(-0.035, 0.035, lit_value);
|
||||||
|
|
||||||
|
// Color superficie (gris ligeramente azulado).
|
||||||
|
vec3 surface = vec3(0.86, 0.88, 0.94) * albedo;
|
||||||
|
|
||||||
|
// Halo cercano al limb iluminado — luz dispersada en el regolito.
|
||||||
|
float outer_glow = smoothstep(moonR * 1.15, moonR * 0.95, r) - disk;
|
||||||
|
vec3 glow = vec3(0.55, 0.70, 0.95) * max(outer_glow, 0.0) * lit * 0.55;
|
||||||
|
|
||||||
return surface * disk * lit * limb_factor + glow;
|
return surface * disk * lit * limb_factor + glow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,13 +358,14 @@ vec3 render_body(int kind, vec2 p, float r, float time, float pulse) {
|
|||||||
vec3 element_cloud(vec2 p, vec2 tip, vec2 outward, vec3 color, float time, int kind) {
|
vec3 element_cloud(vec2 p, vec2 tip, vec2 outward, vec3 color, float time, int kind) {
|
||||||
vec2 perp = vec2(-outward.y, outward.x);
|
vec2 perp = vec2(-outward.y, outward.x);
|
||||||
// Centro de la nube: bien adentro del cuadrante (más allá del tip).
|
// Centro de la nube: bien adentro del cuadrante (más allá del tip).
|
||||||
vec2 cloud_center = tip + outward * 0.22;
|
vec2 cloud_center = tip + outward * 0.28;
|
||||||
vec2 to_p = p - cloud_center;
|
vec2 to_p = p - cloud_center;
|
||||||
float along = dot(to_p, outward);
|
float along = dot(to_p, outward);
|
||||||
float perp_d = dot(to_p, perp);
|
float perp_d = dot(to_p, perp);
|
||||||
// Anisotropía: bien ancha perpendicular, larga a lo largo del eje.
|
// Anisotropía: el aura cubre TODO el cuadrante del cardinal. Sigmas
|
||||||
float sigma_along = 0.42;
|
// grandes → se solapan en las esquinas (NE/NW/SE/SW) y crean mezclas.
|
||||||
float sigma_perp = 0.34;
|
float sigma_along = 0.62;
|
||||||
|
float sigma_perp = 0.62;
|
||||||
float base = exp(-(along * along) / (2.0 * sigma_along * sigma_along)
|
float base = exp(-(along * along) / (2.0 * sigma_along * sigma_along)
|
||||||
-(perp_d * perp_d) / (2.0 * sigma_perp * sigma_perp));
|
-(perp_d * perp_d) / (2.0 * sigma_perp * sigma_perp));
|
||||||
// Textura noise animada por elemento.
|
// Textura noise animada por elemento.
|
||||||
@@ -344,7 +386,7 @@ vec3 element_cloud(vec2 p, vec2 tip, vec2 outward, vec3 color, float time, int k
|
|||||||
// AGUA: ondulaciones grandes que viajan hacia afuera.
|
// AGUA: ondulaciones grandes que viajan hacia afuera.
|
||||||
modulation = 0.50 + 0.50 * sin(time * 0.9 - along * 5.0 + n * 4.0);
|
modulation = 0.50 + 0.50 * sin(time * 0.9 - along * 5.0 + n * 4.0);
|
||||||
}
|
}
|
||||||
return color * base * max(modulation, 0.0) * 0.28;
|
return color * base * max(modulation, 0.0) * 0.26;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chacana de 2 escalones (mística clásica de Tiwanaku).
|
// Chacana de 2 escalones (mística clásica de Tiwanaku).
|
||||||
@@ -589,6 +631,73 @@ void main() {
|
|||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
|
/// Capa overlay de nubes apenas visible, dibujada DESPUÉS de la chacana
|
||||||
|
/// con `blend = SRC_ALPHA, ONE_MINUS_SRC_ALPHA` (compositing normal).
|
||||||
|
/// Dos capas FBM en parallax distinto del fondo, alpha máximo ~0.10.
|
||||||
|
/// Da sensación de niebla / cirros pasando por delante de la escena.
|
||||||
|
///
|
||||||
|
/// Uniforms: `u_resolution`, `u_time`, `u_parallax`.
|
||||||
|
pub const FS_OVERLAY_CLOUDS: &str = "#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
in vec2 v_clip;
|
||||||
|
in vec2 v_uv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform float u_time;
|
||||||
|
uniform vec2 u_parallax;
|
||||||
|
|
||||||
|
float hash21o(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||||
|
}
|
||||||
|
float vnoise_o(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
f = f * f * (3.0 - 2.0 * f);
|
||||||
|
float a = hash21o(i);
|
||||||
|
float b = hash21o(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash21o(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash21o(i + vec2(1.0, 1.0));
|
||||||
|
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||||
|
}
|
||||||
|
float fbm_o(vec2 p) {
|
||||||
|
float v = 0.0;
|
||||||
|
float a = 0.55;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
v += a * vnoise_o(p);
|
||||||
|
p = p * 2.10 + vec2(3.1, 9.4);
|
||||||
|
a *= 0.55;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float aspect = u_resolution.x / max(u_resolution.y, 1.0);
|
||||||
|
vec2 uv = v_clip;
|
||||||
|
uv.x *= aspect;
|
||||||
|
|
||||||
|
// Parallax inverso (las nubes 'delante' se mueven al revés que las del
|
||||||
|
// fondo) → percepción de capa más cercana.
|
||||||
|
vec2 drift1 = vec2( u_time * 0.020, u_time * 0.007) - u_parallax * 0.05;
|
||||||
|
vec2 drift2 = vec2(-u_time * 0.028, u_time * 0.013) - u_parallax * 0.09;
|
||||||
|
|
||||||
|
// Escalas grandes (~0.55 y 1.30) = cúmulos amplios, no granulado.
|
||||||
|
float n1 = fbm_o(uv * 0.55 + drift1);
|
||||||
|
float n2 = fbm_o(uv * 1.30 + drift2);
|
||||||
|
|
||||||
|
// Densidad: sólo las crestas del noise se vuelven nube. Mucha del
|
||||||
|
// viewport queda transparente.
|
||||||
|
float dens = smoothstep(0.55, 0.88, n1) * 0.65
|
||||||
|
+ smoothstep(0.50, 0.82, n2) * 0.35;
|
||||||
|
|
||||||
|
// Color levemente azul-blanco; con baja densidad tira a gris.
|
||||||
|
vec3 cloud_color = mix(vec3(0.55, 0.62, 0.74), vec3(0.90, 0.93, 1.00), dens);
|
||||||
|
|
||||||
|
// Alpha bajísimo: 0.10 máximo (apenas visible, como pidieron).
|
||||||
|
float alpha = dens * 0.10;
|
||||||
|
fragColor = vec4(cloud_color, alpha);
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
/// Geometría del quad fullscreen: dos triángulos en clip-space.
|
/// Geometría del quad fullscreen: dos triángulos en clip-space.
|
||||||
pub const FULLSCREEN_QUAD: [f32; 12] = [
|
pub const FULLSCREEN_QUAD: [f32; 12] = [
|
||||||
-1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
|
-1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user