Home

# Rendering a Billion Stars?

Skyboxes have limitations. They have a finite number of pixels. The further you zoom in, the bigger those pixels get. So what do you do if you're making a space game and want a telescope? Stars should be small both when zoomed in and when zoomed out.

First, a quick lesson: how big is a star? Well, Proxima Centauri is 107,280 km radius. It is also 40,174,990,000,000 km away from earth. This means it has an angular size of 0.00055 arc-seconds (2.67e-09 radians). This is tiny. Like so tiny that even the james web space telescope's impressive 0.07 arcseconds of angular resolution is a couple orders of magnitude short. Ie: a star (ignoring lens artifacts, ccd bloom etc. etc.) should still be a single pixel.

So if a star should always be exactly one pixel in size, how do we render them? Clearly a skybox won't work at narrow zoom angles, so what can we do? One option is to use geometry, and to use a vertex shader to adjust the scale of the star t always be small. However, if we want millions of stars (and remember, there are 100 billion in our galaxy alone), then that is a lot of GPU time. So can we do it in a single shader? Yep, turns out you can.

The naive way of rendering lots of dots in a shader is to calculate the distance to all of them. This has the limitation that again you are iterating, and a billion of anything is a lot (a Ghz is a billionth of a second, so if you can render one star per cycle, a billion stars takes one second). But there is another solution: if we can divide up the sky into small chunks and have each chunk render exactly one star, then... we only have to render one star per pixel.

## Dividing up the Sky

So how do you divide up the sky into equal area chunks. This is something of an open mathematical problem to solve it perfectly, but we don't have to be perfect, we just have to be good enough.

My first pass was to use a 3D grid, and intersect that with a sphere. This looks like: (Look at the random colors, not the lat/long lines. That's just to show you it's a sphere).

We can move the grid points and do a voroni on them and it gets, uh, different.

Still some very unequal areas here. I tried a spiral as well as parabaloid mapping, but it's hard to get coordinates inside those cells, so I settled on a cube map: This is pretty good, but you can see that towards the corners the perspective makes the cells smaller. After some fiddling, I discovered this very simple distortion: float d_edge = 2.0 - max(abs(uv.x), abs(uv.y)); uv *= sqrt(d_edge); This "pulls in" the corners: The cells get a bit distorted, but a lot less than before.

Oh yeah, and we need the coordinates within each cell as well:

## Drawing the stars

If we have coordintes for each cell we can put a star in each cell by computing the distance to the cell center and applying some color: And here's the important part: We can now scale the radius of each dot by the camera's zoom innerCoords = abs(innerCoords - 0.5) * 2.0; float dist = length(innerCoords) * iResolution.y; float radius = max(1.0 - dist / (numCells * lens * STAR_RADIUS), 0.0); (the lens variable represents the FOV, and iResolution is the screen resolution) It's also wise to make the stars grid a bit less obvious by turning some stars off and moving them inside the cells by a small amount to break up lines:

## How many stars?

If we want to avoid needing to do subpixel rendering, each cell needs to be a minimum of a couple pixels on the screen. This means that the number of stars depends on the widest angle we need to render and the resolution of the monitor. I found a nice number was 200 cells per face of the cube. This works well even at quite low screen resolutions (600px or so), and is stable (no flickering as you move the camera)

You can then do a couple "layers" of stars with different noise and cell counts in order to increase the number of stars. I settled for 9 layers of stars. If we didn't have the density map controlling where the stars were, this would be: num_stars = num_cell_divisions * num_cell_divisions * num_sides_of_cube * num_layers num_stars = 200 * 200 * 6 * 9 num_stars = 2,160,000 That is 2 million stars.

## So how to get to a billion?

One way would be to increase the number of cells to ~6000 per edge. This would work fine on an 8k monitor with no other changes required ;). 6000 per edge would work fine when more zoomed in even on more normal screens, so one option would be to fade in a high density star map as you zoom. You may have to twiddle with brightnesses so you don't go "huh, I should have seen that star" - probably few bright stars always visible and then layers of increasing density but decreasing brightness that fade in as you zoom.