Lesson 7 of 10 · The Solar System in GLSL
Saturn
Ice rings ten metres thick. And the planet's shadow across them.
Until now each ray hit one thing. The rings are another surface: a plane through the center, with normal axis. The ray crosses it at t = -dot(ro, axis)/dot(rd, axis). If the ring comes before the planet, it shows in front; if after, the planet hides it.
The trick: for the shadow, start at the point and cast another ray toward the sun. If from the ring that ray hits the sphere, the ring is in the planet's shadow. If from the planet it crosses the rings, the planet is in the rings' shadow. It is the shadow from Ray Marched, with two pieces.
rings(r) gives how full they are at r Saturn radii: the faint C; the dense B; the 4,800 km Cassini division; the A. Edge to edge they span about 270,000 km and are mostly about 10 m thick. The axis is tilted 26.7°. Below: Tilt, Cassini and Shadow.
A vinyl record, with a planet on the spindle.
-dot(ro, axis)/dot(rd, axis) = where it crosses the plane. A ray to the sun that hits the sphere = the planet's shadow. A ray to the sun that crosses the rings = the rings' shadow.tilt from 0.47 to 0.02: rings edge-on, nearly gone. Raise it to 1.1: seen from above. Set cassini to 3.0: two discs.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;
}
// 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);
}