The scene generates up to a million points distributed along spiral branches using polar coordinates and a power based randomness function, so particles cluster more heavily near each branch line and thin out with distance. Color is interpolated per particle between an inside and outside color based on radial distance, then passed to the GPU as a custom attribute. The real magic happens in the vertex shader: instead of animating rotation on the CPU by updating positions every frame, the shader recalculates each particle's angle using uTime, with an offset that depends on distance from the center. Particles closer to the "hole" spin faster, particles farther away spin slower, which gives a convincing differential rotation like a real galaxy or accretion disk. A uHoleSize uniform pushes particles away from a small central void and also affects rotation speed near the core. Point size is calculated per particle using a base scale attribute multiplied by a size uniform, then attenuated by view space depth so distant points shrink naturally. Additive blending combined with a star sprite texture at each point gives the soft glowing look rather than flat dots. dat.GUI exposes almost every parameter (count, radius, branch count, randomness, colors, point size, hole size) so you can tune the look live, and OrbitControls lets you fly the camera around.
This project is a strong introduction to writing and hooking up custom GLSL vertex and fragment shaders inside a ShaderMaterial, including how uniforms and custom attributes get passed from JavaScript into the GPU. Along the way you'll get a concrete feel for the three matrix transforms every vertex passes through, the model matrix, view matrix, and projection matrix, and why they're multiplied through position in that specific order rather than any other. You'll also practice building procedural geometry using BufferGeometry with custom attributes like aScale and aRandomness, instead of relying on a predefined geometry object. This pairs naturally with techniques for organic looking randomness, particularly using Math.pow() with a "randomness power" value to bias particle distribution toward the center of each branch rather than spreading everything uniformly. Color interpolation comes into play as well, since THREE.Color.lerp() lets you create smooth gradients across a large dataset instead of assigning colors to each particle by hand. On the rendering side, the project demonstrates additive blending and how it differs from normal blending when overlapping transparent points are involved, along with why depthWrite: false matters specifically for particle systems like this one. You'll also work through point size attenuation math, and understand why accounting for renderer.getPixelRatio() is necessary to keep point sizes looking consistent across different devices and screen densities. Finally, there's a practical lesson in wiring up dat.GUI controls that trigger full geometry regeneration through onFinishChange, versus controls that simply update a uniform live through onChange, a distinction that has real performance implications once particle counts climb into the hundreds of thousands. Tying it all together is the animation loop pattern itself, using THREE.Clock alongside requestAnimationFrame, and seeing how elapsed time gets passed into a shader as a uniform to drive continuous motion on the GPU.
Read a plain-language overview or a file-by-file breakdown. Ask questions about the project below the overview.
This project creates an interactive 3D rotating galaxy animation using Three.js and custom GLSL shaders. It renders a vast number of star particles that form a spiral galaxy, complete with dynamic spinning motion and color gradients. A user interface, powered by dat.GUI, allows real-time customization of galaxy parameters such as star count, radius, number of branches, randomness, inside and outside colors, point size, and the size of the central "black hole" effect.
The galaxy's structure is generated in JavaScript using THREE.BufferGeometry. This involves calculating the initial positions for hundreds of thousands of individual stars, arranging them into distinct spiral arms. Each star's color is interpolated based on its distance from the galaxy's center, creating a smooth gradient from an inner color to an outer color. Additional attributes like aScale and aRandomness are also generated for each star, providing data that will be used later in the shaders to add visual variety.
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;
// Position calculation
const radius = Math.random() * parameters.radius;
const branchAngle = i % parameters.branches / parameters.branches * Math.PI * 2;
positions[i3] = Math.cos(branchAngle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle) * radius;
// Color interpolation
const mixedColor = insideColor.clone();
mixedColor.lerp(outsideColor, radius / parameters.radius);
colors[i3] = mixedColor.r;
// ...
}This JavaScript loop generates the initial positions and colors for each star, arranging them into spiral arms and applying color gradients.
The visual rendering and animation are handled by custom GLSL (OpenGL Shading Language) vertex and fragment shaders, embedded directly in the HTML. The vertex shader is responsible for calculating the final position and size of each star. It applies a complex spinning motion based on the star's distance from the center and the elapsed time, creating the dynamic rotation effect. It also adjusts the gl_PointSize so that stars appear larger or smaller based on their distance from the camera and their individual aScale.
<script id="vertexShader" type="x-shader/x-vertex">
uniform float uSize;
uniform float uTime;
uniform float uHoleSize;
attribute float aScale;
attribute vec3 aRandomness;
varying vec3 vColor;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// Spin
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.xz) + uHoleSize;
float uTimeOffset = uTime + (uHoleSize * 30.0);
float angleOffset = (1.0 / distanceToCenter) * uTimeOffset * 0.2;
angle += angleOffset;
modelPosition.x = cos(angle) * distanceToCenter;
modelPosition.z = sin(angle) * distanceToCenter;
modelPosition.xyz += aRandomness;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
float scale = uSize * aScale;
gl_PointSize = scale;
gl_PointSize *= ( 1.0 / - viewPosition.z );
vColor = color;
}
</script>This HTML script tag defines the vertex shader program, which calculates the position and size of each star particle, including its spinning motion.
The fragment shader then determines the final color of each pixel that makes up a star particle. It receives the calculated color from the vertex shader and combines it with a star texture (uTexture). This texture, loaded as an image in JavaScript, is applied to each point, making them appear as soft, glowing stars rather than simple squares. The THREE.ShaderMaterial in JavaScript acts as the bridge, linking the generated geometry data and the GLSL shader programs, and passing dynamic values like uTime and uSize as uniforms.
material = new THREE.ShaderMaterial({
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
vertexShader: document.getElementById("vertexShader").textContent,
fragmentShader: document.getElementById("fragmentShader").textContent,
transparent: true,
uniforms: {
uTime: { value: 0 },
uSize: { value: 30 * renderer.getPixelRatio() },
uHoleSize: { value: 0.15 },
uTexture: { value: starTexture },
size: { value: 1.0 } } });This code creates a custom ShaderMaterial in Three.js, instructing the renderer to use the GLSL vertex and fragment shaders defined in the HTML for rendering the galaxy particles.
An animation loop, driven by requestAnimationFrame, continuously updates the uTime uniform in the shader, ensuring the galaxy's smooth, ongoing rotation. User interaction is enabled through OrbitControls, allowing the viewer to pan, zoom, and rotate the camera around the galaxy. The dat.GUI panel provides an intuitive way to modify the galaxy's properties, with most changes triggering a regeneration of the galaxy to reflect the new parameters instantly.
Have a question about how this project works? Ask below and get an answer based on the project code.
Structured technical breakdown for beginners.
No breakdown yet. Generate one to see a file-by-file explanation.
Comments
No comments yet. Sign in to leave a comment.
Sign in to leave a comment.