Files
CavesOfSwift/Sources/JolkEngine/Loaders/ObjReader.swift
2024-05-09 20:52:01 +10:00

402 lines
13 KiB
Swift

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..<model.positions.count
{
model.colours.append(.one)
}
}
model.colours.append(Vec3f(v[3], v[4], v[5]))
}
else if !model.colours.isEmpty && v.count == 3
{
model.colours.append(.one)
}
else if v.count != 3 { throw ObjLoaderError.badTagParameters }
model.positions.append(Vec3f(v[0], v[1], v[2]))
}
file.float(tag: "vn", count: 3) { v in model.normals.append(Vec3f(v[0], v[1], v[2])) }
file.float(tag: "vt", count: 2) { v in model.texCoords.append(Vec2f(v[0], v[1])) }
file.int(tag: "l") { i in mesh.faces.append(.line(p1: i[0], p2: i[1])) }
file.raw(tag: "f") { raw in
let readIndex =
{ (raw: Substring) in
let face = raw.split(separator: "/")
guard face.count == 3,
let posIdx = Int(face[0]), let coordIdx = Int(face[1]), let normIdx = Int(face[2])
else { throw ObjLoaderError.badTagParameters }
return ObjModel.Index(p: posIdx - 1, n: normIdx - 1, t: coordIdx - 1)
}
if raw.count == 3
{
for raw in raw { _ = try readIndex(raw) }
mesh.faces.append(.triangle(
v1: try readIndex(raw[raw.startIndex]),
v2: try readIndex(raw[raw.startIndex + 1]),
v3: try readIndex(raw[raw.startIndex + 2])))
}
else if raw.count == 4
{
mesh.faces.append(.quad(
v1: try readIndex(raw[raw.startIndex]),
v2: try readIndex(raw[raw.startIndex + 1]),
v3: try readIndex(raw[raw.startIndex + 2]),
v4: try readIndex(raw[raw.startIndex + 3])))
}
else if raw.count >= 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<String, ObjMaterial>
{
var file = try ObjDocumentReader(filePath: url)
var materials = Dictionary<String, ObjMaterial>()
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<String, (Int, Option)> =
[
"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..<index])
expectArgs -= 1
if expectArgs == 0
{
try callback()
option = nil
}
}
else if argstr[index] == "-"
{
let start = argstr.index(after: index)
index = argstr[start...].firstIndex(where: \.isWhitespace) ?? argstr.endIndex
let name = String(argstr[start..<index])
guard let params = options[name] else { throw ObjLoaderError.badTagParameters }
(expectArgs, option) = params
}
else
{
break
}
index = argstr[index...].firstIndex { !$0.isWhitespace } ?? argstr.endIndex
}
while index < argstr.endIndex
if index >= 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<String, Handler>()
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[..<index] } else { line[...] }
// Trim leading and trailing whitespace
guard let start = trimmed.firstIndex(where: { !$0.isWhitespace }),
let end = trimmed.lastIndex(where: { !$0.isWhitespace })
else { return nil }
trimmed = trimmed[start...end]
// Split command & rest of string as arguments
guard let cmdEnd = trimmed.firstIndex(where: \.isWhitespace),
let argStart = trimmed.suffix(from: trimmed.index(after: cmdEnd)).firstIndex(where: { !$0.isWhitespace })
else { throw ObjLoaderError.badTagParameters }
return (trimmed.prefix(upTo: cmdEnd), trimmed.suffix(from: argStart))
}
func read() throws
{
for line in file.lines
{
if !line.isEmpty,
let (cmd, argstr) = try parseLine(line),
let handler = handlers[String(cmd)]
{
if let prehandler = _prehandler { try prehandler(cmd) }
try handle(handler, command: cmd, arg: argstr)
}
}
}
}
enum ObjLoaderError: Error
{
case badTagParameters
case unexpectedTag
}