Procedural Gas Giant Rendering with GPU Noise

 

The title sounds scary, but I assure you that you can do this too!

In the main storyline for Seed of Andromeda the player crash-lands on Aldrin, which is a moon of the gas giant Hyperion. In order to provide the player with a dazzling scene from the surface of Aldrin, we needed a way to render beautiful gas giants in real time. We also needed to make sure it was easy to manually create or randomly generate new gas giants so that modders can create their own unique star systems with relative ease. In this blog I will walk you step by step through the process of creating your very own procedural gas giants, assuming that you have little to no knowledge of procedural generation.

Before you start:

• Example code will be in C++ or GLSL, and I assume you at least know enough OpenGL to create and use shader programs, and upload meshes. If you don't, and you wish to learn C++ and OpenGL for game development, I will be your guide!

• Rapid iteration is important, so implement a keypress in your game that will reload the shaders from file. Otherwise, you will have to restart the program every time you make a modification! Data driven design ftw!

John Wiggham's blog also details a gas giant rendering scheme, but his method is a bit different, and he doesn't go into quite as much detail about the basics. If you hate this blog, maybe you will like his.

• The Y axis goes up!

Let's begin shall we?

 

Step 1: Sphere

Let's start simple by making a giant sphere! I chose to use a generated icosphere mesh with 5 subdivision passes. How do you make an icosphere generator? You steal it from us of course! In the future you could change the level of detail based on distance, but for now this seems fine. I used a model matrix to scale the icosphere to 154,190 KM, a realistic gas giant size.

Rather boring isn't it? Let's add some diffuse lighting! How about a directional light behind the camera?

We could stop here, and say that in our universe only solid color gas giants exist, but that's no fun.

Step 2: Color

Let's take a look at a real gas giant and see if we can get any inspiration.

[NASA/JPL/Arizona University]

Thanks to our pals at NASA and their Cassini-Huygens probe for this stunning true color image of Jupiter! If we want to render something like this, we need to note the general characteristics and try to emulate them as efficiently and accurately as we can. You will notice that it is striped with horizontal bands of color of varying sizes. Let's start there!

Let's try using a simple 1D color map texture, where the texture coordinate goes from 0.0 at the south pole to 1.0 at the north pole of the sphere. If the base icosphere mesh (before scaling) has a radius of 1, the following snippet calculates the texCoord for each vertex.

for (int i = 0; i < vertices.size(); i++) {
    vertices[i].texCoord = (vertices[i].pos.y + 1.0f) / 2.0f;

}

Here is a juptitery looking color map that Frank McCoy made. Normally its 2048x1 but I stretched it a bit so you could see it.

When we sample this texture using the texCoord in the fragment shader, we get this.

We're getting somewhere, but Jupiter is WAY cooler than this. It's a good thing Ken Perlin invented gradient noise!

Step 3: Noise

Any time you need to generate realistic looking terrain or textures, always look to gradient noise! I could go heavy into detail about how to implement a gradient noise function, but there's already plenty of good explanations on the internet. Instead I'll just give the ELI5 version and then link you to some code, cause come on.. do you really want to implement all this confusing mathy stuff yourself? Thought so...

What the dickens is a gradient noise function?

In short, a gradient noise function is a type of function that takes in a position and spits out a number typically between -1.0 and 1.0. Noise functions are typically implemented as 2, 3, or 4-dimensional, but they can be defined for any number of dimensions. The functions typically resemble something like this:

float noise2D(float x, float y) {

    ...

}

 

float noise3D(float x, float y, float z) {

    ...

}

 

float noise4D(float x, float y, float z, float w) {

    ...

}

Loop through each pixel in a blank square image. At each pixel, pass the x,y into a 2D noise function, and you will get a number from -1 to 1. You can scale this value to the range of 0-255 with the following snippet:

float pixelColor = ((noiseOutput + 1.0f) / 2.0f) * 255.0f;

Set the greyscale color of the pixel to pixelColor, and you might get something like this:

Pretty nifty huh? It sorta looks like hills. Games such as MineCraft that have procedurally generated terrain or textures almost always use gradient noise, because there's a lot you can do with it! Well learn about some ways to manipulate noise later on. Though if you want to easily get powerful procedural noise support in a C++ application, you can use the popular libnoise.

Popular noise functions

There's a bunch of noise functions out there, but I'll list some of the popular algorithms that I know of here.

