Metalを活用してMacでComputer Graphics


 Apple は iOS 8の発表と同時に、Metal: API for GPU-accelerated 3D graphics を発表しました。Metal は、3Dグラフィックスハードウエアと相互に作用するハードウエアに近いレベルのAPIという意味では、OpenGL ESと類似していますが、クロスプラットフォームではなく、MacOSでのみで作動します。Metal はApple graphics hardware と非常に効率的に作動することを目的に設計されています。OpenGL ES に比較して格段に優れた性能を発揮します。Metal の公式Websiteはここです。

 Xcodeでコードを書くのはMacOSだと思いますので、コードを書いたMacでそのまま実行結果を確認できるのが最適です。MacOSのXcodeプロジェクトでiOS Simulatorが利用できるアプリを作成しているときは、そのままMetalが動作します。それ以外のケースでは、シミュレーションはiPhoneなどの実機で行います。

 このページで使用したMacでは、環境はOS10.14.1、Xcode 10.1、Swift 4.2, iOS 12.1 です。Xcode の簡単な使い方については、Swiftのページを参照してください。

関連記事
OpenGLでcomputer graphics
Webにcomputer graphicsを作成
Swiftのページ
Metalでcomputer graphicsのページ
iPhoneで人工知能を使う
WebGPUのページ
GitHub repositories

Last updated: 2019.2.10



****************************************************
Metalの使い方
****************************************************



Appleの公式サイトで提供されているサンプルコードを実行してみましょう。3角形を描くコードを、公式WebsiteからTopics -> Graphics と行って、"Hello Triangle" というファイル名をダウンロードしてください。HelloTriangle.zipでダウンロードされて、解凍・展開されて、HelloTriangleというフォルダー作成されます。このフォルダー内の "HelloTriangle.xcodeproj" をクリックして、Xcode を起動してください。説明は README.md ファイルに書かれています(英文)。このグラフィックスアプリを実行するためには、SigningのTeam 名にApple ID を入力する必要があります。この入力画面を出すためには、左側のリストの最上位の "HelloTriangle" をクリックしてください。

 メニューバーの3角形をクリックして、コンパイルを実行すると、ウインドウが開いで描画が始まります。以下のような3角形が表示されるはずです。
metal_1.jpg

 自身で3角形を描画するプログラムを書いてみましょう。Xcodeを起動して、Xcodeプロジェクトを立ち上げます。[macOS]→[Cocoa App]を選択します。[Unit Tests]と[UI Tests]のチェックは外しておきます(使わないので)。言語は [Swift ]を選択します。[Team ]と [Organization Identifier] には、後々のことを考慮すると、自身のApple ID を入れたほうが無難です。この段階では、iPhone などでのシミュレーションを行わないので、適当でもいいです。
//
//  ViewController.swift
//  Metal_sample
//
//  Created by 増山幸一 on 2019/02/10.
//  Copyright © 2019 増山幸一. All rights reserved.
//

import Cocoa
import MetalKit

class ViewController: NSViewController {
    
    private let device = MTLCreateSystemDefaultDevice()!
    
    private let positionData: [Float] = [
        +0.00, +0.75, 0, +1,
        +0.75, -0.75, 0, +1,
        -0.75, -0.75, 0, +1
    ]
    
    private let colorData: [Float] = [
        1, 1, 1, 1,
        0, 1, 0, 1,
        0, 1, 1, 1,
        ]
    
    private var commandQueue: MTLCommandQueue!
    private var renderPassDescriptor: MTLRenderPassDescriptor!
    private var bufferPosition: MTLBuffer!
    private var bufferColor: MTLBuffer!
    private var renderPipelineState: MTLRenderPipelineState!
    private var metalLayer: CAMetalLayer!;
    
    override func loadView() {
        view = NSView(frame: NSRect(x: 0, y: 0, width: 960, height: 540))
        view.layer = CALayer()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ビューを初期化
        initLayer();
        
        // Metalのセットアップ
        setupMetal()
        // バッファーを作成
        makeBuffers()
        // パイプラインを作成
        makePipeline()
        // 描画
        draw();
    }
    
    private func initLayer(){
        // レイヤーを作成
        metalLayer = CAMetalLayer()
        metalLayer.device = device
        metalLayer.pixelFormat = .bgra8Unorm
        metalLayer.framebufferOnly = true
        metalLayer.frame = view.layer!.frame
        view.layer!.addSublayer(metalLayer)
    }
    
