0
\$\begingroup\$

I have a transparent RenderTarget2D that I draw every frame. Before the first frame is drawn to the screen, an image is drawn to the render target. The image also contains some transparent pixels. Every frame, that render target is then drawn to the screen.

In response to a mouse click, a second image is drawn to the render target that is masked by any solid pixels, so that the transparent pixels aren't overwritten. I followed ClassicThunder's instructions here and modified the code so that the second image is only drawn when I need it to be. This code does the job well enough to achieve the end result, but exhibits some odd behaviour.

Although the end result is exactly how I want it, for the frame where the render target is modified the alpha channel seems to be ignored. The result is that all transparent pixels turn black for that frame. From the next frame onwards the render target behaves as I'd expect it to when drawn, and the transparency returns.

Edit: Here's everything from Game1.cs. It's a test project and doesn't require any external assets. I should probably also mention that I'm using MonoGame.

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace StencilTest
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Texture2D target;
        Texture2D projectile;
        Random random = new Random();

        RenderTarget2D layer;
        bool notRendered = true;

        bool gotHit = false;
        int hitX = 0;
        int hitY = 0;
        MouseState oldMouseState = new MouseState();

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            IsMouseVisible = true;

            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            target = new Texture2D(GraphicsDevice, 128, 128);
            projectile = new Texture2D(GraphicsDevice, 64, 64);

            Color[] tArray = new Color[128 * 128];
            for (int i = 0; i < 128 * 128; i++)
                if (random.NextDouble() > 0.5d)
                    tArray[i] = Color.White;
                else tArray[i] = Color.Transparent;
            target.SetData<Color>(tArray);

            Color[] pArray = new Color[64 * 64];
            for (int i = 0; i < 64 * 64; i++)
                pArray[i] = Color.FromNonPremultiplied(255, 0, 0, 70);
            projectile.SetData<Color>(pArray);

            layer = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8, 0, RenderTargetUsage.PreserveContents);
        }

        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            MouseState mState = Mouse.GetState();
            if (mState.LeftButton == ButtonState.Pressed)// && oldMouseState.LeftButton == ButtonState.Released)
            {
                hitX = mState.X - 32;
                hitY = mState.Y - 32;
                gotHit = true;
            }

            oldMouseState = mState;
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.DarkBlue);

            if (notRendered)
            {
                var m = Matrix.CreateOrthographicOffCenter(0, graphics.GraphicsDevice.PresentationParameters.BackBufferWidth, graphics.GraphicsDevice.PresentationParameters.BackBufferHeight, 0, 0, 1);

                var a = new AlphaTestEffect(graphics.GraphicsDevice)
                {
                    Projection = m
                };

                var s1 = new DepthStencilState
                {
                    StencilEnable = true,
                    StencilFunction = CompareFunction.Always,
                    StencilPass = StencilOperation.Replace,
                    ReferenceStencil = 1,
                    DepthBufferEnable = false,
                };

                GraphicsDevice.SetRenderTarget(layer);
                GraphicsDevice.Clear(Color.Transparent);
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, s1, null, a);
                spriteBatch.Draw(target, new Vector2(20, 20), Color.White);
                spriteBatch.End();

                GraphicsDevice.SetRenderTarget(null);
                notRendered = false;
            }

            if (gotHit)
            {
                var m = Matrix.CreateOrthographicOffCenter(0, graphics.GraphicsDevice.PresentationParameters.BackBufferWidth, graphics.GraphicsDevice.PresentationParameters.BackBufferHeight, 0, 0, 1);

                var a = new AlphaTestEffect(graphics.GraphicsDevice)
                {
                    Projection = m
                };

                var s2 = new DepthStencilState
                {
                    StencilEnable = true,
                    StencilFunction = CompareFunction.LessEqual,
                    StencilPass = StencilOperation.Keep,
                    ReferenceStencil = 1,
                    DepthBufferEnable = false,
                };

                GraphicsDevice.SetRenderTarget(layer);
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, s2, null, a);
                spriteBatch.Draw(projectile, new Vector2(hitX, hitY), Color.White);
                spriteBatch.End();

                GraphicsDevice.SetRenderTarget(null);
                gotHit = false;
            }
            spriteBatch.Begin();
            spriteBatch.Draw(layer, Vector2.Zero, Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

gotHit will be true whenever I need to draw to the render target, which is when the player presses the mouse. The problem exists whenever gotHit is true.

Edit: It occurred to me that should anybody run the code, it probably wouldn't make much sense. The window's background is set to a dark blue to make things more visible. Upon clicking (or dragging) on the speckled section, a red square is drawn to the render target at that position. It is masked such that it will only affect the white pixels. This works, but during this action the background will become black. From what I can tell, the render target (which is the size of the window) is losing its transparency for frames where it is being modified.

The red square is actually semi-transparent as in the target application it will be this way. It's not a symptom of the issue and doesn't affect the outcome when modified.

\$\endgroup\$
0
\$\begingroup\$

This seems to give the effect you're looking for. Only moved some code around a bit.

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace StencilTest
{
public class Game1 : Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    Texture2D target;
    Texture2D projectile;
    Random random = new Random();

    RenderTarget2D layer;
    bool notRendered = true;

    bool gotHit = false;
    int hitX = 0;
    int hitY = 0;
    MouseState oldMouseState = new MouseState();

    Matrix m;
    AlphaTestEffect a;
    DepthStencilState s1;
    DepthStencilState s2;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        IsMouseVisible = true;

        base.Initialize();
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);

        target = new Texture2D(GraphicsDevice, 128, 128);
        projectile = new Texture2D(GraphicsDevice, 64, 64);

        Color[] tArray = new Color[128 * 128];
        for (int i = 0; i < 128 * 128; i++)
            if (random.NextDouble() > 0.5d)
                tArray[i] = Color.White;
            else tArray[i] = Color.Transparent;
        target.SetData<Color>(tArray);

        Color[] pArray = new Color[64 * 64];
        for (int i = 0; i < 64 * 64; i++)
            pArray[i] = Color.FromNonPremultiplied(255, 0, 0, 70);
        projectile.SetData<Color>(pArray);

        layer = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8, 0, RenderTargetUsage.PreserveContents);


        //initialize the matrix, alpha effect and depthstencil, so you won't need it every draw call.
        m = Matrix.CreateOrthographicOffCenter(0, graphics.GraphicsDevice.PresentationParameters.BackBufferWidth, graphics.GraphicsDevice.PresentationParameters.BackBufferHeight, 0, 0, 1);

        a = new AlphaTestEffect(graphics.GraphicsDevice)
        {
            Projection = m
        };

        s1 = new DepthStencilState
        {
            StencilEnable = true,
            StencilFunction = CompareFunction.Always,
            StencilPass = StencilOperation.Replace,
            ReferenceStencil = 1,
            DepthBufferEnable = false,
        };

        s2 = new DepthStencilState
        {
            StencilEnable = true,
            StencilFunction = CompareFunction.LessEqual,
            StencilPass = StencilOperation.Keep,
            ReferenceStencil = 1,
            DepthBufferEnable = false,
        };

    }

    protected override void UnloadContent()
    {
        // TODO: Unload any non ContentManager content here
    }

    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        MouseState mState = Mouse.GetState();
        if (mState.LeftButton == ButtonState.Pressed)// && oldMouseState.LeftButton == ButtonState.Released)
        {
            hitX = mState.X - 32;
            hitY = mState.Y - 32;
            gotHit = true;
        }

        oldMouseState = mState;
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {

        if (notRendered)
        {

            GraphicsDevice.SetRenderTarget(layer);
            GraphicsDevice.Clear(Color.Transparent);
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, s1, null, a);
            spriteBatch.Draw(target, new Vector2(20, 20), Color.White);
            spriteBatch.End();

            GraphicsDevice.SetRenderTarget(null);
            notRendered = false;
        }

        if (gotHit)
        {


            GraphicsDevice.SetRenderTarget(layer);
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, s2, null, a);
            spriteBatch.Draw(projectile, new Vector2(hitX, hitY), Color.White);
            spriteBatch.End();


            gotHit = false;
        }

        GraphicsDevice.SetRenderTarget(null);
        GraphicsDevice.Clear(Color.DarkBlue); // moved this downwards!

        spriteBatch.Begin();
        spriteBatch.Draw(layer, Vector2.Zero, Color.White);
        spriteBatch.End();

        base.Draw(gameTime);
    }
}

}

