Procedurally Generated Nebulae in Unity3D

Published Jun 09, 2015Last updated Feb 09, 2017

A moon orbits Neptune, backed by a colorful nebula

The Quest Begins

One of the goals I had for the visuals of Luna's Wandering Stars was a beautiful background. Starting out, the game prototype had backgrounds created by yours truly, one for each of the worlds in the game. They weren't bad, but I saw room for improvement.

Hand-crafted background

I yearned for a more dynamic background, something more varied than a static image. Space is vast, and there are many astounding structures that litter its expanse. To limit all you saw to a few stock images, no matter how beautiful, seemed to me a waste of an opportunity. One of the allures of space for myself, and many others I'm sure, is the possibility of discovery, and of seeing new sights. So I decided that a procedurally generated background would be the way to go. Even if the background was limited to a single category of imagery, at least it would be varied.

I spent some time looking for ways to render beautiful nebulae. I didn't find much. There were some examples that looked amazing, but were pre-rendered, or kept their methods secret. Simple solutions and tutorials didn't look nearly as interesting as I wanted, and scientific papers with various rendering techniques required too much processing power. So, I decided to make my own method.

Noise

I needed a starting point, so I began with the classic method of Perlin Clouds. I'd found numerous tutorials that started and ended with Perlin Clouds, so it seemed to be a decent starting point. Perlin Clouds are created by adding different resolutions of Perlin Noise together.

The basic idea is simple. You take some big circles, slap on some smaller circles, and then some even smaller circles, and then your image will look somewhat like a cloud. Perlin Noise and Perlin Clouds are covered in many tutorials across the web (I recommend this one), so I won't be covering them in this tutorial. They're definitely powerful and useful tools, and I'd recommend learning about them if you haven't already.

bicubic Perlin noise

Above is an example of my noise generator's output. It's just Perlin noise added together, and uses bicubic interpolation to reduce how blocky and square it looks. Since it's about to go through a lot of processing, it's not as cloudy or smooth as you might see in a normal Perlin Cloud example.

In actuality, the full image looks like this:

All color channels for Perlin Noise

This is because each of the color channels (Red, Green, Blue) have their own separate pattern. I'm actually putting 3 different black-and-white patterns into this image by making one pattern black-and-red, another black-and-green, and the third black-and-blue. The final image holds all 3 patterns, all I need to do is only look at one color at a time. This way, I can get more information in each texture I use. A solid starting point.

Distortion and Shaders

One of the most interesting applications of Perlin noise I've seen is a simple marble texture. By offsetting the values of a cosine function with Perlin noise, one can take the simple stripes of cos(x) across a plane, and transform them to look convincingly like marble. I decided to take inspiration from this idea, and start distorting my clouds with more noise.

At this point, it becomes necessary to use shaders. Shaders are programs that tell computers how to choose a pixel color, given that a certain object is filling up that pixel. In the simplest form, it just returns a single color: the flat color of the object the computer's trying to display. Stepping up in complexity, you can add shading to the object (thus their name), images to place on the surface of the object, and even distort the object.

This effect is only one of an endless assortment of awesome things that can be accomplished with shaders. The fundamentals of writing shaders is out of the scope of this tutorial, but you should be able to understand the thought process behind the effect without too much knowledge of how to write the shader.

For my own purposes, I was interested in distorting the image on the surface of an object ( in this case a flat plane ). For this, I simply had two textures on my surface. One was used to choose a color for the screen, and the other was used to distort the texture coordinates on the first texture. In CG, the fragment or pixel function (which tells the computer what color to make a pixel) looks something like this:

float4 frag(v2f i) : COLOR{
  //Get the appropriate color value from the first texture
  half4 col = tex2D(_MainTex,i.tex.xy * _MainTex_ST.xy + _MainTex_ST.zw);
  //Offset the coordinates for the second color value
  //Then get the color value from the second texture
  float2 offset = float2(_Distort*(col.r-.5),_Distort*(col.g-.5));
  half4 col2 = tex2D(_MainTex2,i.tex.xy * _MainTex2_ST.xy + _MainTex2_ST.zw + offset);
  //Return the grayscale value in the red channel
  return col2.a;
}

First, I read a color from the main noise texture. Then I use that value to offset the texture coordinates when reading a color from the second noise texture. There's also a variable that controls how much distortion happens, which makes it simple to tweak how the end product looks.

Here's what it looks like in monochrome, with a distortion value of .3 :

Distorted Noise

Now we're getting somewhere! The pattern is a lot more interesting, and looks like its been flowing around a bit. A step up from blobs, if you ask me.

Masking

With the swirls and feeling of movement, we've got some nice texture for the nebulae. But we need to add some shape to it as well! Nebulae often have tendrils or tube-like structures, and this square of swirls doesn't quite have the right feel.

To fix that, we'll add a mask to the whole thing. A Mask simply hides parts of the image. So, we need some tendrils!

