Search code examples
swiftmacosmetal

Rendering a rectangle using Metal


I'm trying to render a rectangle using Metal. But the rectangle is skewed as in the screenshot. I would like to understand what's going wrong here.

It seems like vertices of the rectangle aren't loaded correctly using the vertex index. I'm trying to follow the example as in this article - https://coldfunction.com/mgen/p/5a

enter image description here

Below is the code for MetalView and the shader used -

import Cocoa
import Metal

// Swift doesn't allow to extend a protocol with another protocol; however, we can do default implementation for a specific protocol.
extension NSObjectProtocol {
    /// Makes the receiving value accessible within the passed block parameter.
    /// - parameter block: Closure executing a given task on the receiving function value.
    public func setUp(_ block: (Self)->Void) {
        block(self)
    }
    
    /// Makes the receiving value accessible within the passed block parameter and ends up returning the modified value.
    /// - parameter block: Closure executing a given task on the receiving function value.
    /// - returns: The modified value
    public func set(_ block: (Self)->Void) -> Self {
        block(self)
        return self
    }
}


extension MetalView {
    private struct VertexInput {
        var position: SIMD4<Float>
        var rgba: SIMD4<Float>
    }
}

/// `NSView` handling the first basic metal commands.
final class MetalView: NSView {
    private let device: MTLDevice
    private let queue: MTLCommandQueue
    private let vertexBuffer: MTLBuffer
    private let indexCount: Int
    private let indexBuffer: MTLBuffer
//    private let rectBuffer: MTLBuffer
    private let renderPipeline: MTLRenderPipelineState
    
    init?(frame: NSRect, device: MTLDevice, queue: MTLCommandQueue) {
        // Setup the Device and Command Queue (non-transient objects: expensive to create. Do save it)
        (self.device, self.queue) = (device, queue)
        self.queue.label = App.bundleIdentifier + ".queue"
        
        // Setup shader library
        guard let library = device.makeDefaultLibrary(),
            let vertexFunc = library.makeFunction(name: "rect_vertex"),
            let fragmentFunc = library.makeFunction(name: "rect_fragment") else { return nil }
        
        // Setup pipeline (non-transient)
        let pipelineDescriptor = MTLRenderPipelineDescriptor().set {
            $0.vertexFunction = vertexFunc
            $0.fragmentFunction = fragmentFunc
            $0.colorAttachments[0].pixelFormat = .bgra8Unorm   // 8-bit unsigned integer [0, 255]
        }
        guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { return nil }
        self.renderPipeline = pipelineState
        
        // Setup buffer (non-transient). Coordinates defined in clip space: [-1,+1]
        let vertices =  [VertexInput(position: SIMD4(-0.5, -0.5, 0.0, 1.0), rgba: SIMD4(1.0, 1.0, 1.0, 1.0)),
                         VertexInput(position: SIMD4(0.5, 0.5, 0.0, 1.0), rgba: SIMD4(0.0, 1.0, 1.0, 1.0)),
                         VertexInput(position: SIMD4(-0.5, 0.5, 0.0, 0.0), rgba: SIMD4(1.0, 0.0, 0.0, 1.0)),
                         VertexInput(position: SIMD4(0.5, -0.5, 0.0, 1.0), rgba: SIMD4(0.0, 1.0, 1.0, 1.0))]
        let size = vertices.count * MemoryLayout<VertexInput>.stride
        guard let buffer = device.makeBuffer(bytes: vertices, length: size, options: .cpuCacheModeWriteCombined) else { return nil }
        self.vertexBuffer = buffer.set { $0.label = App.bundleIdentifier + ".buffer" }
        
        // set index info
        let indexInfo : [UInt16] = [2, 1, 0, 0, 3, 1];
        let indexCount = indexInfo.count * MemoryLayout<UInt16>.stride
        guard let indexBuffer = device.makeBuffer(bytes: indexInfo, length: indexCount, options: .cpuCacheModeWriteCombined) else { return nil }
        self.indexBuffer = indexBuffer.set { $0.label = App.bundleIdentifier + ".buffer" }
        self.indexCount = indexInfo.count
        
        super.init(frame: frame)
        
        // Setup layer (backing layer)
        self.wantsLayer = true
        self.metalLayer.setUp { (layer) in
            layer.device = device
            layer.pixelFormat = .bgra8Unorm
            layer.framebufferOnly = true
        }
    }
    
    required init?(coder aDecoder: NSCoder) { fatalError() }
    private var metalLayer: CAMetalLayer { self.layer as! CAMetalLayer }
    override func makeBackingLayer() -> CALayer { CAMetalLayer() }
    
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        
        guard let window = self.window else { return }
        self.metalLayer.contentsScale = window.backingScaleFactor
        self.redraw()
    }
    
    override func setBoundsSize(_ newSize: NSSize) {
        super.setBoundsSize(newSize)
        self.metalLayer.drawableSize = convertToBacking(bounds).size
        self.redraw()
    }
    
    override func setFrameSize(_ newSize: NSSize) {
        super.setFrameSize(newSize)
        self.metalLayer.drawableSize = convertToBacking(bounds).size
        self.redraw()
    }
}

extension MetalView {
    /// Draws a triangle in the metal layer drawable.
    private func redraw() {
        // Setup Command Buffer (transient)
        guard let drawable = self.metalLayer.nextDrawable(),
              let commandBuffer = self.queue.makeCommandBuffer() else { return }
        
        let renderPass = MTLRenderPassDescriptor().set {
            $0.colorAttachments[0].setUp { (attachment) in
                attachment.texture = drawable.texture
                attachment.clearColor = MTLClearColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1)
                attachment.loadAction = .clear
                attachment.storeAction = .store
            }
        }
        
        guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) else { return }
        encoder.setRenderPipelineState(self.renderPipeline)
        encoder.setVertexBuffer(self.vertexBuffer, offset: 0, index: 0)
        encoder.drawIndexedPrimitives(type: .triangle, indexCount: self.indexCount, indexType: .uint16, indexBuffer: self.indexBuffer, indexBufferOffset: 0)
//        encoder.setFrontFacing(.counterClockwise)
        encoder.endEncoding()
        
        // Present drawable is a convenience completion block that will get executed once your command buffer finishes, and will output the final texture to screen.
        commandBuffer.present(drawable)
        commandBuffer.commit()
    }
}

Shader.metal

#include <metal_stdlib>
using namespace metal;

struct VertexInput {
 float4 position [[ position ]];
 float4 rgba;
};

vertex VertexInput rect_vertex(device VertexInput const* const vertices [[buffer(0)]], uint vid [[vertex_id]]) {
    return vertices[vid];
}

fragment float4 rect_fragment(VertexInput vert [[stage_in]]) {
    return vert.rgba;
}

Solution

  • Your third vertex has 0 in the w position when it should have 1