The thing is you need to move the GraphicsDevice.Clear statement after the SetRenderTarget(null). So there is a prerender->clear->render sequence. Your stencil operation should happen during the prerender phase. This was introduced to align graphic operations with the XBox architecture.

(note: during some testing I moved the a,n,s1,s2 variables out of the draw loop)

\$\endgroup\$
  • \$\begingroup\$ Works perfectly! Thank you! That's something that I'll have to bear in mind in the future. \$\endgroup\$ – Toodice Sep 17 '15 at 14:19
0
\$\begingroup\$

I think you need to set the RenderTargetUsage to PreserveContent, if you are not clearing it when modifying.

\$\endgroup\$
  • \$\begingroup\$ PreserveContent is set. The contents of the render target aren't being lost as you would expect with the RenderTargetUsage being left at DiscardContents, as the opaque pixels still show and the transparency reappears when drawn again in the next frame. It's as if the last spriteBatch.Draw() call ignores the transparency only immediately after the render target has been modified. \$\endgroup\$ – Toodice Sep 16 '15 at 20:36
  • \$\begingroup\$ Ahhh, have you tried to enable the DepthBuffer? \$\endgroup\$ – jgallant Sep 16 '15 at 21:24
  • \$\begingroup\$ I have, and it makes no noticeable difference unfortunately. I'll update my question to include all of the source in case it's not actually related to Draw(). \$\endgroup\$ – Toodice Sep 16 '15 at 22:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.