Jesse Solomon

Posted on

# Recreating a classic starfield in GLSL & three.js

A starfield 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 and three.js, 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, and view the GitHub repository here!

(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 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:

``````// 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:

``````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:

``````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:

``````// 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:

``````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!

## Top comments (1)

L

Awesome, just started learning about GLSL and this is very useful :)