Lesson 8 of 10 · The Solar System in GLSL

Uranus

It rolls on its side. The methane keeps the red.

Until now the colour was the surface's. On Uranus, the colour is what the air took from the light along the way. The trick: exp(-k*path), the Beer–Lambert law. Each channel loses light by its own k and by how much air it crossed.

Methane absorbs red: k is 0.55 in red, 0.12 in green and 0.09 in blue. path adds the way in and the way out, each 1/cos of its angle. At the edge the light crosses more air, so the edge comes out more cyan and darker.

Uranus's axis sits at 97.8°: axisTilt at 1.71. Each pole spends 42 years in daylight and 42 in night. Its rings are narrow and dark; the ε ring is at 2 radii. Below: Methane, Axis and Haze.

A marble the colour of pool water. Rolling on its side.

Beer–Lambertexp(-k*path) = what is left of the light. A large red k = methane. A long path at the edge = more cyan.
TrySet methane to 0.0: a snowball. Raise it to 3.0: deep-end blue. Take axisTilt to 0.0: any old planet.

The code

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);
}
// 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 methane = 1.0;    // how much methane the light crosses
float axisTilt = 1.71;  // 97.8° = 1.71 radians: it rolls on its side
float haze = 0.25;
void main(){
  vec2 p = (gl_FragCoord.xy*2.0 - R)/min(R.x, R.y);
  vec3 ro = vec3(0.0, 0.0, 5.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 = normalize(vec3(sin(axisTilt), cos(axisTilt), 0.55));   // tipped a little toward you, to see the rings
  vec3 col = sky(gl_FragCoord.xy);
  float tp = sphere(ro, rd, 1.0);
  if (tp > 0.0){
    vec3 n = normalize(ro + rd*tp);
    float toSun = max(dot(n, sun), 0.0), toEye = max(dot(n, -rd), 0.0);
    // The trick: light goes in and out at a slant; at the edge it crosses more air
    float path = 0.5/max(toSun, 0.05) + 0.5/max(toEye, 0.05);
    vec3 color = exp(-methane*vec3(0.55, 0.12, 0.09)*path);     // Beer–Lambert: the red stays behind on the way
    float lat = asin(dot(n, axis));
    float bands = 0.95 + 0.05*sin(lat*12.0) + haze*smoothstep(0.9, 1.4, lat);   // the cap facing the sun
    col = vec3(0.95)*color*bands*toSun*2.8;
  }
  float tr = -dot(ro, axis)/dot(rd, axis);
  if (tr > 0.0 && (tp < 0.0 || tr < tp)){
    float r = length(ro + rd*tr);
    float ring = smoothstep(0.012, 0.0, abs(r - 2.0)) + 0.5*smoothstep(0.006, 0.0, abs(r - 1.84)) + 0.4*smoothstep(0.005, 0.0, abs(r - 1.66));   // ε and two more: narrow and dark
    col = mix(col, vec3(0.18, 0.2, 0.22), clamp(ring, 0.0, 1.0)*0.8);
  }
  gl_FragColor = vec4(wogl(col), 1.0);
}