Perlin Noise: The first gradient noise function, made by the almighty Ken Perlin. Its pretty quick in 2D or lower, but it can get slow at higher dimensions and can show some ugly artifacts. I don't recommend this one, but it works for MineCraft. Pseudocode with explanation.

Improved Perlin Noise (Simplex Noise): Also made by Ken Perlin, but it addresses some of the issues in his original implementation. It's faster and better looking than Perlin Noise. Unfortunately, Simplex Noise is patented for dimensions > 2, but only for use specifically in Texture Synthesis. I'm fairly certain this means you can use Simplex Noise to generate terrain or normal textures, but I am not a lawyer. Everyone and their grandmother seems to use it anyways... so do with that information what you will. I absolve myself of liability. C++ and Python Implementation.

 • OpenSimplex Noise: This one was made by Kurt Spencer in order to get around the patent for Simplex Noise. It's open sourced and free to use, but it is a tad slower than Simplex Noise. It is still better than Perlin though. It doesn't lend itself well to a GPU implementation due to it's excessive branching. Java Implementation.

 • Simplectic Noise: Michael Powell made this one in response to OpenSimplex noise. He claims that it's faster than OpenSimplex, but I have not verified it. Rust Implementation. The author of Simplectic noise has admitted that it's worse than OpenSimplex.

GPU Noise

Computing noise on the CPU isn't ideal since it's embarrassingly parallel, and the CPU can only process a single sample at a time. If we instead generate the noise on the GPU, we can process lots of samples at the same time and get an enormous speedup, and we can generate noise in screenspace in real time!

If you want to implement a noise function on the GPU, you can use OpenCL, CUDA, or a compute shader (OpenGL 4.3+), or you can simply use a regular shader program with GLSL or HLSL. Since we want per-pixel noise to be generated in real time for our gas giant, we'll use GLSL noise.

You can either implement one of the above algorithms yourself in GLSL, or you can grab the incredibly handy Ashima Simplex GLSL code. Keep in mind this is an implementation of the patented Simplex Noise, even though Ashima released it under the MIT license, though since we aren't using this for Texture Synthesis we should be good. Again... I'm not a lawyer. This isn't the fastest or best quality GLSL implementation, but it's definitely the easiest to pop into your projects, since it requires no setup whatsoever!

EDIT: Here are some other implementations of GLSL noise. I haven't compared quality or performance.

We are going to start with 3D noise for now. We need to use a 3D vector as an input into our noise function, so we can just use the same normal vector that we used for lighting.

Behold gas_giant.frag! Notice that we have GLSL #include support so we don't have to copy paste the noise code everywhere. I highly recommend implementing it in a shader parser for your engine!

// Uniforms

uniform sampler1D unColorLookup;
uniform vec3 unLightDir;

 

// Inputs
in vec3 fNormal;
in float fTexCoord;

 

// Ouputs

out vec4 pColor;

 

#include "Shaders/Noise/snoise3.glsl"

 

float computeDiffuse(vec3 normal) {
    return clamp( dot( normal, unLightDirWorld ), 0,1 );
}

 

void main() {

    // Preturb texture coordinate with noise

    float n = snoise(fNormal);

    float newTexCoord = fTexCoord + n;

    // Lookup the texture

    vec3 texColor = texture(unColorLookup, newTexCoord).rgb * computeDiffuse(fNormal);

    // Output color to pixel

    pColor = vec4(texColor, 1.0);

}

Let's see what we get:

Oh dear... There's two problems here. The first is that the noise has too low of a frequency, causing the noise to have huge wavelengths. We can solve this by multiplying fNormal by a constant.

float n = snoise(fNormal * 100.0);

The other problem is that n is influencing the texture too much. snoise returns a number between -1 and 1, and when we add that to our texCoord on the range of 0 to 1, it's far too drastic. We need to reduce the amplitude by multiplying it with a constant < 1.0.

float n = snoise(fNormal * 100.0) * 0.01;

That's much better, but it looks all splotchy and ugly, and if we zoom in close to the surface the detail is appalling. The solution? Fractals!

Step 4: Fractal Noise

fractal is an infinitely repeating pattern that looks about the same at every scale. Meaning, if you zoom in 10000x on a fractal image, it would resemble the same shape that it did at 1x magnification. The reason we care is that fractal patterns can  simulate natural phenomenon surprisingly well.

