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
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;
}
Your third vertex has 0
in the w
position when it should have 1