Depth buffer π ΒΆ
Resulting code: step052-next
Resulting code: step052-vanilla-next

There is something wrong with the depth.ΒΆ
The Z-Buffer algorithmΒΆ
The issue we are facing with this basic example comes from the problem of visibility. As easy to conceive as it is, the question βdoes this point see that oneβ (i.e., does the line between them intersect any geometry) is hard to answer efficiently.
In particular, when producing a fragment, we must figure out whether the 3D point it corresponds to is seen by the viewpoint in order decide whether it must be blended into the output texture.
The Z-Buffer algorithm is what the GPUβs render pipeline uses to solve the visibility problem:
For each pixel, it stores the depth of the last fragment that has been blended into this pixel, or a default value (that represents the furthest depth possible).
Each time a new fragment is produced, its depth is compared to this value. If the fragment depth is larger than the currently stored depth, it is discarded without being blended. Otherwise, it is blended normally and the stored value is updated to the depth of this new closest fragment.
As a result, only the fragment with the lowest depth is visible in the resulting image. The depth value for each pixel is stored in a special texture called the Z-buffer. This is the only memory overhead required by the Z-buffer algorithm, making it a good fit for real time rendering.
Pipeline StateΒΆ
Since this Z-Buffer algorithm is a critical step of the 3D rasterization pipeline, it is implemented as a fixed-function stage. We configure it through the pipelineDesc.depthStencil
field, which we had left null so far.
DepthStencilState depthStencilState = Default;
{{Describe depth/stencil state}}
pipelineDesc.depthStencil = &depthStencilState;
WGPUDepthStencilState depthStencilState = WGPU_DEPTH_STENCIL_STATE_INIT;
{{Describe depth/stencil state}}
pipelineDesc.depthStencil = &depthStencilState;
Comparison functionΒΆ
The first aspect of the Z-Buffer algorithm that we can configure is the comparison function that is used to decide whether we should keep a new fragment or not.
The most common choice is to set it to Less
to mean that a fragment is blended only if its depth is less than the current value of the Z-Buffer (i.e., it is closer to the viewpoint).
depthStencilState.depthCompare = CompareFunction::Less;
depthStencilState.depthCompare = WGPUCompareFunction_Less;
Depth writeΒΆ
The second option we have is whether or not we want to update the value of the Z-Buffer once a fragment passes the test. It can be useful to deactivate this, when rendering user interface elements for instance, or when dealing with transparent objects, but for a regular use case, we indeed want to write the new depth each time a fragment is blended.
depthStencilState.depthWriteEnabled = OptionalBool::True;
depthStencilState.depthWriteEnabled = WGPUOptionalBool_True;
Note
This field uses the special type WGPUOptionalBool
, which may be either True
, False
or Undefined
because it is supposed to remain undefined when the depth/stencil state only contains a stencil and no depth buffer.
Texture formatΒΆ
Lastly, we must tell the pipeline how the depth values of the Z-Buffer are encoded in memory. We define for this a m_depthTextureFormat
class attribute because we need it in both InitializePipeline()
and when configuring the surface in Initialize()
:
private: // In Application.h
wgpu::TextureFormat m_depthTextureFormat = wgpu::TextureFormat::Depth24Plus;
private: // In Application.h
WGPUTextureFormat m_depthTextureFormat = WGPUTextureFormat_Depth24Plus;
We then use it when describing the depth state:
depthStencilState.format = m_depthTextureFormat;
Important
Depth textures do not use the same formats as color textures, they have their own set of possible values (all starting with Depth
). The same texture is used to represent both the depth and the stencil value, when enabled. It is common to use a depth encoded on 24 bits and leave the last 8 bits to a potential stencil buffer, so that it sums it to 32, which is a nice size for byte alignment.
Depth textureΒΆ
We must allocate the texture where the GPU stores the Z-buffer ourselves. Iβm going to be quick on this part, as we will come back on textures later on.
We first create a texture that has the size of our swap chain texture, with a usage of RenderAttachment
and a format that matches the one declared in depthStencilState.format
.
Note
We create this texture next to the surface configuration (in Initialize()
), because when we will start resizing the window, we will also want to resize the depth buffer:
// Create the depth texture after surface configuration
// Create the depth texture
TextureDescriptor depthTextureDesc = Default;
depthTextureDesc.label = StringView("Z Buffer");
depthTextureDesc.usage = TextureUsage::RenderAttachment;
depthTextureDesc.size = { 640, 480, 1 };
depthTextureDesc.format = m_depthTextureFormat;
Texture depthTexture = m_device.createTexture(depthTextureDesc);
// Create the depth texture
WGPUTextureDescriptor depthTextureDesc = WGPU_TEXTURE_DESCRIPTOR_INIT;
depthTextureDesc.label = toWgpuStringView("Z Buffer");
depthTextureDesc.usage = WGPUTextureUsage_RenderAttachment;
depthTextureDesc.size = { 640, 480, 1 };
depthTextureDesc.format = m_depthTextureFormat;
WGPUTexture depthTexture = wgpuDeviceCreateTexture(m_device, &depthTextureDesc);
We also create a texture view, which is what the render pipeline expects (like we did in chapter First color). The default view descriptor is all we need here, so we are even allowed not to specify a descriptor:
// Create the view of the depth texture manipulated by the rasterizer
m_depthTextureView = depthTexture.createView();
// We can now release the texture and only hold to the view
depthTexture.release();
// Create the view of the depth texture manipulated by the rasterizer
m_depthTextureView = wgpuTextureCreateView(depthTexture, nullptr);
// We can now release the texture and only hold to the view
wgpuTextureRelease(depthTexture);
The view will only be released at the end of the program, which is why we define it at the class level:
private: // In Application.h
wgpu::TextureView m_depthTextureView = nullptr;
private: // In Application.h
WGPUTextureView m_depthTextureView = nullptr;
And in Application::Terimnate()
:
// Release the depth texture view
m_depthTextureView.release();
// Release the depth texture view
wgpuTextureViewRelease(m_depthTextureView);
TODO
Check whether the render pass holds a reference to the texture view, in which case we can even release it right after. Then maybe define the depth texture in InitializePipeline()
actually.
Depth attachmentΒΆ
Like when attaching a color target or binding a uniform buffer, we define an object to βconnectβ our depth texture to the render pipeline. This is the RenderPassDepthStencilAttachment
:
// We already had a color attachment
// e.g., renderPassDesc.colorAttachments = &colorAttachment;
// We now add a depth/stencil attachment:
RenderPassDepthStencilAttachment depthStencilAttachment = Default;
{{Describe depth/stencil attachment}}
renderPassDesc.depthStencilAttachment = &depthStencilAttachment;
// We already had a color attachment
// e.g., renderPassDesc.colorAttachments = &colorAttachment;
// We now add a depth/stencil attachment:
WGPURenderPassDepthStencilAttachment depthStencilAttachment = WGPU_RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_INIT;
{{Describe depth/stencil attachment}}
renderPassDesc.depthStencilAttachment = &depthStencilAttachment;
We must set up clear/store operations for the stencil part as well even if we do not use it:
// Describe depth/stencil attachment
// The view of the depth texture
depthStencilAttachment.view = m_depthTextureView;
// The initial value of the depth buffer, meaning "far"
depthStencilAttachment.depthClearValue = 1.0f;
// Operation settings comparable to the color attachment
depthStencilAttachment.depthLoadOp = LoadOp::Clear;
depthStencilAttachment.depthStoreOp = StoreOp::Store;
// we could turn off writing to the depth buffer globally here
depthStencilAttachment.depthReadOnly = false; // NB: this is the default
// Describe depth/stencil attachment
// The view of the depth texture
depthStencilAttachment.view = m_depthTextureView;
// The initial value of the depth buffer, meaning "far"
depthStencilAttachment.depthClearValue = 1.0f;
// Operation settings comparable to the color attachment
depthStencilAttachment.depthLoadOp = WGPULoadOp_Clear;
depthStencilAttachment.depthStoreOp = WGPUStoreOp_Store;
// we could turn off writing to the depth buffer globally here
depthStencilAttachment.depthReadOnly = false; // NB: this is the default
ShaderΒΆ
The last thing we need to do is to set up a depth for each fragment, which we can do through the vertex shader (and the rasterizer will interpolate it for each fragment):
out.position = vec4f(position.x, position.y * ratio, /* set the depth here */ 1.0);
The depth value must be in the range \((0,1)\). We will build a proper way to define it in the next chapter but for now let use simply remap our position.z
from its range \((-1,1)\) to \((0,1)\):
out.position = vec4f(position.x, position.y * ratio, position.z * 0.5 + 0.5, 1.0);
let angle = uMyUniforms.time; // you can multiply it go rotate faster
let alpha = cos(angle);
let beta = sin(angle);
var position = vec3f(
in.position.x,
alpha * in.position.y + beta * in.position.z,
alpha * in.position.z - beta * in.position.y,
);
out.position = vec4f(position.x, position.y * ratio, position.z * 0.5 + 0.5, 1.0);
// This changes: ^^^^^^^^^^^^^^^^^^^^^^
ConclusionΒΆ
We now fixed the depth issue! And with this depth attachment we have set up an important part of the 3D rendering pipeline that we wonβt have to edit so much. We are now ready for more 3d transforms.

The depth ordering issue is gone!ΒΆ
Resulting code: step052-next
Resulting code: step052-vanilla-next