Model I/Oでモデルをロード

MetalでModel I/O経由でモデルをファイルからロードする方法

公式サンプルよりもシンプルな方法

まだ確認できていない不明点もいろいろあるが、ある程度のモデルのロードはできたので、ひとまずまとめておく

概要

読み込める形式は、

の4つらしい。 objplyはロード確認済

読み込み確認済のモデルは、

といった感じ。Bunnyは.plyでそれ以外は.objで確認

ロードの流れ

流れとしては、

  1. 頂点フォーマットの情報(MDLVertexDescriptor)を作成
  2. 1の頂点フォーマットを指定してモデルのファイルをロード
  3. ロードしたデータからMTKMeshを生成
  4. 頂点データに合わせてMTLRenderPipelineStateを生成
  5. メッシュを描画

という感じになる

4以降は前回を参照

頂点フォーマットの作成

ロードする頂点フォーマットは前回と同じく、シェーダだと以下のような形式とする

struct Vertex {
    float3 position    [[ attribute(0) ]];
    float3 normal      [[ attribute(1) ]];
    float2 texcoord    [[ attribute(2) ]];
};

この各頂点属性を以下のようにして設定する

let mtlVertex = MTLVertexDescriptor()
mtlVertex.attributes[0].format = .float3
mtlVertex.attributes[0].offset = 0
mtlVertex.attributes[0].bufferIndex = 0
mtlVertex.attributes[1].format = .float3
mtlVertex.attributes[1].offset = 12
mtlVertex.attributes[1].bufferIndex = 0
mtlVertex.attributes[2].format = .float2
mtlVertex.attributes[2].offset = 24
mtlVertex.attributes[2].bufferIndex = 0
mtlVertex.layouts[0].stride = 32
mtlVertex.layouts[0].stepRate = 1

let modelDescriptor = MTKModelIOVertexDescriptorFromMetal(mtlVertex)
(modelDescriptor.attributes[0] as! MDLVertexAttribute).name = MDLVertexAttributePosition
(modelDescriptor.attributes[1] as! MDLVertexAttribute).name = MDLVertexAttributeNormal
(modelDescriptor.attributes[2] as! MDLVertexAttribute).name = MDLVertexAttributeTextureCoordinate

ここでの大事なポイントは、各頂点属性にnameの指定が必要であること。 実は、macだと10.11ではこの指定が無くても問題ないが、10.12だとこの指定がないとデータがロードされないという事象が発生して、かなりハマってしまった・・・

ちなみに、MDLVertexDescriptorを直接生成した場合はうまくいかなかったので、一旦、MTLVertexDescriptorで作成してから変換している(理由はナゾ)

モデルファイルをロード

let device = MTLCreateSystemDefaultDevice()!
let allocator = MTKMeshBufferAllocator(device: device)
let url = ... // ファイルのURL
let asset = MDLAsset(url: url,
                     vertexDescriptor: modelDescriptor,
                     bufferAllocator: allocator)

ちなみに、MDLVertexDescriptorで指定した頂点属性が、実際のモデルデータの中に存在しない場合、 その項目は0埋めされる (今回だとTeapotは法線データを持っていないので、normalには全て0が入っている)

メッシュを生成

先ほどロードしたデータからMetal用のMTKMeshを生成する

let mesh = try! MTKMesh.newMeshes(from: asset, device: device, sourceMeshes: nil).first!

なお、モデルはファイルに1つだけという決め打ちにしているので、firstで取得している。 複数のメッシュが生成されるパターンがどういうのか(そもそもそんな形式があるのか)は不明

ただし、MTKMeshsubmeshに複数のメッシュが入るケースはあるので、詳細は後述するが描画が前回と少し変わっている

描画

前回との違いは以下の部分。submeshが複数入る場合があるので、決め打ちではなくそれぞれ描画をしている

mesh.submeshes.forEach {
    renderEncoder.drawIndexedPrimitives(type: $0.primitiveType,
                                        indexCount: $0.indexCount,
                                        indexType: $0.indexType,
                                        indexBuffer: $0.indexBuffer.buffer,
                                        indexBufferOffset: $0.indexBuffer.offset)
}

小ネタ

以下のようにメッシュ生成時にNSArrayを渡すと、MDLMeshの配列を取得できる

var mdlArray: NSArray?
let mesh = try! MTKMesh.newMeshes(from: asset, device: device, sourceMeshes: &mdlArray).first!
let mdl = mdlArray![0] as! MDLMesh

なお、MDLMeshも1つだけという決め打ちにしている

描画だけなら特に不要なのだが、MDLMeshを使うと以下のようなことができる

モデルに法線を追加する

mdl.addNormals(withAttributeNamed: MDLVertexAttributeNormal, creaseThreshold: 1)

とすると、自動計算されたnormalがモデルに追加される(すでにある場合は上書き)

creaseThresholdがスムージングを設定する項目で1だとなし、0に近づくほどスムーズになる

元のモデルの頂点属性にnormalがない場合などに便利

バウンディングボックスの取得

boundingBoxのプロパティでモデルのローカル座標の範囲やスケールが取得できる

時々、モデルのスケールが大幅に違っていたり、原点(0, 0)からずれていて表示されない!ってことがあるが、そういった時にこれをチェックすれば問題の切り分けができて便利

基本的にはモデルをツール側で調整して再出力すれば良いのだが、デモ等でさっくりと表示させたい時用に、ちゃんとだいたい中心にくるような変換行列を用意して調整をかけている

let diff = mdl.boundingBox.maxBounds - mdl.boundingBox.minBounds
let scale = 1.0 / max(diff.x, max(diff.y, diff.z))
let center = (mdl.boundingBox.maxBounds + mdl.boundingBox.minBounds) / vector_float3(2)
modelMatrix = matrix_multiply(Matrix.scale(x: scale, y: scale, z: scale),
                              Matrix.translation(x: -center.x, y: -center.y, z: -center.z))

これも結構、ハマったポイント。あるモデルだけ全然表示されなくて、頂点フォーマット見ると大きな数字が入っていて、何か属性指定を間違えた!?と勘違いしていたら、単にモデルの中心が原点とは全然別の場所にあっただけというオチ(冷静に考えればそんなにハマるようなことではないのだけれど、、、)

ソース

今回はViewController部分だけ。シェーダなどは前回を参照

開発環境