3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftUIでMetalKit

Posted at

SwiftUIでMetalKit(MTKView)を使ってみたくなったので、実装してみた。

ゴール

こんな風に使える様にしたい。

struct ContentView: View {
    @EnvironmentObject var representedObject: Model
    var body: some View {
        VStack {
            Text(representedObject.msg)
                .font(.largeTitle)
            MetalView()
        }.padding()
    }
}

実装

MetalViewでNSViewRepresentableを実装し、MetalViewCoordinatorでMTKViewDelegateを実装した。
MetalViewCoordinatorはNSViewRepresentableのCoordinatorにもなっている。

MetalView

MTKViewを作るが、保持はMetalViewCoordinatorに任せた。
ただし、MTKViewの設定はMetalViewでのみ行う様にした。

struct MetalView: NSViewRepresentable {
    typealias NSViewType = MTKView
    typealias Coordinator = MetalViewCoordinator

    func makeCoordinator() -> MetalViewCoordinator {
        let view = MTKView()
        view.device = view.preferredDevice
        return MetalViewCoordinator(view)
    }

    func makeNSView(context: Context) -> MTKView {
        let view = context.coordinator.view
        view.delegate = context.coordinator
        view.enableSetNeedsDisplay = true
        return view
    }
}

MetalViewCoordinator

MTKViewを保持しMTKViewDelegateを実装している。
Metalに関するものはすべてこのクラスで実装している。

class MetalViewCoordinator: NSObject, MTKViewDelegate {
    var view: MTKView
    var device: MTLDevice!
    init(_ view: MTKView) {
        self.view = view
        self.device = view.device
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
         :
    }

    func draw(in view: MTKView) {
         :
    }
}

AppDelegate

ウィンドウを閉じた時に、アプリケーションを終わらせたかったので、NSApplicationDelegateAdaptorをつかってAppDelegateを実装した。

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
}

struct SwiftUIApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

まとめ

MetalViewCoordinatorのおかげでMTKViewDelegateを実装するクラスを別に準備する必要がなかった。
MTKViewの保持はMetalViewMetalViewCoordinatorのどちらがより相応しいのかはよくわからなかった。

SwUIApp.png

ソースコード

ContentView
ContentView.swift
import SwiftUI
import MetalKit

struct ContentView: View {
    @EnvironmentObject var representedObject: Model
    var body: some View {
        VStack {
            Text(representedObject.msg)
                .font(.largeTitle)
            MetalView()
        }.padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class MetalViewCoordinator: NSObject, MTKViewDelegate {
    var view: MTKView
    var device: MTLDevice!
    var library: MTLLibrary!
    var commandQueue: MTLCommandQueue!
    var renderPipelineState: MTLRenderPipelineState!
    var vertexBuffer: MTLBuffer!

    func makeCommandBuffer() -> MTLCommandBuffer {
        commandQueue.makeCommandBuffer()!
    }

    func makeFunction(_ name: String) -> MTLFunction? {
        library.makeFunction(name: name)
    }

    struct Vertex {
        var position: simd_float3
        var color: simd_float4
    }
    
    var vertices: [Vertex] = [
        Vertex(position: simd_float3( 0,  1, 0), color: simd_float4(1, 0, 0, 1)),
        Vertex(position: simd_float3(-1, -1, 0), color: simd_float4(0, 1, 0, 1)),
        Vertex(position: simd_float3( 1, -1, 0), color: simd_float4(0, 0, 1, 1))
    ]

    init(_ view: MTKView) {
        self.view = view
        self.device = view.device
        library = device.makeDefaultLibrary()
        commandQueue = device.makeCommandQueue()
        super.init()

        let desc = MTLRenderPipelineDescriptor()
        desc.colorAttachments[0].pixelFormat = .bgra8Unorm
        desc.vertexFunction = makeFunction("vertex_function")
        desc.fragmentFunction = makeFunction("fragment_function")
        renderPipelineState = try! device.makeRenderPipelineState(descriptor: desc)
        vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count, options: [])
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        guard view.frame.size != size else { return }
        view.drawableSize = view.frame.size
    }

    func draw(in view: MTKView) {
        let cmdbuf = makeCommandBuffer()
        let cmdenc = cmdbuf.makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor!)!
        cmdenc.setRenderPipelineState(renderPipelineState)
        cmdenc.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        cmdenc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
        cmdenc.endEncoding()
        cmdbuf.present(view.currentDrawable!)
        cmdbuf.commit()
    }
}

struct MetalView: NSViewRepresentable {
    typealias NSViewType = MTKView
    typealias Coordinator = MetalViewCoordinator

    func makeCoordinator() -> MetalViewCoordinator {
        let view = MTKView()
        view.device = view.preferredDevice
        return MetalViewCoordinator(view)
    }

    func makeNSView(context: Context) -> MTKView {
        let view = context.coordinator.view
        view.delegate = context.coordinator
        view.enableSetNeedsDisplay = true
        return view
    }

    func updateNSView(_ view: MTKView, context: Context) { }

    static func dismantleNSView(_ view: MTKView, coordinator: ()) { }
}
Shader
Shader.metal
# include <metal_stdlib>
using namespace metal;

struct Cpu2Vertex {
    float3 position;
    float4 color;
};

struct Vertex2Fragment {
    float4 position [[ position ]];
    float4 color;
};

vertex Vertex2Fragment vertex_function(const device Cpu2Vertex *vertices [[ buffer(0) ]],
                                       uint vertexID [[ vertex_id ]]) {
    Vertex2Fragment r;
    r.position = float4(vertices[vertexID].position, 1);
    r.color = vertices[vertexID].color;
    return r;
}

fragment float4 fragment_function(Vertex2Fragment v [[ stage_in ]]) {
    return v.color;
}
main
App.swift
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) { }
    func applicationWillTerminate(_ notification: Notification) { }
    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
}

class Model: ObservableObject {
    @Published var msg: String = "hello!"
}

@main
struct SwUIApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 300, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity, alignment: .center)
                .environmentObject(Model())
        }
    }
}
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?