Coding – Metal API, SwiftUI, Metal Shaders

If, like us, you found it tough to get into Apple’s brilliant Metal API maybe some of the following how-to’s can help you. The first few examples are all about utilising Metal from within the SwiftUI framework (which we think is brilliant by the way). Later we will add example code for pure Metal coding to run on Mac OS.

We broke down various pieces of code into example routines you can adjust into your code. Feel free to use (at your own risk) as you like.

We would be delighted to hear any feedback.  So, thanks for reading this page and thanks again, in advance, if you now go to the Contact page and send us comments.  We will do our best to answer all emails received.

  • How to open a Metal View from SwiftUI
  • Instancing – draw loads of triangles in a Metal View from SwiftUI
  • Simple way to save data from an iOS game if the game is backgrounded to avoid loss of data if iOS decides to terminate it (COMING)
  • Capture direction of swipe (COMING) and report it into you Metal draw loops
  • Matrix transformations between views in Metal (COMING)
  • Interactive particles – this is very cool and shows just how powerful the most basic phone is if you utilise Metal ! (COMING)
  • Navigating a 3D universe in Metal (COMING)

How to open a Metal View from SwiftUI (in a frame of fixed size)

This executes from top to bottom and is in skeleton form.  It should all work perfectly down to class Renderer: NSObject where you need to set up your Metal queues, libraries, buffers and pipelines and then you need to write the extension to Renderer: MTKViewDelegate so it draws something.  But this code will get you to the fun bit.

import SwiftUI
import Metal
import MetalKit

var body: some View { // Invoke a metal window from swiftUI view
           ZStack() {
                     // Add your own interfaces here
                     SwiftUIView {metalView()}.frame(width: CGFloat(something), height: CGFloat(something))
           }
}

public struct SwiftUIView: UIViewRepresentable {
           public var wrappedView: UIView
           private var handleUpdateUIView: ((UIView, Context) -> Void)?
           private var handleMakeUIView: ((Context) -> UIView)?
           public init(closure: () -> UIView) { wrappedView = closure() }
           public func makeUIView(context: Context) -> UIView {
                      guard let handler = handleMakeUIView else { return wrappedView }
                      return handler(context)
           }
           public func updateUIView(_ uiView: UIView, context: Context) { handleUpdateUIView?(uiView, context) }
}

public extension SwiftUIView {
           mutating func setMakeUIView(handler: @escaping (Context) -> UIView) -> Self {
                      handleMakeUIView = handler
                      return self
           }
           mutating func setUpdateUIView(handler: @escaping (UIView, Context) -> Void) -> Self {
                      handleUpdateUIView = handler
                      return self
           }
}

class metalView: MTKView {
           var renderer: Renderer!  // Once set up you can init this (see below)
           init() {
                      super.init(frame: .zero, device: MTLCreateSystemDefaultDevice()) // 2. Get an instance of the GPU
                      colorPixelFormat = .bgra8Unorm
                      self.framebufferOnly = false
                      self.preferredFramesPerSecond = 60
                      renderer = Renderer(device: device!, metalView: self)
                      delegate = renderer // Go and start the fun stuff in the rendering set-up and loops now the set up is done
           }
           required init(coder: NSCoder) { fatalError(“init(coder:) has not been implemented”) }
}

class Renderer: NSObject {
           // 3. Set up all queues, libraries, buffers and pipelines
}

extension Renderer: MTKViewDelegate {
           public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
                      // 4. Set some one-offs
           }
           func draw(in view: MTKView) {
                      // 5. Draw loop
           }
}

Instancing – draw loads of triangles in a Metal View from SwiftUI

Ok – 101 – Hello some triangles (the Metal render functions and support functions) in a Metal View window invoked from within SwiftUI.  They are drawn in a reasonably efficient way using rudimentary instancing import SwiftUI

import Metal
import MetalKit

// Set up objects (here a simple triangle)
struct Vertex {var position: SIMD2<Float>; var color: SIMD4<Float>; var texCord: SIMD2<Float>;}
var verticesMatrix = [Vertex(position: SIMD2<Float>(0,1), color: SIMD4<Float>(0,1,1,1), texCord: SIMD2<Float>(0.5, 0)), Vertex(position: SIMD2<Float>(-1,-1), color: SIMD4<Float>(1,0,1,1), texCord: SIMD2<Float>(0, 1)), Vertex(position: SIMD2<Float>(1,-1), color: SIMD4<Float>(1,1,1,1), texCord: SIMD2<Float>(1, 1))]
var uniformMatrix = [simd_float4x4]()

