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

Saturno

Anillos de hielo de diez metros de grueso. Y la sombra del planeta encima.

Hasta aquí cada rayo tocaba una sola cosa. Los anillos son otra superficie: un plano que pasa por el centro, de normal axis. El rayo lo cruza en t = -dot(ro, axis)/dot(rd, axis). Si el anillo está antes que el planeta, se ve encima; si está después, el planeta lo tapa.

El truco: para la sombra, sal desde el punto y lanza otro rayo hacia el sol. Si desde el anillo ese rayo toca la esfera, el anillo está en la sombra del planeta. Si desde el planeta cruza los anillos, el planeta está en la sombra de los anillos. Es la sombra de Ray Marched con dos piezas.

rings(r) da qué tan llenos están a r radios de Saturno: el C, tenue; el B, denso; la división de Cassini, de 4 800 km; el A. De orilla a orilla miden unos 270 000 km y casi siempre unos 10 m de grueso. El eje está inclinado 26.7°. Abajo: Inclinación, Cassini y Sombra.

Un disco de vinil, con un planeta en el centro.

el plano y la sombra-dot(ro, axis)/dot(rd, axis) = dónde cruza el plano. Rayo al sol que toca la esfera = sombra del planeta. Rayo al sol que cruza los anillos = sombra de los anillos.
PruebaBaja tilt de 0.47 a 0.02: los anillos de canto, casi desaparecen. Súbelo a 1.1: vista desde arriba. Pon cassini en 3.0: dos discos.

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;
}
// 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 tilt = 0.47;      // 26.7° = 0.47 radians
float cassini = 1.0;    // the width of the Cassini division
float shadow = 1.0;
// How full the rings are at r Saturn radii: C, B, Cassini, A
float rings(float r){
  float gap = 0.04*cassini;
  float C = smoothstep(1.23, 1.25, r)*smoothstep(1.53, 1.51, r)*0.22;
  float B = smoothstep(1.51, 1.55, r)*smoothstep(1.99 - gap, 1.97 - gap, r)*0.95;
  float A = smoothstep(1.99 + gap, 2.01 + gap, r)*smoothstep(2.27, 2.25, r)*0.62;
  return clamp((C + B + A)*(0.78 + 0.22*sin(r*190.0)*sin(r*67.0)), 0.0, 1.0);
}
void main(){
  vec2 p = (gl_FragCoord.xy*2.0 - R)/min(R.x, R.y);
  vec3 ro = vec3(0.0, 0.0, 6.0), rd = normalize(vec3(p, -2.3));   // 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 axis = vec3(0.0, cos(tilt), sin(tilt));      // the planet's axis, tipped toward you
  vec3 col = sky(gl_FragCoord.xy);
  float tp = sphere(ro, rd, 1.0);
  if (tp > 0.0){
    vec3 P = ro + rd*tp, n = normalize(P);
    float lat = asin(dot(n, axis));
    vec3 albedo = mix(vec3(0.80, 0.66, 0.42), vec3(0.62, 0.48, 0.30), 0.5 + 0.5*sin(lat*14.0 + f3(n*vec3(2.0, 8.0, 2.0))*1.2))*0.6;
    float light = max(dot(n, sun), 0.0);
    // The rings' shadow: from the planet, a ray toward the sun. Does it cross the rings?
    float ts = -dot(P, axis)/dot(sun, axis);
    if (ts > 0.0) light *= 1.0 - rings(length(P + sun*ts))*shadow*0.9;
    col = albedo*light*3.0;
  }
  // The rings: the same ray against a plane through the center
  float tr = -dot(ro, axis)/dot(rd, axis);
  if (tr > 0.0 && (tp < 0.0 || tr < tp)){
    vec3 X = ro + rd*tr;
    float density = rings(length(X));
    float lit = 1.0;
    if (sphere(X, sun, 1.0) > 0.0) lit = 1.0 - shadow;          // the planet's shadow on the rings
    vec3 ice = vec3(0.86, 0.76, 0.60)*(0.35 + 0.65*abs(dot(axis, sun)))*lit*2.6;
    col = mix(col, ice*0.6, density);
  }
  gl_FragColor = vec4(wogl(col), 1.0);
}