Sun overcasting effect

Since the objective of the contest is to recreate a nice-looking solar system, an effect I really wanted to implement is sun overcasting, that is the effect of the sunlight glowing inside objects appearing in front of the sun.

The same "overcasting" effect is seen in the Gothic 3 engine and looks really great.

Overcasting effect in Gothic 3

So, how do we implement it?

Theory

Assume we are normally rendering the scene (the solar system): we'd like to able to overimpose the blurred sunny glow on top of the scene in a single pass, like we did with the bloom effect.

But this time we can't simply take the backbuffer, apply a pixel shader to it and alpha blend everything together: we'll have to exactly know where the glow should be placed and which objects are between the camera and sun, obstructing the glow. Therefore, we need a second render pass. The whole rendering architecture I built previously will come in handy.

Anyway, the final passes are:

  1. Render the scene to the backbuffer,
  2. Render the same scene to a separate buffer, using special shaders,
  3. Blur the separate buffer,
  4. Paste the buffer on top of the backbuffer, using additive blending.

What kind of "special shaders" do we need?
First of all, when rendering the sun object we'll have to enlarge the object slighty (in order to create the "halo" around the sun) and use a bright orange color. When rendering all other objects instead, we'll reduce the size of the object slightly (this will make the glow grow inside the outline of the other objects) and render it black (with zero alpha).

The output will be a completely black image with a big orange circle around the sun's position and the black shapes of other objects inside it, like this:

Overcasting effect output

Note that what you see in the image above as black has an alpha channel value of 0 and thus is completely transparent. This is the result when we blend everything together (original + overcasting):

Final output

The code

Assuming you gave a look to my renderer's architecture, our "overcasting component" will take a RenderPort instance and the object instance generating the glow effect (the sun in our case). As soon as the component fires up and is enabled, we register an additional RenderQueueInvocation on a separate render target (this will render our scene using those "special shaders" I mentioned above):

//... RenderQueueInvocation inv = renderPort.CreateRenderQueueInvocation(); inv.RenderOperationQueued += new RenderOperationEventHandler(OnRenderOperationQueued); inv.RenderTarget = blurTarget; inv.RenderTargetClearColor = Color.TransparentBlack; inv.ClearBackBuffer = true; //...

This code creates a new render invocation instance, sets the target to the correct render target (which has been created before this snippet and has same size and format as the main backbuffer) and sets the clear color to TransparentBlack. We also attach an event handler to our render invocation, that will be called every time our renderer adds a RenderOperation instance to the queue:

