Atmosphere Shader Update And Tree/Grass Test

I’ve gone and made the atmosphere shader more complex, sorry. The basic idea is still pretty straight forward, but I added mie scattering to the pixel shader and added extra code to fix issues with the atmosphere being too transparent when near the surface of the planet (stars were showing through).
Also, I worked on pre-calculated/static shadow maps for the terrain and used those with the grass and trees. JohnJ created Paged Geometry, which is built for 2d heightmaps, but I semi-adapted it so that each face of the planet cube has its’ own paged geometry instance. Here’s a video of that:

Note: the atmosphere shader in this video is the old atmosphere shader so the atmosphere is still too transparent on the sunny side of the planet
It has issues like, the billboard trees don’t display right when viewed top down and they don’t fade out based on height like they should. Also there is an abrupt transition from one paged geometry instance to the next when you cross over from one cube planet face to another. I may just have to roll my own simplified version.
And now, here is the NVIDIA FX Composer shader for the current atmosphere shader I’m using:

// outer atmosphere radius
float AtmosphereRadius <
 string UIName =  "Atmosphere Radius";
 string UIWidget = "Slider";
 float UIMin = 0.0;
 float UIMax = 10000.0;
 float UIStep = 1.0;
> = {1200.0f};
// planet surface radius
float SurfaceRadius <
 string UIName =  "Surface Radius";
 string UIWidget = "Slider";
 float UIMin = 0.0;
 float UIMax = 10000.0;
 float UIStep = 1.0;
> = {1024.0f};
// this is the sun position/direction
float4 gLamp0DirPos : POSITION < // or direction, if W==0
    string Object = "Light0";
    string UIName =  "Lamp 0 Position/Direction";
    string Space = (LIGHT_COORDS);
> = {10.0f,10.0f,10.0f,1.0};
// this is the atmosphere 2d gradient
texture gTex  <
    string ResourceName = "AtmosphereGradient";
    string ResourceType = "2D";
    string UIName = "Gradient Texture";
sampler2D gTexSampler = sampler_state
    Texture = ;
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = Clamp;
    AddressV = Clamp;
// this is for setting where the horizon should fall on the sphere
float StretchAmt <
 string UIName =  "Stretch Amount";
 string UIWidget = "Slider";
 float UIMin = 0.0;
 float UIMax = 1.0;
 float UIStep = 0.01;
> = {0.25f};
// this is for mie scattering
float Atmosphere_G <
 string UIName =  "Atmosphere G";
 string UIWidget = "Slider";
 float UIMin = -1.0;
 float UIMax = -0.5;
 float UIStep = 0.001;
