Rending High Quality Atmosphere On The Cheap
James Dolan - jamesdolan.com
My apologies in advanced... I have gotten a lot of questions from people about my atmosphere technique
and never got around to really posting anything about it... I still don't have much time to write something
up so instead I am posting the code and some sample output. It's not a complete project (since when I originally
wrote it, it was tied up in another, very large project). Anyways, here it is...
Just to summarize, this technique leverages heavily on pre-computed data where other technique are either fully dynamic
(usally using some sort of shadow map/buffer/whatever to simulate light being occluded on the far side of the
planet) or are fully static (just texturing the atmosphere or using vertex colors). This technique does an offline brute force
ray march through the atmosphere accumulating light at each point and saves it off to a texture. Because its not just a texture of
the atmosphere but light accumulation at different phases relative to the viewer and light source, its able to still
behave very dynamically (i.e., the light can be dynamically updated and the viewer can view it from many different
angles and see a very volumetric atmosphere without rebuilding the atmosphere texture).
The downside to this technique is it only works with sphereical planets... if you want mountain ranges to affect the
light occlusion and scattering, this won't really work for you (maybe a hybrid approach is possible?). But in general,
when viewing planets from space, such details are not visible from outside the atmosphere anyways.
Screenshots
Thick atmosphere.
Thin atmopshere.