void OnRenderOperationQueued(object sender, RenderOperationEventArgs e) { if (ReferenceEquals(e.Operation.Generator, _overcastSource)) { //Overcast source, render with glow material Material mat = new Material(_matSource); mat.WorldTransform = e.Operation.Material.WorldTransform; mat.ViewTransform = e.Operation.Material.ViewTransform; mat.ProjectionTransform = e.Operation.Material.ProjectionTransform; e.Operation.Material = mat; } else { //Everything else, render black and with alpha = 0.0 Material mat = new Material(_matOcclusion); mat.WorldTransform = e.Operation.Material.WorldTransform; mat.ViewTransform = e.Operation.Material.ViewTransform; mat.ProjectionTransform = e.Operation.Material.ProjectionTransform; e.Operation.Material = mat; } }

The code is pretty simple: the sun object will be rendered with our special "source material", while everything else will be rendered with the "occlusion material" (by material, I actually intend a vertex and a pixel shader with some preset parameters). How do those two materials look?

Glow source (Source.fx)

float4x4 World; float4x4 View; float4x4 Projection; struct VertexShaderInput { float4 Position : POSITION0; }; struct VertexShaderOutput { float4 Position : POSITION0; }; VertexShaderOutput VertexShaderFunction(VertexShaderInput input) { VertexShaderOutput output; //Enlarge model input.Position.xyz = input.Position.xyz * 1.3; float4 worldPosition = mul(input.Position, World); float4 viewPosition = mul(worldPosition, View); output.Position = mul(viewPosition, Projection); return output; } float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { //Orange return float4(0.8, 0.5, 0, 0.65); } technique Technique1 { pass Pass1 { VertexShader = compile vs_1_1 VertexShaderFunction(); PixelShader = compile ps_1_1 PixelShaderFunction(); } }

Occlusion (Occlusion.fx)

float4x4 World; float4x4 View; float4x4 Projection; struct VertexShaderInput { float4 Position : POSITION0; }; struct VertexShaderOutput { float4 Position : POSITION0; }; VertexShaderOutput VertexShaderFunction(VertexShaderInput input) { VertexShaderOutput output; //Shrink model input.Position.xyz = input.Position.xyz * 0.8; float4 worldPosition = mul(input.Position, World); float4 viewPosition = mul(worldPosition, View); output.Position = mul(viewPosition, Projection); return output; } float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { return 0; } technique Technique1 { pass Pass1 { VertexShader = compile vs_1_1 VertexShaderFunction(); PixelShader = compile ps_1_1 PixelShaderFunction(); } }

That's it. As you can see, the "source" object is enlarged slightly before rendering, by the vertex shader, and is then drawed using a predefined bright orange color. All occluded objects are shrinked and rendered with a simple '0' color (that is, transparent black). Now we simply have to overimpose our secondary render target on top of the backbuffer (the following code is inside the overcasting game component):

RenderTarget2D _finalTarget, ///Temp targets used to blur the image _blurTarget, _blur2Target, ///Input target, contains the original scene _inputTarget; SpriteBatch _sprite; Effect _matComposition; //This rect matches the backbuffer width and height Rectangle _deviceRect; //... public override void Draw(GameTime gameTime) { if (Enabled) { //Blur pass DrawTextures(_blur2Target, SpriteBlendMode.None, _blurTarget); //Second blur pass DrawTextures(_blurTarget, SpriteBlendMode.None, _blur2Target); //Compose pass GraphicsDevice.SetRenderTarget(0, _finalTarget); GraphicsDevice.Clear(Color.Black); _sprite.Begin(SpriteBlendMode.Additive, SpriteSortMode.Immediate, SaveStateMode.None); _sprite.Draw(_inputTarget.GetTexture(), _deviceRect, Color.White); _sprite.Draw(_blurTarget.GetTexture(), _deviceRect, Color.White); _sprite.End(); } base.Draw(gameTime); } private void DrawTextures(RenderTarget2D target, SpriteBlendMode blending, RenderTarget2D source) { GraphicsDevice.SetRenderTarget(0, target); GraphicsDevice.Clear(Color.Black); _sprite.Begin(blending, SpriteSortMode.Immediate, SaveStateMode.None); _matComposition.Begin(SaveStateMode.None); _matComposition.CurrentTechnique.Passes[0].Begin(); _sprite.Draw(source.GetTexture(), _deviceRect, Color.White); _matComposition.CurrentTechnique.Passes[0].End(); _matComposition.End(); _sprite.End(); }

This is the pixel shader used for each blur step (it's the usual gaussian blur approximation):

sampler TextureSampler : register(s0); float BlurPower; const float2 offsets[12] = { -0.326212, -0.405805, -0.840144, -0.073580, -0.695914, 0.457137, -0.203345, 0.620716, 0.962340, -0.194983, 0.473434, -0.480026, 0.519456, 0.767022, 0.185461, -0.893124, 0.507431, 0.064425, 0.896420, 0.412458, -0.321940, -0.932615, -0.791559, -0.597705, }; struct PixelShaderInput { float2 TexCoord : TEXCOORD0; }; float4 Overimpose(PixelShaderInput Input) : COLOR0 { //Compute bloom color after gaussian filtering float4 sum = tex2D(TextureSampler, Input.TexCoord); for(int i = 0; i < 12; i++){ sum += tex2D(TextureSampler, Input.TexCoord + BlurPower * offsets[i]); } return sum / 13; } technique Blur { pass P0 { PixelShader = compile ps_2_0 Overimpose(); } }

And this is the result!  :D

The overcasting effect in action!

I'm pretty sure there are more efficient ways to achieve the same effect. Anyways, it looks great!