> = {-0.95f};
float4x4 WorldViewProj : WorldViewProjection;
float4x4 ViewIXf : ViewInverse;
float4x4 WorldXf : World;
void main2VS(
    float3 pos : POSITION,
    uniform float4 lightPos,
    out float4 oPosition: POSITION,
    out float2 oUV: TEXCOORD0,
    out float oAlpha: TEXCOORD1,
    out float3 oCamToPos: TEXCOORD2,
    out float3 oLightDir :TEXCOORD3
    float4 Po = float4(,1);
    float4 Pw = mul(Po,WorldXf);
    float3 position =;
    float4 camPos = float4(ViewIXf[3].xyz,1);
    oPosition = mul(Po, WorldViewProj);
    float radius = length(position);
    float radius2 = radius * radius;
    float camHeight = length(;
    float3 camToPos = position -;
    float farDist = length(camToPos);
    float3 lightDir = normalize(;
    float3 normal = normalize(position);
    float3 rayDir = camToPos / farDist;
    float camHeight2 = camHeight * camHeight;
    // Calculate the closest intersection of the ray with the outer atmosphere
    float B = 2.0 * dot(, rayDir);
    float C = camHeight2 - radius2;
    float det = max(0.0, B*B - 4.0 * C);
    float nearDist = 0.5 * (-B - sqrt(det));
    float3 nearPos = + (rayDir * nearDist);
    float3 nearNormal = normalize(nearPos);
    // get dot products we need
    float lc = dot(lightDir, camPos / camHeight);
    float ln = dot(lightDir, normal);
    float lnn = dot(lightDir, nearNormal);
    // get distance to surface horizon
    float altitude = camHeight - SurfaceRadius;
    float horizonDist = sqrt((altitude*altitude) + (2.0 * SurfaceRadius * altitude));
    float maxDot = horizonDist / camHeight;
    // get distance to atmosphere horizon - use max(0,...) because we can go into the atmosphere
    altitude = max(0,camHeight - AtmosphereRadius);
    horizonDist = sqrt((altitude*altitude) + (2.0 * AtmosphereRadius * altitude));
    // without this, the shift between inside and outside atmosphere is  jarring
    float tweakAmount = 0.1;
    float minDot = max(tweakAmount,horizonDist / camHeight);
    // scale minDot from 0 to -1 as we enter the atmosphere
    float minDot2 = ((camHeight - SurfaceRadius) * (1.0 / (AtmosphereRadius  - SurfaceRadius))) - (1.0 - tweakAmount);
    minDot = min(minDot, minDot2);
    // get dot product of the vertex we're looking out
    float posDot = dot(camToPos / farDist, / camHeight) - minDot;
    // calculate the height from surface in range 0..1
    float height = posDot * (1.0 / (maxDot - minDot));
    // push the horizon back based on artistic taste
    ln = max(0,ln + StretchAmt);
    lnn = max(0,lnn + StretchAmt);
    // the front color is the sum of the near and far normals
    float brightness = saturate(ln + (lnn * lc));
    // use "saturate(lc + 1.0 + StretchAmt)" to make more of the sunset side color be used when behind the planet
    oUV.x = brightness * saturate(lc + 1.0 + StretchAmt);
    oUV.y = height;
    // as the camera gets lower in the atmosphere artificially increase the height
    // so that the alpha value gets raised and multiply the increase amount
    // by the dot product of the light and the vertex normal so that
    // vertices closer to the sun are less transparent than vertices far from the sun.
    height -= min(0.0,minDot2 + (ln * minDot2));
    oAlpha = height * brightness;
    // normalised camera to position ray
    oCamToPos = -rayDir;
    oLightDir = normalize( -;
float4 mainBPS(
    float2 uv : TEXCOORD0,
    float alpha : TEXCOORD1,
    float3 camToPos : TEXCOORD2,
    float3 lightDir :TEXCOORD3,
    uniform sampler2D TexSampler
) : COLOR {
    const float fExposure = 1.5;
    float g = Atmosphere_G;
    float g2 = g * g;
    // atmosphere color
    float4 diffuse = tex2D(TexSampler,uv);
    // sun outer color - might could use atmosphere color
    float4 diffuse2 = tex2D(TexSampler,float2(min(0.5,uv.x),1));
    // this is equivilant but faster than fCos = dot(normalize(,normalize(camToPos));
    float fCos = dot(,camToPos) * rsqrt( dot(, * dot(camToPos,camToPos));
    float fCos2 = fCos * fCos;
    // apply alpha to atmosphere
    float4 diffuseColor = diffuse * alpha;
    // sun glow color
    float fMiePhase = 1.5 * ((1.0 - g2) / (2.0 + g2)) * (1.0 + fCos2) /(1.0 + g2 - 2.0*g*fCos);
    float4 mieColor = diffuse2 * fMiePhase * alpha;
    // use exponential falloff because mie color is in high dynamic range
    // boost diffuse color near horizon because it gets desaturated by falloff
    return 1.0 - exp((diffuseColor * (1.0 + uv.y) + mieColor) * -fExposure);
technique technique1 {
 pass p0 {
     ZEnable = false;
     ZWriteEnable = false;
     CullMode = CCW;
     AlphaBlendEnable = true;
     SrcBlend = One ;
     DestBlend = InvSrcAlpha;
     VertexShader = compile vs_3_0 main2VS(gLamp0DirPos);
     PixelShader = compile ps_3_0 mainBPS(gTexSampler);

Possible Improvements
– pre-calculate camera height, camera height squared and other such variables. you should notice that a lot of the vertex shader code would be the same for every vertex including calculating horizon distance, camera and light only based calculations and parts of the sphere intersection calculations.
– simplify mie scattering equation and remove hdr
– when the camera is inside the atmosphere we no longer need to calculate the atmosphere closest intersection so the program should switch to a simpler version of the shader once inside.
Known Issues
– transition from outer atmosphere to inner atmosphere is not smooth. I suspect this is due to my atmosphere being an un-realistic size so I had to add a tweak amount so that the transition point is actually a bit inside the atmosphere at a point where only the inside of the sphere is visible to the camera. At the point where the camera is the same height as the atmosphere, it can still see the outside of the atmosphere shell.
– when on the surface of the planet looking back at the horizon it looks more like a painted shell than a spherical haze
– when you get really close to the edge of the atmosphere on the side facing the sun there is an artifact that appears that I haven’t fixed yet.
– there are banding issues in the sky gradient when on the side of the planet facing the sun that I haven’t solved yet and I think they’re related to the color choices in my gradient, but I’m not sure.
If you have any suggestions, improvements or bug fixes please let me know in the comments!

, ,
9 comments to “Atmosphere Shader Update And Tree/Grass Test”
  1. Wow, looking great! It looks quiet polished and reminds me of a Black/White like game.
    You write that you fixed the issue that the atmosphere is too transparent within the atmosphere. Watching the video the stars and nebula appear still quiet visible on the sunny side. Is this intended?

  2. You’re right about the atmosphere shader in that video – I made that video before I had made the atmosphere fix. It really just shows the trees and grass on the planet. Also, in the new one the atmosphere isn’t 100% opaque when you’re on the sun side, it goes from 100% opaque at the horizon to about 50% transparent when looking straight up. But probably the horizon shouldn’t be 100% opaque and I might experiment with just making the skybox dim – not sure about this yet.

  3. I´m not an expert in this area but I think that the reason why we don´t see stars at day is just because the sky is much (e.g. 100x) brighter than all possible light sources out there. Except the moon and ofcourse the sun. So when computing the atmospheric effect it might be interesting to simulate the brightness (probably through HDR calculations or even a simplified approach) and leave the transparency more or less the same.

    Just an idea…

  4. How does the rendering goes?
    Do you draw an atmosphere first with the shader (without writing depth) and then the planet?
    As I understood from your previous post the planet surface has some shader too.
    Can you post it, please?

  5. Really nice shader, good job! I have few questions though, what is Po and Pw in the vertex shader? I assume you’re taking the position of the planet to world space? But then you do radius = length(position), how cold that possibly be the radius then? 🙂 Could you explain a bit what’s happening there? Thanks!

  6. @Icewolf
    Po is the original point position and Pw is the point position converted to world space. The radius is the distance from the center of the planet to the vertex in question. Does that help?

  7. @Icewolf

    I suppose I should mention that Po is the vertex position in object space so length(Po) is the radius of the atmosphere shell because every point in the atmosphere sphere shell is the same distance from the center of the sphere.

  8. Thank you very much Alex!

    I’m creating a planet renderer in Flash Player 11 which is coming along very nicely! I’ll make sure to show you some screenshot if everything works as expected. I’ve been converting the shader to AGAL which is the shader language Flash uses. It’s basically an instruction set so I’m at roughly 100 lines of instructions so far for the vertex shader 🙂

    Thankfully your shader seems simple enough since it has no for loops or if conditions. While I was converting the shader, some questions got in my mind. For example the line:
    height -= min(0.0,minDot2 + (ln * minDot2));

    Why is minDot2 being used there, while minDot is used everywhere else, as minDot = min(minDot, minDot2);

    Anyway, I can’t wait to try this after work and I’ll make sure to let you know how it goes 🙂 Thanks!

  9. Hi Alex!
    I’ve been trying to implement a atmosphere shader in my current project for the past few days, and after coming across your example I felt confident I could implement it. After using your example as a reference for an OpenGL version, I made one that, code wise, seemed to work. Sadly after implementing it nothing displayed to the screen. The only thing I could guess is my problem would be the TexSampler. What are you sending in for that sampler2D; an image, correct? If it is an image, then I am using that correctly… Have any ideas as to what could be my problem, from your experiences?

Comments are closed.