    private func setupMetal() {
        // MTLCommandQueueを初期化
        commandQueue = device.makeCommandQueue()
        
        renderPassDescriptor = MTLRenderPassDescriptor()
        // このRender Passが実行されるときの挙動を設定
        renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadAction.clear
        renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreAction.store
        // 背景色は黒にする
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0)
    }
    
    private func makeBuffers() {
        let size = positionData.count * MemoryLayout.size
        // 位置情報のバッファーを作成
        bufferPosition = device.makeBuffer(bytes: positionData, length: size)
        // 色情報のバッファーを作成
        bufferColor = device.makeBuffer(bytes: colorData, length: size)
    }
    
    private func makePipeline() {
        guard let library = device.makeDefaultLibrary() else {fatalError()}
        let descriptor = MTLRenderPipelineDescriptor()
        descriptor.vertexFunction = library.makeFunction(name: "myVertexShader")
        descriptor.fragmentFunction = library.makeFunction(name: "myFragmentShader")
        descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        // レンダーパイプラインステートを作成
        renderPipelineState = try! device.makeRenderPipelineState(descriptor: descriptor)
    }
    
    func draw() {
        // ドローアブルを取得
        guard let drawable = metalLayer.nextDrawable() else {fatalError()}
        renderPassDescriptor.colorAttachments[0].texture = drawable.texture
        // コマンドバッファを作成
        guard let cBuffer = commandQueue.makeCommandBuffer() else {fatalError()}
        // エンコーダ生成
        let encoder = cBuffer.makeRenderCommandEncoder(
            descriptor: renderPassDescriptor
            )!
        encoder.setRenderPipelineState(renderPipelineState)
        // バッファーを頂点シェーダーに送る
        encoder.setVertexBuffer(bufferPosition, offset: 0, index: 0)
        encoder.setVertexBuffer(bufferColor, offset: 0, index:1)
        // 三角形を作成
        encoder.drawPrimitives(type: MTLPrimitiveType.triangle,
                               vertexStart: 0,
                               vertexCount: 3)
        // エンコード完了
        encoder.endEncoding()
        // 表示するドローアブルを登録
        cBuffer.present(drawable)
        // コマンドバッファをコミット(エンキュー)
        cBuffer.commit()
    }
}


次に、shaders.metal ファイルを作成します。ファイルをクリックして、[new] -> [file] ->[MacOS] -> [Metal File] と選択して、ファイル名を 'shaders' として、nextと行く。'shaders.metal' の内容を以下のように修正します。
//shadres.metal

#include 

using namespace metal;

// 構造体を定義
struct MyVertex {
    // 座標
    float4 position [[position]];
    // 色
    float4 color;
};

// 頂点シェーダー
vertex MyVertex myVertexShader(device float4 *position [[ buffer(0) ]],
                               device float4 *color [[ buffer(1) ]],
                               uint vid [[vertex_id]]) {
    MyVertex v;
    // 0番目のバッファーから頂点座標を設定
    v.position = position[vid];
    // 1番目のバッファーから頂点に色を設定
    v.color = color[vid];
    return v;
}

// 断片シェーダー
fragment float4 myFragmentShader(MyVertex vertexIn [[stage_in]]) {
    // 塗りの色を指定
    return vertexIn.color;
}

これで、プログラムは完成です。シミュレーションしてみましょう。メニューバーの3角形印をクリックすると、コンパイルが開始して、プログラムが実行されます。以下のようなウインドウが表示されれば、成功です。このコードは、このサイトからお借りしました。
metal_2.jpg

 一般的に、モデルデータやテクスチャなどのオブジェクトは「shared CPU/GPU memory buffer」上に格納されます。CPUはアプリケーションの実行において様々な仕事を担当しますが、GPUへの命令を作成して渡してあげるのも役割の1つです。GPUがレンダリング処理をするために必要なデータやプログラムをコマンドと言う形にまとめて、コマンドキューにプッシュします。 コマンドキューからポップされたコマンドをGPUが受け取り、レンダリング処理を行って画面に出力します。

 このコードは、MetalKit をimport しているので、記述が少し単純化されています。コードの内容については、後ほど説明します。

 ここまでは、コードの内容に踏み込まずに、Mac でMetal アプリを使うことに徹しました。ここから、iPhone 実機でのシミュレーションをするため手順について説明します。

 Website上でのMetalのサンプルはiOS向けに提供されているものが多いです。iOS端末におけるMetalの利点は、CPUとGPUが同じプロセッサ内に存在するという機構の利点を活かせること。具体的には、CPUとGPUでメモリの共有ができること。しかし、デメリットとしてiOSシミュレーターではMetalが動作しないため、実機に転送しないと試せません。開発中のちょっとした確認でも、iPhone実機に転送しないと駄目です。 XcodeからiPhone実機に転送しようとすると、色々とエラーが出ます。

 Metalを利用するには最低限、以下の7つのクラスを生成する必要があります。
MTLDevice
CAMetalLayer
Vertex Buffer
Vertex Shader
Fragment Shader
Render Pipeline
Command Queue
各処理の詳細については以下で説明しますが、Metalで描画する際のおおまかな流れです。

 上記の通り、最初に、'import Metal'時受する必要があります。その後、上記のクラスを作成します。例えば、以下のような記述を含みます。

import UIKit
import Metal

