init dump
This commit is contained in:
249
Sources/JolkEngine/Loaders/G3DbLoader.swift
Normal file
249
Sources/JolkEngine/Loaders/G3DbLoader.swift
Normal file
@ -0,0 +1,249 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = [Mesh.Vertex]()
|
||||
var indices = [UInt16]()
|
||||
var subMeshes = OrderedDictionary<String, Mesh.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..<numVerts
|
||||
{
|
||||
var srcIdx = i * stride
|
||||
var position: Vec3f = .zero, normal: Vec3f = .zero
|
||||
var texCoord: Vec2f = .zero
|
||||
for a in mesh.attributes
|
||||
{
|
||||
switch a
|
||||
{
|
||||
case .position:
|
||||
position = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
mesh.vertices[srcIdx + 2],
|
||||
-mesh.vertices[srcIdx + 1])
|
||||
case .normal:
|
||||
normal = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
mesh.vertices[srcIdx + 2],
|
||||
-mesh.vertices[srcIdx + 1])
|
||||
case .texCoord(id: 0):
|
||||
texCoord = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
1.0 - mesh.vertices[srcIdx + 1])
|
||||
default: break
|
||||
}
|
||||
srcIdx += attributeSize(a)
|
||||
}
|
||||
vertices.append(.init(position: position, normal: normal, texCoord: texCoord))
|
||||
}
|
||||
for part in mesh.parts
|
||||
{
|
||||
subMeshes[part.key] = .init(start: indices.count, length: part.value.indices.count)
|
||||
indices += part.value.indices.map { UInt16($0) }
|
||||
}
|
||||
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
|
||||
}
|
||||
|
||||
mutating func readModel() throws -> 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<String, G3dModelMeshPart> = .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
|
||||
}
|
50
Sources/JolkEngine/Loaders/NSImageLoader.swift
Normal file
50
Sources/JolkEngine/Loaders/NSImageLoader.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import AppKit
|
||||
|
||||
|
||||
enum ImageLoaderError: Error
|
||||
{
|
||||
case loadFailed(_ message: String)
|
||||
}
|
||||
|
||||
struct NSImageLoader: LoaderProtocol
|
||||
{
|
||||
typealias T = Image
|
||||
|
||||
func load(url: URL) -> T?
|
||||
{
|
||||
guard let image = try? NSImageLoader.loadImage(url: url) else { return nil }
|
||||
return image
|
||||
}
|
||||
|
||||
static func loadImage(url: URL) throws -> Image
|
||||
{
|
||||
try autoreleasepool
|
||||
{
|
||||
// Open as a Core Graphics image
|
||||
guard let nsImg = NSImage(contentsOf: url),
|
||||
let image = nsImg.cgImage(forProposedRect: nil, context: nil, hints: nil)
|
||||
else { throw ImageLoaderError.loadFailed("Failed to open image \"\(url.absoluteString)\"") }
|
||||
|
||||
// Convert to 8-bit ARGB (SRGB) w/ premultiplied alpha
|
||||
let alphaInfo = image.alphaInfo == .none ? CGImageAlphaInfo.noneSkipLast : CGImageAlphaInfo.premultipliedLast
|
||||
guard let colourspace = CGColorSpace(name: CGColorSpace.sRGB),
|
||||
let context = CGContext(data: nil,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: image.width * 4,
|
||||
space: colourspace,
|
||||
bitmapInfo: alphaInfo.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
|
||||
else { throw ImageLoaderError.loadFailed("Coudn't create graphics context") }
|
||||
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(image.height))
|
||||
context.concatenate(flipVertical)
|
||||
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
|
||||
|
||||
guard let data = context.data else { throw ImageLoaderError.loadFailed("what") }
|
||||
return Image(
|
||||
pixels: Data(bytes: data, count: 4 * image.width * image.height),
|
||||
width: image.width,
|
||||
height: image.height)
|
||||
}
|
||||
}
|
||||
}
|
92
Sources/JolkEngine/Loaders/ObjLoader.swift
Normal file
92
Sources/JolkEngine/Loaders/ObjLoader.swift
Normal file
@ -0,0 +1,92 @@
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
|
||||
/*
|
||||
extension ObjMaterial
|
||||
{
|
||||
func convert() -> Material
|
||||
{
|
||||
var m = Material()
|
||||
m.diffuse = self.diffuse.setAlpha(self.alpha)
|
||||
if ![ .colour, .lambert, .shadowOnly ].contains(self.model)
|
||||
{
|
||||
m.specular = self.specular
|
||||
m.specularExp = self.specularExp
|
||||
}
|
||||
return m
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public struct ObjLoader: LoaderProtocol
|
||||
{
|
||||
public typealias T = Mesh
|
||||
|
||||
public init() {}
|
||||
|
||||
public func load(url: URL) -> T?
|
||||
{
|
||||
try? Self.read(url: url)
|
||||
}
|
||||
|
||||
private static func read(url: URL) throws -> Mesh
|
||||
{
|
||||
return try read(model: try ObjReader.read(url: url))
|
||||
}
|
||||
|
||||
public static func read(model: ObjModel) throws -> Mesh
|
||||
{
|
||||
var subMeshes: OrderedDictionary<String, Mesh.SubMesh> = .init()
|
||||
var vertices = [Mesh.Vertex]()
|
||||
var indices = [UInt16]()
|
||||
|
||||
let readIndex =
|
||||
{ (v: ObjModel.Index) -> UInt16 in
|
||||
let vertex = Mesh.Vertex(
|
||||
position: model.positions[v.p],
|
||||
normal: model.normals[v.n],
|
||||
texCoord: model.texCoords[v.t])
|
||||
if let index = vertices.firstIndex(of: vertex)
|
||||
{
|
||||
indices.append(UInt16(index))
|
||||
return UInt16(index)
|
||||
}
|
||||
else
|
||||
{
|
||||
let index = UInt16(vertices.count)
|
||||
indices.append(index)
|
||||
vertices.append(vertex)
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
for mesh in model.objects
|
||||
{
|
||||
let start = indices.count
|
||||
for face: ObjModel.Face in mesh.value.faces
|
||||
{
|
||||
switch face
|
||||
{
|
||||
case .triangle(let v1, let v2, let v3):
|
||||
for v in [ v1, v2, v3 ] { _ = readIndex(v) }
|
||||
case .quad(let v1, let v2, let v3, let v4):
|
||||
let n1 = readIndex(v1)
|
||||
_ = readIndex(v2)
|
||||
indices.append(readIndex(v3))
|
||||
_ = readIndex(v4)
|
||||
indices.append(n1)
|
||||
case .ngon(_): fallthrough
|
||||
default: break
|
||||
}
|
||||
}
|
||||
let length = indices.count - start
|
||||
if length > 0
|
||||
{
|
||||
subMeshes[mesh.key] = .init(start: start, length: length)
|
||||
}
|
||||
}
|
||||
|
||||
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
|
||||
}
|
||||
}
|
372
Sources/JolkEngine/Loaders/ObjReader.swift
Normal file
372
Sources/JolkEngine/Loaders/ObjReader.swift
Normal file
@ -0,0 +1,372 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct ObjReader
|
||||
{
|
||||
public static func read(url: URL) throws -> ObjModel
|
||||
{
|
||||
var file = try ObjDocumentReader(filePath: url)
|
||||
|
||||
var model = ObjModel()
|
||||
var materials = Dictionary<String, ObjMaterial>()
|
||||
var name: String? = nil
|
||||
var object = ObjModel.Object()
|
||||
|
||||
file.string(tag: "mtllib") { s in
|
||||
let mats = try ObjMtlLoader.read(url: url.deletingLastPathComponent().appendingPathComponent(s[0]))
|
||||
materials.merge(mats, uniquingKeysWith: { (_, new) in new } )
|
||||
}
|
||||
file.string(tag: "o", count: 1) { s in
|
||||
if !object.faces.isEmpty
|
||||
{
|
||||
model.objects[name!] = object
|
||||
object = .init()
|
||||
}
|
||||
name = String(s[0])
|
||||
}
|
||||
file.float(tag: "v", count: 3) { v in 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.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) }
|
||||
object.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
|
||||
{
|
||||
object.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
|
||||
{
|
||||
object.faces.append(.ngon(try raw.map { try readIndex($0) }))
|
||||
}
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}
|
||||
file.int(tag: "l") { i in object.faces.append(.line(p1: i[0], p2: i[1])) }
|
||||
try file.read()
|
||||
|
||||
if !object.faces.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
|
||||
}
|
Reference in New Issue
Block a user