Metalで三角形を描画するぞ!!

はじめに

前回背景色を適用するまでの処理をまとめたんですが、今回はいよいよシェーダを書いて三角形を描画してみます。

今回実装していくのは前回飛ばした画像の赤線で示している部分です。

三角形を書く

シェーダをロードする

シェーダ関数オブジェクトの取得

プロジェクトに登録されたシェーダはアプリのビルド時にmetal -> 中間オブジェクト -> ライブラリの順にロードされます。

ライブラリはMTLDeviceのmakeDefaultLibrary()で取得することができます。

※まだmetalファイルの作成はしていないので現時点ではreturnされます。

guard let library = device.makeDefaultLibrary() else {
    return
}
//metalファイルで記述したシェーダを取得する
guard let vertexFunction = library.makeFunction(name: "vertexShader"),
      let fragmentFunction = library.makeFunction(name: "fragmentShader") else {
    return
}

レンダーパイプライン状態オブジェクト作成

レンダーパイプラインはオブジェクトを画面に描画するまでの様々な工程(vertexShaderやfragmentShaderでの処理)を表す言葉です。

let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexFunction
pipelineStateDescriptor.fragmentFunction = fragmentFunction
pipelineStateDescriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat

do {
    self.pipelineState = try device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
} catch let error {
    print(error)
}

ビューポート(表示領域)を設定する

ビューポートに設定するサイズというのはドローアブルのサイズです。ドローアブルのサイズが変わるとMTKViewDelegateプロトコルのmtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)メソッドが呼ばれます。

あとはsetViewportでサイズをセットすればOKです。

private var viewportSize: CGSize = CGSize()

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

func draw(in view: MTKView) {
 ...省略...
    encoder.setViewport(MTLViewport(originX: 0,
                                    originY: 0,
                                    width: Double(viewportSize.width),
                                    height: Double(viewportSize.height),
                                    znear: 0.0,
                                    zfar: 1.0))
    
    encoder.endEncoding()
    
    if let drawable = view.currentDrawable {
        commandBuffer.present(drawable)
    }
    
    commandBuffer.commit()
}

シェーダーの共有定義ファイルの作成

シェーダ側とアプリ側で定数や構造体を共有する情報があり食い違うとうまく動かないので両方から参照される共有ファイルを作成します。

simdはベクトル計算を行うためのものです。身近なものだとAccelerateフレームワークとかで使われているようです。

enumで定義している物ですが、今回は頂点の座標と色ビューポートのサイズを渡すので渡す物を定義しています。MetalではGPUに渡すとき4KB以下のデータにする必要があるようなので引数を追加する際は注意してください。

ShaderVertexの構造体はVertexShaderで使用する座標と色を定義しています。このenumとstructを両方から参照します

#ifndef ShaderTypes_h
#define ShaderTypes_h

#include <simd/simd.h>

enum {
    kShaderVertexInputIndexVertices = 0;
    kShaderVertexInputIndexViewPortSize = 1;
};

typedef struct {
    vector_float2 position;
    vector_float4 color;
} ShaderVertex;

#endif /* ShaderTypes_h */

BridgingHeaderファイルを追加する

headerファイルを新規作成してShaderTypesをincludeします

#ifndef BridgingHeader_h
#define BridgingHeader_h

#include "ShaderTypes.h"

#endif /* BridgingHeader_h */

BridgingHeaderファイルを作成しBuildSettingのSwift Compiler-Generalで以下のようにパスを指定します

$(SRCROOT)/プロジェクト名/BridgingHeader.h

描画コマンドの発行

三角形を描画するために頂点の座標とビューポートをシェーダに渡して三角形を描画します。

private var vertices: [ShaderVertex] = [ShaderVertex]()

func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    viewportSize = size
    
    //三角形の頂点の座標を定義
    let color = vector_float4(1.0, 0.0, 0.0, 1.0)
    let width = Float(min(size.width, size.height))
    self.vertices = [ShaderVertex(position: vector_float2(0.0, width/4.0), color: color),
                     ShaderVertex(position: vector_float2(-width/4.0, -width/4.0), color: color),
                     ShaderVertex(position: vector_float2(width/4.0, -width/4.0), color: color)]
    
}


func draw(in view: MTKView) {
 ...省略
    encoder.setViewport(MTLViewport(originX: 0,
                                    originY: 0,
                                    width: Double(viewportSize.width),
                                    height: Double(viewportSize.height),
                                    znear: 0.0,
                                    zfar: 1.0))
    
    if let pipeline = pipelineState {
        //パイプライン状態オブジェクトを設定する
        encoder.setRenderPipelineState(pipeline)
        
        //Vertex関数に渡す引数を設定
        //頂点座標と色をセットする
        encoder.setVertexBytes(self.vertices,
                               length: MemoryLayout<ShaderVertex>.size * vertices.count,
                               index: kShaderVertexInputIndexVertices)
        
        //ビューポートをセットする
        var vpSize = vector_float2(Float(viewportSize.width/2.0),
                                   Float(viewportSize.height/2.0))
     
        encoder.setVertexBytes(&vpSize,
                               length: MemoryLayout<vector_float2>.size,
                               index: kShaderVertexInputIndexViewPortSize)
        
        encoder.drawPrimitives(type: .triangle,
                               vertexStart: 0,
                               vertexCount: 3)
        
    }
    
    encoder.endEncoding()
    
    if let drawable = view.currentDrawable {
        commandBuffer.present(drawable)
    }
    
    commandBuffer.commit()
}

シェーダを追加する

vertexShader

vertexIDは頂点が送られてきますdrawPrimitivesで3を指定しているので0, 1, 2が入ってきます処理は並列で行われます。

第二引数と第三引数に関してはアプリ側で渡した引数を指定しています(頂点と描画領域)

