While working on the game engine for Fondusi’s, I had to change how my tile engine was rendered in order to add the lighting. At first, all tiles were just rendered directly to the screen (back buffer, actually) but with the lighting, I was tweaking pixel alpha values and it ended up causing issues with stacked tiles. So, what I needed to do was to draw my tiles on one surface, draw the lighting on a second surface, and then draw the lighting surface onto the tiled surface with it’s own pixel shader effect. (Really, I just sample the lighting surface in the pixel shader while drawing the tile surface to the back-buffer.)
In order to draw it like that, I had to add new render targets. Now, I was following a blog post on Shawn Hargreaves’ Blog which was (quite!) out of date. So needless to say, I had a few issues implementing it the same way. Fortunately, they’ve made it a lot easier to use render targets in XNA 4.0 and that what I’m going to show you below.
Declaration
The first thing you need to do is add a RenderTarget variable to your game class. So, go into your game class and find the part (near the beginning) where the spriteBatch variable is declared. On the next line, add this bit of code:
1 |
RenderTarget2D mainRenderTarget; |
This declares our new render target at the class level.
LoadContent()
Next, go into your LoadContent method and add the following code:
1 2 3 4 5 6 7 8 9 |
//Get the current presentation parameters var pp = graphics.GraphicsDevice.PresentationParameters; //Create our new render target mainRenderTarget = new RenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth, //Same width as backbuffer pp.BackBufferHeight, //Same height false, //No mip-mapping pp.BackBufferFormat, //Same colour format pp.DepthStencilFormat); //Same depth stencil |
So, first of all, let me say that it’s very important that you don’t create the render target during the Draw call. You only need to create the render target once and then call .Clear on it during the Draw loop. This code is basically just saying “Create a new RenderTarget2D with the same specifications as the current back-buffer.”
Draw()
Ok, so we’ve defined our new RenderTarget and created an instance of it. The final piece of the puzzle is in the draw call. We need to tell the graphics device what target to render to so let’s see how it’s all done. Add this code to your draw call:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//Set the current render target to our mainRenderTarget graphics.GraphicsDevice.SetRenderTarget(mainRenderTarget); //Clear the render target (make it all black) graphics.GraphicsDevice.Clear(Color.Black); //Start our spritebatch //Note: Add parameters to this as is necessary for your game spriteBatch.Begin(); //... //Do all your drawing here //... //End the spritebatch spriteBatch.End(); //Set the render target to the back buffer again by passing null graphics.GraphicsDevice.SetRenderTarget(null); //Clear the back buffer graphics.GraphicsDevice.Clear(Color.Black); //Start the spritebatch we're going to use to draw what we've // rendered (using a cool effect) to the screen //Note: the SpriteSortMode and BlendState may be different in // your case. spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, coolEffect); //Draw the render target to the screen with no tint spriteBatch.Draw(mainRenderTarget, Vector2.Zero, Color.White); //End the spritebatch spriteBatch.End(); |
So what you have to imagine here, is that you’re drawing all of your stuff onto a Texture2D and then drawing that to the screen. As of XNA 4.0 RenderTarget2D inherits directly from Texture2D (previously, there was a property called GetTexture()) so this greatly simplifies things.
At first, we tell the graphics device that we want to render onto our render target (mainRenderTarget) and then we tell it to clear it:
1 2 |
graphics.GraphicsDevice.SetRenderTarget(mainRenderTarget); graphics.GraphicsDevice.Clear(Color.Black); |
Next, we start our sprite batch and draw all of our textures to our render target:
1 2 3 4 |
spriteBatch.Begin(); //... //Do all your drawing here //... |
Then we end the sprite batch and tell the graphics device to switch back to drawing to the back buffer. We also tell it to clear the back buffer:
1 2 3 |
spriteBatch.End(); graphics.GraphicsDevice.SetRenderTarget(null); graphics.GraphicsDevice.Clear(Color.Black); |
We then start our sprite batch again, but this time we pass in our cool pixel shader effect:
1 2 |
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, coolEffect); |
Our render target is then drawn to the back buffer using the pixel shader:
1 |
spriteBatch.Draw(mainRenderTarget, Vector2.Zero, Color.White); |
The thing to note here is that rather than applying our pixel shader (effect file) to every draw call, and thus affecting all pixels of every texture we draw, we’re only applying it to the entire scene.
After that, we end the sprite batch to close off our Draw loop:
1 |
spriteBatch.End(); |
That’s It!
I hope this post has been helpful for you. Please feel free to drop a comment if you have any questions or if I’ve made an error.