Loading and Rendering .OBJ Models (Metal Part 15)

z4gon
z4gon

Mesh References describing the .OBJ asset and how to load it. Mesh Loader to actually use ModelIO and load the MTKMeshes out of the .OBJ file, using a MTKModelIOVertexDescriptor. Extending the Mesh class to have two subclasses, the BuiltInMesh and the ModelMesh. The Model Mesh will have a reference to the MTKMesh with all the submeshes, vertex and index buffers. Modifying the Mesh Renderer to be able to render MTKMeshes. Using Texture References and Texture Loaders to load any needed texture for the models.

Source Code

See Project in GitHub 👩‍💻

References

Table of Content


Model Mesh Reference

The Model Mesh Reference will point to the asset that we want to load from.

It will also have a load() method that will be in charge of initializing the actual Mesh object.

1protocol MeshReference { 2 var id: String! { get } 3 func load()->Mesh 4} 5 6class ModelMeshReference : MeshReference { 7 public var name: String! 8 public var fileExtension: String! = "obj" 9 public var meshIndex: Int = 0 10 11 public var id: String! { 12 return "\(name!).\(fileExtension!):\(meshIndex)" 13 } 14 15 init( 16 _ name: String, 17 fileExtension: String = "obj", 18 meshIndex: Int = 0 19 ){ 20 self.name = name 21 self.fileExtension = fileExtension 22 self.meshIndex = meshIndex 23 } 24 25 func load()->Mesh { 26 return ModelMeshLoader(self).load() 27 } 28}

This will act as an identifier when caching model meshes in the corresponding cache.

1 public var id: String! { 2 return "\(name!).\(fileExtension!):\(meshIndex)" 3 }
1class MeshCache : Cache<MeshReference, Mesh> { 2 3 private static var _meshes: [String: Mesh] = [:] 4 5 override class func get(_ meshReference: MeshReference)->Mesh{ 6 7 if(!_meshes.keys.contains(meshReference.id)) { 8 _meshes.updateValue(meshReference.load(), forKey: meshReference.id) 9 } 10 11 return _meshes[meshReference.id]! 12 } 13}

Model Mesh Loader

Will be in charge of actually using ModelIO to load the data for the vertices from the .obj file.

It will initialize a vertex descriptor off of our metal BasicVertexDescriptor.

Since .obj files can contain multiple meshes inside, we will also just return the one specified in the mesh reference.

