Model I/Oで立体図形のメッシュを生成
Mar 5, 2017 · ios · macmetalswift3
Metalで描画のテストなどでさくっとモデルを表示したい時に、Model I/Oを使って3Dのモデルを生成する方法
法線やUV座標も作ってくれるので地味に便利である
生成されるモデル
生成できる図形の種類は、
- ボックス
- 球体
- シリンダ
- 円錐
- 平面
- 正20面体
生成時には大きさや分割数も指定できる
各頂点データの中身は、シェーダで書くと以下の感じ
struct Vertex {
float3 position [[ attribute(0) ]];
float3 normal [[ attribute(1) ]];
float2 texcoord [[ attribute(2) ]];
};
生成の方法
流れとしては、
MDLMesh
のクラスメソッドで指定した図形のメッシュを生成- Metalで扱えるように
MTKMesh
へ変換 - 頂点データに合わせて
MTLRenderPipelineState
を生成 - メッシュを描画
という感じになる
メッシュを生成
例えば、ボックスを生成したい場合は以下のとおり
let device = MTLCreateSystemDefaultDevice()!
let allocator = MTKMeshBufferAllocator(device: device)
let mdlMesh = MDLMesh.newBox(withDimensions: vector_float3(1),
segments: vector_uint3(2),
geometryType: .triangles,
inwardNormals: false,
allocator: allocator)
MDLMesh.newBox
がボックスのメッシュを生成するクラスメソッドで、
引数のdimensions
でサイズ、segments
で分割数を指定している。
図形の種類によって引数のパラメータは異なる(円筒なら半径も指定など)
inwardNormals
は法線の向きの指定。
通常はfalse
で良いが、スカイボックスや部屋の中のように立体の内側に視点がある場合はtrue
にする
メッシュを変換
MDLMesh
をMTKMesh
(Metalで描画する時に必要な情報全部がまとまったクラス)へ変換する
let mesh = try! MTKMesh(mesh: mdlMesh, device: device)
MTLRenderPipelineStateを生成
MTLRenderPipelineState
を生成する際にメッシュの頂点情報を与えて生成する
let vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(mesh.vertexDescriptor)
let renderDescriptor = MTLRenderPipelineDescriptor()
renderDescriptor.vertexDescriptor = vertexDescriptor
// ここで通常どおりシェーダなどの設定をする
let renderState = try! device.makeRenderPipelineState(descriptor: renderDescriptor)
ただし、生成されるメッシュの頂点形式は固定のようなので手動で設定するのもありかもしれない
メッシュを描画
renderEncoder.setVertexBuffer(mesh.vertexBuffers[0].buffer,
offset: 0, at: 0)
renderEncoder.drawIndexedPrimitives(
type: mesh.submeshes[0].primitiveType,
indexCount: mesh.submeshes[0].indexCount,
indexType: mesh.submeshes[0].indexType,
indexBuffer: mesh.submeshes[0].indexBuffer.buffer,
indexBufferOffset: mesh.submeshes[0].indexBuffer.offset)
メッシュのデータはインデックスバッファ形式で格納されているので、drawIndexedPrimitives
を使う。
また、VertexBuffer
は一つのみ、サブメッシュも一つのみ生成されるようなので固定にしている
ソース
必要最低限(ライト固定、テクスチャなし)のコードは以下の感じ
mac用だがiOSへもViewController周り以外はそのまま使える
import MetalKit | |
struct FrameUniforms { | |
var projectionViewMatrinx: matrix_float4x4 | |
var normalMatrinx: matrix_float3x3 | |
} | |
class ViewController: NSViewController, MTKViewDelegate { | |
private let defaultCameraMatrix = Matrix.lookAt(eye: float3(0, 2, 6), center: float3(), up: float3(0, 1, 0)) | |
@IBOutlet private weak var mtkView: MTKView! | |
private var device: MTLDevice! | |
private var commandQueue: MTLCommandQueue! | |
private var library: MTLLibrary! | |
private var renderState: MTLRenderPipelineState! | |
private var depthStencilState: MTLDepthStencilState! | |
private var frameUniformBuffer: MTLBuffer! | |
private let semaphore = DispatchSemaphore(value: 1) | |
private var mesh: MTKMesh! | |
private var modelMatrix = matrix_identity_float4x4 | |
// MARK: - | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// Do any additional setup after loading the view. | |
setupMetal() | |
loadModel() | |
mtkView.draw() | |
} | |
override var representedObject: Any? { | |
didSet { | |
// Update the view, if already loaded. | |
} | |
} | |
// MARK: - | |
private func setupMetal() { | |
device = MTLCreateSystemDefaultDevice()! | |
commandQueue = device.makeCommandQueue() | |
library = device.newDefaultLibrary()! | |
mtkView.sampleCount = 4 | |
mtkView.depthStencilPixelFormat = .depth32Float_stencil8 | |
mtkView.colorPixelFormat = .bgra8Unorm | |
mtkView.clearColor = MTLClearColorMake(0.3, 0.3, 0.3, 1) | |
mtkView.device = device | |
mtkView.delegate = self | |
let depthDescriptor = MTLDepthStencilDescriptor() | |
depthDescriptor.depthCompareFunction = .less | |
depthDescriptor.isDepthWriteEnabled = true | |
depthStencilState = device.makeDepthStencilState(descriptor: depthDescriptor) | |
frameUniformBuffer = device.makeBuffer(length: MemoryLayout<FrameUniforms>.size, options: []) | |
} | |
private func loadModel() { | |
let allocator = MTKMeshBufferAllocator(device: device) | |
let mdlMesh = MDLMesh.newBox(withDimensions: vector_float3(1), | |
segments: vector_uint3(2), | |
geometryType: .triangles, | |
inwardNormals: false, | |
allocator: allocator) | |
mesh = try! MTKMesh(mesh: mdlMesh, device: device) | |
let vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(mesh.vertexDescriptor) | |
let renderDescriptor = MTLRenderPipelineDescriptor() | |
renderDescriptor.vertexDescriptor = vertexDescriptor | |
renderDescriptor.sampleCount = mtkView.sampleCount | |
renderDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat | |
renderDescriptor.vertexFunction = library.makeFunction(name: "lambertVertex") | |
renderDescriptor.fragmentFunction = library.makeFunction(name: "lambertFragment") | |
renderDescriptor.depthAttachmentPixelFormat = mtkView.depthStencilPixelFormat | |
renderDescriptor.stencilAttachmentPixelFormat = mtkView.depthStencilPixelFormat | |
renderState = try! device.makeRenderPipelineState(descriptor: renderDescriptor) | |
} | |
// MARK: - MTKViewDelegate | |
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { | |
} | |
func draw(in view: MTKView) { | |
autoreleasepool { | |
semaphore.wait() | |
let commandBuffer = commandQueue.makeCommandBuffer() | |
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor!) | |
renderEncoder.pushDebugGroup("Render Object") | |
renderEncoder.setRenderPipelineState(renderState) | |
renderEncoder.setDepthStencilState(depthStencilState) | |
updateFramUniforms() | |
renderEncoder.setVertexBuffer(mesh.vertexBuffers[0].buffer, offset: 0, at: 0) | |
renderEncoder.setVertexBuffer(frameUniformBuffer, offset: 0, at: 1) | |
renderEncoder.drawIndexedPrimitives(type: mesh.submeshes[0].primitiveType, | |
indexCount: mesh.submeshes[0].indexCount, | |
indexType: mesh.submeshes[0].indexType, | |
indexBuffer: mesh.submeshes[0].indexBuffer.buffer, | |
indexBufferOffset: mesh.submeshes[0].indexBuffer.offset) | |
renderEncoder.popDebugGroup() | |
renderEncoder.endEncoding() | |
commandBuffer.present(view.currentDrawable!) | |
commandBuffer.addCompletedHandler { _ in | |
self.semaphore.signal() | |
} | |
commandBuffer.commit() | |
} | |
} | |
private func updateFramUniforms() { | |
let p = frameUniformBuffer.contents().assumingMemoryBound(to: FrameUniforms.self) | |
let cameraMatrix = Matrix.lookAt(eye: float3(0, 2, 4), center: float3(), up: float3(0, 1, 0)) | |
let projectionMatrix = Matrix.perspective(fovyRadians: radians(fromDegrees: 75), | |
aspect: Float(mtkView.drawableSize.width / mtkView.drawableSize.height), | |
nearZ: 0.1, | |
farZ: 100) | |
let viewModelMatrix = matrix_multiply(cameraMatrix, modelMatrix) | |
p.pointee.projectionViewMatrinx = matrix_multiply(projectionMatrix, viewModelMatrix) | |
let mat3 = Matrix.toUpperLeft3x3(from4x4: viewModelMatrix) | |
p.pointee.normalMatrinx = matrix_invert(matrix_transpose(mat3)) | |
} | |
} | |
Matrix
周りはこちらを参照
開発環境
- Xcode 8.2
- iOS 10
- macOS 10.12