typedef struct {
    float4 position [[position]];
    float4 color;
} RasterizerData;

vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                                   constant ShaderVertex *vertices [[buffer(kShaderVertexInputIndexVertices)]],
                                   constant vector_float2 *viewportSize [[buffer(kShaderVertexInputIndexViewPortSize)]]) {
    RasterizerData result = {};
    result.position = float4(0.0, 0.0, 0.0, 1.0);
    result.position.xy = vertices[vertexID].position / (*viewportSize);
    result.color = vertices[vertexID].color;
    return result;
}

fragmentShader

fragmentShaderでは渡された色をそのまま返しています。

fragment float4 fragmentShader(RasterizerData in [[stage_in]]) {
    return in.color;
}

以下のように三角形が表示できていれば成功です!!

コード一覧

MetalView

import UIKit
import MetalKit

class MetalView: UIView {
    
    @IBOutlet weak var metalView: MTKView! {
        didSet {
            metalView.delegate = self
            metalView.clearColor = MTLClearColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1.0)
            metalView.device = MTLCreateSystemDefaultDevice()
        }
    }
    
    private var commandQueue: MTLCommandQueue?
    private var pipelineState: MTLRenderPipelineState?
    private var viewportSize: CGSize = CGSize()
    private var vertices: [ShaderVertex] = [ShaderVertex]()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadNib()
    }
    
    private func loadNib() {
        if let view = Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
            
            setup()
        }
    }
    
    func setup(){
        if let device = metalView.device {
            commandQueue = device.makeCommandQueue()
            setupPipelineState(device: device)
        }
    }
    
    private func setupPipelineState(device: MTLDevice) {
        guard let library = device.makeDefaultLibrary() else {
            return
        }
        
        //metalファイルで記述したシェーダを取得する
        guard let vertexFunction = library.makeFunction(name: "vertexShader"),
              let fragmentFunction = library.makeFunction(name: "fragmentShader") else {
            return
        }
        
        let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
        pipelineStateDescriptor.depthAttachmentPixelFormat = metalView.depthStencilPixelFormat
        pipelineStateDescriptor.label = "Triangle Pipeline"
        pipelineStateDescriptor.vertexFunction = vertexFunction
        pipelineStateDescriptor.fragmentFunction = fragmentFunction
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
        
        do {
            self.pipelineState = try device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
        } catch let error {
            print(error)
        }
    }

}

extension MetalView: MTKViewDelegate {
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        viewportSize = size
        
        //三角形の頂点の座標を定義
        let color = vector_float4(1.0, 0.0, 0.0, 1.0)
        let width = Float(min(size.width, size.height))
        self.vertices = [ShaderVertex(position: vector_float2(0.0, width/4.0), color: color),
                         ShaderVertex(position: vector_float2(-width/4.0, -width/4.0), color: color),
                         ShaderVertex(position: vector_float2(width/4.0, -width/4.0), color: color)]
        
    }
    
    func draw(in view: MTKView) {
        guard let commandBuffer = commandQueue?.makeCommandBuffer() else {
            return
        }
        
        guard let renderPassDescriptor = view.currentRenderPassDescriptor else {
            return
        }
        
        guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
            return
        }
        
        encoder.setViewport(MTLViewport(originX: 0,
                                        originY: 0,
                                        width: Double(viewportSize.width),
                                        height: Double(viewportSize.height),
                                        znear: 0.0,
                                        zfar: 1.0))
        
        if let pipeline = pipelineState {
            //パイプライン状態オブジェクトを設定する
            encoder.setRenderPipelineState(pipeline)
            
            //Vertex関数に渡す引数を設定
            //頂点座標と色をセットする
            encoder.setVertexBytes(self.vertices,
                                   length: MemoryLayout<ShaderVertex>.size * vertices.count,
                                   index: kShaderVertexInputIndexVertices)
            
            //ビューポートをセットする
            var vpSize = vector_float2(Float(viewportSize.width/2.0),
                                       Float(viewportSize.height/2.0))
            encoder.setVertexBytes(&vpSize,
                                   length: MemoryLayout<vector_float2>.size,
                                   index: kShaderVertexInputIndexViewPortSize)
            
            encoder.drawPrimitives(type: .triangle,
                                   vertexStart: 0,
                                   vertexCount: 3)
            
        }
        
        encoder.endEncoding()
        
        if let drawable = view.currentDrawable {
            commandBuffer.present(drawable)
        }
        
        commandBuffer.commit()
    }
}

Shader.metal

#include <metal_stdlib>
#include "ShaderTypes.h"

typedef struct {
    float4 position [[position]];
    float4 color;
} RasterizerData;

vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                                   constant ShaderVertex *vertices [[buffer(kShaderVertexInputIndexVertices)]],
                                   constant vector_float2 *viewportSize [[buffer(kShaderVertexInputIndexViewPortSize)]]) {
    RasterizerData result = {};
    result.position = float4(0.0, 0.0, 0.0, 1.0);
    result.position.xy = vertices[vertexID].position / (*viewportSize);
    result.color = vertices[vertexID].color;
    return result;
}

fragment float4 fragmentShader(RasterizerData in [[stage_in]]) {
    return in.color;
}

BridgingHeader

#ifndef BridgingHeader_h
#define BridgingHeader_h

#include "ShaderTypes.h"

#endif /* BridgingHeader_h */

ShaderTypes

#ifndef ShaderTypes_h
#define ShaderTypes_h

#include <simd/simd.h>

enum {
    kShaderVertexInputIndexVertices = 0,
    kShaderVertexInputIndexViewPortSize = 1,
};

typedef struct {
    vector_float2 position;
    vector_float4 color;
} ShaderVertex;

#endif /* ShaderTypes_h */