import Foundation import OrderedCollections class G3DbLoader: LoaderProtocol { typealias T = Mesh func load(url: URL) -> T? { guard var reader = try? G3DbReader(url) else { return Optional.none } return try? reader.read() } func load(url: URL, content: inout ContentManager) -> T? { return load(url: url) } } fileprivate struct G3DbReader { private let version: [Int16] = [0, 1] private var reader: UBJsonReader init(_ url: URL) throws { guard let file = try? FileHandle(forReadingFrom: url) else { throw G3DbReaderError.fileNotFound } self.reader = UBJsonReader(file: file) } mutating func read() throws -> Mesh { let model = try readModel() let mesh = model.meshes[0] var vertices = [VertexPositionNormalTexcoord]() var indices = [UInt16]() var subMeshes = OrderedDictionary.SubMesh>() let attributeSize = { (attrib: G3DAttribute) in switch attrib { case .position, .normal, .colour, .tangent, .bitangent: return 3 case .texCoord(_), .blendWeight(_): return 2 case .colourPacked: return 1 } } var stride = 0 for a in mesh.attributes { stride += attributeSize(a) } let numVerts = mesh.vertices.count / stride vertices.reserveCapacity(numVerts) for i in 0.. G3DModel { let root = try reader.read() let version = try root.getArray(key: "version") guard try version.count == 2 && (try version.map({ try $0.int16 }) == self.version) else { throw G3DbReaderError.versionMismatch } var model = G3DModel() model.id = try root.getString(key: "id", default: "") try readMeshes(root, &model) try readMaterials(root, &model) return model } mutating func readMeshes(_ root: UBJsonToken, _ model: inout G3DModel) throws { let meshes = try root.getArray(key: "meshes") for obj in meshes { var mesh = G3DModelMesh() mesh.id = try obj.getString(key: "id", default: "") for attrib in try obj.getArray(key: "attributes") { mesh.attributes.append(try .resolve(try attrib.string)) } mesh.vertices = try obj.getFloatArray(key: "vertices") for partObj in try obj.getArray(key: "parts") { let id = try partObj.getString(key: "id") if mesh.parts.keys.contains(id) { throw G3DbReaderError.duplicateIDs } var part = G3dModelMeshPart() part.mode = try .resolve(try partObj.getString(key: "type")) part.indices = try partObj.getInt16Array(key: "indices") mesh.parts[id] = part } model.meshes.append(mesh) } } mutating func readMaterials(_ root: UBJsonToken, _ model: inout G3DModel) throws { } } fileprivate struct G3DModel { var id: String = .init() var meshes: [G3DModelMesh] = .init() } fileprivate struct G3DModelMesh { var id: String = .init() var attributes: [G3DAttribute] = .init() var vertices: [Float] = .init() var parts: Dictionary = .init() } fileprivate struct G3dModelMeshPart { var mode: G3DPrimativeType = .invalid var indices: [Int16] = .init() } fileprivate enum G3DAttribute: Equatable { case position case normal case tangent case bitangent case colour case colourPacked case texCoord(id: UInt8) case blendWeight(id: UInt8) static let order = [ .position, .colour, .colourPacked, .normal, texCoord(id: 0), .blendWeight(id: 0), .tangent, .bitangent ] } extension G3DAttribute { static func resolve(_ attrib: String) throws -> Self { let getAttributeId = { (attrib: String, offset: Int) throws -> UInt8 in let idIdx = attrib.index(attrib.startIndex, offsetBy: offset) let idStr = attrib.suffix(from: idIdx) guard let id = UInt8(idStr) else { throw G3DbReaderError.badAttribute } return id } return switch attrib { case "POSITION": .position case "NORMAL": .normal case "COLOR": .colour case "COLORPACKED": .colourPacked case "TANGENT": .tangent case "BINORMAL": .bitangent default: if attrib.starts(with: "TEXCOORD") { .texCoord(id: try getAttributeId(attrib, 8)) } else if attrib.starts(with: "BLENDWEIGHT") { .blendWeight(id: try getAttributeId(attrib, 11)) } else { throw G3DbReaderError.badAttribute } } } } fileprivate enum G3DPrimativeType { case invalid case triangles case lines case points case triangleStrip case lineStrip } extension G3DPrimativeType { static func resolve(_ key: String) throws -> Self { switch key { case "TRIANGLES": .triangles case "LINES": .lines case "POINTS": .points case "TRIANGLE_STRIP": .triangleStrip case "LINE_STRIP": .lineStrip default: throw G3DbReaderError.invalidFormat } } } fileprivate enum G3DbReaderError: Error { case fileNotFound case versionMismatch case unsupportedFormat case invalidFormat case badAttribute case duplicateIDs }