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)
Awesome, just started learning about GLSL and this is very useful :)