Lesson 9 of 10 · The Solar System in GLSL

Neptune

The fastest wind in the solar system. And an edge of blue air.

On Uranus the air took colour away. On Neptune it also adds some: the air scatters blue light toward you. The trick: a second sphere, of radius 1.0 + air, for the air. The ray enters at t0 and leaves at t1, or at the ground if it hits. t1 - t0 is how much air it crossed.

Head-on you cross little air; grazing the edge, a lot. That is why the rim glows. The blue comes from Rayleigh: air scatters each colour as 1/λ⁴, so blue scatters about 5 times more than red. lit turns the air off on the night side.

Neptune has the fastest winds ever measured: 2,100 km/h. Voyager 2 photographed the Great Dark Spot in 1989. The white clouds are frozen methane: they float above the methane and are not tinted. In 2024 those photos were recalibrated: its true colour is only slightly bluer than Uranus. Below: Air, Wind and Blue.

A gas-stove flame, around a marble.

two spheresSmall sphere = the ground. Large sphere = the air. t1 - t0 = the path through the air. Rayleigh = blue scatters as 1/λ⁴.
TryRaise air from 0.05 to 0.15: a lamp halo. Set blue to 0.0: Neptune with no sky. wind to 1.0: 2,100 km/h.

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);
}
// 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);
}