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(pos.xyz,1); float4 Pw = mul(Po,WorldXf); float3 position = Pw.xyz; float4 camPos = float4(ViewIXf[3].xyz,1); oPosition = mul(Po, WorldViewProj); float radius = length(position); float radius2 = radius * radius; float camHeight = length(camPos.xyz); float3 camToPos = position - camPos.xyz; float farDist = length(camToPos); float3 lightDir = normalize(lightPos.xyz); 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(camPos.xyz, rayDir); float C = camHeight2 - radius2; float det = max(0.0, B*B - 4.0 * C); float nearDist = 0.5 * (-B - sqrt(det)); float3 nearPos = camPos.xyz + (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,-camPos.xyz / 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(lightPos.xyz - position.xyz); } 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(lightDir.xyz),normalize(camToPos)); float fCos = dot(lightDir.xyz,camToPos) * rsqrt( dot(lightDir.xyz,lightDir.xyz) * 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!