Recreating a classic starfield in GLSL & three.js
Tags: glsl, threejs, javascript
Published: 2021-02-21 19:37:38

A [starfield](https://youtu.be/17WoOqgXsRM) was one of the first things I built when learning to program. It's been quite some time now, and I've started learning shader programming with [GLSL](https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders) and [three.js](https://threejs.org/), so I decided why not go back to where it all started!

# The Final Product

If you're in a hurry and just want to see what I've put together, you can look at [the final product here](https://starfield.jessesolomon.dev/), and view the GitHub repository [here](https://github.com/JesseSolomon/starfield)!

(I'd drop a gif, but you couldn't really make out what's happening 🤷‍♂️)

# Let's Build It!

If you're not familiar with shader programming, don't worry! I'll be sure to keep it informative, but accessible.

Also, there's a lot of boring padding code to get everything running, so all the GLSL here is paraphrased for your enjoyment (and my own sanity). Take a look at the [repository](https://github.com/JesseSolomon/starfield) for the real code.

### Part 1 - The classical approach

let's start with the most straight forward way to do this, a rough GLSL port of what you might write in JavaScript:

```glsl
// Loop through all the stars we want
for (int i = 0; i < STAR_COUNT; i++) {
	// Give the star a random position
	vec2 star = vec2((random(i) - 0.5) * 2.0, (random(i) - 0.5) * 2.0);
	
	// Get the direction from the center to the star, and scale it by time and a random offset
	star = normalize(star) * mod(time + random(float(i) * 16.0), 1.414214);
	
	// If the star is within 0.1% if the viewport size then draw it as white
	if (distance(screenPosition, star) < 0.001) {
		color = vec3(1, 1, 1);
		break;
	}
}
```

So what's wrong with this method? Mostly, that it just doesn't scale. GLSL runs your shader for every pixel, you can think about it like this:

```javascript
for (let pixel of screen) {
	for (let star of stars) {
		...code
	}
}
```

This is horribly inefficient!

So how can we make this more performant, and maybe make it even better?

### Part 2 - Let's make it better!

In order to make this thing awesome, we're going to need to fix the biggest issue. Iterating over hundreds of stars.

My favorite thing to do in a situation like this is to try a totally new perspective. Like, what if instead of each star being a point emitted from the center, it was a point along a column that went from the center to the edge?

Imagine a pie that covered the entire screen, each slice would represent one star traveling from the center to the edge.

Since the "slices" wouldn't move, we could map `screenPosition` to a slice, and figure out what star to process:

```glsl
vec2 direction = normalize(floor(normalize(screenPosition) * STAR_DENSITY) / STAR_DENSITY)
```

We can define `STAR_DENSITY` for the number of slices we want.

Now, instead of using `i` to figure out the stars offset, we can convert `direction` from a point, to a float and use that instead:

```glsl
// I'm using `scale` because `distance` is a built-in method
float scale = mod(time + random(direction.x + direction.y * 10.0), 1.414214);
```

With a `direction` and a `scale` we've now defined our star using polar coordinates, using just the `screenPosition`!

We can now do our distance check like this: 

```glsl
if (abs(scale - distance(screenPosition, vec3(0, 0, 0)) < 0.001) {
	...
}
```

🎉 Tada, mission accomplished! We've now not only improved performance, but created a super dense starfield visualization that you couldn't do in JavaScript!

Thanks for reading, I hope you enjoyed the article, I aim to make more of these (hopefully better) so if you have any feedback please let me know!