import Foundation public struct ObjReader { public static func read(url: URL) throws -> ObjModel { var file = try ObjDocumentReader(filePath: url) var model = ObjModel() var name: String? = nil var object = ObjModel.Object() var mesh = ObjModel.Mesh() file.string(tag: "mtllib") { s in let mats = try ObjMtlLoader.read(url: url.deletingLastPathComponent().appendingPathComponent(s[0])) model.materials.merge(mats, uniquingKeysWith: { (_, new) in new } ) } file.string(tag: "o", count: 1) { s in if !mesh.faces.isEmpty { object.meshes.append(mesh) } if !object.meshes.isEmpty { model.objects[name!] = object object = .init() mesh = .init() } name = String(s[0]) } file.float(tag: "v") { v in if v.count == 6 { if model.colours.isEmpty && !model.positions.isEmpty { for _ in 0..= 5 { mesh.faces.append(.ngon(try raw.map { try readIndex($0) })) } else { throw ObjLoaderError.badTagParameters } } file.string(tag: "usemtl", count: 1) { s in if !mesh.faces.isEmpty { object.meshes.append(mesh) mesh = .init() } mesh.material = s[0] } // file.int(tag: "s", count: 1) { i in } //TODO: Smoothing groups try file.read() if !mesh.faces.isEmpty { object.meshes.append(mesh) } if !object.meshes.isEmpty { model.objects[name!] = object } return model } } fileprivate struct ObjMtlLoader { static func read(url: URL) throws -> Dictionary { var file = try ObjDocumentReader(filePath: url) var materials = Dictionary() var name: String = "" var mat: ObjMaterial? = nil file.string(tag: "newmtl", count: 1) { s in if mat != nil { materials[name] = mat! } mat = .init() name = String(s[0]) } file.preHandle { s in if s != "newmtl" && mat == nil { throw ObjLoaderError.unexpectedTag } } file.float(tag: "Ka", count: 3) { f in mat!.ambient = Colour(r: f[0], g: f[1], b: f[2]) } // "Ambient colour" file.float(tag: "Kd", count: 3) { f in mat!.diffuse = Colour(r: f[0], g: f[1], b: f[2]) } // "Diffuse colour" file.float(tag: "Ks", count: 3) { f in mat!.specular = Colour(r: f[0], g: f[1], b: f[2]) } // "Specular colour" file.float(tag: "Ns", count: 1) { f in mat!.specularExp = f[0] } // "Specular exponent/shininess" file.float(tag: "Ni", count: 1) { f in mat!.refraction = f[0] } // "Optical density/refraction index" file.float(tag: "d", count: 1) { f in mat!.alpha = f[0] } // "Dissolve" 0.0 = transparent, 1.0 = opaque file.float(tag: "Tr", count: 1) { f in mat!.alpha = 1.0 - f[0] } // "Transparent" 0.0 = opaque, 1.0 = transparent file.int(tag: "illum", count: 1) { i in mat!.model = try .resolve(i[0]) } // "Illumination model" file.textureMap(tag: "map_Ka") { s in } // "Ambient colourmap" file.textureMap(tag: "map_Kd") { t in mat!.diffuseMap = t } // "Albedo map" file.textureMap(tag: "map_Ks") { t in mat!.specularMap = t } // "Specular colourmap" file.textureMap(tag: "map_Ns") { t in mat!.specularExpMap = t } // "Specular exponent map" file.textureMap(tag: "map_d") { t in mat!.dissolveMap = t } // "Dissolve map" //file.string(tag: "map_Tr") { t in } // "Translucency map" for t in ["map_Bump", "bump"] { file.textureMap(tag: t) { t in mat!.bumpMap = t } } // "Bump map" file.textureMap(tag: "disp") { t in mat!.displacementMap = t } // "Displacement map" file.textureMap(tag: "decal") { t in mat!.decalMap = t } // ? // PBR extensions file.float(tag: "Pr", count: 1) { f in mat!.roughness = f[0] } // "Roughness" file.float(tag: "Pm", count: 1) { f in mat!.metallic = f[0] } // "Metallic" file.float(tag: "Ps", count: 1) { f in mat!.sheen = f[0] } // "Sheen" file.float(tag: "Pc", count: 1) { f in } // "Clearcoat thickness" file.float(tag: "Pcr", count: 1) { f in } // "Clearcoat roughness" file.float(tag: "Ke", count: 3) { f in } // "Emmission colour" file.float(tag: "aniso", count: 1) { f in } // "Anisotropy" file.float(tag: "anisor", count: 1) { f in } // "Anisotropy rotation" file.textureMap(tag: "map_Pr") { t in mat!.roughnessMap = t } // "Roughness texturemap" file.textureMap(tag: "map_Pm") { t in mat!.metallicMap = t } // "Metallic texturemap" file.textureMap(tag: "map_Ps") { t in mat!.sheenMap = t } // "Sheen texturemap" file.textureMap(tag: "map_Ke") { t in mat!.emmisionMap = t } // "Emmision texturemap" file.textureMap(tag: "norm") { t in mat!.normalMap = t } // "Normal map" try file.read() if let material = mat { materials[name] = material } return materials } } fileprivate extension ObjMaterial.IlluminationModel { static func resolve(_ id: Int) throws -> Self { switch id { case 0: .colour case 1: .lambert case 2: .blinnPhong case 3: .reflectionRaytrace case 4: .transparentGlassReflectionRaytrace case 5: .reflectionRaytraceFresnel case 6: .transparentRefractionReflectionRaytrace case 7: .transparentRefractionReflectionRaytraceFresnel case 8: .reflection case 9: .transparentGlassReflection case 10: .shadowOnly default: throw ObjLoaderError.badTagParameters } } } fileprivate extension ObjMaterial.TextureMap { static func parse(_ argstr: Substring) throws -> Self { var map = Self() var args = [Substring]() let parseFlag = { (flag: Flags) in let arg = args[0].lowercased() if arg == "on" { map.flags.insert(flag) } else if arg == "off" { map.flags.remove(flag) } else { throw ObjLoaderError.badTagParameters } } let parseInt = { (arg: Int) in guard let result = Int(args[arg]) else { throw ObjLoaderError.badTagParameters } return result } let parseFloat = { (arg: Int) in guard let result = Float(args[arg]) else { throw ObjLoaderError.badTagParameters } return result } let parseVector = { Vec3f(try parseFloat(0), try parseFloat(1), try parseFloat(2)) } typealias Option = () throws -> Void let options: Dictionary = [ "blendu": (1, { try parseFlag(.blendHoriz) }), "blendv": (1, { try parseFlag(.blendVert) }), "bm": (1, { map.blendMul = try parseFloat(0) }), "cc": (1, { try parseFlag(.colourCorrection) }), "clamp": (1, { try parseFlag(.clamp) }), "imfchan": (1, { map.imfChan = try .resolve(args[0]) }), "mm": (2, { map.mmBaseGain = (try parseFloat(0), try parseFloat(1)) }), "o": (3, { map.offset = try parseVector() }), "s": (3, { map.scale = try parseVector() }), "t": (3, { map.turbulence = try parseVector() }), "texres": (1, { map.textureResolution = try parseInt(0) }) ] var expectArgs = 0, option: Option? = nil var index = argstr.startIndex repeat { if expectArgs > 0, let callback = option { let start = index index = argstr[start...].firstIndex(where: \.isWhitespace) ?? argstr.endIndex args.append(argstr[start..= argstr.endIndex || expectArgs > 0 { throw ObjLoaderError.badTagParameters } map.path = String(argstr[index...]) return map } } fileprivate extension ObjMaterial.TextureMap.ImfChan { static func resolve(_ token: Substring) throws -> Self { switch token { case "r": .r case "g": .g case "b": .b case "m": .m case "l": .l case "z": .z default: throw ObjLoaderError.badTagParameters } } } fileprivate struct ObjDocumentReader { private enum Handler { case string(closure: ([String]) throws -> Void, count: Int? = nil) case float(closure: ([Float]) throws -> Void, count: Int? = nil) case int(closure: ([Int]) throws -> Void, count: Int? = nil) case textureMap(closure: (ObjMaterial.TextureMap) throws -> Void) case raw(closure: ([Substring]) throws -> Void) } private var file: TextFile typealias Callback = (Substring) throws -> Void private var _prehandler: Callback? = nil private var handlers = Dictionary() init(filePath: URL) throws { file = try TextFile(fileURL: filePath) } mutating func string(tag: String, count: Int? = nil, closure: @escaping ([String]) throws -> Void) { handlers[tag] = .string(closure: closure, count: count) } mutating func float(tag: String, count: Int? = nil, closure: @escaping ([Float]) throws -> Void) { handlers[tag] = .float(closure: closure, count: count) } mutating func int(tag: String, count: Int? = nil, closure: @escaping ([Int]) throws -> Void) { handlers[tag] = .int(closure: closure, count: count) } mutating func textureMap(tag: String, closure: @escaping (ObjMaterial.TextureMap) throws -> Void) { handlers[tag] = .textureMap(closure: closure) } mutating func raw(tag: String, closure: @escaping ([Substring]) throws -> Void) { handlers[tag] = .raw(closure: closure) } mutating func preHandle(closure: @escaping Callback) { _prehandler = closure } private func handle(_ handler: Handler, command: Substring, arg: Substring) throws { switch handler { case .string(let closure, let count): let args = arg.split(separator: " ") if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters } try closure(args.map({ String($0) })) case .float(let closure, let count): let args = arg.split(separator: " ") if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters } try closure(args.map( { if let value = Float($0) { return value } else { throw ObjLoaderError.badTagParameters } })) case .int(let closure, let count): let args = arg.split(separator: " ") if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters } try closure(args.map( { if let value = Int($0) { return value } else { throw ObjLoaderError.badTagParameters } })) case .textureMap(let closure): try closure(ObjMaterial.TextureMap.parse(arg)) case .raw(let closure): try closure(arg.split(separator: " ")) } } private func parseLine(_ line: String) throws -> (Substring, Substring)? { // Trim comment if present var trimmed = if let index = line.firstIndex(of: "#") { line[..