init dump

This commit is contained in:
2024-05-05 17:01:56 +10:00
commit 608cf45822
53 changed files with 19224 additions and 0 deletions

View 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
}

View 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)
}
}
}

View 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)
}
}

View 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
}