Lección 9 de 10 · El Sistema Solar en GLSL

Neptuno

El viento más rápido del sistema solar. Y una orilla de aire azul.

En Urano el aire quitaba color. En Neptuno también lo agrega: el aire esparce la luz azul hacia ti. El truco: una segunda esfera, de radio 1.0 + air, para el aire. El rayo entra en t0 y sale en t1, o en el suelo si lo toca. t1 - t0 es cuánto aire cruzó.

De frente cruzas poco aire; rozando la orilla, mucho. Por eso el borde brilla. El azul sale de Rayleigh: el aire esparce cada color como 1/λ⁴, y el azul se esparce unas 5 veces más que el rojo. lit apaga el aire del lado de noche.

Neptuno tiene los vientos más rápidos medidos: 2 100 km/h. La Gran Mancha Oscura la fotografió el Voyager 2 en 1989. Las nubes blancas son metano congelado: flotan arriba del metano y no se tiñen. En 2024 se recalibraron esas fotos: su color real es apenas más azul que el de Urano. Abajo: Aire, Viento y Azul.

La flama azul de la estufa, alrededor de una canica.

dos esferasEsfera chica = el suelo. Esfera grande = el aire. t1 - t0 = el camino por el aire. Rayleigh = el azul se esparce como 1/λ⁴.
PruebaSube air de 0.05 a 0.15: halo de foco. Pon blue en 0.0: Neptuno sin cielo. wind a 1.0: 2 100 km/h.

El código

precision highp float;
// The exact sphere: how far along the ray it is hit, or -1 if the ray misses
float sphere(vec3 ro, vec3 rd, float r){
  float b = dot(ro, rd), c = dot(ro, ro) - r*r, h = b*b - c;
  return h < 0.0 ? -1.0 : -b - sqrt(h);
}
// WoGL close: knee at 0.70, sRGB, chroma 1.3
vec3 wogl(vec3 c){
  float l = dot(c, vec3(0.2126, 0.7152, 0.0722)), k = max(l - 0.7, 0.0);
  c *= l > 0.0 ? (l - k + k/(1.0 + k))/l : 0.0;
  c = pow(clamp(c, 0.0, 1.0), vec3(0.4545));
  l = dot(c, vec3(0.2126, 0.7152, 0.0722));
  return clamp(mix(vec3(l), c, 1.3), 0.0, 1.0);
}
// Noise in 3D: the usual fb with one more dimension. On a globe it leaves no seam.
float h3(vec3 p){ return h2(p.xy + p.z*vec2(37.0, 17.0)); }
float n3(vec3 p){
  vec3 i = floor(p), f = fract(p);
  f = f*f*(3.0 - 2.0*f);
  return mix(mix(mix(h3(i), h3(i + vec3(1,0,0)), f.x), mix(h3(i + vec3(0,1,0)), h3(i + vec3(1,1,0)), f.x), f.y),
             mix(mix(h3(i + vec3(0,0,1)), h3(i + vec3(1,0,1)), f.x), mix(h3(i + vec3(0,1,1)), h3(i + vec3(1,1,1)), f.x), f.y), f.z);
}
float f3(vec3 p){
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 5; i++){ v += a*n3(p); p = p*2.03 + vec3(1.7, 9.2, 4.1); a *= 0.5; }
  return v;
}
// A 2D turn (to spin the globe about its axis)
mat2 turn(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
// The background: black, with a star now and then
vec3 sky(vec2 f){
  float s = h2(floor(f));
  return vec3(0.0002, 0.0003, 0.0006) + vec3(0.7, 0.7, 0.8)*step(0.9982, s)*h2(floor(f) + 3.1);
}
float air = 0.05;       // the air's thickness, in radii
float wind = 0.35;      // 2,100 km/h in the fastest wind
float blue = 1.0;       // how much blue the air scatters
void main(){
  vec2 p = (gl_FragCoord.xy*2.0 - R)/min(R.x, R.y);
  vec3 ro = vec3(0.0, 0.0, 3.2), rd = normalize(vec3(p, -2.4));   // the camera looks at the center
  vec3 sun = normalize(vec3(M.x*4.0 - 3.8, M.y*3.0 - 1.0, 1.0));   // you move the sun: it is the pointer
  vec3 col = sky(gl_FragCoord.xy);
  float tp = sphere(ro, rd, 1.0);
  if (tp > 0.0){
    vec3 n = normalize(ro + rd*tp);
    float lat = asin(n.y);
    vec3 q = n; q.xz = turn(T*(0.05 - wind*0.3*cos(2.0*lat)))*q.xz;   // the equator runs against the poles
    float toSun = max(dot(n, sun), 0.0), toEye = max(dot(n, -rd), 0.0);
    vec3 color = exp(-vec3(1.0, 0.28, 0.10)*(0.5/max(toSun, 0.05) + 0.5/max(toEye, 0.05)));
    vec3 c0 = vec3(cos(-0.35)*sin(0.15), sin(-0.35), cos(-0.35)*cos(0.15));
    vec3 d = q - c0;
    float spot = smoothstep(1.0, 0.5, length(vec2(dot(d, normalize(cross(vec3(0, 1, 0), c0)))/0.2, (q.y - c0.y)/0.1)));   // the Great Dark Spot
    float white = smoothstep(0.56, 0.74, f3(q*vec3(3.0, 14.0, 3.0)))*smoothstep(0.55, 0.15, abs(lat + 0.15));       // frozen-methane clouds: they float above the methane, so they are not tinted
    col = color*(1.0 - 0.7*spot)*toSun*1.8;
    col = mix(col, vec3(0.85)*toSun*2.2, white*0.85);
  }
  // The trick: a second sphere, slightly larger, for the air. In at t0, out at t1.
  float R1 = 1.0 + air, b = dot(ro, rd), h = b*b - dot(ro, ro) + R1*R1;
  if (h > 0.0){
    float t0 = -b - sqrt(h), t1 = -b + sqrt(h);
    if (tp > 0.0) t1 = tp;                                   // if it hits the planet, the air ends at the ground
    float path = max(t1 - t0, 0.0);
    vec3 m = ro + rd*(t0 + path*0.5);
    float lit = smoothstep(-0.3, 0.5, dot(normalize(m), sun));
    col += vec3(0.16, 0.40, 1.0)*blue*path*lit*1.6;          // Rayleigh: blue scatters as 1/λ⁴
  }
  gl_FragColor = vec4(wogl(col), 1.0);
}