skip to Main Content

I’m using Metal with Swift to build a 3D viewer for iOS and I have some issues to make the depth working. From now, I can draw and render a single shape correctly in 3D (like a simple square plane (4 triangles (2 for each face)) or a tetrahedron (4 triangles)).
However, when I try to draw 2 shapes together, the depth between these two shapes doesn’t work. For example, a plane is placed at Z axes = 0 behind a tetra which is placed at Z > 0. If I look a this scene from the back (camera placed somewhere at Z < 0), it’s ok. But when I look at this scene from the front (camera placed somewhere at Z > 0), it doesn’t work. The plane is drawn before the tetra even if it is placed behind the tetra.

Look from the back
Look from the front

I think that the plane is always drawn on the screen before the tetra (no matter the position of the camera) because the call of drawPrimitives for the plane is done before the call for the tetra. However, I was thinking that all the depth and stencil settings will deal with that properly.
I don’t know if the depth isn’t working because depth texture, stencil state and so on are not correctly set or because each shape is drawn in a different call of drawPrimitives.
In other words, do I have to draw all shapes in the same call of drawPrimitives to make the depth working ? The idea of this multiple call to drawPrimitives is to deal with different kinds of primitive type for each shape (triangle or line or …).

This is how I set the depth stencil state and the depth texture and the render pipeline :

   init() {
   // some miscellaneous initialisation …
   // … 
   // all MTL stuff : 
   commandQueue = device.makeCommandQueue()
   // Stencil descriptor
   let depthStencilDescriptor = MTLDepthStencilDescriptor()
   depthStencilDescriptor.depthCompareFunction = .less
   depthStencilDescriptor.isDepthWriteEnabled = true
   depthStencilState = device.makeDepthStencilState(descriptor: depthStencilDescriptor)!
   
   // Library and pipeline descriptor & state
   let library = try! device.makeLibrary(source: shaders, options: nil)
   // Our vertex function name
   let vertexFunction = library.makeFunction(name: "basic_vertex_function")
   // Our fragment function name
   let fragmentFunction = library.makeFunction(name: "basic_fragment_function")
   // Create basic descriptor
   let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
   // Attach the pixel format that si the same as the MetalView
   renderPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
   renderPipelineDescriptor.depthAttachmentPixelFormat = .depth32Float_stencil8
   renderPipelineDescriptor.stencilAttachmentPixelFormat = .depth32Float_stencil8
    
    
    //renderPipelineDescriptor.stencilAttachmentPixelFormat = .stencil8
   // Attach the shader functions
   renderPipelineDescriptor.vertexFunction = vertexFunction
   renderPipelineDescriptor.fragmentFunction = fragmentFunction
    
   // Try to update the state of the renderPipeline
   do {
        renderPipelineState = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
   } catch {
        print(error.localizedDescription)
   }
    
   // Depth Texture 
   let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .stencil8, width: 576, height: 723, mipmapped: false)
   desc.storageMode = .private
   desc.usage = .pixelFormatView
   depthTexture = device.makeTexture(descriptor: desc)!
   // Uniforms buffer
   modelMatrix = Matrix4()
   modelMatrix.multiplyLeft(worldMatrix)
   
   uniformBuffer = device.makeBuffer( length: MemoryLayout<Float>.stride*16*2, options: [])
   let bufferPointer = uniformBuffer.contents()
   memcpy(bufferPointer, &modelMatrix.matrix.m, MemoryLayout<Float>.stride * 16)
   memcpy(bufferPointer + MemoryLayout<Float>.stride * 16, &projectionMatrix.matrix.m, MemoryLayout<Float>.stride * 16)
   }

And the draw function :

function draw(in view: MTKView) {
    // create render pass descriptor
    guard let drawable = view.currentDrawable,
          let renderPassDescriptor = view.currentRenderPassDescriptor else {
        return
    } 

    renderPassDescriptor.depthAttachment.texture = depthTexture
    renderPassDescriptor.depthAttachment.clearDepth = 1.0
    //renderPassDescriptor.depthAttachment.loadAction = .load
    renderPassDescriptor.depthAttachment.loadAction = .clear
    renderPassDescriptor.depthAttachment.storeAction = .store

    // Create a buffer from the commandQueue
    let commandBuffer = commandQueue.makeCommandBuffer()
    let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
    commandEncoder?.setRenderPipelineState(renderPipelineState)
    commandEncoder?.setFrontFacing(.counterClockwise)
    commandEncoder?.setCullMode(.back)
    commandEncoder?.setDepthStencilState(depthStencilState)
    
    // Draw all obj in objects
    // objects = array of Object; each object describing vertices and primitive type of a shape
    // objects[0] = Plane, objects[1] = Tetra
    for obj in objects {
        createVertexBuffers(device: view.device!, vertices: obj.vertices)          
        commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        commandEncoder?.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
       
        commandEncoder?.drawPrimitives(type: obj.primitive, vertexStart: 0, vertexCount: obj.vertices.count)
    }
    
    commandEncoder?.endEncoding()
    commandBuffer?.present(drawable)
    commandBuffer?.commit()
    

}

Does anyone has an idea of what is wrong or missing ?
Any advice is welcome !

Edited 09/23/2022: Code updated

2

Answers


  1. Your depth texture pixel format is not correct, try to change its pixel format to: MTLPixelFormatDepth32Float or MTLPixelFormatDepth32Float_Stencil8.

    Login or Signup to reply.
  2. Few things of the top of my head:

    • First

      let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .depth32Float_stencil8, width: 576, height: 723, mipmapped: false)

    • Second

      renderPipelineDescriptor.depthAttachmentPixelFormat = .depth32Float_stencil8

    Notice the pixeFormat should be same in both places, and since you seem to be using stencil test as well so depth32Float_stencil8 will be perfect.

    • Third

      Now another thing you seem to be missing is, clearing depth texture before every render pass, am I right?
      So, you should set load action of depth attachment to .clear, like this:
      renderPassDescriptor.depthAttachment.loadAction = .clear

    • Fourth (Subjective to your usecase)*

      If none of the above works, you might need to discard framents with alpha = 0 in your fragment function by calling discard_fragment() when color you are returning has alpha 0


    Also note for future:

    Ideally you want depth texture to be fresh and empty when every new frame starts getting rendered (first draw call of a render pass) and then reuse it for subsequent draw calls in same render pass by setting load action .load and store action .store.

    ex: Assuming you have 3 draw calls, say drawing polygons wiz triangle, rectangle, sphere in one frame, then your depth attachment setup should be like this:

    Frame 1 Starts:

    • First Draw: triangle

      • loadAction: Clear
      • storeAction: Store
    • Second Draw: rectangle

      • loadAction: load
      • storeAction: Store
    • Third Draw: sphere

      • loadAction: load
      • storeAction: store/dontcare

    Frame 2 Starts: Notice you clear depth buffer for 1st draw call of new frame

    • First Draw: triangle

      • loadAction: Clear
      • storeAction: Store
    • Second Draw: rectangle

      • loadAction: load
      • storeAction: Store
    • Third Draw: sphere

      • loadAction: load
      • storeAction: store/dontcare
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search