Model I/Oでモデルをロード
Mar 29, 2017 · ios · macmetalswift3
MetalでModel I/O経由でモデルをファイルからロードする方法
公式サンプルよりもシンプルな方法
まだ確認できていない不明点もいろいろあるが、ある程度のモデルのロードはできたので、ひとまずまとめておく
概要
読み込める形式は、
- .obj
- .ply
- .stl
- .abc
の4つらしい。
obj
とply
はロード確認済
読み込み確認済のモデルは、
- Infinite, 3D Head Scan
- Teapot
- Stanford Bunny
- realship(公式のサンプル付属モデル)
といった感じ。Bunnyは.ply
でそれ以外は.obj
で確認
ロードの流れ
流れとしては、
- 頂点フォーマットの情報(
MDLVertexDescriptor
)を作成 - 1の頂点フォーマットを指定してモデルのファイルをロード
- ロードしたデータから
MTKMesh
を生成 - 頂点データに合わせて
MTLRenderPipelineState
を生成 - メッシュを描画
という感じになる
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
で取得している。
複数のメッシュが生成されるパターンがどういうのか(そもそもそんな形式があるのか)は不明
ただし、MTKMesh
のsubmesh
に複数のメッシュが入るケースはあるので、詳細は後述するが描画が前回と少し変わっている
描画
前回との違いは以下の部分。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部分だけ。シェーダなどは前回を参照
開発環境
- Xcode 8.2
- iOS 10
- macOS 10.12 / macOS 10.11