How do I switch to a different Rendertarget without clearing the DepthBuffer?

I’m having trouble trying to draw a refractive surface (a simple water plane) on top of an existing big complex scene, without having to draw the whole scene twice. To that end, I’m using the following steps:

  1. Initialise both RenderTargets with PreserveContents, in the superstitious hope that that would help (it didn’t)…
primaryRenderTarget = new RenderTarget2D(GraphicsDevice, 
    pp.BackBufferWidth, 
    pp.BackBufferHeight, 
    false, 
    pp.BackBufferFormat, 
    pp.DepthStencilFormat, 
    pp.MultiSampleCount, 
    RenderTargetUsage.PreserveContents);

bloomRenderTarget = new RenderTarget2D(GraphicsDevice, 
    pp.BackBufferWidth, 
    pp.BackBufferHeight, 
    false, 
    pp.BackBufferFormat, 
    pp.DepthStencilFormat, 
    pp.MultiSampleCount, 
    RenderTargetUsage.PreserveContents);
  1. Draw the whole scene to a rendertarget
GraphicsDevice.SetRenderTarget(primaryRenderTarget);
// Draw big complex scene
  1. Start up a new RenderTarget (this one will later be used for the bloom lighting post-process) and clear only the colour target, theoretically leaving the depth buffer right where it is.
GraphicsDevice.SetRenderTarget(bloomRenderTarget);
GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 1.0f, 0);
  1. Draw the previously rendered scene onto the new RenderTarget, being careful not to overwrite the depth buffer.
spriteBatch.Begin(SpriteSortMode.Immediate, 
                  BlendState.Opaque, 
                  null,
                  DepthStencilState.None);
spriteBatch.Draw(primaryRenderTarget, 
                  new Rectangle(0, 0, screenWidth, screenHeight),
                  Color.White);
spriteBatch.End();
  1. And finally, draw my refractive surface on top, feeding the already-drawn scene image into the refraction shader.
waterEffect.Parameters["refractionTexture"].SetValue(refractionMap);
waterPlane.Draw(GraphicsDevice, cameraMatrices, gameTime);

Straightforward enough in concept, except for the bit where it doesn’t work. The water plane simply ignores the depth buffer and draws itself on top of everything else, and I can’t work out why, even after a fair bit of searching.

This is an OpenGL project, if that matters.

Can anybody help?

Did you re-enable the DepthStencilState after the SpriteBatch … it will be still None after the SpriteBatch.

1 Like

I hadn’t been. Gave that a try just now. Unfortunately, now the water plane is not appearing at all, presumably as a result of being drawn entirely underneath the scene instead of on top of it.

that’s what the depthbuffer is supposed to do for geometry underneath other geometry. It’s basically just a written z-value per pixel, the GPU does not really have a sense for underneath - it just paints in the order you say - the depthbuffer just holds extra information about the written zvalue per pixel (in view space) and the DepthStencilState defines how to handle it (per pixel)

1 Like

To clarify: I meant the water plane is being drawn underneath/behind the entire scene image, including the stuff it would be in front of in 3d space. As you can see here, the water plane is not being rendered at all:

This is the result of adding this line before drawing the water plane:

GraphicsDevice.DepthStencilState = DepthStencilState.Default;

there is a bit less information … so everything in blue is done by some water plane shader as well? maybe some other geometry writes the depthbuffer - like another plane for the caustic effects seen on screen?

edit: Also set it to DepthStencilState.ReadOnly for your water effect, as you maybe dont want to write to the depthbuffer

also be aware, that SpriteBatch will also set BlendState etc - so you should reset that as well before the water shader (otherwise it’s still Opaque)

1 Like

so everything in blue is done by some water plane shader as well?

Yes, the terrain automatically applies blue fog and lighting effects, not related to the current problem. The actual water plane geometry is the reflective/refractive surface seen in the top image.

like another plane for the caustic effects seen on screen?

What you’re seeing there is refraction caused by the water plane itself. Those effects should not be happening on the grass that is not under the water.

Still no luck, I’m afraid. BlendState was already being reset in the WaterPlane.Draw() method.

not sure if you saw my edit - try DepthStencilState.ReadOnly - in case your water plane effect is multistaged it would otherwise write itselfs to the debthbuffer.

beside that - looks like either is writing to the depthbuffer which causes the water plane things to get discarded upon draw

I did try DepthStencilState.DepthRead. Same result as DepthStencilState.Default: the scene is drawn without the water plane.

Are we meant to be able to use the depth buffer across multiple targets? I’d have thought so, otherwise what’s the point of ClearOptions.DepthBuffer, but maybe I’m misunderstanding something?

Yes it’s meant to carry the DephtBuffer over multiple targets … it actually is default, but XBox had some stuff going with shared memory and XBox had to copy the whole stuff around, as it was discarded by default … there are some technical details about why it like that in XNA - but yea, it’s meant to be carried around.

When you turn off DepthState your plane is drawn, so the plane is drawn anyway - the pixels just get discarded when you turn on DepthBuffer, which basically means something is writing in the depthbuffer before the plane to make it get every pixel discarded.

When you turn off DepthState your plane is drawn, so the plane is drawn anyway - the pixels just get discarded when you turn on DepthBuffer, which basically means something is writing in the depthbuffer before the plane to make it get every pixel discarded.

