Creating size-dependent resources
In this recipe, we will look at how the included sample framework deals with the initialization of size-dependent Direct3D resources within the base Direct3D application class. We review the base class's implementation, and then implement an override for a descending class.
We also review two important graphics pipeline preparation steps that are dependent upon the render target size: creating the viewport for the Rasterizer Stage (RS) and creating a depth/stencil buffer and view for the Output Merger (OM) stage.
Getting ready
We continue on from where we left off in the Using the sample rendering framework recipe.
How to do it…
The application base class D3DApplicationBase
initializes the swap chain buffers and render targets within the CreateSizeDependentResources
method, which is an event-handler attached to the D3DApplicationBase.OnSizeChanged
event. This method has been implemented as follows:
- The base implementation is a protected virtual method that allows the descending classes to extend the default behavior.
protected virtual void CreateSizeDependentResources(D3DApplicationBase app) { ... }
- After retrieving the device and device context from the device manager, the first action should be ensuring that any previous references to buffers have been released.
// Retrieve references to device and context var device = DeviceManager.Direct3DDevice; var context = DeviceManager.Direct3DContext; // Before swapchain can resize, buffers must be released RemoveAndDispose(ref _backBuffer); RemoveAndDispose(ref _renderTargetView); RemoveAndDispose(ref _depthStencilView); RemoveAndDispose(ref _depthBuffer); RemoveAndDispose(ref _bitmapTarget);
- If we are resizing an existing swap chain, then we will resize it using the
ResizeBuffers
method.// If the swap chain already exists, resize it. if (_swapChain != null) { _swapChain.ResizeBuffers( _swapChain.Description1.BufferCount, Width, Height, SharpDX.DXGI.Format.B8G8R8A8_UNorm, SharpDX.DXGI.SwapChainFlags.None); } // Otherwise, create a new one. else { ... create swap chain }
- If the swap chain has not already been initialized (or the device was reset), we need to create a new swap chain. This is done almost exactly as described in the Initializing a Direct3D 11.1/11.2 device and swap chain recipe in Chapter 1, Getting Started with Direct3D.
// SwapChain description var desc = CreateSwapChainDescription(); // Rather than create a new DXGI Factory we reuse the one // that has been used internally to create the device. // Retrieve the underlying DXGI Device from the D3D Device. // Access the adapter used for that device and then create // the swap chain using (var dxgiDevice2 = device.QueryInterface<SharpDX.DXGI.Device2>()) using (var dxgiAdapter = dxgiDevice2.Adapter) using (var dxgiFactory2 = dxgiAdapter.GetParent<SharpDX.DXGI.Factory2>()) using (var output = dxgiAdapter.Outputs.First()) { // The CreateSwapChain method allows us to override the // method of swap chain creation. _swapChain = ToDispose(CreateSwapChain(dxgiFactory2, device,desc)); // Retrieve the list of supported display modes DisplayModeList = output.GetDisplayModeList(desc.Format,DisplayModeEnumerationFlags.Scaling }
- With the swap chain resized or initialized, we retrieve the back buffer and create a render target view (RTV) for it.
// Obtain the backbuffer for this window BackBuffer = ToDispose( Texture2D.FromSwapChain<Texture2D>(_swapChain, 0)); // Create an RTV for the rendertarget. RenderTargetView = ToDispose(new RenderTargetView(device, BackBuffer));
- Next, we create the viewport. This is used by the rasterizer stage to map vertices from 3D clip space to 2D render target positions. We assign the viewport to the rasterizer stage using the
SetViewport
method.// Create a viewport descriptor of the render size. var viewport = new SharpDX.ViewportF( (float)RenderTargetBounds.X, (float)RenderTargetBounds.Y, (float)RenderTargetBounds.Width, (float)RenderTargetBounds.Height, 0.0f, // min depth 1.0f); // max depth // Set the current viewport for the rasterizer stage. context.Rasterizer.SetViewport(viewport);
Note
It is not necessary for the viewport to be of the same size as the render target, as vertices will be scaled to fit the viewport dimensions. If the view port is smaller than the target, then the output will only render to a subregion of the render target and appears zoomed-out. If the view port is larger than the target, then a portion of the output render will not be visible and the visible region will appear zoomed-in.
- Next, we create the depth buffer and a depth stencil view (DSV). After creating the DSV, we set the DSV and RTV as the render targets of the OM.
// Create a descriptor for the depth/stencil buffer. // Allocate a 2-D texture as the depth/stencil buffer. // Create a DSV to use on bind. this.DepthBuffer = ToDispose(new Texture2D(device, new Texture2DDescription() { Format = SharpDX.DXGI.Format.D24_UNorm_S8_UInt, ArraySize = 1, MipLevels = 1, Width = (int)RenderTargetSize.Width, Height = (int)RenderTargetSize.Height, SampleDescription = SwapChain.Description.SampleDescription, BindFlags = BindFlags.DepthStencil, })); this.DepthStencilView = ToDispose(new DepthStencilView( device, DepthBuffer, new DepthStencilViewDescription() { Dimension = (SwapChain.Description.SampleDescription.Count > 1 || SwapChain.Description.SampleDescription.Quality > 0) ? DepthStencilViewDimension.Texture2DMultisampled : DepthStencilViewDimension.Texture2D })); // Set the OutputMerger targets context.OutputMerger.SetTargets(DepthStencilView, RenderTargetView);
Tip
Using a 16-bit depth buffer can result in undesirable artifacts known as z-fighting or flimmering. A 24- or 32-bit buffer performs much better. However, it isn't possible to completely eliminate artifacts without the use of additional algorithms. Moving the near z-plane as far from the camera as possible can help against depth fighting artefacts. It is then possible to use the 16-bit depth buffer more effectively.
- Lastly, an example of the
CreateSizeDependentResources
method within a descending class to override the RS viewport might look something like the following code snippet:protected override void CreateSizeDependentResources( D3DApplicationBase app) { // Call base implementation base.CreateSizeDependentResources(app); // Retrieve device immediate context var context = this.DeviceManager.Direct3DContext; // Create a viewport descriptor of the render size. this.Viewport = new SharpDX.ViewportF( (float)RenderTargetBounds.X + 100, (float)RenderTargetBounds.Y + 100, (float)RenderTargetBounds.Width - 200, (float)RenderTargetBounds.Height - 200, 0.0f, // min depth 1.0f); // max depth // Set the current viewport for the rasterizer. context.Rasterizer.SetViewport(Viewport); }
How it works…
Before trying to create/resize a swap chain, we first make sure that any existing views that access swap chain resources are released. This is necessary to allow the call to SwapChain.ResizeBuffers
to work, and it is a good practice when reinitializing to ensure resources are released in a timely manner.
When creating a new swap chain instance, the swap chain description is retrieved by a call to the protected virtual function CreateSwapChainDescription
, and the actual creation of the swap chain is moved into the protected abstract function CreateSwapChain
. This allows the descending classes to override the default swap chain behavior. Then, it creates the swap chain from an Hwnd
handle for the desktop applications or with the Windows.UI.Core.CoreWindow
or SwapChainPanel
object for the Windows Store apps.
The DSV allows the OM to determine which fragments (all the information necessary to create a single pixel) will become actual pixels in the render target. The depth buffer (also known as the Z-buffer) is represented by a Texture2D
instance. It stores one or two components, the depth and, optionally, the stencil. We use the 64-bit D32_Float_S8X24_UInt
format that provides 32 bits for the depth and 8 bits for the stencil, while the remaining 24-bits are unused. The BindFlags
instance indicates that this texture will be bound as a DSV.
All render targets (including the depth buffer) bound to the OM must be of the same size and dimension; therefore when creating the depth buffer, it is important to use the same SampleDescription
structure that was used to create the back buffer (that is, the one returned from CreateSwapChainDescription
). If multisampling is enabled then we must use DepthStencilViewDimension.Texture2DMultisampled
as the Dimension
value in the DepthStencilViewDescription
structure; otherwise, we pass DepthStencilViewDimension.Texture2D
.
Finally, our descendent class's overridden method shows how to call the base implementation to prepare the swap chain, default render target, and depth/stencil buffer. Then it changes the viewport settings to render to the center of the render target with a 200 x 200 pixel reduction in size.
There's more…
The way the depth buffer works is better explained with a simple example. Imagine that a scene to be rendered includes a wall, and a cube is located behind this wall (from the viewer's perspective). If the wall is drawn first, then when the OM is determining whether the fragments for the cube should be rendered to pixels, it will check the depth buffer. If it sees that the depth of the wall is closer, then the cube fragments will not be drawn. If the cube was drawn first and then the wall, the pixels for the cube in the render target will be discarded. Then the fragments of the wall will be rendered as pixels in their place.
The stencil is used as a mask on a per-pixel basis, determining whether or not a pixel should be rendered. This can be used for rendering techniques such as dissolves, decaling (for example, scratches on a wall), outlining, silhouettes, shadows, fades, swipes, and composites (for example, a rear view mirror in a driving simulation), or within deferred rendering techniques for determining where lighting should be applied.
See also
- Chapter 3, Rendering Meshes, provides further information about the depth buffer