Hybrid approach for procedural planets
Part IX - Atmospheric Scattering
Atmospheric scattering, in principal, is as easy thing to understand. Light hits the atmosphere and is scattered by it. The scattering varies with the wavelength of the light. It can either be scattered towards the eye, or away from it. Hence skies are blue and sunsets red.
As usual when you start to look at it in detail, it gets kinda complicated.
There are a lot of tutorials that cover atmospheric scattering, usually with code that nobody can get to work, and stunning graphics.
Well I am not going to try and explain it all to you, hell some of it I don't understand myself. I am just going to give you a shader that works and then explain how you can use it.
Here is the shader in it's full, faded, glory.
As usual when you start to look at it in detail, it gets kinda complicated.
There are a lot of tutorials that cover atmospheric scattering, usually with code that nobody can get to work, and stunning graphics.
Well I am not going to try and explain it all to you, hell some of it I don't understand myself. I am just going to give you a shader that works and then explain how you can use it.
Here is the shader in it's full, faded, glory.
float4x4 World; float4x4 View; float4x4 Projection; float3 v3LightPos; // The light's current position float3 v3CameraPos; // The camera's current position float3 v3LightDir; // Direction vector to the light source float3 v3InvWavelength; // 1 / pow(wavelength, 4) for RGB float fCameraHeight; // The camera's current height float fCameraHeight2; // fCameraHeight^2 float fOuterRadius; // The outer (atmosphere) radius float fOuterRadius2; // fOuterRadius^2 float fInnerRadius; // The inner (planetary) radius float fInnerRadius2; // fInnerRadius^2 float fKrESun; // Kr * ESun float fKmESun; // Km * ESun float fKr4PI; // Kr * 4 * PI float fKm4PI; // Km * 4 * PI float fScale; // 1 / (fOuterRadius - fInnerRadius) float fScaleOverScaleDepth; // fScale / fScaleDepth float fScaleDepth; // The scale depth (i.e. the altitude at which the atmosphere's average density is found) float fSamples; int nSamples; float g =-0.90f; float g2 = 0.81f; float fExposure =2; struct VertexShaderInput { float4 Position : POSITION0; }; struct VertexShaderOutput { float4 Position : POSITION0; float4 Color0 : COLOR0; float4 Color1 : COLOR1; float3 v3Direction : COLOR2; }; float scale(float fCos) { float x = 1.0 - fCos; return fScaleDepth * exp(-0.00287 + x*(0.459 + x*(3.83 + x*(-6.80 + x*5.25)))); } VertexShaderOutput VertexShaderFunction(VertexShaderInput input) { VertexShaderOutput output; float4 worldPosition = mul(input.Position, World); float4 viewPosition = mul(worldPosition, View); float3 v3Pos = worldPosition.xyz; float3 v3Ray = v3Pos - v3CameraPos; float fFar = length(v3Ray); v3Ray /= fFar; // Calculate the closest intersection of the ray with the outer atmosphere // (which is the near point of the ray passing through the atmosphere) float B = 2.0 * dot(v3CameraPos, v3Ray); float C = fCameraHeight2 - fOuterRadius2; float fDet = max(0.0, B*B - 4.0 * C); float fNear = 0.5 * (-B - sqrt(fDet)); // Calculate the ray's start and end positions in the atmosphere, // then calculate its scattering offset float3 v3Start = v3CameraPos + v3Ray * fNear; fFar -= fNear; //float fStartAngle = dot(v3Ray, v3Start) / fOuterRadius; //float fStartDepth = exp(-1.0 / fScaleDepth); //float fStartOffset = fStartDepth * scale(fStartAngle); // Initialize the scattering loop variables float fSampleLength = fFar / fSamples; float fScaledLength = fSampleLength * fScale; float3 v3SampleRay = v3Ray * fSampleLength; float3 v3SamplePoint = v3Start + (v3SampleRay * 0.5); float fHeight = length(v3SamplePoint); float fStartAngle = dot(v3Ray, v3Start) / fHeight; float fStartDepth = exp(fScaleOverScaleDepth * (fInnerRadius - fHeight)); float fStartOffset = fStartDepth * scale(fStartAngle); // Now loop through the sample points float3 v3FrontColor = float3(0.0, 0.0, 0.0); for(int i=0; i |
Nice and simple?
Anyway, to use it we simply render our sphere over the top of the planet and clouds with a scale factor slightly bigger than both.
In principle, it really is as simple as that. The truth is though that the shader is incredibly picky about input parameters. Get a single parameter wrong, and the whole shader can fail to draw anything.
So the most important thing I can do for you is to explain what the key variables do and show you how to tweak them to get something you are happy with.
Anyway, to use it we simply render our sphere over the top of the planet and clouds with a scale factor slightly bigger than both.
In principle, it really is as simple as that. The truth is though that the shader is incredibly picky about input parameters. Get a single parameter wrong, and the whole shader can fail to draw anything.
So the most important thing I can do for you is to explain what the key variables do and show you how to tweak them to get something you are happy with.
Input mesh
The mesh we have been using so far has a very low polygon count. If you try to use that with the atmosphere shader you get horrible artifacts as the linear approximations in the shader go wrong. So I have generated a new mesh for you. Note that if you generate your own, the mesh MUST be based around 0,0,0 and have a radius of 4. If you have one with a different radius, use the scale function in the meshes properties (under content processor) to convert it to this scale.
Sphere sizes
I have defined three variables which define the scale of the planet, the cloud layer, and the atmosphere. The important thing to remember here is that the atmosphere MUST be 2.5% bigger than the planet, and the cloud layer has to fit between the two. These variables are defined in Planet.cs.
float p_radius = 200; float a_radius = 205; float c_radius = 200.5f; |
v3InvWavelength, fKrESun, fKmESun
Now here we can have some fun. v3InvWavelength is based on the wavelengths of the light striking the planet. Changing these variables changes the colour of the effect. The other two variables control the overall strength of the Rayleigh and Mie scattering. So we can play with these as well.
As a standard I use this, which produces blue skies and red sunsets.
As a standard I use this, which produces blue skies and red sunsets.
atmosphere.Parameters["fKrESun"].SetValue(0.0000025f * 10); atmosphere.Parameters["fKmESun"].SetValue(0.00015f * 10); atmosphere.Parameters["v3InvWavelength"].SetValue(new Vector3(1.0f / (float)Math.Pow(0.650f, 4), 1.0f / (float)Math.Pow(0.570f, 4), 1.0f / (float)Math.Pow(0.475f, 4))); |
By now you will have realized that when I get a chance to add variation to the planet system, I do. For the molten planets I use these variables, which give a red sky and blue sunsets.
atmosphere.Parameters["fKrESun"].SetValue(0.0000025f * 5); atmosphere.Parameters["fKmESun"].SetValue(0.00015f * 10); atmosphere.Parameters["v3InvWavelength"].SetValue(new Vector3(1.0f / (float)Math.Pow(0.350f, 4), 1.0f / (float)Math.Pow(0.970f, 4), 1.0f / (float)Math.Pow(0.975f, 4))); |
For gas giants I keep the colour as standard, but reduce the strength of the effect considerably.
atmosphere.Parameters["fKrESun"].SetValue(0.0000025f * 5); atmosphere.Parameters["fKmESun"].SetValue(0.00015f * 5); atmosphere.Parameters["v3InvWavelength"].SetValue(new Vector3(1.0f / (float)Math.Pow(0.650f, 4), 1.0f / (float)Math.Pow(0.570f, 4), 1.0f / (float)Math.Pow(0.475f, 4))); |
Here are a few examples so you can see the difference.
The rest of the variables are pretty obvious, you can look at the source code for default values, though I don't think playing with them will improve anything.
So I have added all this into the draw method of Planet.cs.
So I have added all this into the draw method of Planet.cs.
if (atmosphere != null) { float scale = 1.0f / (a_radius - p_radius); atmosphere.Parameters["fOuterRadius"].SetValue(a_radius); atmosphere.Parameters["fInnerRadius"].SetValue(p_radius); atmosphere.Parameters["fOuterRadius2"].SetValue(a_radius * a_radius); atmosphere.Parameters["fInnerRadius2"].SetValue(p_radius * p_radius); atmosphere.Parameters["fKr4PI"].SetValue(0.0025f * 4 * MathHelper.Pi); atmosphere.Parameters["fKm4PI"].SetValue(0.0015f * 4 * MathHelper.Pi); atmosphere.Parameters["fScale"].SetValue(scale); atmosphere.Parameters["fScaleDepth"].SetValue(0.25f); atmosphere.Parameters["fScaleOverScaleDepth"].SetValue(scale / 0.5f); atmosphere.Parameters["fSamples"].SetValue(2.0f); atmosphere.Parameters["nSamples"].SetValue(2); switch (Type) { case PlanetType.Gas: atmosphere.Parameters["fKrESun"].SetValue(0.0000025f * 5); atmosphere.Parameters["fKmESun"].SetValue(0.00015f * 5); atmosphere.Parameters["v3InvWavelength"].SetValue(new Vector3(1.0f / (float)Math.Pow(0.650f, 4), 1.0f / (float)Math.Pow(0.570f, 4), 1.0f / (float)Math.Pow(0.475f, 4))); break; case PlanetType.Molten: atmosphere.Parameters["fKrESun"].SetValue(0.0000025f * 5); atmosphere.Parameters["fKmESun"].SetValue(0.00015f * 10); atmosphere.Parameters["v3InvWavelength"].SetValue(new Vector3(1.0f / (float)Math.Pow(0.350f, 4), 1.0f / (float)Math.Pow(0.970f, 4), 1.0f / (float)Math.Pow(0.975f, 4))); break; default: atmosphere.Parameters["fKrESun"].SetValue(0.0000025f * 10); atmosphere.Parameters["fKmESun"].SetValue(0.00015f * 10); atmosphere.Parameters["v3InvWavelength"].SetValue(new Vector3(1.0f / (float)Math.Pow(0.650f, 4), 1.0f / (float)Math.Pow(0.570f, 4), 1.0f / (float)Math.Pow(0.475f, 4))); break; } World = Matrix.CreateScale(a_radius) * Matrix.CreateRotationX(pitch); Vector3 vl = - Game1.LightPosition; vl.Normalize(); atmosphere.Parameters["World"].SetValue(World); atmosphere.Parameters["View"].SetValue(View); atmosphere.Parameters["Projection"].SetValue(Projection); atmosphere.Parameters["v3CameraPos"].SetValue(Game1.CameraPosition); atmosphere.Parameters["v3LightDir"].SetValue(vl); atmosphere.Parameters["v3LightPos"].SetValue(Game1.LightPosition); atmosphere.Parameters["fCameraHeight"].SetValue(Game1.CameraPosition.Length()); atmosphere.Parameters["fCameraHeight2"].SetValue(Game1.CameraPosition.LengthSquared()); graphics.RenderState.CullMode = CullMode.CullClockwiseFace; graphics.RenderState.DepthBufferEnable = true; foreach (ModelMesh mesh in Game1.sphere.Meshes) { for (int i = 0; i < mesh.MeshParts.Count; i++) { // Set this MeshParts effect (currentEffect) to the desired effect (from .fx file) mesh.MeshParts[i].Effect = atmosphere; } mesh.Draw(); } graphics.RenderState.CullMode = CullMode.None; graphics.RenderState.DepthBufferEnable = true; } |
The next thing we have to do is add a light, without a light source, no light gets scattered. So you can see the effect properly I have to animate the light, so I have added this code to Game1.cs in the update method. LightPosition is a static Vector3.
angle += (float)gameTime.ElapsedGameTime.Milliseconds / 10000.0f; LightPosition.X = (float)(1000.0f * Math.Cos(angle)); LightPosition.Z = (float)(1000.0f * Math.Sin(angle)); |
So there we are. We now have a planet with clouds and atmospheric effects.
The new drop has all the changes in it and I have also modified the rock planet code a little to give more variation. Hope you like it.
New zip here
The new drop has all the changes in it and I have also modified the rock planet code a little to give more variation. Hope you like it.
New zip here
In the next part of the tutorial I will add some optional extras to the planets.
Next part of the tutorial
Next part of the tutorial