class Renderer: NSObject { // 3. Set up all queues, libraries, buffers and pipelines

    var queue: MTLCommandQueue!
    var renderState: MTLRenderPipelineState!
    var vertexMatrixBuffer: MTLBuffer!
    var instances: Int!

    init(device: MTLDevice, metalView: MTKView) {

        queue = device.makeCommandQueue()
        let library = device.makeDefaultLibrary()
        renderState = buildPipelineState(device: device) // 4. Set comm framework
        vertexMatrixBuffer = device.makeBuffer(bytes: verticesMatrix, length: MemoryLayout<Vertex>.stride * verticesMatrix.count, options: [])

        instances = 5

    }
}

extension Renderer: MTKViewDelegate {
  func draw(in view: MTKView) {

    guard let commandBuffer = queue.makeCommandBuffer(), let drawable = view.currentDrawable else { return } // Set up draw run

    // Render some meshes
    guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor!) else { return }
    renderEncoder.setRenderPipelineState(renderState)
    renderEncoder.setVertexBuffer(vertexMatrixBuffer, offset: 0, index: 0)
    uniformMatrix.removeAll()
      for count in 0 ..< instances {uniformMatrix.append((matrixTransform(transPosition: [Float(count) / 20, 0.5, 0,1]) * scalingMatrix(scaleX: 0.1, scaleY: 0.1, scaleZ: 1.0) ))}
    renderEncoder.setVertexBytes(&uniformMatrix, length: MemoryLayout<simd_float3x3>.size * instances, index: 1)
    renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: verticesMatrix.count, instanceCount: instances)
    renderEncoder.endEncoding()

    commandBuffer.present(drawable)
    commandBuffer.commit()
  }
 
  public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
}

func buildPipelineState(device: MTLDevice) -> MTLRenderPipelineState {

    let pipeDesc = MTLRenderPipelineDescriptor()
    pipeDesc.vertexFunction = device.makeDefaultLibrary()?.makeFunction(name: “baseVertFunc”) // 4a. Convert mesh to 2D
    pipeDesc.fragmentFunction = device.makeDefaultLibrary()?.makeFunction(name: “baseFragFunc”) // 4b. Colour the gaps
    pipeDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
   
    return try! device.makeRenderPipelineState(descriptor: pipeDesc)
   
}

import simd //  Maths

func matrixTransform(transPosition: SIMD4<Float>) -> simd_float4x4 {
    return simd_float4x4(columns:(SIMD4<Float>(1, 0, 0, 0), SIMD4<Float>(0, 1, 0, 0), SIMD4<Float>(0, 0, 0, 0), SIMD4<Float>(transPosition.x, transPosition.y, 0, 1)))
}

func scalingMatrix(scaleX: Float, scaleY: Float, scaleZ: Float) -> simd_float4x4 {
    return simd_float4x4(columns:(SIMD4<Float>(scaleX, 0, 0, 0), SIMD4<Float>(0, scaleY, 0, 0), SIMD4<Float>(0, 0, scaleZ, 0), SIMD4<Float>(0, 0, 0, 1)))
}

// You need a super simple shader function

#include <metal_stdlib>
using namespace metal;

struct VertexIn {float4 position;float4 color;float2 texCord;};

struct PerInstanceUniforms {float4x4 positioning;};

struct VertexOut {
    float4 position [[ position ]];
    float4 color;
    float2 texCord;
};

vertex VertexOut baseVertFunc(const device VertexIn *vertices [[ buffer(0) ]], constant PerInstanceUniforms *perInstanceUniforms [[ buffer(1) ]], uint vertexID [[ vertex_id  ]], uint iid [[ instance_id ]]) {

    VertexOut vOut;
    vOut.position = perInstanceUniforms[iid].positioning * float4(vertices[vertexID].position.x, vertices[vertexID].position.y, 0.0, 1);
    vOut.color = vertices[vertexID].color;
    vOut.texCord = vertices[vertexID].texCord;
    return vOut;
}

fragment float4 baseFragFunc(VertexOut vIn [[ stage_in ]]) {
    return vIn.color;
}

Copyright © 2021 geopoesis.com