Think of it this way. With a single snoise sample, you could generate a hill. If you did a second pass at half the frequency and amplitude, you could add 4 half sized hills on top of that first hill. Another pass and you have 16 even smaller hills on top of those 4 hills on top of that first hill. See where this is going? We can't make infinite smaller hills without some really clever trickery, but we can make a lot of them, enough to where the hill looks large from space, and when you get close you see small details.

Let's implement a Fractal Noise function, also known as multi-octave noise.

 float noise(vec3 position, int octaves, float frequency, float persistence) {
    float total = 0.0; // Total value so far
    float maxAmplitude = 0.0; // Accumulates highest theoretical amplitude
    float amplitude = 1.0;
    for (int i = 0; i < octaves; i++) {

        // Get the noise sample
        total += snoise(position * frequency) * amplitude;

        // Make the wavelength twice as small
        frequency *= 2.0;

        // Add to our maximum possible amplitude
        maxAmplitude += amplitude;

        // Reduce amplitude according to persistence for the next octave
        amplitude *= persistence;
    }

    // Scale the result by the maximum amplitude
    return total / maxAmplitude;
}

Octaves is the number of iterations, or the depth of the fractal. The more octaves, the better quality, but performance suffers. Keep this number as small as you can.

Frequency determines the wavelength of your noise. A low frequency means a few wide hills, and a high frequency means many skinny hills. Each successive octave doubles the frequency, which is what gives us the 4 hills on top of 1 in the above example.

Persistence determines how much each successive octave affects the end result. If it's 1.0, then every octave holds the same weight. If it's 0.0, then only the first octave does anything. At 0.5, each successive octave applies half as much weight to the end product. A good value is typically in the range of 0.8, depending on what you are trying to make.

Remember that the number of octaves determines how slow your shader will run. The key is to minimize octaves while maximizing quality. You will have to keep playing around with different noise functions to find a healthy balance between quality and speed, unless you aren't working in real-time. Note that since all the noise is calculated in the fragment shader, we are fill rate limited, so it will be slower at higher resolutions, or when the camera is closer to the planet. Take care to reduce overdraw.

Let's change our n variable to use 6 octaves of fractal noise with frequency 0.1 and persistence 0.8:

float n = noise(fNormal, 6, 0.1, 0.8) * 0.01;

Now we're talking! It looks much more natural, and the detail stays good as you get closer to the surface. We could do with a bit more variation though. Let's try to improve it even further by leveraging Ridged fractal noise, which is typically used for making mountains. The ridgedNoise() function is exactly the same as the noise() function, except for the line where we actually get the noise sample:

total += ((1.0 - abs(snoise(position * frequency))) * 2.0 - 1.0) * amplitude;

ridgedNoise() produces values that are biased slightly positive, so I subtract 0.01.

float n1 = noise(fNormal, 6, 10.0, 0.8) * 0.01;
float n2 = ridgedNoise(fNormal, 5, 5.8, 0.75) * 0.015 - 0.01;

float n = n1 + n2;

It doesn't look like Jupiter, but it looks believable, and that's what matters! Theres other cool ways to manipulate noise, here are a few more for your experimental needs.

Step 5: Storms

Jupiter has a few large storms dotting its surface, but how can we achieve that effect using only noise? Any noise we use gets applied to the entire surface.

The trick is to use a threshold. We need to define an area where only storms can appear. If we take a low frequency noise sample, subtract a constant, then clamp it so that negative numbers become zero, we can use the output as a multiplier for storms.

Can you guess what image editor I used?

However, just a single threshold noise sample doesn't produce good results, since the resulting shapes often won't be round. Instead we should intersect three threshold functions, which will usually result in a round-ish shape.

To test our threshold, we can add it to our red color channel. I am only using a single octave for each threshold function, so we can just use the raw snoise() function. Notice that for t2 and t3 I am adding a constant to fNormal. Otherwise, they will all sample the same location and result in the same output.

// Get the three threshold samples

float s = 0.6;
float t1 = snoise(fNormal * 2.0) - s;
float t2 = snoise((fNormal + 800.0) * 2.0) - s;
float t3 = snoise((fNormal + 1600.0) * 2.0) - s;

 

// Intersect them and get rid of negatives

float threshold = max(t1 * t2 * t3, 0.0);

 

...

 

// Add to red color channel for debugging

pColor += vec4(threshold * 3.0, 0.0, 0.0, 0.0);

Our gas giant hit puberty! We can turn those zits into storms with an additional low-frequency noise sample.

// Storms

