Formeln

Thursday, April 18, 2013

Depth/Stencil Buffer

This tutorial is one part of a series of tutorials about generating procedural meshes. See here for an outline.

Problem

So far, we haven't cared about setting up and using the depth buffer. This results in the following problem, when rendering more than one object: the objects that is drawn last is painted over all other objects, regardless of their real order in 3D space.


In the picture above the grid is drawn first and then the cube is drawn. Because the cube is painted over the grid, the line of the gird marked in the red rectangle is not visible.


Conversely, if the grid is drawn last, the grid is always visible and the cube looks transparent. This looks cool when playing around with the camera, but is not exactly what we want in every situation ;)

The solution for painting objects in the right order to the screen according to their 3D positions is using a depth buffer.

Depth/Stencil Buffer 

A depth buffer, also called z-buffer, is a texture with the same size as the render target. The depth buffer is applied in one of the last stages of the pipeline, namely the output merger stage. What happens at the output merger stage?

To explain this, we need to take a step back in the rending pipeline and look at the rasterizer stage. (See here for the MSDN documentation on the Direct3D 11 graphics pipeline) When you provided some geometry and applied all the transformations to finally render your objects to the screen, your objects have to be rasterized. This means the scene gets sampled and discretized into pixels, or in other words: a raster is laid over your scene and each cell of this raster represents a pixel. This pixel is written to the render target.


At this point the depth buffer and the output merger stage comes into play. The depth buffer has the same height and width as you render target, because each pixel in the depth buffer corresponds to a pixel on the render target. When a object is rasterized into pixels and painted to the render target, the GPU has information about the depth of this pixel, or how far it is away from the camera. The depth buffer is for holding the depth of the current pixel on the render target. If the pixel of the current object in nearer to the camera than the pixel that is already at the according position, the old pixel is overwritten on the render target and the depth value in the depth buffer is updated to the new pixels depth. If the depth of a pixel is higher than an existing pixel, the old pixel will not be overwritten.
To sum up: the depth buffer keeps track of the depth of the pixels on the render target during the rasterization process. Only the nearest pixels of the scene make it to the final render target buffer.

As we don't need the stencil buffer right now, I won't go into much detail. Depth and stencil buffer are often combined in one texture. A common format is using 24 bit for the depth buffer and 8 bit for the stencil buffer.
The stencil is, as its name implies, a  template that can be applied in the output merger stage. This buffer can be used in various ways according to the flags set in the GPU. You can use it as a mask and don't allow rending to these pixels or you can use the buffer to count your write accesses to this pixel in one render cycle.

Creating the Depth/Stencil Buffer

Like I mentioned above, we need a texture for the depth buffer. This is initialized with the width and height of the control, we are rendering on. With this texture we can now create the depth/stencil buffer, which is in SlimDX a class called DepthStencilView:


Texture2D DSTexture = new Texture2D(
  device,
  new Texture2DDescription()
  {
    ArraySize = 1,
    MipLevels = 1,
    Format = Format.D32_Float,
    Width = form.ClientSize.Width,
    Height = form.ClientSize.Height,
    BindFlags = BindFlags.DepthStencil,
    CpuAccessFlags = CpuAccessFlags.None,
    SampleDescription = new SampleDescription(1, 0),
    Usage = ResourceUsage.Default
  }
);

depthStencil = new DepthStencilView(
  device,
  DSTexture,
  new DepthStencilViewDescription()
  {
    ArraySize = 0,
    FirstArraySlice = 0,
    MipSlice = 0,
    Format = Format.D32_Float,
    Dimension = DepthStencilViewDimension.Texture2D
  }
);

Setting the DepthStencil State

Now that we have set up the depth buffer, we need to create a depth/stencil state and assign it to the DepthStencilState of the output merger stage.

context.OutputMerger.DepthStencilState = DepthStencilState.FromDescription(
  device,
  new DepthStencilStateDescription()
  {
    DepthComparison = Comparison.Always,
    DepthWriteMask = DepthWriteMask.All,
    IsDepthEnabled = true,
    IsStencilEnabled = false
  }
);

context.OutputMerger.SetTargets(depthStencil, renderTarget);

