XNA 4.0 Use Multiple Effects In One Batch

1.9k views Asked by At

I'm working on a graphics engine for XNA 4.0, and I've encountered a problem that I can't find any solution for. Currently I'm trying to implement light-effects by using shaders. The graphics engine also consists of an particle engine and therefore it is very important for me to atleast consider performance. The combination of those 2 things creates the problem.

First of all, I've done a lot of reading and research and from what I understand, you get better performance the lesser draw calls you make. And by draw calls, I mean when the spritebatch sends the actual geometry and textures to the GPU. Therefore, I'm trying to draw as much as possible within one single batch.

And now comes the problem. I have 2 overloads for my engines drawing method:

public static void Draw(Texture texture)
{
    // Call spriteBatch.Draw(...);
}

public static void Draw(Light light)
{
    // Call spriteBatch.Draw(...);
}

The first overload is just going to draw an ordinary texture with a default shader. The second overload is going to draw a light that uses another shader. What I would like to do is:

public static void Draw(Texture texture)
{
    // Use default shader
    // Call spriteBatch.Draw(...);
}

public static void Draw(Light light)
{
    // Use light shader
    // Call spriteBatch.Draw(...);
}

However, the SpriteBatch doesn't support multiple shaders at once, so therefore I tried to do this with effect passes instead:

public static void Draw(Texture texture)
{
    effect.CurrentTechnique.Passes[0].Apply();
    // Call spriteBatch.Draw(...);
}

public static void Draw(Light light)
{
    effect.CurrentTechnique.Passes[1].Apply();
    // Call spriteBatch.Draw(...);
}

This didn't work either because the shader is not used when spriteBatch.Draw() is called, but instead when the spriteBatch.End() is called. Therefore, the above code rendered everything with the pass that I applied last.

My third try was to use 2 SpriteBatches:

public static void Begin()
{
    spriteBatchColor.Begin([...], null); // Default shader.
    spriteBatchLight.Begin([...], effect); // Light shader.
}

public static void Draw(Texture texture)
{
    // Call spriteBatchColor.Draw(...);
}

public static void Draw(Light light)
{
    // Call spriteBatchLight.Draw(...);
}

public static void End()
{
    spriteBatchColor.End();
    spriteBatchLight.End();
}

This did actually work, but my layer depth got totally messed up, which isn't so strange because the spriteBatchLight.End() is called last, so it will always be drawn above everything that spriteBatchColor did draw.

I know that I can set the SpriteSortMode to SpriteSortMode.Immediate, but then I'll get a large performance penality because that mode draws everything directly. As performance is very important in this case and I therefore want to optimize by making as few draw calls to the GPU as possible, SpriteSortMode.Immediate is not an option for me.

So, my question is:

Is there any way that I can use 2 different effects/techniques/passes within one batch? Or is there any way that I can use 2 different SpriteBatches but then combine them when I make the final draw so the layer depth doesn't get messed up?

1

There are 1 answers

0
Plato On

Wow, so disregard my previous answer because it is dead wrong.

The actual way to do this is to create a List<RenderTarget2D> in your main game class and pass it to your functions/classes that are drawing.

In Engine Draw Method:

public static void Draw(Texture texture, List<RenderTarget2D> targetList)
{
    RenderTarget2D target = new RenderTarget2D(...);

    //Tell spriteBatch to draw to target instead of the screen
    spriteBatch.GraphicsDevice.SetRenderTarget(target);

    //Make the renderTarget's background clear
    spriteBatch.GraphicsDevice.Clear(new Color(0,0,0,0)) 

    spriteBatch.Begin();
    //Apply effect and draw
    spriteBatch.End();

    targetList.Add(target);
}

Then you draw each texture to the screen.

In Game Draw Method:

void Draw()
{
    List<RenderTarget2D> targetList = new List<RenderTarget2D>();

    engine.Draw(texture, targetList);
    engine.Draw(light, targetList);

    //Tell spriteBatch to draw to the screen
    spriteBatch.GraphicsDevice.SetRenderTarget(null)

    //Make the screen background black
    spriteBatch.GraphicsDevice.Clear(Color.Black)

    spriteBatch.Begin();
    //Draw each target in target list to screen
    spriteBatch.End();
}