Sunday, January 24, 2010

Simple Flexibile Atmosphere Shaders

Uploaded a video of the effect

First, why no posting since September? Well, in October my wife went into pre-term labor so she was on bed rest and that meant I had to become a mom till our son was born late November. Then in early December it was holiday madness and new baby madness!

Slowly, after the holidays I started work on the planet again. I've mostly been converting it to an Ogre Plugin and refactoring it. I'll do a separate blog post about the architecture of the plugin later I hope, but for now this post is about my work in NVidia FX Composer 2.5 working on some flexible atmosphere shaders.

If you don't know, my earlier shaders were based entirely off Sean O'Neil's free atmosphere shaders. Now, Sean does things the right way, which is why his work looks so good! His shaders represent the atmosphere of the earth very accurately. The one thing that prevented me from cutting and pasting them into my project was they rely on your atmosphere radius being a certain ratio to the surface radius (an earth sized ratio). My atmosphere's are much exaggerated so it didn't really work and I was kind of lost in all the math.

Side note:If you're ever struggling with this stuff I highly recommend you contact him because he's very kind and helpful.

So I started work on a shader that isn't mathematically correct, it just uses a look up table for the blend colors based on 2 things:
- dot product between the camera and the light
- dot products between the two sphere intersections of a ray from the camera to the vertex being drawn and the light.

Here's the 32x16 look up table for the atmosphere shell.

On the horizontal axis values on the left are used when the point on the atmosphere is pointing away from the sun and values on the right are used when the point on the atmosphere is pointing toward the sun. The vertical axis values are for blending between points near the surface of the planet (bottom) and points at the edge of the atmosphere (top).

So here is a side view of the planet that shows the gradient well.

To calculate the color to use on the horizontal axis I use this:

float StretchAmount = 0.5;
ln = dot(lightDir, farVertexPosition);
lnn = dot(lightDir, nearVertexPosition);
lc = dot(lightDir, cameraPosition);

ln = max(0,ln + StretchAmount);
lnn = max(0,lnn + StretchAmount);

uv.x = saturate(ln + (lnn * lc)) * saturate(lc + 1.0 + StretchAmount);

So when the camera is between the planet and the sun ln will be 0 and lnn will be 1 and lc will be 1 so you'll get uv.x = 1 (the blue color of the atmosphere). When the camera is on the back side of the planet ln will be 1, lnn will be 0 so uv.x would be 1.0 (the blueish color of the atmosphere) from the back too except the saturate(lc + 1.0 + StretchAmount) bit scales the value based on lc, which would be -1, so -1 plus 1 + stretch amount = 0.5, so uv.x ends up being 0.5 and you end up seeing the redish colour.

The StretchAmount is used to determine where the sunset should begin.

I also multiple the color by the height so it fades out as the atmosphere gets thinner, and also multiply by another amount so values at the back of the sphere fade out. This value is defined by:

float StretchAmt2 = 0.5;

float ln2 = max(0,ln + StretchAmt + StretchAmt2);
float lnn2 = max(0,lnn + StretchAmt + StretchAmt2 );
float alpha = saturate(ln2 + (lnn2 * lc));

float finalAlpha = height * alpha;

I wanted to have the alpha fade out after the point at which the sunset starts and this allows for that. Notice it is basically identical to the uv.x calculation it just offsets the ln and lnn values so that I can control when to start fading out the atmosphere. If StretchAmt2 is 0 then the atmosphere will be faded out by the horizon, a value of 0.5 is what is used in the pictures and it lets the atmosphere fade out about a quarter way around the back of the planet.

The surface shader uses a smaller 32x2 gradient map.

The top row corresponds to the atmosphere color and the bottom row corresponds to the sun color. Horizontal values correspond to the dot product between the surface position and the sun position.

Currently the pixel shader color output comes from this:

outColor = (diffuse * sunColor) + (atmosphereColor * min(AtmosphereTransparency,attenuation)) * sunColor;

The AtmosphereTransparency is how transparent I want the atmosphere to be, and presently I'm using 0.4 based on personal preference.

Lastly, the attenuation factor is based on the distance from the camera to the vertex being drawn and the code to calculate it is this:

float vd = min(AtmosphereHeight,VisibilityDistance);
outAttenuation = saturate((cameraToVertexDistance - VisibilityDistance)/ (AtmosphereHeight + vd));

VisibilityDistance is the minimum visibility distance so you can adjust it for personal taste. A large visibility distance will give the atmosphere a very thin impression and a small visibility distance would be a very dense atmosphere.

Here's a boring picture from the surface:

Next step is to put these shaders into Ogre and see how they look. Until then, don't forget to read the NVidia GPU Guide and ATI GPU Guides every night before you go to bed.