DepthStencilStateDescription dssd = new DepthStencilStateDescription
{
  IsDepthEnabled = true,
  IsStencilEnabled = false,
  DepthWriteMask = DepthWriteMask.All,
  DepthComparison = Comparison.Less,
};

DepthStencilState depthStencilStateNormal;
depthStencilStateNormal = DepthStencilState.FromDescription(DeviceManager.Instance.device, dssd);
DeviceManager.Instance.context.OutputMerger.DepthStencilState = depthStencilStateNormal;

Putting it all together

This is the complete code of my method to create the depth/stencil buffer and to set the state:


public void CreateDepthStencilBuffer(System.Windows.Forms.Control form)
{
  Texture2D DSTexture = new Texture2D(
    device,
    new Texture2DDescription()
    {
      ArraySize = 1,
      MipLevels = 1,
      Format = Format.D32_Float,
      Width = form.ClientSize.Width,
      Height = form.ClientSize.Height,
      BindFlags = BindFlags.DepthStencil,
      CpuAccessFlags = CpuAccessFlags.None,
      SampleDescription = new SampleDescription(1, 0),
      Usage = ResourceUsage.Default
    }
  );

  depthStencil = new DepthStencilView(
    device,
    DSTexture,
    new DepthStencilViewDescription()
    {
      ArraySize = 0,
      FirstArraySlice = 0,
      MipSlice = 0,
      Format = Format.D32_Float,
      Dimension = DepthStencilViewDimension.Texture2D
    }
   );

  context.OutputMerger.DepthStencilState = DepthStencilState.FromDescription(
    device,
    new DepthStencilStateDescription()
    {
      DepthComparison = Comparison.Always,
      DepthWriteMask = DepthWriteMask.All,
      IsDepthEnabled = true,
      IsStencilEnabled = false
    }
  );

  context.OutputMerger.SetTargets(depthStencil, renderTarget);

  DepthStencilStateDescription dssd = new DepthStencilStateDescription
  {
    IsDepthEnabled = true,
    IsStencilEnabled = false,
    DepthWriteMask = DepthWriteMask.All,
    DepthComparison = Comparison.Less,
  };

  DepthStencilState depthStencilStateNormal;
  depthStencilStateNormal = DepthStencilState.FromDescription(DeviceManager.Instance.device, dssd);
  DeviceManager.Instance.context.OutputMerger.DepthStencilState = depthStencilStateNormal;
}


Clearing the Depth/Stencil Buffer

Because the GPU writes in every render cycle to the depth/stencil buffer, we need to clear this buffer in every render cycle. This is done with ClearDepthStencilView.

public void RenderScene()
{
  while (true)
  {
    fc.Count();
       
    DeviceManager dm = DeviceManager.Instance;
    dm.context.ClearDepthStencilView(dm.depthStencil, DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil, 1.0f, 0);
    dm.context.ClearRenderTargetView(dm.renderTarget, new Color4(0.75f, 0.75f, 0.75f));

    Scene.Instance.render();

    dm.swapChain.Present(syncInterval, PresentFlags.None);
  }
}


Camera

The next thing to do is to check, if you are setting the values for znear and zfar right, when setting up the perspective for the camera. When using the depth buffer, these values come into play in order to decide in which interval (from znear to zfar) objects are rendered.

I create my perspective matrix this way:

perspective = Matrix.PerspectiveFovLH((float)Math.PI / 4, 1.3f, 0.1f, 1000.0f);


Result

As you can see in the picture in the green rectangle, the grid is now rendered above the cube, if a line is in front of the cube. 


If you take a look at the right rectangle, you can see that the wireframe on the cube seems to be painted in dotted lines. But this is not how it is supposed to look. What is happening here is called z-buffer fighting. Because the cube and the wireframe are rendered on base of the same geometry, the output merger stage can't decide which pixel to render, because both pixels have the same depth.

To avoid this z-fighting, I add a depth bias to the wireframe in the rasterizer state of the shader:

RasterizerState WireframeState
{
  FillMode = Wireframe;
  SlopeScaledDepthBias = -0.5f;
};

Here is the documentation on MSDN on depth bias:
http://msdn.microsoft.com/en-us/library/windows/desktop/cc308048(v=vs.85).aspx

Now everything looks OK:



You can download the source code to this tutorial here.


No comments:

Post a Comment