The problem with that theory is, I’ve shown you all the relevant code. The only draw operations between the render target being set and the water plane being drawn are the Clear and the Spritebatch, both of which are instructed to leave the depth buffer alone. Here, this is the entirety of this section of code:

            this.settings = new BloomSettings(temperature, fertility);

            if (Visible)
            {
                GraphicsDevice.SetRenderTarget(sceneRenderTarget);
            }

            graphics.GraphicsDevice.Clear(ClearOptions.Target, Color.Transparent, 1.0f, 0);

            spriteBatch.Begin(SpriteSortMode.Immediate,
                              BlendState.Opaque,
                              null,
                              DepthStencilState.None);
            spriteBatch.Draw(primaryRenderTarget, new Rectangle(0, 0, screenWidth, screenHeight), Color.White);
            spriteBatch.End();

            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            if (world.currentBackgroundSetting.WaterType != WaterType.None)
            {
                    Texthre2D refractionMap = primaryRenderTarget;
	            waterPlane.Effect.Parameters["heightTexture"].SetValue(heightMap);
	            waterPlane.Effect.Parameters["refractionTexture"].SetValue(refractionMap);
	            waterPlane.Effect.Parameters["reflectionTexture"].SetValue(waterReflectionTarget);
	            waterPlane.Effect.Parameters["waterHeight"].SetValue(waterHeight);
	            waterPlane.Effect.Parameters["waterHeightRatio"].SetValue(waterHeightRatio);
	            //waterPlane.Effect.Parameters["waveOffset"].SetValue(waveModifier);
	            waterPlane.Effect.Parameters["terrainTotalWidth"].SetValue(absoluteTerrainWidth);
	            waterPlane.Effect.Parameters["cameraPosition"].SetValue(cCamera.Position);
	            waterPlane.Effect.Parameters["nTexture"].SetValue(waterNormalTexture);
	            waterPlane.Effect.Parameters["fertTempTexture"].SetValue(FertilityTemperatureMap);
	            waterPlane.Effect.Parameters["iceTexture"].SetValue(iceTexture);
	            waterPlane.Effect.Parameters["sulphurTexture"].SetValue(sulphurTexture);

	            effect.CurrentTechnique = effect.Techniques[technique];
	            effect.Parameters["World"].SetValue(worldMatrix);
	            effect.Parameters["View"].SetValue(camera.viewMatrix);
	            effect.Parameters["Projection"].SetValue(camera.projectionMatrix);
         
	            effect.Parameters["xTexture"].SetValue(texture);
	            effect.Parameters["xTime"].SetValue(time);

	            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
	            {
	                pass.Apply();

	                device.SetVertexBuffer(vertexBuffer);
            	    	                device.DrawPrimitives(Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleList, 0, vertexBuffer.VertexCount / 3);
            	}
	
	            GraphicsDevice.BlendState = BlendState.Opaque;
		}

This article implies this may end up being far more complicated than I had hoped.

The aforementioned complex scene uses dozens of different shaders, so I was hoping to avoid having to add special depth drawing code to all of them.

In MG the depth buffer is bound to the render target. If you set a render target, the associated depth buffer will be set too.

I see. That’s unfortunate for my purposes.

Is there a way to transfer an already-written depth buffer to a new render target without manually writing to a seperate depth texture from every single scene shader? I’m using a wide variety of effects in my scene and adding depth writes to all of them is going to be time consuming.

Oof. I just realized that writing a depth texture is going to be harder than I thought because BasicEffect doesn’t support it. So it’s not just adding it to existing shaders, I also have to replace BasicEffect in all the classes that use it. Which, like I said… oof.

That’s not worth it just for refraction. If I end up going that far I may as well just go all the way and implement proper deferred rendering. Where appropriate, of course. My terrain could benefit from it.

I haven’t marked this as answered because I’m still holding out hope that someone knows a trick I could use, but if not I’ll have to put this off and focus development on things with a better cost/benefit ratio.

I do, it’s called hacking the MonoGame source code.
It might actually not be all that difficult.

Alright, well I’m game to go diving I guess. Can’t test it right now, but I’ve got the source in front of me. Let’s see what we can see…

SetRenderTarget calls SetRenderTargets which calls ApplyRenderTargets. Seems like here is where most of the work is done.

Therer’s a Clear() at the very bottom of ApplyRenderTargets, and an InvalidateReadFramebuffer() in PlatformResolveRenderTargets(), but neither of those are called unless RenderTargetUsage == DiscardContents.

So by process of elimination, I’m going to make a guess that the problem is here, in PlatformApplyRenderTargets().

this.framebufferHelper.GenFramebuffer(out glFramebuffer);
this.framebufferHelper.BindFramebuffer(glFramebuffer);
var renderTargetBinding = this._currentRenderTargetBindings[0];
var renderTarget = renderTargetBinding.RenderTarget as IRenderTarget;
this.framebufferHelper.FramebufferRenderbuffer((int) FramebufferAttachment.DepthAttachment, renderTarget.GLDepthBuffer, 0);
this.framebufferHelper.FramebufferRenderbuffer((int) FramebufferAttachment.StencilAttachment, renderTarget.GLStencilBuffer, 0);

I’m well out of my depth by now, but I think this is generating a new Framebuffer and then setting it, a depth buffer and a stencil buffer to the device. Since the depth buffer is new, it has no information on it.

Simply removing the FramebufferRenderbuffer line won’t help, since then we’d just end up setting a FrameBuffer with only Colour and Stencil buffers on the device. But I’m not sure what I actually need to do here.

(Edit) I’m dumb. renderTarget.GLDepthBuffer isn’t a constant like I assumed, it’s a pointer. Specifically, to the new RenderTarget’s depth buffer, which was set up in the rendertarget constructor.

So in theory, all I have to do is inject the old RenderTarget’s GLDepthBuffer variable, and bob’s your uncle, the new RenderTarget inherits the old depth buffer.

Holy moly it actually worked. Just had to change the GLDepthBuffer.

I’m still processing it. I’d all-but given up on implementing refraction for the time being, and now it works. Still lots of tweaks to do, but it works. Thank you! [happy quasar noises]

3 Likes

Need to do something at the water edges next…

1 Like