To create an appropriate mask, I wound up using a simple cellular automaton (think Conway's Game of Life). Shoutouts to Tom Blanchet, who found this method. The process itself is pretty simple. First, seed a grid with random values between 0 and 2. Then, have each value sum itself with its 8 neighbors. If the sum is greater than 5, change its own value to 1. Otherwise, change its own value to 0. Repeat several times, and you have some tendrils! A little blurring helps too.

Tendrils

As with the noise, I put 3 masks into a single image using the red, green, and blue color channels. With one mask texture, we can use quite a few different masks, if we want! If we don't need so many masks, we should just use a grayscale image instead of a color image.

Once we apply this mask to the swirls, our clouds of gas have more shape to them.

Masked Swirls

The cutoff is a bit too sharp and defined for clouds, so we can distort this a bit too. We'll just mask our mask with a different mask (in a separate color channel)! This essentially combines the two masks, and softens up their shapes a bit.

Cloudier masking

One major flaw remains with the shape of our nebula. It's a square! We'll add a simple round mask to hide the fact its a box.

Round Mask

Now that's a decent puff of cloud!

Coloring

Now that we've got a nicely shaped cloud, we need to add some color. I wound up just applying a gradient over a monochrome image. It was a simple way to have clouds with some color variation and depth to them.

Rather than using just the image above, though, I used one of the yet unused channels of the distorted noise. This creates color that's unrelated to the structure of the cloud, which looks a lot better in my opinion.

Colored puff

That's starting to look pretty good. But it's just one component of a full nebula! Nebulae contain various types of gas, and that has to be accounted for. I identified three different types of gas/structure I wanted to include. First, the large swaths of color that make up the bulk of a nebula. Next, dark clouds of matter that block light. They're a key part of some of the most gorgeous pictures of nebulae. Finally, some bright, detailed structures to add to the complexity.

These three different parts only require some changes to the coloring, blending, and brightness. Here's a puff with some details added to it.

Cloud with some details

Now our base components are finished! Time to build some nebulae!

Finished Fragment Shader

Let's take a look at the final shader's fragment function. It's not the most compact it could be, but in exchange it should be easier to read.

//fragment shader
float4 frag(v2f i) : COLOR{
   //Get the colors at the right point on the first texture
   half4 col = tex2D(_MainTex,i.tex.xy * _MainTex_ST.xy + _MainTex_ST.zw);

   //Use that to create an offset for the second texture
   float2 offset = float2(_Distort*(col.x-.5),_Distort*(col.y-.5));

   //Get the colors from the second texture, using the offset to distort the image
   half4 col2 = tex2D(_Tex2,i.tex.xy * _MainTex_ST.xy + _MainTex_ST.zw + offset);
   
   //Create a circular mask: if we're close to the edge the value is 0
   //If we're by the center the value is 1
   //By multipling the final alpha by this, we mask the edges of the box
   fixed radA = max(1-max(length(half2(.5,.5)-i.tex.xy)-.25,0)/.25,0);

   //Get the mask color from our mask texture
   half4 mask = tex2D(_MaskTex,i.tex.xy*_MaskTex_ST.xy + _MaskTex_ST.zw);
   
   //Add the color portion : apply the gradient from the highlight to the color
   //To the gray value from the blue channel of the distorted noise
   float3 final_color = lerp(_HighLight,_Color, col2.b*.5).rgb
   
   //calculate the final alpha value:
   //First combine several of the distorted noises together
   float final_alpha = col2.a*col2.g*col.b;

   //Apply the a combination of two tendril masks
   final_alpha *= mask.g*mask.r;

   //Apply the circular mask
   final_alpha *= radA;
   
   //Raise it to a power to dim it a bit 
   //it should be between 0 and 1, so the higher the power
   //the more transparent it becomes
   final_alpha = pow(final_alpha, _Pow);
   
   //Finally, makes sure its never more than 90% opaque
   final_alpha = min(final_alpha, .9);
   
   //We're done! Return the final pixel color!
   return float4(final_color, final_alpha);
}

Final Pieces

For the final background, we're going to need a lot more than just one patch of gas. Each patch is just a flat square with our shader rendering it. I ended up using around four patches for the major color swathes, and 12 each for the dark portions and the detailed, bright portions. Add in some randomization for position, scale, and rotation, and here's what we get:

Putting the pieces together

The dimming that's done in the shader is very important. Since the nebulae are background elements, they can't become too bright or they'll risk interfering with the foreground of the game.

Add in some stars...

Starry night

And for a final touch, make the background a different color. While unrealistic, it adds a lot to the final feel of the environment. In particular, it makes the dark sections of the nebula a lot more noticeable. The background color can also cause certain colors in the nebula to really stand out. The blue here makes the yellow clouds stand out a bit more.

Colored background

And we're done! With different color palettes, there's a distinct feel for each world, while allowing for a huge amount of variety as well. And best of all, as long as the noise and masks are pre-calculated (you only need a few of each), your computer doesn't have to struggle to create a stunning and dynamic background.

Mars, asteroids, a moon, and nebulae!

Thanks for reading!

Discover and read more posts from Takumi
get started
Enjoy this post?

Leave a like and comment for Takumi

1
1
1Reply
Jeremy Reed
2 years ago

Can you post the code to the entire shader? I’m trying to emulate your results and am having trouble writing the shader.

Get curated posts in your inbox

Read more posts to become a better developer