Back to Blog

Make a Spinning Particle Galaxy in Three.js (TypeScript)

Celest KimCelest Kim

Video: Make a Spinning Particle Galaxy in Three.js (TypeScript) by CelesteAI

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

A glowing spiral galaxy made of thousands of points looks like a lot of work. It isn’t. The whole thing is one loop that places each star, a material that makes overlapping points glow, and a render loop that slowly spins it. About fifty lines of Three.js.

This build uses TypeScript and Vite — scaffolded from the command line, written in the editor, and run in the browser.

Set up the project

Three.js is an ES module, so use a real bundler. Vite scaffolds a TypeScript project in one command:

npm create vite@latest particle-galaxy -- --template vanilla-ts
cd particle-galaxy
npm install
npm install three

Vite resolves the bare three import for you and the type definitions ship with the package — no extra @types needed. The starter gives you an index.html, a tsconfig.json, and a src/ folder with main.ts.

Trim index.html down to a full-screen canvas:

<canvas id="bg"></canvas>
<script type="module" src="/src/main.ts"></script>

Scene, camera, renderer

import * as THREE from "three";

const canvas = document.getElementById("bg") as HTMLCanvasElement;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setClearColor(0x05060a, 1);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 3.2, 6);
camera.lookAt(0, 0, 0);

The camera sits above and back from the origin, looking down at where the galaxy will sit.

The parameters

A galaxy is just a few numbers. Pull them out so they’re easy to tweak:

const COUNT = 14000;       // stars
const BRANCHES = 3;        // spiral arms
const RADIUS = 5;
const SPIN = 1.1;          // how hard the arms twist
const RANDOMNESS = 0.45;   // scatter around each arm
const RAND_POWER = 3;      // bias scatter toward the arm
const INSIDE = new THREE.Color(0xff6030);   // warm core
const OUTSIDE = new THREE.Color(0x1b3984);  // cool edge

The loop that makes the galaxy

This is the only interesting part. For each star: pick a random radius, drop it onto one of the branches, and rotate it by a spin angle that grows with the radius — that twist is what turns straight arms into a spiral. A little random scatter (biased toward the arm with Math.pow) gives the arms thickness, and the color lerps from the warm center to the cool rim.

const positions = new Float32Array(COUNT * 3);
const colors = new Float32Array(COUNT * 3);

for (let i = 0; i < COUNT; i++) {
  const i3 = i * 3;
  const r = Math.random() * RADIUS;
  const branchAngle = ((i % BRANCHES) / BRANCHES) * Math.PI * 2;
  const spinAngle = r * SPIN;
  const scatter = () => Math.pow(Math.random(), RAND_POWER) * (Math.random() < 0.5 ? 1 : -1) * RANDOMNESS * r;

  positions[i3] = Math.cos(branchAngle + spinAngle) * r + scatter();
  positions[i3 + 1] = scatter() * 0.5;
  positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * r + scatter();

  const mixed = INSIDE.clone().lerp(OUTSIDE, r / RADIUS);
  colors[i3] = mixed.r; colors[i3 + 1] = mixed.g; colors[i3 + 2] = mixed.b;
}

Geometry, material, and the glow

Hand the flat arrays to a BufferGeometry as attributes, then use a PointsMaterial with additive blending — where stars overlap, their colors add together and bloom into bright white, exactly like a real galaxy core. vertexColors tells the material to use the per-star color attribute.

const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

const material = new THREE.PointsMaterial({
  size: 0.025,
  sizeAttenuation: true,
  depthWrite: false,
  blending: THREE.AdditiveBlending,
  vertexColors: true,
});

const galaxy = new THREE.Points(geometry, material);
scene.add(galaxy);

Spin it

A clock-driven render loop rotates the whole Points object on its Y axis:

const clock = new THREE.Clock();
function tick() {
  galaxy.rotation.y = clock.getElapsedTime() * 0.18;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();

Run it

npm run dev

Vite prints a local URL — open it and the galaxy turns. Change BRANCHES to 5, swap the two colors, or push SPIN higher, and you get a completely different galaxy from the same loop.

Recap

  • A particle system is just flat arrays of positions and colors handed to a BufferGeometry — no per-star objects.
  • The spiral is one idea: twist each star further the farther out it sits (spinAngle = r * SPIN).
  • Additive blending is what makes overlapping points glow like a galaxy core.

Want the deeper dive?

The full source is on GitHub — clone it, npm install, npm run dev, and start tweaking the parameters.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.