Sample output from the Pre-Compute step. This was used to render the "Thin" atmosphere above.
Offline Pre-Compute
static const uint32 g_numComponents = 3;
static vec4 calcColor(const vec3 &surfNormal, const vec3 &surfToEye, const vec3 &surfToLight, real planetRadius, real atmosphereRadius)
{
vec4 color(0);
const vec3 r0 = surfNormal * atmosphereRadius;
const vec3 eyeDir = -surfToEye;
const vec3 lightDir = surfToLight;
real t1 = 0;
// find the point where we exit the atmosphere...
real a = dot(eyeDir,eyeDir);
real b = 2 * dot(r0, eyeDir);
real c = dot(r0,r0) - atmosphereRadius*atmosphereRadius;
real ans0, ans1;
if(xen::quadratic(ans0, ans1, a, b, c))
{
if(ans0>t1) t1 = ans0;
if(ans1>t1) t1 = ans1;
}
// see if we intersect the planet as well...
c = dot(r0,r0) - planetRadius*planetRadius;
if(xen::quadratic(ans0, ans1, a, b, c))
{
if(ans0<t1) t1 = ans0;
if(ans1<t1) t1 = ans1;
}
// march along the ray accuulating light.
const real marchDist = 0.005f;
const vec4 lightScale = vec4(0.1f, 0.1f, 0.3f, 0) * (marchDist);
for(real t=0; t<t1; t+=marchDist)
{
const vec3 currPos = r0 + eyeDir * t;
a = dot(lightDir,lightDir);
b = 2 * dot(currPos, lightDir);
c = dot(currPos, currPos) - planetRadius*planetRadius;
ans0 = ans1 = 0;
if(!xen::quadratic(ans0, ans1, a, b, c) || (ans0 <= 0 && ans1 <= 0))
{
const real alt = (length(currPos) - planetRadius) / (atmosphereRadius - planetRadius);
const real airdensity = pow(1-alt, 3);
color += lightScale * airdensity;
}
}
return saturate(color);
}
//c = findVectorFromDot(a, b, d);
// dot(a, c) == d
static vec3 findVectorFromDot(const vec3 &a, const vec3 &b, real d)
{
vec3 r = cross(a, b);
mat3x3 t;
t.euler(r*acos(d));
vec3 c = t * a;
return c;
}
static bool generateAtmosphere(uint8 *buffer, uint32 size, real planetRadius, real atmosphereRadius)
{
const vec3 surfNormal(0,0,1);
const vec3 surfTangent(1,0,0);
for(uint32 y=0; y<size; y++)
{
const real ndl = (((real)y) / (real)(size-1)) * 2.0f - 1.0f; // == dot(surfNormal, surfToLight);
const vec3 surfToLight = findVectorFromDot(surfNormal, surfTangent, ndl);
for(uint32 x=0; x<size; x++)
{
const real eds = ((real)x) / (real)(size-1); // == dot(surfNormal, surfToEye);
const vec3 surfToEye = findVectorFromDot(surfNormal, surfTangent, eds);
vec4 color = saturate(calcColor(surfNormal, surfToEye, surfToLight, planetRadius, atmosphereRadius));
buffer[0] = (uint8)(color.x * 255);
buffer[1] = (uint8)(color.y * 255);
buffer[2] = (uint8)(color.z * 255);
buffer += g_numComponents;
}
}
return true;
}
bool savePNG(FILE *outfile, uint32 width, uint32 height, unsigned char *rgb)
{
png_structp png_ptr;
png_infop info_ptr;
png_bytep row_ptr;
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);
if(!png_ptr)
{
return false;
}
info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr)
{
png_destroy_write_struct(&png_ptr, (png_infopp) NULL);
return false;
}
png_init_io(png_ptr, outfile);
png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
for(uint32 i = 0; i < height; i++)
{
row_ptr = rgb + 3 * i * width;
png_write_rows(png_ptr, &row_ptr, 1);
}
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
return true;
}
bool atmosphereTool(uint32 numArgs, const char *const*args)
{
bool ok = false;
allocator_default alc;
uint32 size = 0;
real planetRadius = 0;
real atmosphereRadius = 0;
const char *outputPath = 0;
ok = numArgs == 4 ? true : false;
// read texture size...
if(ok && args[0])
{
int32 i = ascii_toint(args[0]);
if(i>0) size = (uint32)i;
else ok = false;
}
// read planet radius...
if(ok && args[1])
{
real f = ascii_toreal(args[1]);
if(f>0) planetRadius = f;
else ok = false;
}
// read atmosphere radius...
if(ok && args[2])
{
real f = ascii_toreal(args[2]);
if(f>planetRadius) atmosphereRadius = f;
else ok = false;
}
// read the output path...
if(ok && args[3])
{
if(*args[3]) outputPath = args[3];
else ok = false;
}
if(ok)
{
printf("Running atmosphere tools... this may take a long time to compute.\n");
printf(" texture size = %dx%d\n", size, size);
printf(" planet radius = %f\n", planetRadius);
printf(" atmosphere radius = %f\n", atmosphereRadius);
printf(" output path = \"%s\"\n", outputPath);
uint8 *buffer = (uint8*)XEN_ALLOC(alc, sizeof(uint8)*size*size*g_numComponents);
if(!buffer)
{
printf("ALLOCATION FAILURE!\n");
ok = false;
}
if(buffer)
{
ok = generateAtmosphere(buffer, size, planetRadius, atmosphereRadius);
if(ok)
{
FILE *file = 0;
#if defined(_CRT_INSECURE_DEPRECATE)
fopen_s(&file, outputPath, "wb");
#else
file = fopen(outputPath, "wb");
#endif
if(file)
{
savePNG(file, size, size, buffer);
fclose(file);
}
else
{
printf("Failed to open file for write!\n");
ok = false;
}
}
XEN_DEALLOC(alc, buffer);
}
}
if(!ok)
{
printf("atmosphere <size> <planetRadius> <atmosphereRadius> <outputPath>\n");
}
return ok;
}
Runtime Fragment Shader (could also be done in a vertex shader)
fragment fragment_main(xen_surface surface, uniform sampler2D texture0 : TEXUNIT0)
{
fragment output;
#if defined(PASS_DIRECTIONAL_LIGHT)
const float3 surfToEye = normalize(xen_state.eye.worldSpacePosition - surface.worldSpacePosition);
const float3 surfToLight = xen_state.light.worldSpaceDirection;
float2 texcoord;
texcoord.x = dot(surface.worldSpaceNormal, surfToEye);
texcoord.y = dot(surface.worldSpaceNormal, surfToLight)*0.5+0.5;
output.color = tex2D(texture0, texcoord);
#else
// we should't get here! And if we do we don't want to contribute any more color.
// TODO: Support more light types.
output.color = 0;
#endif
return output;
}
Renderer State
As for setting up your renderer state... I personally just rendered the atmosphere with additive blending, this way I could
layer many passes for many lights if I needed. But this could certainly be done many different ways.
|