gas-giant
The cloud tops of a gas giant, as one shader function. Banded, sheared by
winds that alternate direction from belt to belt, with a long-lived storm.
It colours a point on a sphere, so the same code draws a giant hanging in a
moon's sky (<tosi-b3d-moon gas="jupiter">) and one you orbit.
Demo
A giant as a body you can fly round:
<tosi-b3d-planet gas="saturn">. Drag to orbit it. The sun
is where the scene's sun is, so swing round to the night side. The bands'
three colours and the ring are yours to set: pick another planet and note how
thin and faint every ring but Saturn's is.
import { b3d, b3dPlanet, b3dSun, b3dSkybox, GAS_GIANTS, select3d, slider3d, label3d, colorPicker3d } from 'tosijs-3d'
import { tosi } from 'tosijs'
const start = GAS_GIANTS.saturn
const { orbit } = tosi({ orbit: {
kind: 'saturn', seed: 7, spin: 0.03,
bands: start.bands, turbulence: start.turbulence, storm: start.storm,
zones: start.colors[0], belts: start.colors[1], accent: start.colors[2],
rings: 1, ringInner: start.rings.inner, ringOuter: start.rings.outer, ringOpacity: start.rings.opacity, ringColor: start.rings.color,
} })
const planet = b3dPlanet({ gas: orbit.kind, seed: orbit.seed, rotationSpeed: orbit.spin, radius: 50 })
// Everything that changes the planet's look rebuilds it (a sphere: cheap).
const rebuild = () => {
Object.assign(planet, {
bands: orbit.bands.value, turbulence: orbit.turbulence.value, storm: orbit.storm.value,
bandColors: [orbit.zones.value, orbit.belts.value, orbit.accent.value].join(','),
rings: orbit.rings.value, ringInner: orbit.ringInner.value, ringOuter: orbit.ringOuter.value,
ringOpacity: orbit.ringOpacity.value, ringColor: orbit.ringColor.value,
})
planet.regenerate()
}
// Choosing a planet sets every dial to that planet's own numbers.
orbit.kind.observe(() => {
const look = GAS_GIANTS[orbit.kind.value]
Object.assign(orbit, {
bands: look.bands, turbulence: look.turbulence, storm: look.storm,
zones: look.colors[0], belts: look.colors[1], accent: look.colors[2],
ringInner: look.rings.inner, ringOuter: look.rings.outer, ringOpacity: look.rings.opacity, ringColor: look.rings.color,
})
})
for (const key of Object.keys(orbit.valueOf())) if (key !== 'spin') orbit[key].observe(rebuild)
const planetColours = Object.values(GAS_GIANTS).flatMap((look) => look.colors)
preview.append(
b3d(
{
scenePanelOpen: true,
scenePanel: () => [
label3d({ text: 'From space' }),
select3d({ label: 'planet', value: orbit.kind, options: Object.keys(GAS_GIANTS) }),
slider3d({ label: 'bands', value: orbit.bands, min: 1, max: 30, step: 1 }),
slider3d({ label: 'turbulence', value: orbit.turbulence, min: 0, max: 1.5, step: 0.05 }),
slider3d({ label: 'storm', value: orbit.storm, min: 0, max: 2, step: 0.05 }),
colorPicker3d({ label: 'zones', value: orbit.zones, alpha: false, presets: planetColours }),
colorPicker3d({ label: 'belts', value: orbit.belts, alpha: false, presets: planetColours }),
colorPicker3d({ label: 'accent', value: orbit.accent, alpha: false, presets: planetColours }),
label3d({ text: 'Rings' }),
slider3d({ label: 'amount', value: orbit.rings, min: 0, max: 1, step: 0.05 }),
slider3d({ label: 'inner edge (radii)', value: orbit.ringInner, min: 1.05, max: 3, step: 0.05 }),
slider3d({ label: 'outer edge (radii)', value: orbit.ringOuter, min: 1.1, max: 4, step: 0.05 }),
slider3d({ label: 'opacity', value: orbit.ringOpacity, min: 0, max: 1, step: 0.01 }),
colorPicker3d({ label: 'ring colour', value: orbit.ringColor, alpha: false }),
label3d({ text: 'Motion' }),
slider3d({ label: 'seed', value: orbit.seed, min: 1, max: 96, step: 1 }),
slider3d({ label: 'rotation', value: orbit.spin, min: 0, max: 0.3, step: 0.01 }),
],
sceneCreated(el, BABYLON) {
const camera = new BABYLON.ArcRotateCamera('orbit', -Math.PI / 3, Math.PI / 2.4, 220, BABYLON.Vector3.Zero(), el.scene)
camera.lowerRadiusLimit = 70
camera.upperRadiusLimit = 700
camera.maxZ = 3000
camera.attachControl(el.querySelector('canvas'), true)
el.setActiveCamera(camera)
},
},
b3dSun(),
b3dSkybox({ timeOfDay: 15, realtimeScale: 0, atmosphere: 0, starfieldCube: '/sky/nebula', starfieldData: '/sky/stars' }),
planet,
)
)
rebuild()
.preview { height: 100%; }
In a moon's sky
The same planet hanging over the horizon of one of its moons,
<tosi-b3d-moon gas="jupiter">. Pick a planet, then pull it
about: the dials start at the preset's own values.
import { b3d, b3dSun, b3dSkybox, b3dMoon, b3dGround, GAS_GIANTS, slider3d, select3d, label3d } from 'tosijs-3d'
import { tosi } from 'tosijs'
const { gg } = tosi({ gg: { kind: 'jupiter', size: 28, time: 16, tilt: 10, ...GAS_GIANTS.jupiter } })
const planet = b3dMoon({
frame: 'ground', azimuth: 22, elevation: 22,
gas: gg.kind, size: gg.size, axisTilt: gg.tilt,
bands: gg.bands, turbulence: gg.turbulence, storm: gg.storm,
})
// Choosing a planet resets the dials to that planet's numbers.
gg.kind.observe(() => {
const look = GAS_GIANTS[gg.kind.value]
gg.bands.value = look.bands
gg.turbulence.value = look.turbulence
gg.storm.value = look.storm
})
preview.append(
b3d(
{
scenePanelOpen: true,
scenePanel: () => [
label3d({ text: 'Gas giant' }),
select3d({ label: 'planet', value: gg.kind, options: Object.keys(GAS_GIANTS) }),
slider3d({ label: 'bands', value: gg.bands, min: 1, max: 30, step: 1 }),
slider3d({ label: 'turbulence', value: gg.turbulence, min: 0, max: 1.5, step: 0.05 }),
slider3d({ label: 'storm', value: gg.storm, min: 0, max: 2, step: 0.05 }),
slider3d({ label: 'tilt', value: gg.tilt, min: -45, max: 45, step: 1 }),
slider3d({ label: 'size (degrees)', value: gg.size, min: 2, max: 80, step: 1 }),
slider3d({ label: 'time of day (its phase)', value: gg.time, min: 0, max: 24, step: 0.25 }),
],
sceneCreated(el, BABYLON) {
const cam = new BABYLON.FreeCamera('c', new BABYLON.Vector3(0, 2, 0), el.scene)
cam.setTarget(new BABYLON.Vector3(0, 6, 10))
cam.attachControl(el.querySelector('canvas'), true)
el.setActiveCamera(cam)
},
},
b3dSun({}),
b3dSkybox(
{ timeOfDay: gg.time, realtimeScale: 0, atmosphere: 0, starfieldCube: '/sky/nebula', starfieldData: '/sky/stars', starfieldTilt: '12,25,58' },
planet,
),
b3dGround({ width: 80, height: 80, color: '#6a6250' }),
)
)
.preview { height: 100%; }
How it works
It is the cloud deck's idea turned on its side: a noise field advected by wind. There the wind blows across a plane; here each latitude has its own wind, which is what draws the bands out into streaks.
What the numbers mean
bands |
How many light and dark stripes from pole to pole, counting both. Jupiter is about 14, Neptune 6. Those near the equator are the broadest |
turbulence |
How far the edges of the bands are torn, 0…1. 0 is ruled lines; 1 is Jupiter |
storm |
Size of the great storm, 0…1 of a band's width. 0 is none |
colors |
Three: the bright zones, the dark belts, and the accent (the storm, the poles) |
seed |
A different planet with the same look |
rings |
The preset's ring: inner and outer edge in planet radii, opacity and color. Saturn's is broad and bright; the others are thin and nearly invisible, which is true to life |
Presets
GAS_GIANTS has jupiter, saturn, uranus and neptune. resolveGasGiant
takes a preset name and any overrides and returns the numbers the shader
wants.
The shader
GAS_GIANT_GLSL defines
vec3 b3dGasGiant(vec3 n, vec4 k, vec3 c0, vec3 c1, vec3 c2, float t)
n is the unit normal in the planet's own frame (Y is the spin axis), k is
(bands, turbulence, storm, seed), the colours are linear RGB and t is
seconds. It returns the unlit colour. No textures and no other functions are
needed: it carries its own noise.
vec3 b3dGasGiantLit(vec3 albedo, float nDotL, float nDotV, vec3 haze)
lights it as a ball of gas: brightness falls away from the point under the
sun and again toward the limb, and a thin rim of haze shows on the day
side. A flat disc with a hard terminator reads as a coin; this is what makes
it a sphere. gas-giant-material wraps both for a mesh.