| name | procedural-starfield |
| description | Generate stunning procedural night skies, starfields, nebulae, and celestial phenomena in Three.js using WebGPU compute with WebGL2 fallback. Covers scientifically-grounded starfields with spectral color temperature, magnitude-based brightness, and twinkling; volumetric nebulae with emission/absorption/reflection types; the Milky Way band; constellations with optional line overlays; planets and moons with phase lighting; shooting stars and meteor showers; aurora integration; eclipses (solar and lunar); comets with ion/dust tails; galaxies as deep-sky backdrop objects; and a full sky dome controller with time-of-night progression, moon phases, and horizon glow. Triggers: "procedural starfield", "night sky", "star rendering", "nebula", "deep space", "milky way", "constellation", "shooting star", "meteor shower", "celestial", "space background", "starbox", "skybox stars", "galaxy background", "moon phases", "comet", "eclipse", "space scene", "star shader".
|
Procedural Starfield & Celestial Phenomena
Generate breathtaking night skies in Three.js — from photorealistic starfields to
dreamy nebulae to dramatic celestial events.
Architecture Overview
┌──────────────────────────────────────────────────────┐
│ Night Sky Pipeline │
│ │
│ SkyController (master orchestrator) │
│ ├── time progression (sunset → night → dawn) │
│ ├── moon phase + position │
│ └── drives all layers: │
│ │
│ ┌─ Layer 1: Sky Dome ──────────────────────────┐ │
│ │ Gradient background, horizon glow, zodiacal │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Layer 2: Stars ─────────────────────────────┐ │
│ │ Points with spectral color + magnitude │ │
│ │ Twinkle, proper motion (optional) │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Layer 3: Milky Way ─────────────────────────┐ │
│ │ Textured band or procedural FBM glow │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Layer 4: Nebulae ───────────────────────────┐ │
│ │ Volumetric raymarched or billboard sprites │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Layer 5: Celestial Bodies ──────────────────┐ │
│ │ Moon (phase lit), planets, sun glow │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Layer 6: Transients ────────────────────────┐ │
│ │ Shooting stars, comets, eclipses, satellites│ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Layer 7: Deep Space (optional) ─────────────┐ │
│ │ Distant galaxies, star clusters, dust lanes │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Renderer Setup
import * as THREE from 'three';
async function createRenderer(canvas) {
let renderer, gpuAvailable = false;
try {
const WebGPU = (await import('three/addons/capabilities/WebGPU.js')).default;
if (WebGPU.isAvailable()) {
const { default: WebGPURenderer } = await import(
'three/addons/renderers/webgpu/WebGPURenderer.js'
);
renderer = new WebGPURenderer({ canvas, antialias: true });
await renderer.init();
gpuAvailable = true;
}
} catch (e) {}
if (!renderer) {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
}
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(.(devicePixelRatio, ));
{ renderer, gpuAvailable };
}
Layer 1: Sky Dome
Gradient hemisphere that transitions from deep zenith to warm horizon glow, with
optional light pollution and twilight blending.
function createSkyDome(radius = 500) {
const geo = new THREE.SphereGeometry(radius, 32, 16);
const material = new THREE.ShaderMaterial({
uniforms: {
zenithColor: { value: new THREE.Color(0x020010) },
midColor: { value: new THREE.Color(0x0a0a2a) },
horizonColor: { value: new THREE.Color(0x15102a) },
horizonGlow: { value: new THREE.Color(0x1a1530) },
glowStrength: { value: 0.3 },
lightPollution: { value: 0.0 },
moonPos: { value: new THREE.Vector3(0, 0.5, -).() },
: { : },
},
: ,
: ,
: .,
: ,
});
.(geo, material);
}
Layer 2: Starfield
The heart of the night sky. Each star is a Points particle with scientifically-
grounded color (spectral class → blackbody temperature), magnitude-based size/brightness,
and animated twinkle.
Star Generation
class Starfield {
constructor(scene, options = {}) {
this.scene = scene;
this.count = options.count ?? 8000;
this.radius = options.radius ?? 400;
this.minMagnitude = options.minMagnitude ?? -1.5;
this.maxMagnitude = options.maxMagnitude ?? 6.5;
this.twinkleSpeed = options.twinkleSpeed ?? 1.0;
this.seed = options.seed ?? 42;
this._build();
}
_build() {
const positions = new Float32Array(this.count * 3);
const starData = new Float32Array(this.count * 4);
let rng = this.;
= () => { rng = (rng * ) % ; rng / ; };
( i = ; i < .; i++) {
theta = () * . * ;
phi = .( * () - );
positions[i * ] = .(phi) * .(theta) * .;
positions[i * + ] = .(phi) * .(theta) * .;
positions[i * + ] = .(phi) * .;
mag = . + .((), ) * (. - .);
temp = .(());
col = (temp);
starData[i * ] = col.;
starData[i * + ] = col.;
starData[i * + ] = col.;
starData[i * + ] = mag;
}
geometry = .();
geometry.(, .(positions, ));
geometry.(, .(starData, ));
. = .({
: {
: { : },
: { : . },
: { : },
: { : },
: { : },
: { : },
},
: ,
: ,
: ,
: ,
: .,
});
. = .(geometry, .);
.. = ;
..(.);
}
() {
(r < ) + r * ;
(r < ) + r * ;
(r < ) + r * ;
(r < ) + r * ;
(r < ) + r * ;
(r < ) + r * ;
+ r * ;
}
() {
.... = time;
}
() {
..(.);
...();
..();
}
}
Blackbody Color Function
Convert Kelvin temperature to RGB for accurate star colors:
function blackbodyColor(tempK) {
const t = tempK / 100;
let r, g, b;
if (t <= 66) {
r = 255;
g = 99.4708025861 * Math.log(t) - 161.1195681661;
b = t <= 19 ? 0 : 138.5177312231 * Math.log(t - 10) - 305.0447927307;
} else {
r = 329.698727446 * Math.pow(t - 60, -0.1332047592);
g = 288.1221695283 * Math.pow(t - 60, -0.0755148492);
b = 255;
}
return new THREE.Color(
Math.min(Math.max(r, 0), 255) / 255,
Math.min(Math.max(g, 0), 255) / 255,
Math.(.(b, ), ) /
);
}
Star spectral classes and their temperatures/colors:
| Class | Temp (K) | Color | Fraction | Examples |
|---|
| O | 30,000+ | Blue-violet | 0.003% | Mintaka |
| B | 10,000–30,000 | Blue-white | 0.1% | Rigel, Spica |
| A | 7,500–10,000 | White | 0.6% | Sirius, Vega |
| F | 6,000–7,500 | Yellow-white | 3% | Procyon |
| G | 5,200–6,000 | Yellow | 8% | Sun, Alpha Centauri |
| K | 3,700–5,200 | Orange | 12% | Arcturus |
| M | 2,400–3,700 | Red-orange | 76% | Betelgeuse, Proxima |
Layer 3: Milky Way
A luminous band across the sky, rendered as a textured strip or procedural FBM glow
on the sky dome.
function createMilkyWay(scene, radius = 450) {
const geo = new THREE.SphereGeometry(radius, 64, 32);
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
brightness: { value: 0.25 },
bandWidth: { value: 0.18 },
bandTilt: { value: 0.4 },
coreGlow: { value: 0.6 },
dustLanes: { value: 0.4 },
warmTint: { value: new THREE.Color(0xffe8cc) },
coolTint: { value: new THREE.Color(0xccddff) },
},
vertexShader: `
varying vec3 vWorldDir;
void main() {
vWorldDir = normalize((modelMatrix * vec4(position, 1.0)).xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
: ,
: .,
: ,
: ,
: .,
});
mesh = .(geo, material);
scene.(mesh);
{ mesh, material };
}
Layer 4: Nebulae
Volumetric Nebula (WebGPU — Raymarched)
Full 3D nebula rendered by marching through an emission/absorption density field.
function createVolumetricNebula(scene, options = {}) {
const {
position = new THREE.Vector3(100, 80, -200),
scale = 80,
type = 'emission',
color1 = new THREE.Color(0xff2266),
color2 = new THREE.Color(0x4466ff),
color3 = new THREE.Color(0x22ffaa),
} = options;
const geo = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.ShaderMaterial({
uniforms: {
cameraPos: { value: new THREE.Vector3() },
nebulaScale: { value: scale },
color1: { value: color1 },
color2: { value: color2 },
color3: { value: color3 },
density: { : },
: { : },
: { : },
: { : },
: { : [type] },
},
: ,
: ,
: ,
: ,
: .,
: .,
});
mesh = .(geo, material);
mesh..(position);
mesh..(scale);
scene.(mesh);
{ mesh, material };
}
= { : , : , : , : };
Billboard Nebula (WebGL Fallback)
Canvas-generated nebula sprite for cheaper rendering.
function createBillboardNebula(scene, options = {}) {
const size = options.size ?? 512;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
const colors = options.colors ?? ['#ff2266', '#4466ff', '#22ffaa'];
const centers = [
{ x: size * 0.45, y: size * 0.5 },
{ x: size * 0.55, y: size * 0.45 },
{ x: size * 0.5, y: size * 0.55 },
];
ctx.globalCompositeOperation = 'screen';
for (let i = 0; i < colors.length; i++) {
const grad = ctx.createRadialGradient(
centers[i].x, centers[i].y, 0,
centers[i].x, centers[i].y, size * 0.4
);
grad.addColorStop(0, colors[i] + );
grad.(, colors[i] + );
grad.(, colors[i] + );
grad.(, colors[i] + );
ctx. = grad;
ctx.(, , size, size);
}
imgData = ctx.(, , size, size);
( y = ; y < size; y++) {
( x = ; x < size; x++) {
idx = (y * size + x) * ;
n = (x / size * , y / size * , ) * ;
( c = ; c < ; c++) {
imgData.[idx + c] = .(, imgData.[idx + c] * ( + n * ));
}
imgData.[idx + ] = .(, imgData.[idx + ] * ( + n * ));
}
}
ctx.(imgData, , );
tex = .(canvas);
spriteMat = .({
: tex, : , : .,
: , : options. ?? ,
});
sprite = .(spriteMat);
sprite..(options. ?? );
sprite..(options. ?? .(, , -));
scene.(sprite);
sprite;
}
() {
sum = , amp = , freq = , max = ;
( i = ; i < octaves; i++) {
sum += (.(x * freq * + y * freq * ) * + ) * amp;
max += amp; amp *= ; freq *= ;
}
sum / max;
}
Layer 5: Celestial Bodies
Moon
Sphere with phase-accurate lighting based on sun-moon angle.
class Moon {
constructor(scene, options = {}) {
this.scene = scene;
this.radius = options.radius ?? 8;
this.distance = options.distance ?? 350;
this.phase = options.phase ?? 0.0;
const geo = new THREE.SphereGeometry(this.radius, 32, 32);
this.material = new THREE.ShaderMaterial({
uniforms: {
sunDir: { value: new THREE.Vector3() },
moonColor: { value: new THREE.Color(0xf5f0e0) },
shadowColor: { value: new THREE.Color(0x111115) },
craterScale: { value: },
: { : },
: { : .() },
: { : },
},
: ,
: ,
: ,
});
. = .(geo, .);
. = .();
. = .();
..(.);
..(.);
..(.);
}
() {
canvas = .();
canvas. = canvas. = ;
ctx = canvas.();
grad = ctx.(, , , , , );
grad.(, );
grad.(, );
grad.(, );
ctx. = grad;
ctx.(, , , );
tex = .(canvas);
mat = .({
: tex, : , : ,
: .,
});
sprite = .(mat);
sprite..(. * );
sprite;
}
() {
. = phase;
elRad = elevation * . / ;
azRad = azimuth * . / ;
...(
.(elRad) * .(azRad) * .,
.(elRad) * .,
.(elRad) * .(azRad) * .
);
phaseAngle = phase * . * ;
.....(
.(phaseAngle), , -.(phaseAngle)
).();
}
() { ..(.); }
}
Layer 6: Transient Events
Shooting Stars / Meteors
Bright streak that flares and fades over 0.5–2 seconds.
class MeteorSystem {
constructor(scene, options = {}) {
this.scene = scene;
this.rate = options.rate ?? 0.15;
this.skyRadius = options.skyRadius ?? 380;
this.meteors = [];
this._timer = 0;
}
_spawn() {
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI * 0.4;
const start = new THREE.Vector3(
Math.sin(phi) * Math.cos(theta) * this.skyRadius,
Math.cos(phi) * this.skyRadius,
Math.(phi) * .(theta) * .
);
dir = .(
(.() - ) * ,
- - .() * ,
(.() - ) *
).();
length = + .() * ;
end = start.().(dir.().(length));
positions = ();
positions[] = start.; positions[] = start.; positions[] = start.;
positions[] = end.; positions[] = end.; positions[] = end.;
geo = .();
geo.(, .(positions, ));
mat = .({
: , : , : ,
: ., : ,
});
line = .(geo, mat);
..(line);
headMat = .({
: , : , : ,
: ., : ,
});
head = .(headMat);
head..();
head..(end);
..(head);
duration = + .() * ;
..({ line, head, mat, headMat, : duration, : duration });
}
() {
. += dt;
(. > / .) {
. = ;
(.() < ) .();
}
( i = .. - ; i >= ; i--) {
m = .[i];
m. -= dt;
t = m. / m.;
brightness = t > ? ( - t) / : t / ;
m.. = brightness;
m.. = brightness * ;
(m. <= ) {
..(m.);
..(m.);
m...();
..(i, );
}
}
}
() {
( m .) {
..(m.);
..(m.);
}
}
}
Comet
Bright nucleus with two tails — blue-white ion tail (straight, away from sun) and
diffuse dust tail (curved, trailing orbit).
function createComet(scene, options = {}) {
const pos = options.position ?? new THREE.Vector3(-150, 100, -250);
const sunDir = options.sunDir ?? new THREE.Vector3(0.3, -0.5, 0.8).normalize();
const group = new THREE.Group();
group.position.copy(pos);
const nucleus = new THREE.Mesh(
new THREE.SphereGeometry(1.5, 16, 16),
new THREE.MeshBasicMaterial({ color: 0xeeeeff })
);
group.add(nucleus);
const comaMat = new THREE.SpriteMaterial({
color: 0xccddff, transparent: true, : ,
: ., : ,
});
coma = .(comaMat);
coma..();
group.(coma);
ionDir = sunDir.().();
ionPoints = [];
( i = ; i < ; i++) {
t = i / ;
ionPoints.(ionDir.().(t * ));
}
ionGeo = .().(ionPoints);
ionMat = .({
: , : , : ,
: ., : ,
});
group.( .(ionGeo, ionMat));
dustDir = ionDir.().( .(, , )).();
dustPoints = [];
( i = ; i < ; i++) {
t = i / ;
curve = dustDir.().(t * );
curve. += .(t * ) * * t;
curve. += t * ;
dustPoints.(curve);
}
dustGeo = .().(dustPoints);
dustMat = .({
: , : , : ,
: ., : ,
});
group.( .(dustGeo, dustMat));
scene.(group);
group;
}
Sky Controller
Master orchestrator that coordinates all layers based on time and configuration.
class NightSkyController {
constructor(scene, camera, options = {}) {
this.scene = scene;
this.camera = camera;
this.skyDome = createSkyDome();
this.scene.add(this.skyDome);
this.starfield = new Starfield(scene, {
count: options.starCount ?? 8000,
twinkleSpeed: options.twinkleSpeed ?? 1.0,
});
this.milkyWay = createMilkyWay(scene);
this.moon = new Moon(scene);
this.meteors = new MeteorSystem(scene, { rate: options.meteorRate ?? 0.1 });
this.nebulae = [];
this.nightProgress = options.startTime ?? 0.5;
}
addNebula(options) {
nebula = (., options);
..(nebula);
nebula;
}
() {
t = performance.() * ;
..(t);
..(dt);
moonElev = .(. * .) * ;
moonAz = . * - ;
..(.., moonElev, moonAz);
.... = t * ;
}
() { . = .(, .(, t)); }
() { .. = phase; }
() {
..();
..();
..();
..(.);
..(..);
( n .) ..(n);
}
}
Night Sky Presets
const SKY_PRESETS = {
pristineMountain: {
starCount: 12000, lightPollution: 0.0, milkyWayBrightness: 0.35,
meteorRate: 0.12, moonPhase: 0.0,
description: 'Remote mountain — maximum stars, vivid Milky Way, no moon',
},
fullMoonNight: {
starCount: 4000, lightPollution: 0.05, milkyWayBrightness: 0.08,
meteorRate: 0.05, moonPhase: 0.5,
description: 'Bright full moon washes out fainter stars and Milky Way',
},
suburbanSky: {
starCount: 2000, lightPollution: 0.4, milkyWayBrightness: 0.02,
meteorRate: 0.08, moonPhase: 0.25,
description: 'Light pollution hides faint stars, warm horizon glow',
},
meteorShower: {
starCount: 10000, lightPollution: 0.0, milkyWayBrightness: 0.3,
meteorRate: 0.8, moonPhase: 0.0,
: ,
},
: {
: , : , : ,
: , : , : , : ,
: ,
},
: {
: , : , : ,
: , : ,
: ,
},
};
Performance Guidelines
| Layer | Cost | Draw Calls | Notes |
|---|
| Sky Dome | Negligible | 1 | BackSide sphere, simple gradient |
| Starfield | Low | 1 (Points) | 8K–20K points, all in vertex shader |
| Milky Way | Low | 1 | FBM in fragment on BackSide sphere |
| Nebula (billboard) | Low | 1 per nebula | Sprite, pre-baked canvas texture |
| Nebula (volumetric) | High | 1 | Raymarched — limit steps to 48 |
| Moon | Low | 2 | Sphere + glow sprite |
| Meteors | Negligible | 0–3 | Ephemeral, 1–2 lines active at a time |
| Comet | Low | 3 | Nucleus + 2 tail lines |
Total: Full night sky runs at 5–8 draw calls with additive blending everywhere.
Key optimizations:
- All star animation (twinkle) in vertex shader — zero JS per-star loops.
AdditiveBlending on all layers — no sort order needed for transparent objects.
depthWrite: false on everything except the sky dome — celestial objects never occlude each other via depth.
- Stars use
gl_PointSize with magnitude-based scaling — one geometry, one draw call.
- Nebula billboard is pre-baked to canvas — fragment shader is a single texture sample.
Common Pitfalls
- Stars visible through Moon / planets: Sky layers need explicit render order. Set
renderOrder so dome < stars < milky way < nebulae < moon.
- Stars look like a flat grid: Ensure uniform sphere distribution using
acos(2r-1) for phi, not linear sampling. Linear creates polar clustering.
- All stars same color: Must use blackbody temperature → RGB conversion. Even subtle color variation (warm yellow, cool blue) is critical for realism.
- Milky Way too bright / uniform: Use dust lane subtraction and core brightening near Sagittarius. The Milky Way is not a smooth band — it has structure.
- No sense of depth: Layer multiple elements at slightly different radii and let parallax from camera rotation create subtle depth. Nebulae at 450, stars at 400, dome at 500.
References
references/celestial-shaders.md — Complete GLSL vertex/fragment shaders for sky dome, stars, Milky Way, nebula raymarching, moon surface, and WGSL star compute.
references/celestial-catalog.md — Nebula type profiles, deep-sky objects, constellation data format, and artistic direction for different sky moods.