float n3 = snoise(fNormal * 0.1) * threshold;

float n = n1 + n2 + n3;

Works like magic! Except math... not magic. Note that it only took 4 noise samples to make the storms, you could probably get better quality storms by tweaking the parameters and/or using fractal noise.

Step 6: Time

Since we are rendering the noise in real time, we can make it evolve! Pass in an additional uniform variable unTime that gets incremented on the CPU every frame. unTime should increase VERY slowly. Keep in mind that you might hit precision issues eventually, in which case it might be a good idea to wrap unTime back to 0 or even reverse its direction.

You could use 4D noise with the 4th parameter being unTime, but it looks much more natural and is cheaper to instead offset fNormal in the X and Z direction by unTime. The video embedded at the bottom of this blog shows the evolution in action.

Here is the full noise description using time:

// Use fNormal + time for evolution

vec3 position = fNormal + vec3(unTime, 0.0, unTime);

 

// Base noise

float n1 = noise(position , 6, 10.0, 0.8) * 0.01;
float n2 = ridgedNoise(position , 5, 5.8, 0.75) * 0.015 - 0.01;

 

// Get the three threshold samples

float s = 0.6;
float t1 = snoise(position * 2.0) - s;
float t2 = snoise((position + 800.0) * 2.0) - s;
float t3 = snoise((position + 1600.0) * 2.0) - s;

 

// Intersect them and get rid of negatives

float threshold = max(t1 * t2 * t3, 0.0);

 

// Storms

float n3 = snoise(position * 0.1) * threshold;

float n = n1 + n2 + n3;

Step 7: Atmosphere

This step is optional. You can add some atmospheric scattering to your gas giants to make them look more colorful. The scattering method I used is Sean O'Neals Accurate Atmospheric Scattering, which is also good for planet rendering. You can download his example source here. I computed the scattering color in the vertex shader and passed it to the fragment shader, then added it to pColor. To get the orangey color I had to mess with the light wavelength parameters. I probably could have done a better job but whatever.

I also rendered an outer atmosphere layer using a second, larger icosphere with opposite face culling, which is detailed in the O'Neal article. For the outer atmosphere I instead computed the scatter color in the fragment shader in order to get a better quality image. Note that you can reuse the same icosphere mesh for the outer atmosphere by just scaling it differently.

 

Neat!

Step 8: Performance Optimization

Our sexy gas giant calls snoise() 15 times. On a decent GPU this is no issue, but on lower end hardware it can be problematic, especially at high resolution. If you would like to further optimize the renderer, you can try the following:

• Make the number of octaves vary based on distance from the planet for fractal noise. This doesn't help the worst case, when you are really close to the surface.

• Allow users to lower the detail in the options menu, which will reduce the number of octaves or switch to a different method entirely.

• Instead of rendering the noise to the icosphere every frame, use an imposter billboard and render the noise to it once every few frames, or only when the camera moves drastically. John  Wiggham uses a raytraced billboard in his implementation, which could possibly be extended for this.

• Reduce the vertex count of the icosphere based on distance. (Probably won't help since we're fill rate limited)

• Use a faster GLSL noise implementation, like one with pre-computed tables as textures.

• Try John Wiggham's method. He uses a volume texture to reduce the number of noise samples needed for a convincing image.

• Buy quad GTX Titans and wire em up in SLI, then render 10000 octaves of noise and laugh at the filthy peasants.

Step 9: Rings

For this one I'll let John Wiggham take over, since he provides a great explanation and it would be pointless for me to reiterate.

Random Gas Giants

If you want to randomly generate gas giants, you will most likely want a separate fragment shader for each one. Since GLSL shaders are compiled from text, with clever programming you can generate random strings with different noise functions and values and then compile them each into separate fragment shaders. Alternatively, you can use a single fragment shader and just pass in different uniform variables, but you will get more control with separate shaders.

You will also need to randomly generate the color lookup textures. Theres many ways to do it, one being to randomly place N pixels of a random color pallet on the 1D texture and then fill in the gaps with linear interpolation.

Conclusion

Our end result doesn't look quite as incredible as the Jupiter photo, but it looks fantastic considering how easy it was to make. I didn't spend a whole lot of time messing with the values, so I am sure you can come up with much better looking gas giants than this! I hope you all learned something about noise, and how it can be used in creative ways to model the universe!

Video

 

Keep a look out for the next technical blog, which will detail procedural star rendering.

Benjamin - Lead Developer