How to make an efficient/performant 3D sky shader starfield?

Godot Version

4.2.1

Question

I am trying to make a 3D game set in space, where I want to have a WorldEnivornment Sky with a rich starfield, as well as visible sun.

Ideally I would like to procedurally generate the starfield, so that I can vary the appearance procedurally for each game cycle, and I want to experiment with creating a “band” of stars representing a “milky way”-like nearest galaxy.

I have been experimenting with a sky shader to accomplish this, but I am new to shaders.

My current shader implementation:

shader_type sky;

group_uniforms sun; // First DirectionalLight3D will be the sun
	uniform vec3 sun_color : source_color = vec3( 10.0, 8.0, 1.0 );
	uniform vec3 sun_sunset_color : source_color = vec3( 10.0, 0.0, 0.0 );
	uniform float sun_size : hint_range( 0.01, 1.0 ) = 0.012;
	uniform float sun_blur : hint_range( 0.01, 20.0 ) = 0.01;

uniform lowp vec2 starpos_array[1000];


void sky() {
	float color = 0.0;
	COLOR = vec3(color);
	float _sun_distance = 0.0;
	if( LIGHT0_ENABLED )
	{
		_sun_distance = distance( EYEDIR, LIGHT0_DIRECTION );
		// Bigger sun near the horizon
		float _sun_size = sun_size + cos( LIGHT0_DIRECTION.y * PI ) * sun_size * 0.25;
		// Finding sun disc and edge blur
		float _sun_amount = clamp(( 1.0 - _sun_distance / _sun_size ) / sun_blur, 0.0, 1.0 );
		if( _sun_amount > 0.0 )
		{
			// Changing color of the sun during sunset
			vec3 _sun_color = sun_color;
			// Leveling the "glow" in color
			if( _sun_color.r > 1.0 || _sun_color.g > 1.0 || _sun_color.b > 1.0 )
				_sun_color *= _sun_amount;
			COLOR = mix( COLOR, _sun_color, _sun_amount );
		}
	}
	float _star_distance = 0.0;
	for (int i = 0; i < starpos_array.length(); i++) 
	{
		_star_distance = distance( SKY_COORDS, starpos_array[i] );
		if (_star_distance<0.0001)
		{
			COLOR = vec3(1.0,1.0,1.0);
		}
	}
}

The star x-y positions are given randomly by this simple loop:

    static class RandomStarfield
    {
        public static Godot.Vector2[] Generate(int Nstars)
        {
            Random random = new Random(123);
            List<Godot.Vector2> list = new List<Godot.Vector2>(Nstars);   
            for (int i = 0; i < Nstars; i++)
            {
                float x = (float)(random.NextDouble() - 0.5) * 2;
                float y = (float)(random.NextDouble() - 0.5) * 2;
                list.Add(new Godot.Vector2(x, y));
            }
            return list.ToArray();
        }
    }

This creates a sun and 1000 stars, where I feed in the star positions using a uniform array. This works in the sense that stars have fixed positions, but performance is very poor, probably because for each pixel the shader has to run through a for loop of size 1000 and do 1000 distance calculations.

Already at 1000 stars, this halves the FPS of my game from 60 to 30, and ideally I would want way more stars than this, and more detail, maybe adding stars of different sizes etc.

Ideas I have had to improve the shader performance is to sort the input for for instance the x-coordinate, and maybe implement some sort of search algorithm like binary search, and repalcing the for-loop with a do/while of some sort. Fundamentally you will still have to do an awful lot of distance calculations, so I was wondering if anyone has any better suggestions, because I feel like I am maybe approaching this issue in the wrong way, seeing as the number of stars will be massive.

(I have looket at godotshaders.com, but have not found anything that solves this issue. )

Yeah, looping through thousands of sky for every pixel is way too much :slight_smile:

Luckily, you can approach this differently. You can divide the texture space ( or whichever space you want ) into a grid with cells of a fixed size. From there you can determine the cell’s grid coordinates ( i.e. divide your uv’s with the cell size and floor them ).

You can hash this cell coordinate to get random values, which you can use to determine the star’s position inside the cell. You can even use a hash that provides one extra value, which you can use as a random chance to determine if there’s a star in that cell or not. This lets you control star density.

You can also apply multiple layers of this, with varying cell- and star sizes, to create layers of different star types. You can also modulate your random chance value with some predetermined function, to create your milky way.

1 Like

You can hash this cell coordinate to get random values, which you can use to determine the star’s position inside the cell. You can even use a hash that provides one extra value, which you can use as a random chance to determine if there’s a star in that cell or not. This lets you control star density.

That is pretty clever! I will give it a go. Thanks!

2 Likes

Good luck! For hash functions, I tend to use these two links:

1 Like

I played around with this yesterday, and I can confirm that this solution gave much better performance. Thanks to u/elementbound for the good input.

The issue I found was that the grids in UV-coordinates are not in equal area across y = [0,1]. At the very top and very bottom of the sky-sphere all gridlines eventuall meet, and very close to this point the grids are very small, thus to make an even starfield is a bit fiddly, because you probably need to vary the density of stars based on the y-value, close ot y=0 and y=1 the density will need to be smaller.

1 Like

Right, I’m assuming because the UVs are wrapped around on a sphere. You can convert the UVs to the coordinates on the sphere surface ( use u * 2 * pi, lerp(-pi/2, pi/2, v) as polar coordinates ), and then hash those. That will probably give you a more even distribution.

Either way, I think you’re pretty much set for now, happy to hear the hashing approach worked!

you can use ChatGPT or Aria artificial intelligence to convert these shaders into Godot shaders, you will get some errors but most of the conversion is good.