class ViewController: UIViewController {

var device: MTLDevice!
var metalLayer: CAMetalLayer!
var vertexBuffer: MTLBuffer!
var pipelineState: MTLRenderPipelineState!
var commandQueue: MTLCommandQueue!
 . . . 

OpenGL SL と同じく、vertex shader を記述す必要があります。

 プログラムを書いてきましょう。Xcode を起動します。Xcodeプロジェクトを立ち上げます。[iOS]→[Single View App]を選択します。product name 欄に適当な名前を入れます。言語は [Swift ]を選択します。[Team ]と [Organization Identifier] には、後々のことを考慮すると、自身のApple ID を入れたほうが無難です。プロジェクトの保存先を聞いてきますので適当な場所を指定します。「create」をクリックします。Xcode が立ち上がりいよいよアプリ作成の準備ができました。

 ViewController.swift をクリックして、エディターを開きます。中身を以下のようにします。
// ViewController.swift
//
//  Copyright (c) 2016 Razeware LLC
//

import UIKit
import Metal

class ViewController: UIViewController {

  let vertexData:[Float] = [
	0.0, 1.0, 0.0,
	-1.0, -1.0, 0.0,
	1.0, -1.0, 0.0]
  
  var device: MTLDevice!
  var metalLayer: CAMetalLayer!
  var vertexBuffer: MTLBuffer!
  var pipelineState: MTLRenderPipelineState!
  var commandQueue: MTLCommandQueue!
  var timer: CADisplayLink!
  
  override func viewDidLoad() {
	super.viewDidLoad()
	// Do any additional setup after loading the view, typically from a nib.
	
	device = MTLCreateSystemDefaultDevice()
	
	metalLayer = CAMetalLayer()          // 1
	metalLayer.device = device           // 2
	metalLayer.pixelFormat = .bgra8Unorm // 3
	metalLayer.framebufferOnly = true    // 4
	metalLayer.frame = view.layer.frame  // 5
	view.layer.addSublayer(metalLayer)   // 6
	
	let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0]) // 1
	vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: []) // 2
	
	// 1
	let defaultLibrary = device.newDefaultLibrary()!
	let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
	let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")
	
	// 2
	let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
	pipelineStateDescriptor.vertexFunction = vertexProgram
	pipelineStateDescriptor.fragmentFunction = fragmentProgram
	pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
	
	// 3
	pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
	
	commandQueue = device.makeCommandQueue()
	
	timer = CADisplayLink(target: self, selector: #selector(ViewController.gameloop))
	timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
  }
  
  func render() {
	guard let drawable = metalLayer?.nextDrawable() else { return }
	let renderPassDescriptor = MTLRenderPassDescriptor()
	renderPassDescriptor.colorAttachments[0].texture = drawable.texture
	renderPassDescriptor.colorAttachments[0].loadAction = .clear
	renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)
	
	let commandBuffer = commandQueue.makeCommandBuffer()
	let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
	renderEncoder.setRenderPipelineState(pipelineState)
	renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
	renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
	renderEncoder.endEncoding()
	
	commandBuffer.present(drawable)
	commandBuffer.commit()
  }
  
  func gameloop() {
	autoreleasepool {
	  self.render()
	}
  }

}


次に、上で説明したように、'Shader.metal' を書きます。
//
//  Shaders.metal
//  HelloMetal
//
//  Created by Andriy K. on 11/12/16.
//  Copyright © 2016 razeware. All rights reserved.
//

#include 
using namespace metal;

vertex float4 basic_vertex(                           // 1
						   const device packed_float3* vertex_array [[ buffer(0) ]], // 2
						   unsigned int vid [[ vertex_id ]]) {                 // 3
  return float4(vertex_array[vid], 1.0);              // 4
}

fragment half4 basic_fragment() { // 1
  return half4(1.0);              // 2
}


このコードの詳しい説明は、このサイトを参照してください。実は、このサイトで説明されている通りに、コードを記述していって、コンパイルをしたら、エラーが出てしまいました。原因がよくわからないので、コンパイルに成功した完成バージョンをのせました。

 iPhone 実機でのシミュレーションをするためには、iPhone と Mac 本体をusb接続します。コンパイル・エラーを出さないためには、Teamの欄に自身のApple ID を入力します。ケースバイケースですが、Organization Identifierにも同様に自身のIDを書き入れます。Xcode の「Preferences…」を開き、タブを「Accounts」にし、左下の「+」をクリックして「 Apple ID」を選択して、右下の「Manage Certificates…」をクリックして、認証します。Certificateが作成され、「Done」で終了です。Xcode のシミュレータを自身の実機に設定します。

 メニューバーの3角形をクリックして、コンパイルを実行すると、アプリがiPhone に送られます。アプリをクリックすると、ウインドウが開いて、以下のような3角形が描画表示されます。
HellowMetal.png



*********** 続く **************


トップ・ページに行く