Navigate
Search topics across all sections
Galaxy Generator
A cosmic paint splatter made from math. Each particle is positioned using polar coordinates -- an angle and a radius -- with randomness creating the organic spiral arm look. The result: thousands of colored dots that form a procedural spiral galaxy, generated entirely from code.
You generate thousands of particles but they appear as a uniform circular blob. There are no visible spiral arms -- just a dense, round cluster of particles in the center.
Galaxy has no spiral structure. Particles form a round disk with no arms visible.
Real-world
Imagine you are a cosmic painter with a lazy susan (a spinning turntable). You put a canvas on it and start flicking paint from the center outward. As the turntable spins, each droplet curves into a spiral arc instead of flying straight.
Droplets near the center barely curve because they have not traveled far. But droplets near the edge curve dramatically because the turntable has rotated significantly by the time they reach the rim. This is the spin parameter -- how much extra rotation each particle gets based on its distance from the center.
Now you do not flick paint in every direction equally. You aim at 3 or 4 specific angles (the arms). And your flick is not perfect -- each droplet scatters slightly off-course (the randomness). Together: arm direction + spin + scatter = a spiral galaxy.
How the Galaxy is Built
From random numbers to a spiral galaxy in four stages.
Polar Coords
Random radius + arm angle
Spin
Outer particles rotate more
Scatter
Random offset from arm line
Color
Inner=warm, outer=cool
Building the Galaxy Step by Step
We position every particle with math, then let the GPU render them all in a single draw call.
Step 1 -- Place particles on spiral arms
for (let i = 0; i < count; i++) {
const radius = Math.random() * 5;
// Which arm (evenly spaced around the circle)
const armAngle = ((i % arms) / arms) * Math.PI * 2;
// Spin: more rotation for outer particles
const spinAngle = radius * spin;
const angle = armAngle + spinAngle;
positions[i * 3] = Math.cos(angle) * radius;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = Math.sin(angle) * radius;
}Each particle gets a random radius (distance from center) and is assigned to one of the spiral arms using modulo. The spin parameter adds extra rotation proportional to the radius -- this is what curves straight lines into spirals.
Step 2 -- Add organic scatter
// Cubic distribution: concentrated near center
const rx = Math.pow(Math.random(), 3)
* (Math.random() < 0.5 ? 1 : -1)
* randomness * radius * 0.3;
positions[i3] += rx;
positions[i3 + 1] += ry; // slight vertical spread
positions[i3 + 2] += rz;The key trick: pow(random, 3) creates a cubic distribution. Most values cluster near 0 (close to the arm), with occasional outliers. The random sign (positive or negative) scatters in both directions. Multiplying by radius makes outer regions wider, matching real galaxy arm behavior.
Step 3 -- Color from center to edge
const colorInner = new THREE.Color(innerColor);
const colorOuter = new THREE.Color(outerColor);
const temp = new THREE.Color();
// Per-particle color based on distance
const radiusNorm = radius / maxRadius;
temp.copy(colorInner).lerp(colorOuter, radiusNorm);
colors[i3] = temp.r;
colors[i3 + 1] = temp.g;
colors[i3 + 2] = temp.b;Real galaxies have hot, bright cores and cooler outer regions. We mimic this by lerping between an inner color (warm orange) and an outer color (cool blue) based on each particle's normalized radius. Each particle gets its own color stored as a buffer attribute.
Step 4 -- Custom shader for circular points
// Vertex: size attenuation
gl_PointSize = uSize * aScale * (200.0 / -mvPos.z);
// Fragment: circular shape with soft edges
float dist = length(gl_PointCoord - vec2(0.5));
if (dist > 0.5) discard;
float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
gl_FragColor = vec4(vColor, alpha);Without a custom fragment shader, each point is a visible square. gl_PointCoord gives the UV of each pixel within the point sprite. We compute the distance from center, discard pixels outside radius 0.5, and use smoothstep for soft edges. The result: each particle is a soft glowing circle.
What you just learned
Spiral arms are created by giving particles an angle based on their arm index, then adding radius-proportional spin.
pow(random, 3) creates a cubic distribution that concentrates scatter near the arm centerline with natural-looking outliers.
Per-particle attributes (color, scale) are passed as bufferAttributes and read as 'attribute' variables in the vertex shader.
gl_PointCoord in the fragment shader gives the UV within each point sprite, enabling circular masking with discard.
A single Points draw call can render 10,000+ particles -- far more efficient than individual meshes.
Question
The galaxy uses a flat random distribution for radius: Math.random() * 5. This means particles are spread evenly from center to edge. But real galaxies have many more stars near the center. What would you change in the radius calculation to create a denser core?
Think about it...
You set spin to 0 and armCount to 4. What shape do you see?
Hint: Spin controls how much extra rotation particles get based on their distance from center. With spin=0, what happens to the spiral?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Set armCount to 2 — simple spiral
Try This!
Beginner
Set armCount to 8 — complex galaxy
Try This!
Beginner
Max spin — tight spiral
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
// Every particle sits exactly on the arm curve
const angle = armAngle + radius * spin;
positions[i3] = Math.cos(angle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(angle) * radius;
// Result: thin lines, not a galaxy// Fragment shader just sets a flat color
void main() {
gl_FragColor = vec4(vColor, 1.0);
// Result: visible square particles
}// Using a uniform -- same color for all
uniform vec3 uColor;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}Best Practices
Use additive blending
Galaxies are made of light. Additive blending makes overlapping particles brighter (like real stars clustered together). Dense regions glow naturally without any extra code.
Concentrate scatter at center
Multiply scatter by radius so outer particles spread more than inner ones. This matches real galaxy morphology where the core is dense and compact while arms fan out at the edges.
Vary particle sizes
Give each particle a random scale attribute. A mix of sizes creates visual variety -- a few large bright stars among many small dim ones looks far more convincing than uniform dots.
Slow rotation animation
A very slow rotation (0.05 radians per second) makes the galaxy feel alive without being distracting. Too fast and it breaks the illusion of cosmic scale.