Hybrid approach for procedural planets
Part X - Rings and Stars
A planet floating in a black void is not very impressive, no matter how clever your terrain generation system is.
So we will start by putting the planet into a solar system.
There are lots of different types of stars, I am going to consider seven. I am not going to try and do anything particularly clever with them because we are going to be a long way from them. So all we really need is a sphere of the correct colour, with a glow effect, and maybe a lens flare.
I have added a Star class to the code, and setup an enumeration to cover the classes of stars we shall be using.
So we will start by putting the planet into a solar system.
There are lots of different types of stars, I am going to consider seven. I am not going to try and do anything particularly clever with them because we are going to be a long way from them. So all we really need is a sphere of the correct colour, with a glow effect, and maybe a lens flare.
I have added a Star class to the code, and setup an enumeration to cover the classes of stars we shall be using.
public enum StarType { M, K, G, F, A, B, O, Undefined, }; |
A quick search on the Internet gave me some rough approximations to work with. I am only going to consider size, and colour so it is easy to setup.
public void SetType(StarType _type) { type = _type; switch (type) { case StarType.A: Scale = 4; Colour = new Vector3(0.5f, 1, 0.5f); break; case StarType.B: Scale = 6; Colour = new Vector3(0.8f, 0.8f, 1.0f); break; case StarType.F: Scale = 1; Colour = new Vector3(1, 1, 1); break; case StarType.G: Scale = 0.8f; Colour = new Vector3(1, 1, 0.3f); break; case StarType.K: Scale = 0.7f; Colour = new Vector3(1, 0.8f, 0.3f); break; case StarType.M: Scale = 0.5f; Colour = new Vector3(1, 0.3f, 0.3f); break; case StarType.O: Scale = 10; Colour = new Vector3(0.3f, 0.3f, 1); break; } glowSize = 30 * Scale; } |
Now all we need to do is draw it.
We already have a sphere in play, so lets re-use that. So we just need a very simple shader to draw a coloured sphere.
We already have a sphere in play, so lets re-use that. So we just need a very simple shader to draw a coloured sphere.
float4x4 wvp; float3 colour; struct VertexShaderInput { float4 Position : POSITION0; }; struct VertexShaderOutput { float4 Position : POSITION0; }; VertexShaderOutput VertexShaderFunction(VertexShaderInput input) { VertexShaderOutput output; output.Position = mul(input.Position,wvp); return output; } float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { return float4(colour, 1); } technique Technique1 { pass Pass1 { VertexShader = compile vs_1_1 VertexShaderFunction(); PixelShader = compile ps_1_1 PixelShaderFunction(); } } |
About the simplest shader in the world. Drawing the sphere is trivial as well, it's our normal XNA object draw
graf.RenderState.DepthBufferEnable = true; Matrix World = Matrix.CreateScale(Scale*200) * Matrix.CreateTranslation(-Game1.LightPosition); Matrix wvp = World * View * Projection; starEffect.Parameters["wvp"].SetValue(wvp); starEffect.Parameters["colour"].SetValue(Colour); for (int pass = 0; pass < starEffect.CurrentTechnique.Passes.Count; pass++) { for (int msh = 0; msh < Game1.sphere.Meshes.Count; msh++) { ModelMesh mesh = Game1.sphere.Meshes[msh]; for (int prt = 0; prt < mesh.MeshParts.Count; prt++) mesh.MeshParts[prt].Effect = starEffect; mesh.Draw(); } } |
Okay now this is when it gets interesting. We want to draw a glow and some lens flares, these are going to be 2D, so we need a way of fitting them into the scene. To do that we need to be able to find out what the 2D screen location of the sphere is, and check to see if it is visible.
To convert from a 3D location to a 2D screen coordinate I use the following method. Note I don't use XNA's built in system as I have found it very unreliable.
To convert from a 3D location to a 2D screen coordinate I use the following method. Note I don't use XNA's built in system as I have found it very unreliable.
public Vector2 TransformPosition(Matrix View, Matrix Projection, Vector3 oPosition) { Vector4 oTransformedPosition = Vector4.Transform(oPosition, View * Projection); if (oTransformedPosition.W != 0) { oTransformedPosition.X = oTransformedPosition.X / oTransformedPosition.W; oTransformedPosition.Y = oTransformedPosition.Y / oTransformedPosition.W; oTransformedPosition.Z = oTransformedPosition.Z / oTransformedPosition.W; } Vector2 oPosition2D = new Vector2( oTransformedPosition.X * graf.PresentationParameters.BackBufferWidth / 2 + graf.PresentationParameters.BackBufferWidth / 2, -oTransformedPosition.Y * graf.PresentationParameters.BackBufferHeight / 2 + graf.PresentationParameters.BackBufferHeight / 2); return oPosition2D; } |
So now we know where to draw our glow, but we don't know if we should draw it. We don't want to plonk the glow on the screen if the star is behind the planet. So we need to know if the star is visible.
Thankfully XNA includes a handy tool for this job. OcclusionQuery. It basically works by counting the number of pixels that are drawn by a render. If it is zero, then the object is not in view. Any other value means at least part of the object is on screen. Dead handy.
It's easy to use as well. Before you do your draw you check to see if a query is already running. If it isn't you start one. If it is you check to see if it has finished, and grab the results, then start a new one.
We have to have an extra little bit of code because generating a new planet can take a little while. This could cause problems if a query is still running, so I have added a holdoff bool just to make sure.
Thankfully XNA includes a handy tool for this job. OcclusionQuery. It basically works by counting the number of pixels that are drawn by a render. If it is zero, then the object is not in view. Any other value means at least part of the object is on screen. Dead handy.
It's easy to use as well. Before you do your draw you check to see if a query is already running. If it isn't you start one. If it is you check to see if it has finished, and grab the results, then start a new one.
We have to have an extra little bit of code because generating a new planet can take a little while. This could cause problems if a query is still running, so I have added a holdoff bool just to make sure.
// Give up if the current graphics card does not support occlusion queries. if (occlusionQuery.IsSupported) { if (occlusionQueryActive) { // If the previous query has not yet completed, wait until it does. if (occlusionQuery.IsComplete) { const float queryArea = 16 * 16; if (occlusionQuery.PixelCount > 0) { occlusionAlpha = Math.Min(occlusionQuery.PixelCount / queryArea, 1); } else { occlusionAlpha = 0; } occlusionQuery.Begin(); holdoff = false; } else { holdoff = true; } } else { occlusionQuery.Begin(); occlusionQueryActive = true; holdoff = false; } } |
I have used a fixed query area, really you should work out how many pixels are displayed when the sphere is fully on screen and use that, but it is only usefull if you want the glow to gradually fade out as the star becomes obscured by the planet.
After you have drawn the star, you need to tell the occlusion query you are done with it.
After you have drawn the star, you need to tell the occlusion query you are done with it.
if (occlusionQueryActive) { if (!holdoff) occlusionQuery.End(); } |
The end result is a float value occlusionAlpha which we can use to draw the glow with.
When it comes to actually drawing the effect, I have used the code from the lens flare sample on the XNA website. No point in reinventing the wheel. If you want details of how it works, go and grab a copy.
I have also dropped in a sky sphere. So now we have our planet in a nice colourful scene.
When it comes to actually drawing the effect, I have used the code from the lens flare sample on the XNA website. No point in reinventing the wheel. If you want details of how it works, go and grab a copy.
I have also dropped in a sky sphere. So now we have our planet in a nice colourful scene.
Rings
Some planets have rings. So it would be stupid of us to ignore them. The ring is formed from particles that range in size from microscopic dust fragments all the way up to chunks of rock the size of buses.
We cannot hope to handle that many particles, so as usual we have to cheat.
First we are going to need a 3D mesh to render, a simple very thin cylinder will do. The only important thing about it is the UV coordinates. Make sure you use a cylinder cap UV mapping as we are going to use the UV coordinates to colour our rings.
The texture I used is this.
We cannot hope to handle that many particles, so as usual we have to cheat.
First we are going to need a 3D mesh to render, a simple very thin cylinder will do. The only important thing about it is the UV coordinates. Make sure you use a cylinder cap UV mapping as we are going to use the UV coordinates to colour our rings.
The texture I used is this.
This is based on an image I found of Saturn's rings.
In the shader I use the UV coordinates to work out how far from the centre of the planet a pixel is, and then use that to look up the colour.
In the shader I use the UV coordinates to work out how far from the centre of the planet a pixel is, and then use that to look up the colour.
// texture the pixel float2 tc = input.TexCoord; tc =(tc-0.5)/2; tc.x=length(tc)*4; tc.y=0; float4 result = tex2D(RingSampler,tc); result.w*=0.5; |
On it's own this just isn't good enough. The first thing you notice is that the planet does not cast a shadow onto the ring. You could bolt on a proper shadow system into your code, which would be nice as you could get the shadow of space craft on the ring. However I am just going to cheat.
I just form a ray from the pixel to the star and do a ray-sphere intersection check against the planet.
To do this we need to pass the world coordinate to the pixel shader from the vertex shader, but that is trivial.
The ray sphere intersection test reduces to a quadratic, so it is a little bit of shader code.
I just form a ray from the pixel to the star and do a ray-sphere intersection check against the planet.
To do this we need to pass the world coordinate to the pixel shader from the vertex shader, but that is trivial.
The ray sphere intersection test reduces to a quadratic, so it is a little bit of shader code.
float3 dst = LightPosition; float3 Ray = LightPosition-input.World; float lr = length(Ray); float ld = length(dst); Ray=normalize(Ray); float B = dot(dst,Ray); float C = dot(dst, dst) - scale2; float D = B*B - C; if (D>0) { if (lr < ld) { result.xyz=0; } } |
The variable scale2 is the radius of the planet squared.
Okay now we have something that is a bit better, but we have treated the rings as a solid, and we know they are not. So what can we do about it?
We could rip out the rings and generate a LOD based mesh instead, but that's a lot of work. Maybe another time. Instead I have mixed in some noise based on the distance of the pixel from the camera.
This is easy to do and gives some detail to the rings when close to the camera.
Okay now we have something that is a bit better, but we have treated the rings as a solid, and we know they are not. So what can we do about it?
We could rip out the rings and generate a LOD based mesh instead, but that's a lot of work. Maybe another time. Instead I have mixed in some noise based on the distance of the pixel from the camera.
This is easy to do and gives some detail to the rings when close to the camera.
float3 depth = CameraPosition-input.World; float blend=length(depth); if (blend<100) { blend/=100; noise=(tex2D(NoiseSampler,input.TexCoord)); } else { blend=1; } |
That's it. At last we have something that looks good instead of a load of programming experiments.
New Zip here.
New Zip here.
In the next part of the tutorial I will start our quest for planetary scale terrain.