DEV Community

loading...

Recreating a classic starfield in GLSL & three.js

Jesse Solomon
・3 min read

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (1)

Collapse
signo profile image
L

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