1class ModelMeshLoader { 2 private var _meshReference: ModelMeshReference! 3 4 init(_ meshReference: ModelMeshReference) { 5 _meshReference = meshReference 6 } 7 8 public func load()->Mesh { 9 guard let url = Bundle.main.url(forResource: _meshReference.name, withExtension: _meshReference.fileExtension) else { 10 fatalError("ERROR::LOADING::MODEL::__\(_meshReference.name!).\(_meshReference.fileExtension!)__::does not exist") 11 } 12 13 let vertexDescriptor = MTKModelIOVertexDescriptorFromMetal(VertexDescriptorCache.get(.Basic)) 14 15 // make each attribute mapped to each attribute type 16 (vertexDescriptor.attributes[0] as! MDLVertexAttribute).name = MDLVertexAttributePosition 17 (vertexDescriptor.attributes[1] as! MDLVertexAttribute).name = MDLVertexAttributeNormal 18 (vertexDescriptor.attributes[2] as! MDLVertexAttribute).name = MDLVertexAttributeColor 19 (vertexDescriptor.attributes[3] as! MDLVertexAttribute).name = MDLVertexAttributeTextureCoordinate 20 21 let bufferAllocator = MTKMeshBufferAllocator(device: Engine.device) 22 let asset: MDLAsset = MDLAsset( 23 url: url, 24 vertexDescriptor: vertexDescriptor, 25 bufferAllocator: bufferAllocator 26 ) 27 28 var meshes: [Any]! = [] 29 do { 30 meshes = try MTKMesh.newMeshes(asset: asset, device: Engine.device).metalKitMeshes 31 } catch { 32 print("ERROR::LOADING::MODEL::__\(_meshReference.name!).\(_meshReference.fileExtension!)__::\(error)") 33 } 34 35 return ModelMesh(loadedMesh: meshes[_meshReference.meshIndex]) 36 } 37}

Mesh

Meshes now differentiate, BuiltIn Meshes have just a vertex buffer and an index buffer.

Model Meshes have a MTKMesh inside, that will grant access to the different vertex and index buffers.

1class Mesh {} 2 3class BuiltInMesh : Mesh { 4 public var vertices: [Vertex]! = [] 5 public var indices: [UInt32]! = [] 6 7 public var vertexBuffer: MTLBuffer! 8 public var indexBuffer: MTLBuffer! 9 10 override init() { 11 super.init() 12 createMesh() 13 14 vertexBuffer = Engine.device.makeBuffer(bytes: vertices, length: Vertex.stride * vertices.count, options: []) 15 16 if(indices.count > 0){ 17 indexBuffer = Engine.device.makeBuffer(bytes: indices, length: UInt32.stride * indices.count, options: []) 18 } 19 } 20 21 func createMesh() {} 22} 23 24class ModelMesh : Mesh { 25 public var mtkMesh: MTKMesh? = nil 26 27 init(loadedMesh: Any) { 28 super.init() 29 30 if let metalKitMesh = loadedMesh as? MTKMesh { 31 self.mtkMesh = metalKitMesh 32 } 33 } 34}

Mesh Renderer

The Mesh Renderer will decide how to render the mesh, depending on if it's a built in mesh, or a model mesh.

1func doRender() { 2 3 Graphics.renderCommandEncoder.setRenderPipelineState(RenderPipelineStateCache.get(_material)) 4 5 // Vertex Shader data 6 Graphics.renderCommandEncoder.setVertexBytes(&_modelConstants, length: ModelConstants.stride, index: 1) // model matrix 7 8 _material.setGpuValues() 9 10 Graphics.renderCommandEncoder.setDepthStencilState(DepthStencilStateCache.get(.Less)) 11 12 let mesh = MeshCache.get(_meshReference) 13 14 if let builtInMesh = mesh as? BuiltInMesh { 15 renderBuiltInMesh(builtInMesh) 16 } else if let modelMesh = mesh as? ModelMesh { 17 renderModelMesh(modelMesh) 18 } 19} 20 21func renderBuiltInMesh(_ mesh: BuiltInMesh) { 22 Graphics.renderCommandEncoder.setVertexBuffer(mesh.vertexBuffer, offset: 0, index: 0) 23 24 if(mesh.indices.count > 0){ 25 Graphics.renderCommandEncoder.drawIndexedPrimitives( 26 type: MTLPrimitiveType.triangle, 27 indexCount: mesh.indices.count, 28 indexType: MTLIndexType.uint32, 29 indexBuffer: mesh.indexBuffer, 30 indexBufferOffset: 0, 31 instanceCount: 1 // for now, might change in the future 32 ) 33 } else { 34 Graphics.renderCommandEncoder.drawPrimitives( 35 type: MTLPrimitiveType.triangle, 36 vertexStart: 0, 37 vertexCount: mesh.vertices.count 38 ) 39 } 40} 41 42func renderModelMesh(_ mesh: ModelMesh) { 43 44 if(mesh.mtkMesh == nil) { return } 45 46 for vertexBuffer in mesh.mtkMesh!.vertexBuffers { 47 Graphics.renderCommandEncoder.setVertexBuffer(vertexBuffer.buffer, offset: vertexBuffer.offset, index: 0) 48 49 for submesh in mesh.mtkMesh!.submeshes { 50 Graphics.renderCommandEncoder.drawIndexedPrimitives( 51 type: submesh.primitiveType, 52 indexCount: submesh.indexCount, 53 indexType: submesh.indexType, 54 indexBuffer: submesh.indexBuffer.buffer, 55 indexBufferOffset: submesh.indexBuffer.offset, 56 instanceCount: 1 // for now, might change in the future 57 ) 58 } 59 } 60}

Textures

Similar to how we handle meshes, Textures will have a Texture Reference that will act as an id in the Texture Cache.

1class TextureReference { 2 public var name: String! 3 public var fileExtension: String! = "jpg" 4 public var textureLoaderOrigin: MTKTextureLoader.Origin! = MTKTextureLoader.Origin.topLeft 5 6 public var id: String! { 7 return "\(name!).\(fileExtension!):\(textureLoaderOrigin!)" 8 } 9 10 init( 11 _ name: String, 12 fileExtension: String = "jpg", 13 textureLoaderOrigin: MTKTextureLoader.Origin = MTKTextureLoader.Origin.topLeft 14 ){ 15 self.name = name 16 self.fileExtension = fileExtension 17 self.textureLoaderOrigin = textureLoaderOrigin 18 } 19 20 func load()->MTLTexture { 21 return TextureLoader(self).load() 22 } 23}

And the Texture Reference will use the Texture Loader to load the corresponding MTLTexture.

1class TextureLoader { 2 private var _textureReference: TextureReference! 3 4 init(_ textureReference: TextureReference) { 5 _textureReference = textureReference 6 } 7 8 public func load()->MTLTexture { 9 var result: MTLTexture! 10 11 guard let url = Bundle.main.url(forResource: _textureReference.name, withExtension: _textureReference.fileExtension) else { 12 fatalError("ERROR::LOADING::TEXTURE::__\(_textureReference.name!).\(_textureReference.fileExtension!)__::does not exist") 13 } 14 15 let loader = MTKTextureLoader(device: Engine.device) 16 let options: [MTKTextureLoader.Option: Any] = [MTKTextureLoader.Option.origin : _textureReference.textureLoaderOrigin!] 17 18 do { 19 result = try loader.newTexture(URL: url, options: options) 20 result.label = _textureReference.name 21 } catch let error as NSError { 22 print("ERROR::LOADING::TEXTURE::__\(_textureReference.name!).\(_textureReference.fileExtension!)__::\(error)") 23 } 24 25 return result 26 } 27}

This will allow us to load any texture asset, without needing an enumerator.

1class TextureCache : Cache<TextureReference, MTLTexture> { 2 3 private static var _textures: [String: MTLTexture] = [:] 4 5 override class func get(_ textureReference: TextureReference)->MTLTexture{ 6 7 if(!_textures.keys.contains(textureReference.id)) { 8 _textures.updateValue(textureReference.load(), forKey: textureReference.id) 9 } 10 11 return _textures[textureReference.id]! 12 } 13}

Model Game Object

Will take in a Mesh Reference and a Texture Reference.

Internally it will add a Mesh Renderer using the mesh reference, and also will set the Material to sample the texture using the texture coordinates.

1class ModelGameObject : GameObject { 2 3 init( 4 modelMeshReference: ModelMeshReference, 5 textureReference: TextureReference 6 ) { 7 super.init() 8 9 let material = TextureSampleMaterial(textureReference) 10 11 self.addComponent(MeshRenderer(meshReference: modelMeshReference, material: material)) 12 self.addComponent(RotateYComponent()) 13 } 14}

To set up the scene that uses our Samus.obj file, we need to do:

1let samusGameObject = ModelGameObject( 2 modelMeshReference: ModelMeshReference("samus"), 3 textureReference: TextureReference("samus", fileExtension: "png", textureLoaderOrigin: MTKTextureLoader.Origin.bottomLeft) 4)

Result

The screen now shows the 3D model of Samus, with its base texture correctly sampled.

Picture