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,205 @@
import Foundation
import SDL
public protocol ApplicationImplementation
{
func create(render: inout Renderer)
func quit()
func loadContent(content: inout ContentManager) throws
func resize(width: Int, height: Int)
func update(deltaTime: Float)
func draw(render: inout Renderer, deltaTime: Float)
}
extension ApplicationImplementation
{
public func quit() {}
}
public struct ApplicationConfiguration
{
let resizable: Bool
let vSync: VSyncMode
let windowWidth: Int32
let windowHeight: Int32
let bundle: Bundle
public init(resizable: Bool, vSync: VSyncMode, windowWidth: Int32, windowHeight: Int32, bundle: Bundle)
{
self.resizable = resizable
self.vSync = vSync
self.windowWidth = windowWidth
self.windowHeight = windowHeight
self.bundle = bundle
}
}
//FIXME: Wrap these
public var sdlPad: OpaquePointer? = nil
var joyId: Int32 = -1
public class Application
{
private let implementation: ApplicationImplementation
private let configuration: ApplicationConfiguration
private var window: OpaquePointer?
private var render: Renderer
private var content: ContentManager
typealias ResizeEvent = (width: Int32, height: Int32)
private var resize: ResizeEvent? = nil
func openController(index: Int32) -> (pad: OpaquePointer, joy: Int32)?
{
guard let sdlPad = SDL_GameControllerOpen(index) else { return nil }
let joyId = SDL_JoystickGetDeviceInstanceID(index)
print("Using gamepad #\(joyId), \"\(String(cString: SDL_GameControllerName(sdlPad)))\"")
return (pad: sdlPad, joy: joyId)
}
public init(application: ApplicationImplementation, config: ApplicationConfiguration)
{
self.implementation = application
self.configuration = config
self.render = OpenGL(version: .init(major: 1, minor: 5))
self.content = ContentManager(&self.render, bundle: configuration.bundle)
}
public func run()
{
guard SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) == 0
else { fatalError("SDL_Init: \(String(cString: SDL_GetError()))") }
let winPos = Int32(SDL_WINDOWPOS_UNDEFINED_MASK)
let winWidth: Int32 = configuration.windowWidth, winHeight: Int32 = configuration.windowHeight
var winFlags = SDL_WINDOW_RESIZABLE.rawValue | SDL_WINDOW_ALLOW_HIGHDPI.rawValue
if render is OpenGL { winFlags |= SDL_WINDOW_OPENGL.rawValue }
window = SDL_CreateWindow("Something", winPos, winPos, winWidth, winHeight, winFlags)
guard window != nil else { fatalError("SDL_CreateWindow: \(String(cString: SDL_GetError()))") }
do
{
try render.create(sdlWindow: window!)
try render.setVsync(mode: configuration.vSync)
}
catch RendererError.sdlError(let message)
{
fatalError("idk error \(message)")
}
catch { fatalError("piss") }
for i in 0...SDL_NumJoysticks()
{
if SDL_IsGameController(i) != SDL_TRUE { continue }
if let open = openController(index: i)
{
sdlPad = open.pad
joyId = open.joy
break
}
}
do
{
implementation.create(render: &render)
content.setLoader(extension: "obj", loader: ObjLoader())
content.setLoader(extension: "g3db", loader: G3DbLoader())
content.setLoader(extensions: ["png", "jpg"], loader: NSImageLoader())
try implementation.loadContent(content: &content)
}
catch
{
fatalError("loadContent()")
}
let timeMul = 1 / Double(SDL_GetPerformanceFrequency());
var prevTime = SDL_GetPerformanceCounter();
resize = displaySize
mainLoop: while true
{
Input.instance.newTick()
var event = SDL_Event()
while SDL_PollEvent(&event) > 0
{
switch SDL_EventType(event.type)
{
case SDL_QUIT: break mainLoop
case SDL_KEYDOWN:
if event.key.repeat == 0
{
Input.instance.pressEvent(scan: event.key.keysym.scancode)
}
case SDL_KEYUP:
Input.instance.releaseEvent(scan: event.key.keysym.scancode)
case SDL_WINDOWEVENT:
switch SDL_WindowEventID(UInt32(event.window.event))
{
case SDL_WINDOWEVENT_RESIZED:
resize = displaySize
default: break
}
case SDL_CONTROLLERDEVICEADDED:
if sdlPad == nil && SDL_IsGameController(event.cdevice.which) == SDL_TRUE
{
if let open = openController(index: event.cdevice.which)
{
sdlPad = open.pad
joyId = open.joy
}
}
case SDL_CONTROLLERDEVICEREMOVED:
if event.cdevice.which == joyId
{
SDL_GameControllerClose(sdlPad)
sdlPad = nil
joyId = -1
}
default: break
}
}
let time = SDL_GetPerformanceCounter();
let deltaTime = Float(Double(time - prevTime) * timeMul)
prevTime = time
implementation.update(deltaTime: min(deltaTime, 1.0 / 15.0))
repaint(deltaTime)
}
implementation.quit()
content.releaseAll()
render.delete()
SDL_DestroyWindow(window)
SDL_Quit()
}
private func repaint(_ deltaTime: Float)
{
if resize != nil
{
render.resize(width: resize!.width, height: resize!.height)
implementation.resize(width: Int(resize!.width), height: Int(resize!.height))
resize = nil
}
render.newFrame()
implementation.draw(render: &render, deltaTime: deltaTime)
SDL_GL_SwapWindow(window)
}
private var displaySize: ResizeEvent
{
var width: Int32 = 0, height: Int32 = 0
SDL_GL_GetDrawableSize(window, &width, &height)
return (width, height)
}
}

View File

@ -0,0 +1,95 @@
import SDL2
import HSLuv
public struct Colour: Equatable { let r, g, b, a: Float }
public extension Colour
{
static var zero: Self { .init(r: 0, g: 0, b: 0, a: 0) }
static var black: Self { .init(r: 0, g: 0, b: 0, a: 1) }
static var white: Self { .init(r: 1, g: 1, b: 1, a: 1) }
init(r: Float, g: Float, b: Float)
{
self.init(r: r, g: g, b: b, a: 1.0)
}
init(fromSdl: SDL_Color)
{
self.init(
r: Float(fromSdl.r) / 0xFF,
g: Float(fromSdl.g) / 0xFF,
b: Float(fromSdl.b) / 0xFF,
a: Float(fromSdl.a) / 0xFF)
}
init(fromRgb24: UInt32)
{
self.init(
r: Float((fromRgb24 >> 16) & 0xFF) / 0xFF,
g: Float((fromRgb24 >> 8) & 0xFF) / 0xFF,
b: Float((fromRgb24 >> 0) & 0xFF) / 0xFF,
a: 1)
}
init(fromRgba32: UInt32)
{
self.init(
r: Float((fromRgba32 >> 24) & 0xFF) / 0xFF,
g: Float((fromRgba32 >> 16) & 0xFF) / 0xFF,
b: Float((fromRgba32 >> 8) & 0xFF) / 0xFF,
a: Float((fromRgba32 >> 0) & 0xFF) / 0xFF)
}
func mix(with rhs: Colour, _ amount: Float) -> Colour
{
let x = amount.saturate
return Colour(
r: Float.lerp(r, rhs.r, x),
g: Float.lerp(g, rhs.g, x),
b: Float.lerp(b, rhs.b, x),
a: Float.lerp(a, rhs.a, x))
}
func lighten(by: Double) -> Colour
{
var hue = CGFloat(), sat = CGFloat(), light = CGFloat()
rgbToHsluv(CGFloat(r), CGFloat(g), CGFloat(b), &hue, &sat, &light)
let gamma: CGFloat = 3.0
light = pow((pow(light / 100, 1 / gamma) + CGFloat(by) * 0.8).saturate, gamma) * 100
var newR = CGFloat(), newG = CGFloat(), newB = CGFloat()
hsluvToRgb(hue, sat, light, &newR, &newG, &newB)
return Colour(r: Float(newR), g: Float(newG), b: Float(newB), a: a)
}
var linear: Colour { get
{
Colour(
r: linearFromSrgb(r),
g: linearFromSrgb(g),
b: linearFromSrgb(b),
a: a)
} }
func setAlpha(_ newAlpha: Float) -> Colour
{
return Colour(r: r, g: g, b: b, a: newAlpha)
}
}
@inline(__always) fileprivate func srgbFromLinear(_ x: Float) -> Float
{
return (x < 0.0031308)
? x * 12.92
: 1.055 * pow(x, 1.0 / 2.4) - 0.055
}
@inline(__always) fileprivate func linearFromSrgb(_ x: Float) -> Float
{
return (x < 0.04045)
? x / 12.92
: pow((x + 0.055) / 1.055, 2.4)
}

View File

@ -0,0 +1,165 @@
import Foundation
public protocol ContentLoaderParametersProtocol
{
associatedtype T: Resource
}
public struct ContentManager
{
private var loaders = Dictionary<String, any LoaderProtocol>()
private var resources = [any RendererResource]()
private let renderer: Renderer
private let bundle: Bundle
}
extension ContentManager
{
internal init(_ renderer: inout Renderer, bundle: Bundle)
{
self.renderer = renderer
self.bundle = bundle
}
public mutating func setLoader<T: Resource, Loader: LoaderProtocol>(extensions exts: [String], loader: Loader) where Loader.T == T
{
for i in exts
{
setLoader(extension: i, loader: loader)
}
}
public mutating func setLoader<T: Resource, Loader: LoaderProtocol>(extension ext: String, loader: Loader) where Loader.T == T
{
loaders[ext] = loader
}
public mutating func create(mesh: Mesh) throws -> RenderMesh
{
let rendMesh = try renderer.createMesh(mesh: mesh)
resources.append(rendMesh)
return rendMesh
}
public mutating func create(texture: Image, params: Texture2DParameters = .init()) throws -> Texture2D
{
try texture.pixels.withUnsafeBytes
{
raw in
let data = raw.baseAddress!
let rendTex = try renderer.createTexture(
data: data, width: texture.width, height: texture.height,
filter: params.magFilter, mipMode: params.mipMode)
resources.append(rendTex)
return Texture2D(id: rendTex, width: texture.width, height: texture.height)
}
}
public func getResource(_ path: String) throws -> URL
{
guard let extIndex = path.lastIndex(of: ".")
else { throw ContentError.badPath }
let name = String(path[..<extIndex]), ext = String(path[extIndex...])
guard let resourceUrl: URL = bundle.url(
forResource: name,
withExtension: ext)
else { throw ContentError.resourceNotFound }
return resourceUrl
}
public mutating func load<T: Resource>(_ path: String) throws -> T
{
let resourceUrl = try getResource(path)
let loader = loaders[resourceUrl.pathExtension]
guard let resource = loader?.load(url: resourceUrl)
else { throw ContentError.loadFailure }
if T.self == Mesh.self
{
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
return mesh as! T
}
if T.self == Image.self
{
guard let image = resource as? Image else { throw ContentError.loadFailure }
return image as! T
}
if T.self == Texture2D.self
{
guard let image = resource as? Image else { throw ContentError.loadFailure }
let texture2D = try self.create(texture: image)
return texture2D as! T
}
throw ContentError.loadFailure
}
public mutating func load<T: Resource, Params: ContentLoaderParametersProtocol>(_ path: String, params: Params) throws
-> T where Params.T == T
{
let resourceUrl = try getResource(path)
let loader = loaders[resourceUrl.pathExtension]
guard let resource = loader?.load(url: resourceUrl)
else { throw ContentError.loadFailure }
if T.self == Mesh.self
{
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
return mesh as! T
}
if T.self == Image.self
{
guard let image = resource as? Image else { throw ContentError.loadFailure }
return image as! T
}
if T.self == Texture2D.self
{
guard let image = resource as? Image else { throw ContentError.loadFailure }
let texture2D = try self.create(texture: image, params: params as! Texture2DParameters)
return texture2D as! T
}
throw ContentError.loadFailure
}
public mutating func load<T: RendererResource>(_ path: String) throws -> T
{
let resourceUrl = try getResource(path)
let loader = loaders[resourceUrl.pathExtension]
guard let resource = loader?.load(url: resourceUrl)
else { throw ContentError.loadFailure }
if T.self == RenderMesh.self
{
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
let renderResource = try self.create(mesh: mesh)
return renderResource as! T
}
throw ContentError.loadFailure
}
internal mutating func releaseAll()
{
for resource in resources.reversed()
{
if resource is RenderMesh
{
renderer.deleteMesh(resource as! RenderMesh)
}
else if resource is RenderTexture2D
{
renderer.deleteTexture(resource as! RenderTexture2D)
}
}
resources.removeAll()
}
}
public enum ContentError: Error
{
case badPath //else { fatalError("Malformed resource path \"\(path)\"") }
case resourceNotFound //else { fatalError("Resource \"\(path)\" doesn't exist") }
case loadFailure //else { fatalError("Failed to load resource \"\(path)\"") }
}

View File

@ -0,0 +1,9 @@
import Foundation
public struct Image: Resource
{
let pixels: Data
let width: Int
let height: Int
}

View File

@ -0,0 +1,9 @@
import Foundation
public protocol LoaderProtocol
{
associatedtype T: Resource
func load(url: URL) -> T?
}

View File

@ -0,0 +1,54 @@
import OrderedCollections
public struct Model: Resource
{
public let meshes: [Mesh]
}
public struct Mesh: Resource
{
public typealias Index = UInt16
public struct Vertex: Equatable
{
public let position: Vec3f
public let normal: Vec3f
public let texCoord: Vec2f
public init(position: Vec3f, normal: Vec3f, texCoord: Vec2f)
{
self.position = position
self.normal = normal
self.texCoord = texCoord
}
}
public struct SubMesh
{
public let start, length: Int
public init(start: Int, length: Int)
{
self.start = start
self.length = length
}
}
public let vertices: [Vertex]
public let indices: [Index]
public let subMeshes: OrderedDictionary<String, SubMesh>
}
public extension Mesh
{
static let empty = Self(vertices: .init(), indices: .init(), subMeshes: .init())
init(vertices: [Vertex], indices: [Index])
{
self.init(
vertices: vertices,
indices: indices,
subMeshes: .init())
}
}

View File

@ -0,0 +1,120 @@
import Foundation
public struct ObjModel
{
public var positions = [Vec3f]()
public var normals = [Vec3f]()
public var texCoords = [Vec2f]()
public var objects = Dictionary<String, Object>()
public struct Object { public var faces = [Face]() }
public struct Index { public let p: Int, n: Int, t: Int }
public enum Face
{
case line(p1: Int, p2: Int)
case triangle(v1: Index, v2: Index, v3: Index)
case quad(v1: Index, v2: Index, v3: Index, v4: Index)
case ngon(_ v: [Index])
}
}
public struct ObjMaterial
{
var model: IlluminationModel = .lambert
var ambient: Colour = .black
var diffuse: Colour = .white
var specular: Colour = .zero
var specularExp: Float = 30
var refraction: Float = 1.0
var alpha: Float = 1.0
var ambientMap: TextureMap? = nil
var diffuseMap: TextureMap? = nil
var specularMap: TextureMap? = nil
var specularExpMap: TextureMap? = nil
var dissolveMap: TextureMap? = nil
var bumpMap: TextureMap? = nil
var displacementMap: TextureMap? = nil
var decalMap: TextureMap? = nil
// PBR extensions
var roughness: Float = 0.5
var metallic: Float = 0.0
var sheen: Float = 0.0
var clearcoatThickness: Float = 0.0
var clearcoatRoughness: Float = 0.03 // dunno what the default should be so here's the blender one
var emmision: Vec3f = .zero
var anisotropy: Float = 0.0
var anisotropyRotation: Float = 0.0
var roughnessMap: TextureMap? = nil
var metallicMap: TextureMap? = nil
var sheenMap: TextureMap? = nil
var emmisionMap: TextureMap? = nil
var normalMap: TextureMap? = nil
// Ft Fresnel reflectance
// Ft Fresnel transmittance
// Ns Specular exponent/shininess
// Ia Ambient light
// I Light intensity
// Ir Intensity from reflected direction (reflection map and/or raytracing)
// It Intensity from transmitted direction
// Ka Ambient reflectance
// Kd Diffuse reflectance
// Ks Specular reflectance
// Tf Transmission filter
// H Unit vector bisector between L and V
// L Unit light vector
// N Unit surface normal
// V Unit view vector
//
// j = piss
// Fr = Fresnel
// lambert = KaIa + Kd(N * Lj)Ij
// blinnPhong = lambert + Ks((H * Hj) ^ Ns)Ij
//
// Notes: Clara.io only usees 0 and 1, maya always exports 4, blender can export 1, 3, 6
public enum IlluminationModel
{
case colour // Kd only
case lambert
case blinnPhong
case reflectionRaytrace // + Ir = (intensity of reflection map) + (ray trace)
case transparentGlassReflectionRaytrace // + Ir = (glass) + (intensity of reflection map) + (raytrace)
case reflectionRaytraceFresnel // Fr(Lj * Hj, Ks, Ns)Ij + Fr(N * V, Ks, Ns)Ir
case transparentRefractionReflectionRaytrace // + Ir + (1 - Ks)TfIt
case transparentRefractionReflectionRaytraceFresnel // Fr(Lj * Hj, Ks, Ns)Ij + Fr(N * V, Ks, Ns)Ir
case reflection // + Ir = (intensity of reflection map)
case transparentGlassReflection // + Ir = (glass) + (intensity of reflection map) + (raytrace)
case shadowOnly
}
public struct TextureMap
{
var path: String = .init()
var flags: Flags = [ .blendHoriz, .blendVert ]
var blendMul: Float = 1.0
var boost: Float = 0.0 // non-negative
var imfChan: ImfChan = .l // decal ? .m : .l
var mmBaseGain: (Float, Float) = (0.0, 1.0)
var offset: Vec3f = .zero
var scale: Vec3f = .one
var turbulence: Vec3f = .zero
var textureResolution: Int = 0
struct Flags: OptionSet
{
let rawValue: UInt8
static let blendHoriz: Self = Self(rawValue: 1 << 0)
static let blendVert: Self = Self(rawValue: 1 << 1)
static let colourCorrection: Self = Self(rawValue: 1 << 2)
static let clamp: Self = Self(rawValue: 1 << 3)
}
enum ImfChan { case r, g, b, m, l, z }
}
}

View File

@ -0,0 +1,4 @@
public protocol Resource
{
}

View File

@ -0,0 +1,155 @@
import SDL2
public class Input
{
private static let _instance = Input()
public static var instance: Input { _instance }
//private let _UP: UInt8 = 0
//private let _PRESS: UInt8 = 1
//private let _DOWN: UInt8 = 2
//private let _RELEASE: UInt8 = 3
private static let _UP: UInt8 = 0b000
private static let _DOWN: UInt8 = 0b010
private static let _IMPULSE: UInt8 = 0b001
private static let _REPEAT: UInt8 = 0b100
private static let _PRESS: UInt8 = _DOWN | _IMPULSE
private static let _RELEASE: UInt8 = _UP | _IMPULSE
private var _keys = [UInt8](repeating: _UP, count: Int(SDL_NUM_SCANCODES.rawValue))
private init() {}
internal func newTick()
{
//_keys = _keys.map({ i in (i & 0x1) == 0x1 ? ((i &+ 1) & 0x3) : i })
_keys = _keys.map({ $0 & ~Input._IMPULSE })
//for i in 0..<Int(SDL_NUM_SCANCODES.rawValue)
//{
// _keys[i] &= ~0x1
//}
}
internal func pressEvent(scan: SDL_Scancode)
{
_keys[Int(scan.rawValue)] = Input._PRESS
}
internal func releaseEvent(scan: SDL_Scancode)
{
_keys[Int(scan.rawValue)] = Input._RELEASE
}
public func keyDown(_ key: Key) -> Bool
{
//SDL_GetKeyboardState(nil).withMemoryRebound(to: UInt8.self, capacity: Int(SDL_NUM_SCANCODES.rawValue))
//{ state in
// state[Int(key.sdlScancode.rawValue)] != 0
//}
_keys[Int(key.sdlScancode.rawValue)] & Input._DOWN == Input._DOWN
}
public func keyPressed(_ key: Key) -> Bool
{
_keys[Int(key.sdlScancode.rawValue)] == Input._PRESS
}
public func keyReleased(_ key: Key) -> Bool
{
_keys[Int(key.sdlKeycode.rawValue)] == Input._RELEASE
}
}
public enum Key
{
case a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
case right, left, down, up
}
extension Key
{
internal var sdlKeycode: SDL_KeyCode
{
switch self
{
case .a: SDLK_a
case .b: SDLK_a
case .c: SDLK_a
case .d: SDLK_a
case .e: SDLK_a
case .f: SDLK_a
case .g: SDLK_a
case .h: SDLK_a
case .i: SDLK_a
case .j: SDLK_a
case .k: SDLK_a
case .l: SDLK_a
case .m: SDLK_a
case .n: SDLK_a
case .o: SDLK_a
case .p: SDLK_a
case .q: SDLK_a
case .r: SDLK_a
case .s: SDLK_a
case .t: SDLK_a
case .u: SDLK_a
case .v: SDLK_a
case .w: SDLK_a
case .x: SDLK_a
case .y: SDLK_a
case .z: SDLK_a
case .left: SDLK_LEFT
case .right: SDLK_RIGHT
case .up: SDLK_UP
case .down: SDLK_DOWN
}
}
internal var sdlScancode: SDL_Scancode
{
switch self
{
case .a: SDL_SCANCODE_A
case .b: SDL_SCANCODE_B
case .c: SDL_SCANCODE_C
case .d: SDL_SCANCODE_D
case .e: SDL_SCANCODE_E
case .f: SDL_SCANCODE_F
case .g: SDL_SCANCODE_G
case .h: SDL_SCANCODE_H
case .i: SDL_SCANCODE_I
case .j: SDL_SCANCODE_J
case .k: SDL_SCANCODE_K
case .l: SDL_SCANCODE_L
case .m: SDL_SCANCODE_M
case .n: SDL_SCANCODE_N
case .o: SDL_SCANCODE_O
case .p: SDL_SCANCODE_P
case .q: SDL_SCANCODE_Q
case .r: SDL_SCANCODE_R
case .s: SDL_SCANCODE_S
case .t: SDL_SCANCODE_T
case .u: SDL_SCANCODE_U
case .v: SDL_SCANCODE_V
case .w: SDL_SCANCODE_W
case .x: SDL_SCANCODE_X
case .y: SDL_SCANCODE_Y
case .z: SDL_SCANCODE_Z
case .left: SDL_SCANCODE_LEFT
case .right: SDL_SCANCODE_RIGHT
case .up: SDL_SCANCODE_UP
case .down: SDL_SCANCODE_DOWN
}
}
}
extension SDL_KeyCode
{
internal init(scancode: SDL_Scancode)
{
self.init(scancode.rawValue | UInt32(SDLK_SCANCODE_MASK))
}
}

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
}

View File

@ -0,0 +1,291 @@
import Foundation
import simd
public extension FloatingPoint
{
@inline(__always) var saturate: Self { min(max(self , 0), 1) }
@inline(__always) static func lerp(_ a: Self, _ b: Self, _ x: Self) -> Self { a * (1 - x) + b * x }
@inline(__always) static func deg(fromRad: Self) -> Self { fromRad * (180 / Self.pi) }
@inline(__always) static func rad(fromDeg: Self) -> Self { fromDeg * (Self.pi / 180) }
fileprivate func axisDeadzone(_ min: Self, _ max: Self) -> Self
{
let xabs = abs(self)
return if xabs <= min { 0 }
else if xabs >= max { Self(signOf: self, magnitudeOf: 1) }
else { Self(signOf: self, magnitudeOf: xabs - min) / (max - min) }
}
}
public extension SIMD2 where Scalar: FloatingPoint
{
@inline(__always) var len2: Scalar { x * x + y * y }
//@inline(__always) var len2: Scalar { simd_dot(self, self) }
@inline(__always) var len: Scalar { len2.squareRoot() }
@inline(__always) var normalised: Self { self / len }
@inline(__always) func dot(_ b: Self) -> Scalar { x * b.x + y * b.y }
@inline(__always) func reflect(_ n: Self) -> Self { self - (n * 2 * self.dot(n)) }
@inline(__always) func project(_ n: Self) -> Self { n * self.dot(n) }
@inline(__always) func cross(_ b: Self) -> Scalar { x * b.y - y * b.x }
@inline(__always) func lerp(_ b: Self, _ x: Scalar) -> Self
{
let invX = 1 - x
return Self(self.x * invX + b.x * x, self.y * invX + b.y * x)
}
func cardinalDeadzone(min: Scalar, max: Scalar) -> Self
{
Self(self.x.axisDeadzone(min, max), self.y.axisDeadzone(min, max))
}
func radialDeadzone(min: Scalar, max: Scalar) -> Self
{
let magnitude = self.len
if magnitude == .zero || magnitude < min { return Self.zero }
if magnitude > max { return self / magnitude }
let rescale = (magnitude - min) / (max - min)
return self / magnitude * rescale
}
}
public extension SIMD3 where Scalar: FloatingPoint
{
@inline(__always) static var X: Self { Self(1, 0, 0) }
@inline(__always) static var Y: Self { Self(0, 1, 0) }
@inline(__always) static var Z: Self { Self(0, 0, 1) }
@inline(__always) static var up: Self { Y }
@inline(__always) static var down: Self { -Y }
@inline(__always) static var left: Self { -X }
@inline(__always) static var right: Self { X }
@inline(__always) static var forward: Self { Z }
@inline(__always) static var back: Self { -Z }
@inline(__always) var len2: Scalar { x * x + y * y + z * z }
@inline(__always) var len: Scalar { len2.squareRoot() }
@inline(__always) var normalised: Self { self / len }
@inline(__always) mutating func normalise() { self /= len }
@inline(__always) func lerp(_ b: Self, _ x: Scalar) -> Self
{
let invX = 1 - x
return Self(self.x * invX + b.x * x, self.y * invX + b.y * x, self.z * invX + b.z * x)
}
@inline(__always) func dot(_ b: Self) -> Scalar { x * b.x + y * b.y + z * b.z }
@inline(__always) func cross(_ b: Self) -> Self { Self(y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x) }
@inline(__always) func project(_ n: Self) -> Self { n * self.dot(n) }
}
public extension SIMD4 where Scalar: FloatingPoint
{
@inline(__always) static var X: Self { Self(1, 0, 0, 0) }
@inline(__always) static var Y: Self { Self(0, 1, 0, 0) }
@inline(__always) static var Z: Self { Self(0, 0, 1, 0) }
@inline(__always) static var W: Self { Self(0, 0, 0, 1) }
}
public extension simd_float4x4
{
@inline(__always) static var identity: Self { Self(diagonal: .one) }
@inline(__always) static func translate(_ v: Vec3f) -> Self
{
Self(
.init( 1, 0, 0, 0),
.init( 0, 1, 0 ,0),
.init( 0, 0, 1, 0),
.init(v.x, v.y, v.z, 1))
}
@inline(__always) static func scale(_ v: Vec3f) -> Self
{
Self(
.init(v.x, 0, 0, 0),
.init( 0, v.y, 0, 0),
.init( 0, 0, v.z, 0),
.init( 0, 0, 0, 1))
}
@inline(__always) static func scale(scalar v: Float) -> Self
{
Self(
.init(v, 0, 0, 0),
.init(0, v, 0, 0),
.init(0, 0, v, 0),
.init(0, 0, 0, 1))
}
static func rotate(axis v: Vec3f, angle theta: Float) -> Self
{
//FIXME: THIS IS FUCKED UP FOR EVERYTHING OTHER THAN X AXIS ROTATION LOL
/*
let vv = v * v
let xy = v.x * v.y, xz = v.x * v.z, yz = v.y * v.z
let ts = sin(angle), tc = cos(angle)
return Self(
.init(
vv.x + (tc * (1 - vv.x)),
(xz - (tc * xy)) + (ts * v.z),
(xz - (tc * xz)) - (ts * v.y), 0),
.init(
(xy - (tc * xy)) - (ts * v.z),
vv.y + (tc * (1 - vv.y)),
(xz - (tc * xz)) + (ts * v.x), 0),
.init(
(xz - (tc * xz)) + (ts * v.y),
(xz - (tc * xz)) - (ts * v.y),
vv.z + (tc * (1 - vv.z)), 1),
.init(0, 0, 0, 1))
*/
let vxx = v.x * v.x, vxy = v.x * v.y, vxz = v.x * v.z
let vyy = v.y * v.y, vyz = v.y * v.z
let vzz = v.z * v.z
let ts = sin(theta), tc = cos(theta)
let ic = 1 - tc
return Self(
.init(
ic * vxx + tc,
ic * vxy - v.z * ts,
ic * vxz + v.z * ts,
0),
.init(
ic * vxy + v.z * ts,
ic * vyy + tc,
ic * vyz - v.x * ts,
0),
.init(
ic * vxz - v.y * ts,
ic * vyz + v.x * ts,
ic * vzz + tc,
0),
.init(0, 0, 0, 1))
}
@inline(__always) static func rotate(yawPitch: Vec2f) -> Self { return rotate(yaw: yawPitch.x, pitch: yawPitch.y) }
static func rotate(yaw ytheta: Float, pitch xtheta: Float) -> Self
{
let xc = cos(xtheta), xs = sin(xtheta)
let yc = cos(ytheta), ys = sin(ytheta)
return Self(
.init(yc, ys * xs, -ys * xc, 0),
.init( 0, xc, xs, 0),
.init(ys, yc * -xs, yc * xc, 0),
.init( 0, 0, 0, 1))
}
static func rotate(yaw ytheta: Float, pitch xtheta: Float, roll ztheta: Float) -> Self
{
//FIXME: this doesn't null against control
let xc = cos(xtheta), xs = sin(xtheta)
let yc = cos(ytheta), ys = sin(ytheta)
let zc = cos(ztheta), zs = sin(ztheta)
let ysxs = ys * xs, ycxs = yc * xs
let result = Mat4f(
.init(yc * zc + ysxs * zs, yc * -zs + ysxs * zc, -ys * xc, 0),
.init( xc * zs, xc * zc, xs, 0),
.init(ys * zc - ycxs * zs, ys * -zs - ycxs * zc, yc * xc, 0),
.init( 0, 0, 0, 1))
let shouldBe = .rotate(z: ztheta) * .rotate(x: xtheta) * .rotate(y: ytheta)
let epsilon: Float = .ulpOfOne
if (result != shouldBe)
{
assert(abs(result[0][0] - shouldBe[0][0]) <= epsilon) // epsilon
assert(result[1][0] == shouldBe[1][0])
assert(abs(result[2][0] - shouldBe[2][0]) <= epsilon) // epsilon
assert(result[3][0] == shouldBe[3][0])
assert(abs(result[0][1] - shouldBe[0][1]) <= epsilon) // epsilon
assert(result[1][1] == shouldBe[1][1])
assert(abs(result[2][1] - shouldBe[2][1]) <= epsilon) // epsilon
assert(result[3][1] == shouldBe[3][1])
assert(result[0][2] == shouldBe[0][2])
assert(result[1][2] == shouldBe[1][2])
assert(result[2][2] == shouldBe[2][2])
assert(result[3][2] == shouldBe[3][2])
assert(result[0][3] == shouldBe[0][3])
assert(result[1][3] == shouldBe[1][3])
assert(result[2][3] == shouldBe[2][3])
assert(result[3][3] == shouldBe[3][3])
}
return result
}
@inline(__always) static func rotate(x theta: Float) -> Self
{
let c = cos(theta), s = sin(theta)
return Self(
.init(1, 0, 0, 0),
.init(0, c, s, 0),
.init(0, -s, c, 0),
.init(0, 0, 0, 1))
}
@inline(__always) static func rotate(y theta: Float) -> Self
{
let c = cos(theta), s = sin(theta)
return Self(
.init(c, 0, -s, 0),
.init(0, 1, 0, 0),
.init(s, 0, c, 0),
.init(0, 0, 0, 1))
}
@inline(__always) static func rotate(z theta: Float) -> Self
{
let c = cos(theta), s = sin(theta)
return Self(
.init(c, -s, 0, 0),
.init(s, c, 0, 0),
.init(0, 0, 1, 0),
.init(0, 0, 0, 1))
}
static func perspective(fovY: Float, aspect: Float, zNear: Float, zFar: Float) -> Self
{
let h = 1 / tanf(fovY * 0.5)
let w = h / aspect
let invClipRange = 1 / (zFar - zNear)
let z = -(zFar + zNear) * invClipRange
let z2 = -(2 * zFar * zNear) * invClipRange
return simd_matrix(
.init(w, 0, 0, 0),
.init(0, h, 0, 0),
.init(0, 0, z, -1),
.init(0, 0, z2, 0))
}
static func lookAt(from: Vec3f = .zero, to: Vec3f, up: Vec3f = .up) -> Self
{
let forward = (to - from).normalised
let bitangent = forward.cross(up).normalised
let tangent = bitangent.cross(forward).normalised
let normal = -forward
return simd_matrix(
.init(bitangent.x, tangent.x, normal.x, 0.0),
.init(bitangent.y, tangent.y, normal.y, 0.0),
.init(bitangent.z, tangent.z, normal.z, 0.0),
.init( 0.0, 0.0, 0.0, 1.0))
}
}
public typealias Vec2f = SIMD2<Float>
public typealias Vec2d = SIMD2<Double>
public typealias Vec3f = SIMD3<Float>
public typealias Vec3d = SIMD3<Double>
public typealias Vec4f = SIMD4<Float>
public typealias Vec4d = SIMD4<Double>
public typealias Mat4f = simd_float4x4

View File

@ -0,0 +1,81 @@
public enum Fog
{
public enum Mode
{
case distance
case depth
}
public enum RangeType
{
case linear
case smooth
}
public enum FactorType
{
case exp
case exp2
}
case none
case range(colour: Colour, mode: Mode, type: RangeType, start: Float, end: Float)
case factor(colour: Colour, mode: Mode, type: FactorType, density: Float)
}
public enum Light
{
case directional(colour: Colour, direction: Vec3f)
case point(colour: Colour, position: Vec3f, intensity: Float)
}
public struct Environment
{
public var fog: Fog = .none
public var ambient: Colour = .black
public var lights: [Light] = .init()
public init() {}
public init(fog: Fog, ambient: Colour, lights: [Light])
{
self.fog = fog
self.ambient = ambient
self.lights = lights
}
}
public extension Environment
{
enum FogFalloff
{
case range(type: Fog.RangeType, start: Float, end: Float)
case factor(type: Fog.FactorType, density: Float)
}
mutating func setFog(colour: Colour, mode: Fog.Mode, falloff: FogFalloff)
{
fog = switch falloff
{
case .range(let type, let start, let end):
.range(colour: colour, mode: mode, type: type, start: start, end: end)
case .factor(let type, let density):
.factor(colour: colour, mode: mode, type: type, density: density)
}
}
mutating func addAmbientLight(colour: Colour)
{
ambient = colour
}
mutating func addDirectionalLight(direction: Vec3f, colour: Colour)
{
lights.append(.directional(colour: colour, direction: direction))
}
mutating func addPointLight(position: Vec3f, colour: Colour, intensity: Float)
{
lights.append(.point(colour: colour, position: position, intensity: intensity))
}
}

View File

@ -0,0 +1,20 @@
public struct Material
{
public var id: String
public var diffuse: Colour, specular: Colour
public var specularExp: Float
public var texture: RenderTexture2D
public init(id: String = "",
diffuse: Colour = .white,
specular: Colour = .zero,
gloss: Float = 0,
texture: RenderTexture2D = .empty)
{
self.id = id
self.diffuse = diffuse
self.specular = specular
self.specularExp = gloss
self.texture = texture
}
}

View File

@ -0,0 +1,440 @@
//#set OPENGL3
import Foundation
import SDL2
#if OPENGL3
import OpenGL
#else
import OpenGL.GL3
#endif
class OpenGL: Renderer
{
private let srgb = true
private var glCtx: SDL_GLContext? = nil
private var state = OpenGLState()
struct Version { let major, minor: Int32 }
let glVersion: Version
init(version: Version)
{
self.glVersion = version
}
func create(sdlWindow: OpaquePointer) throws
{
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, Int32(SDL_GL_CONTEXT_PROFILE_COMPATIBILITY.rawValue))
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glVersion.major)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glVersion.minor)
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1)
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0)
if let context = SDL_GL_CreateContext(sdlWindow) { glCtx = context }
else { throw RendererError.sdlError(message: "SDL_GL_CreateContext: \(String(cString: SDL_GetError()))") }
guard SDL_GL_MakeCurrent(sdlWindow, glCtx) == 0
else { throw RendererError.sdlError(message: "SDL_GL_MakeCurrent: \(String(cString: SDL_GetError()))") }
state.enable([.texture2D, .cullFace, .depthTest, .rescaleNormal, .colourMaterial])
if srgb { state.enable(.frameBufferSrgb) }
state.cullFace = .back
state.clearDepth = 1
state.depthFunc = .less
state.setHint(target: .fog, hint: .dontCare)
}
func delete()
{
SDL_GL_DeleteContext(glCtx)
}
func resize(width: Int32, height: Int32)
{
glViewport(0, 0, width, height)
}
func newFrame()
{
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
}
private var _clearColour = Colour.black
var clearColour: Colour
{
get { _clearColour }
set(newColour)
{
state.clearColour(srgb ? newColour.linear : newColour)
_clearColour = newColour
}
}
func setVsync(mode: VSyncMode) throws
{
guard SDL_GL_SetSwapInterval(mode.sdlInterval) == 0
else { throw RendererError.sdlError(message: "SDL_GL_SetSwapInterval: \(String(cString: SDL_GetError()))") }
}
func createMesh(mesh: Mesh) throws -> RenderMesh
{
var buffers = [GLuint](repeating: 0, count: 2)
buffers.withUnsafeMutableBufferPointer
{
glGenBuffers(2, $0.baseAddress!)
}
state.arrayBuffer = buffers[0]
state.elementArrayBuffer = buffers[1]
glBufferData(GLenum(GL_ARRAY_BUFFER),
MemoryLayout<Mesh.Vertex>.stride * mesh.vertices.count,
mesh.vertices, GLenum(GL_STATIC_DRAW))
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
MemoryLayout<Mesh.Index>.stride * mesh.indices.count,
mesh.indices, GLenum(GL_STATIC_DRAW))
state.elementArrayBuffer = OpenGLState.defaultBuffer
state.arrayBuffer = OpenGLState.defaultBuffer
var subMeshes = [Mesh.SubMesh]()
if mesh.subMeshes.isEmpty
{
subMeshes.append(Mesh.SubMesh(start: 0, length: mesh.indices.count))
}
else
{
for subMesh in mesh.subMeshes
{
if ["Collision", "Collision3D"].contains(subMesh.key) { continue }
subMeshes.append(Mesh.SubMesh(
start: subMesh.value.start,
length: subMesh.value.length))
}
}
return RenderMesh(
vbo: Int(buffers[0]),
ibo: Int(buffers[1]),
subMeshes: subMeshes)
}
func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
{
try createTexture(data: data, width: width, height: height, filter: .linear, mipMode: .off)
}
func createTexture(data: UnsafeRawPointer, width: Int, height: Int,
filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
{
let min: Int32 = switch mipMode
{
case .off: filter.gl
case .nearest: filter.glMip
case .linear: filter.glLinearMip
}
return RenderTexture2D(try loadTexture(data: data,
width: GLsizei(width), height: GLsizei(height),
minFilter: min, magFilter: filter.gl))
}
private func loadTexture(
data: UnsafeRawPointer,
width: GLsizei, height: GLsizei,
minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR,
wrap: Int32 = GL_REPEAT) throws -> GLuint
{
// Create & upload raw bytes as texture
var texId: GLuint = 0
glGenTextures(1, &texId)
state.bindTexture2D(active: 0, texture: texId)
state.texture2DParameter(active: 0, param: .minFilter, int: minFilter)
state.texture2DParameter(active: 0, param: .magFilter, int: magFilter)
state.texture2DParameter(active: 0, param: .wrapS, int: wrap)
state.texture2DParameter(active: 0, param: .wrapT, int: wrap)
let format: GLint = srgb ? GL_SRGB8 : GL_RGBA
glTexImage2D(GLenum(GL_TEXTURE_2D),
0, format, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
// Generate mipmaps if needed
if [GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST]
.contains(minFilter) { glGenerateMipmap(GLenum(GL_TEXTURE_2D)) }
state.bindTexture2D(active: 0, texture: OpenGLState.defaultTexture)
return texId
}
func deleteMesh(_ mesh: RenderMesh)
{
var buffers = [GLuint](repeating: 0, count: 2)
buffers[0] = GLuint(mesh.iboHnd)
buffers[1] = GLuint(mesh.vboHnd)
glDeleteBuffers(1, buffers)
}
func deleteTexture(_ texture: RenderTexture2D)
{
var texId = GLuint(texture.id)
glDeleteTextures(1, &texId)
}
func setProjection(matrix: Mat4f)
{
state.matrixMode = .projection
loadMatrix(matrix)
}
func setView(matrix: Mat4f)
{
state.matrixMode = .modelView
loadMatrix(matrix)
}
func setMaterial(_ mat: Material)
{
glColor4f(mat.diffuse.r, mat.diffuse.g, mat.diffuse.b, mat.diffuse.a)
state.bindTexture2D(active: 0, texture: mat.texture.isValid ? mat.texture.id : 0)
}
private func draw(subMesh: Mesh.SubMesh)
{
glDrawElements(
GLenum(GL_TRIANGLES),
GLsizei(subMesh.length),
GLenum(GL_UNSIGNED_SHORT),
.init(bitPattern: MemoryLayout<Mesh.Index>.stride * subMesh.start));
}
private func draw(mesh: RenderMesh)
{
state.enableClient([ .vertex, .normal, .textureCoord ])
state.arrayBuffer = UInt32(mesh.vboHnd)
state.elementArrayBuffer = UInt32(mesh.iboHnd)
let stride = GLsizei(MemoryLayout<Mesh.Vertex>.stride)
glVertexPointer(3, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.position)!))
glNormalPointer(GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.normal)!))
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.texCoord)!))
for subMesh in mesh.subMeshes
{
draw(subMesh: subMesh)
}
state.elementArrayBuffer = OpenGLState.defaultBuffer
state.arrayBuffer = OpenGLState.defaultBuffer
state.disableClient([ .vertex, .normal, .textureCoord ])
}
func draw(mesh: RenderMesh, model: Mat4f, environment env: Environment)
{
if (mesh.subMeshes.isEmpty) { return }
applyEnvironment(env)
state.matrixMode = .modelView
glPushMatrix()
mulMatrix(model)
draw(mesh: mesh)
glPopMatrix()
}
func draw(mesh: RenderMesh, environment env: Environment)
{
if (mesh.subMeshes.isEmpty) { return }
applyEnvironment(env)
draw(mesh: mesh)
}
private func loadMatrix(_ matrix: Mat4f)
{
withUnsafePointer(to: matrix.columns)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
{ (values: UnsafePointer<GLfloat>) in
glLoadMatrixf(values)
}
}
}
private func mulMatrix(_ matrix: Mat4f)
{
withUnsafePointer(to: matrix.columns)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
{ (values: UnsafePointer<GLfloat>) in
glMultMatrixf(values)
}
}
}
private func applyEnvironment(_ env: Environment)
{
let coordSource =
{ (mode: Fog.Mode) -> OpenGLState.FogCoordinateSource in
switch mode
{
case .depth: .fragmentDepth
case .distance: .coordinate
}
}
switch env.fog
{
case .none:
state.disable(.fog)
case .range(let colour, let mode, _, let start, let end):
state.fogMode = .linear
state.fogColour = srgb ? colour.linear : colour
state.fogCoodinateSource = coordSource(mode)
state.fogStart = start
state.fogEnd = end
state.enable(.fog)
break
case .factor(let colour, let mode, let type, let density):
state.fogMode = switch type
{
case .exp: .exp
case .exp2: .exp2
}
state.fogColour = srgb ? colour.linear : colour
state.fogCoodinateSource = coordSource(mode)
state.fogDensity = density
state.enable(.fog)
break
}
state.lightModelAmbient = env.ambient
let lightCaps: [OpenGLState.Capability] = [
.light0, .light1, .light2, .light3,
.light4, .light5, .light6, .light7 ]
if !env.lights.isEmpty
{
state.enable(.lighting)
for i in 0..<8
{
if i < env.lights.count
{
switch env.lights[i]
{
case .directional(let colour, let direction):
state.lightPosition[i] = Vec4f(direction, 0)
state.lightDiffuse[i] = srgb ? colour.linear : colour
case .point(let colour, let position, let intensity):
state.lightPosition[i] = Vec4f(position, 1)
state.lightDiffuse[i] = srgb ? colour.linear : colour
state.lightConstantAttenuation[i] = 0
state.lightLinearAttenuation[i] = intensity
state.lightQuadraticAttenuation[i] = 0
}
state.enable(lightCaps[i])
}
else
{
state.disable(lightCaps[i])
}
}
}
else
{
state.disable(.lighting)
for cap in lightCaps { state.disable(cap) }
}
}
func drawGizmos(lines: [Line])
{
state.disable([ .texture2D, .depthTest ])
var gizmoEnv = Environment()
gizmoEnv.fog = .none
applyEnvironment(gizmoEnv)
glBegin(GLenum(GL_LINES))
for line in lines
{
let colour = srgb ? line.colour.linear : line.colour
glColor4f(colour.r, colour.g, colour.b, colour.a)
glVertex3f(line.from.x, line.from.y, line.from.z)
glVertex3f(line.to.x, line.to.y, line.to.z)
}
glEnd()
state.enable([ .texture2D, .depthTest ])
}
}
extension VSyncMode
{
fileprivate var sdlInterval: Int32
{
switch self
{
case .off: 0
case .on: 1
case .adaptive: -1
}
}
}
extension FilterMode
{
fileprivate var gl: Int32
{
switch self
{
case .point: GL_NEAREST
case .linear: GL_LINEAR
}
}
fileprivate var glMip: Int32
{
switch self
{
case .point: GL_NEAREST_MIPMAP_NEAREST
case .linear: GL_LINEAR_MIPMAP_NEAREST
}
}
fileprivate var glLinearMip: Int32
{
switch self
{
case .point: GL_NEAREST_MIPMAP_LINEAR
case .linear: GL_LINEAR_MIPMAP_LINEAR
}
}
}
extension WrapMode
{
fileprivate var gl: Int32
{
switch self
{
case .clamping: GL_CLAMP
case .clampBorder: GL_CLAMP_TO_BORDER
case .clampEdge: GL_CLAMP_TO_EDGE
case .mirrored: GL_MIRRORED_REPEAT
case .repeating: GL_REPEAT
}
}
}

View File

@ -0,0 +1,796 @@
import OpenGL
struct OpenGLState
{
struct Capability: OptionSet
{
let rawValue: UInt32
static let alphaTest = Self(rawValue: 1 << 0)
static let blend = Self(rawValue: 1 << 1)
static let colourMaterial = Self(rawValue: 1 << 2)
static let colourSum = Self(rawValue: 1 << 3)
static let cullFace = Self(rawValue: 1 << 4)
static let depthTest = Self(rawValue: 1 << 5)
static let dither = Self(rawValue: 1 << 6)
static let fog = Self(rawValue: 1 << 7)
static let light0 = Self(rawValue: 1 << 8)
static let light1 = Self(rawValue: 1 << 9)
static let light2 = Self(rawValue: 1 << 10)
static let light3 = Self(rawValue: 1 << 11)
static let light4 = Self(rawValue: 1 << 12)
static let light5 = Self(rawValue: 1 << 13)
static let light6 = Self(rawValue: 1 << 14)
static let light7 = Self(rawValue: 1 << 15)
static let lighting = Self(rawValue: 1 << 16)
//static let lineSmooth = Self(rawValue: 1 << 17)
static let lineStipple = Self(rawValue: 1 << 18)
static let multiSample = Self(rawValue: 1 << 19)
//static let pointSmooth = Self(rawValue: 1 << 20)
static let scissorTest = Self(rawValue: 1 << 21)
static let stencilTest = Self(rawValue: 1 << 22)
//static let texture1D = Self(rawValue: 1 << 23)
static let texture2D = Self(rawValue: 1 << 24)
static let clipPlane0 = Self(rawValue: 1 << 25)
static let clipPlane1 = Self(rawValue: 1 << 26)
static let clipPlane2 = Self(rawValue: 1 << 27)
static let clipPlane3 = Self(rawValue: 1 << 28)
static let clipPlane4 = Self(rawValue: 1 << 29)
static let clipPlane5 = Self(rawValue: 1 << 30)
//static let colourLogicOp = Self(rawValue: 1 << 31)
static let sampleCoverage = Self(rawValue: 1 << 17)
static let alphaToOne = Self(rawValue: 1 << 20)
static let rescaleNormal = Self(rawValue: 1 << 23)
static let frameBufferSrgb = Self(rawValue: 1 << 31)
static let all: [Self] = [ .alphaTest, .blend, .colourMaterial, .colourSum, .cullFace, .depthTest, .dither,
.fog, .light0, .light1, .light2, .light3, .light4, .light5, .light6, .light7, .lighting, /*.lineSmooth,*/
.lineStipple, /*.pointSmooth,*/ .multiSample, .scissorTest, .stencilTest, /*.texture1D,*/ .texture2D,
.clipPlane0, .clipPlane1, .clipPlane2, .clipPlane3, .clipPlane4, .clipPlane5, /*.colourLogicOp,*/
.sampleCoverage, .alphaToOne, .rescaleNormal, .frameBufferSrgb ]
static let initial: Self = [ .dither, .multiSample ]
var glCap: GLenum
{
let capEnum = switch self
{
case .alphaTest: GL_ALPHA_TEST
case .blend: GL_BLEND
case .colourMaterial: GL_COLOR_MATERIAL
case .colourSum: GL_COLOR_SUM
case .cullFace: GL_CULL_FACE
case .depthTest: GL_DEPTH_TEST
case .dither: GL_DITHER
case .fog: GL_FOG
case .light0: GL_LIGHT0
case .light1: GL_LIGHT1
case .light2: GL_LIGHT2
case .light3: GL_LIGHT3
case .light4: GL_LIGHT4
case .light5: GL_LIGHT5
case .light6: GL_LIGHT6
case .light7: GL_LIGHT7
case .lighting: GL_LIGHTING
//case .lineSmooth: GL_LINE_SMOOTH
case .lineStipple: GL_LINE_STIPPLE
case .multiSample: GL_MULTISAMPLE
//case .pointSmooth: GL_POINT_SMOOTH
case .scissorTest: GL_SCISSOR_TEST
case .stencilTest: GL_STENCIL_TEST
//case .texture1D: GL_TEXTURE_1D
case .texture2D: GL_TEXTURE_2D
case .clipPlane0: GL_CLIP_PLANE0
case .clipPlane1: GL_CLIP_PLANE1
case .clipPlane2: GL_CLIP_PLANE2
case .clipPlane3: GL_CLIP_PLANE3
case .clipPlane4: GL_CLIP_PLANE4
case .clipPlane5: GL_CLIP_PLANE5
//case .colourLogicOp: GL_COLOR_LOGIC_OP
case .sampleCoverage: GL_SAMPLE_COVERAGE
case .alphaToOne: GL_SAMPLE_ALPHA_TO_ONE
case .rescaleNormal: GL_RESCALE_NORMAL
case .frameBufferSrgb: GL_FRAMEBUFFER_SRGB
default: fatalError()
}
return GLenum(capEnum)
}
}
private var _capabilities: Capability = .initial
private mutating func toggle(_ cap: Capability, _ en: Bool)
{
if en
{
if !_capabilities.contains(cap)
{
glEnable(cap.glCap)
_capabilities.insert(cap)
}
}
else
{
if _capabilities.contains(cap)
{
glDisable(cap.glCap)
_capabilities.remove(cap)
}
}
}
mutating func enable(_ caps: Capability)
{
if Capability.all.contains(caps)
{
toggle(caps, true)
}
else
{
for i in Capability.all
{
if i.intersection(caps) == i { toggle(i, true) }
}
}
}
mutating func disable(_ caps: Capability)
{
if Capability.all.contains(caps)
{
toggle(caps, false)
}
else
{
for i in Capability.all
{
if i.intersection(caps) == i { toggle(i, false) }
}
}
}
struct ClientState: OptionSet
{
let rawValue: UInt32
static let colour = Self(rawValue: 1 << 0)
static let edgeFlag = Self(rawValue: 1 << 1)
static let fogCoord = Self(rawValue: 1 << 2)
static let index = Self(rawValue: 1 << 3)
static let normal = Self(rawValue: 1 << 4)
static let secondaryColour = Self(rawValue: 1 << 5)
static let textureCoord = Self(rawValue: 1 << 6)
static let vertex = Self(rawValue: 1 << 7)
static let all: [Self] = [ .colour, .edgeFlag, .fogCoord, .index, .normal, .secondaryColour, .textureCoord, .vertex ]
static let initial: Self = []
var glCap: GLenum
{
let capEnum = switch self
{
case .colour: GL_COLOR_ARRAY
case .edgeFlag: GL_EDGE_FLAG_ARRAY
case .fogCoord: GL_FOG_COORD_ARRAY
case .index: GL_INDEX_ARRAY
case .normal: GL_NORMAL_ARRAY
case .secondaryColour: GL_SECONDARY_COLOR_ARRAY
case .textureCoord: GL_TEXTURE_COORD_ARRAY
case .vertex: GL_VERTEX_ARRAY
default: fatalError()
}
return GLenum(capEnum)
}
}
private var _clientState: ClientState = .initial
private mutating func toggleClientState(_ cap: ClientState, _ en: Bool)
{
if en
{
if !_clientState.contains(cap)
{
glEnableClientState(cap.glCap)
_clientState.insert(cap)
}
}
else
{
if _clientState.contains(cap)
{
glDisableClientState(cap.glCap)
_clientState.remove(cap)
}
}
}
mutating func enableClient(_ caps: ClientState)
{
if ClientState.all.contains(caps)
{
toggleClientState(caps, true)
}
else
{
for i in ClientState.all
{
if i.intersection(caps) == i { toggleClientState(i, true) }
}
}
}
mutating func disableClient(_ caps: ClientState)
{
if ClientState.all.contains(caps)
{
toggleClientState(caps, false)
}
else
{
for i in ClientState.all
{
if i.intersection(caps) == i { toggleClientState(i, false) }
}
}
}
private var _activeTexture2D = 0
private mutating func setActiveTexture2D(_ active: Int)
{
if active != _activeTexture2D
{
let slots = [ GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2, GL_TEXTURE3, GL_TEXTURE4, GL_TEXTURE5, GL_TEXTURE6,
GL_TEXTURE7, GL_TEXTURE8, GL_TEXTURE9, GL_TEXTURE10, GL_TEXTURE11, GL_TEXTURE12, GL_TEXTURE13,
GL_TEXTURE14, GL_TEXTURE15, GL_TEXTURE16, GL_TEXTURE17, GL_TEXTURE18, GL_TEXTURE19, GL_TEXTURE20,
GL_TEXTURE21, GL_TEXTURE22, GL_TEXTURE23, GL_TEXTURE24, GL_TEXTURE25, GL_TEXTURE26, GL_TEXTURE27,
GL_TEXTURE28, GL_TEXTURE29, GL_TEXTURE30, GL_TEXTURE31 ]
glActiveTexture(GLenum(slots[active]))
_activeTexture2D = active
}
}
static let defaultTexture: UInt32 = 0
private var _texture2D: GLuint = GLuint(defaultTexture)
mutating func bindTexture2D(active: Int, texture: UInt32)
{
if GLuint(texture) != _texture2D
{
setActiveTexture2D(active)
glBindTexture(GLenum(GL_TEXTURE_2D), GLuint(texture))
_texture2D = GLuint(texture)
}
}
enum TexureParameter { case magFilter, minFilter, wrapS, wrapT }
mutating func texture2DParameter(active: Int, param: TexureParameter, int value: Int32)
{
setActiveTexture2D(active)
let pname = switch param
{
case .magFilter: GL_TEXTURE_MAG_FILTER
case .minFilter: GL_TEXTURE_MIN_FILTER
case .wrapS: GL_TEXTURE_WRAP_S
case .wrapT: GL_TEXTURE_WRAP_T
}
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(pname), GLint(value))
}
private var _clearDepth: GLclampd = 1
var clearDepth: Double
{
get { Double(_clearDepth) }
set(newDepth)
{
if GLclampd(newDepth) != _clearDepth
{
glClearDepth(newDepth)
_clearDepth = GLclampd(newDepth)
}
}
}
private var _clearColour: Colour = .zero
mutating func clearColour(_ newColour: Colour)
{
if newColour != _clearColour
{
glClearColor(newColour.r, newColour.g, newColour.b, newColour.a)
_clearColour = newColour
}
}
enum CullFace { case front, back, frontAndBack }
private var _cullFace: CullFace = .back
var cullFace: CullFace
{
get { _cullFace }
set(newMode)
{
if newMode != _cullFace
{
let modeEnum = switch newMode
{
case .front: GL_FRONT
case .back: GL_BACK
case .frontAndBack: GL_FRONT_AND_BACK
}
glCullFace(GLenum(modeEnum))
_cullFace = newMode
}
}
}
enum DepthFunc { case never, less, equal, lessOrEqual, greater, notEqual, greaterOrEqual, always }
private var _depthFunc: DepthFunc = .less
var depthFunc: DepthFunc
{
get { _depthFunc }
set(newFunc)
{
if newFunc != _depthFunc
{
let funcEnum = switch newFunc
{
case .never: GL_NEVER
case .less: GL_LESS
case .equal: GL_EQUAL
case .lessOrEqual: GL_LEQUAL
case .greater: GL_GREATER
case .notEqual: GL_NOTEQUAL
case .greaterOrEqual: GL_GEQUAL
case .always: GL_ALWAYS
}
glDepthFunc(GLenum(funcEnum))
_depthFunc = newFunc
}
}
}
enum FogMode { case linear, exp, exp2 }
private var _fogMode: FogMode = .exp
var fogMode: FogMode
{
get { _fogMode }
set(newMode)
{
if newMode != _fogMode
{
let modeEnum = switch newMode
{
case .linear: GL_LINEAR
case .exp: GL_EXP
case .exp2: GL_EXP2
}
glFogi(GLenum(GL_FOG_MODE), modeEnum)
_fogMode = newMode
}
}
}
private var _fogDensity: Float = 1
var fogDensity: Float
{
get { _fogDensity }
set(newDensity)
{
if newDensity != _fogDensity
{
glFogf(GLenum(GL_FOG_DENSITY), newDensity)
_fogDensity = newDensity
}
}
}
private var _fogStart: Float = 0
var fogStart: Float
{
get { _fogStart }
set(newStart)
{
if newStart != _fogStart
{
glFogf(GLenum(GL_FOG_START), newStart)
_fogStart = newStart
}
}
}
private var _fogEnd: Float = 1
var fogEnd: Float
{
get { _fogEnd }
set(newEnd)
{
if newEnd != _fogEnd
{
glFogf(GLenum(GL_FOG_END), newEnd)
_fogEnd = newEnd
}
}
}
private var _fogColour: Colour = .zero
var fogColour: Colour
{
get { _fogColour }
set(newColour)
{
if newColour != _fogColour
{
withUnsafePointer(to: newColour)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
{
glFogfv(GLenum(GL_FOG_COLOR), $0)
}
}
}
}
}
enum FogCoordinateSource { case coordinate, fragmentDepth }
private var _fogCoordinateSource: FogCoordinateSource = .fragmentDepth
var fogCoodinateSource: FogCoordinateSource
{
get { _fogCoordinateSource }
set(newCoordinateSource)
{
if newCoordinateSource != _fogCoordinateSource
{
let coordinateSourceEnum = switch newCoordinateSource
{
case .coordinate: GL_FOG_COORD
case .fragmentDepth: GL_FRAGMENT_DEPTH
}
glFogi(GLenum(GL_FOG_COORD_SRC), coordinateSourceEnum)
_fogCoordinateSource = newCoordinateSource
}
}
}
enum Hint
{
case fog
case generateMipmap
case lineSmooth
case perspectiveCorrection
case pointSmooth
case polygonSmooth
case textureCompression
}
enum HintBehaviour { case fastest, nicest, dontCare }
func setHint(target: Hint, hint: HintBehaviour)
{
let target = switch target
{
case .fog: GL_FOG_HINT
case .generateMipmap: GL_GENERATE_MIPMAP_HINT
case .lineSmooth: GL_LINE_SMOOTH_HINT
case .perspectiveCorrection: GL_PERSPECTIVE_CORRECTION_HINT
case .pointSmooth: GL_POINT_SMOOTH_HINT
case .polygonSmooth: GL_POLYGON_SMOOTH_HINT
case .textureCompression: GL_TEXTURE_COMPRESSION_HINT
}
let mode = switch hint
{
case .fastest: GL_FASTEST
case .nicest: GL_NICEST
case .dontCare: GL_DONT_CARE
}
glHint(GLenum(target), GLenum(mode))
}
struct LightAmbientProperty
{
private var _lightAmbient = [Colour](repeating: .black, count: 8)
subscript(index: Int) -> Colour
{
get { _lightAmbient[index] }
set(newAmbient)
{
if newAmbient != _lightAmbient[index]
{
withUnsafePointer(to: newAmbient)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
{
// glLightModelfv(GLenum(GL_LIGHT_MODEL_AMBIENT), ambientColour)
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_AMBIENT), $0)
}
}
_lightAmbient[index] = newAmbient
}
}
}
}
var lightAmbient = LightAmbientProperty()
struct LightDiffuseProperty
{
private var _lightDiffuse: [Colour] = [ .white, .black, .black, .black, .black, .black, .black, .black ]
subscript(index: Int) -> Colour
{
get { _lightDiffuse[index] }
set(newDiffuse)
{
if newDiffuse != _lightDiffuse[index]
{
withUnsafePointer(to: newDiffuse)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
{
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_DIFFUSE), $0)
}
}
_lightDiffuse[index] = newDiffuse
}
}
}
}
var lightDiffuse = LightDiffuseProperty()
struct LightSpecularProperty
{
private var _lightSpecular: [Colour] = [ .white, .black, .black, .black, .black, .black, .black, .black ]
subscript(index: Int) -> Colour
{
get { _lightSpecular[index] }
set(newSpecular)
{
if newSpecular != _lightSpecular[index]
{
withUnsafePointer(to: newSpecular)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
{
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_SPECULAR), $0)
}
}
_lightSpecular[index] = newSpecular
}
}
}
}
var lightSpecular = LightSpecularProperty()
struct LightPositionProperty
{
private var _lightPosition = [Vec4f](repeating: .Z, count: 8)
subscript(index: Int) -> Vec4f
{
get { _lightPosition[index] }
set(newPosition)
{
withUnsafePointer(to: newPosition)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
{
glLightfv(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_POSITION), $0)
}
}
_lightPosition[index] = newPosition
}
}
}
var lightPosition = LightPositionProperty()
//private var _lightSpotDirection = [Vec4f](repeating: .zero, count: 8)
//private var _lightSpotExponent = [Vec4f](repeating: .zero, count: 8)
//private var _lightSpotCutoff = [Vec4f](repeating: .zero, count: 8)
struct LightConstantAttenuationProperty
{
private var _lightConstantAttenuation = [Float](repeating: 1, count: 8)
subscript(index: Int) -> Float
{
get { _lightConstantAttenuation[index] }
set(newConstantAttenuation)
{
if newConstantAttenuation != _lightConstantAttenuation[index]
{
glLightf(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_CONSTANT_ATTENUATION), newConstantAttenuation)
_lightConstantAttenuation[index] = newConstantAttenuation
}
}
}
}
var lightConstantAttenuation = LightConstantAttenuationProperty()
struct LightLinearAttenuationProperty
{
private var _lightLinearAttenuation = [Float](repeating: 0, count: 8)
subscript(index: Int) -> Float
{
get { _lightLinearAttenuation[index] }
set(newLinearAttenuation)
{
if newLinearAttenuation != _lightLinearAttenuation[index]
{
glLightf(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_LINEAR_ATTENUATION), newLinearAttenuation)
_lightLinearAttenuation[index] = newLinearAttenuation
}
}
}
}
var lightLinearAttenuation = LightLinearAttenuationProperty()
struct LightQuadraticAttenuation
{
private var _lightQuadraticAttenuation = [Float](repeating: 0, count: 8)
subscript(index: Int) -> Float
{
get { _lightQuadraticAttenuation[index] }
set(newQuadraticAttenuation)
{
if newQuadraticAttenuation != _lightQuadraticAttenuation[index]
{
glLightf(GLenum(GL_LIGHT0 + Int32(index)), GLenum(GL_QUADRATIC_ATTENUATION), newQuadraticAttenuation)
_lightQuadraticAttenuation[index] = newQuadraticAttenuation
}
}
}
}
var lightQuadraticAttenuation = LightQuadraticAttenuation()
var _lightModelAmbient = Colour(r: 0.2, g: 0.2, b: 0.2, a: 1)
var lightModelAmbient: Colour
{
get { _lightModelAmbient }
set(newAmbient)
{
if newAmbient != _lightModelAmbient
{
withUnsafePointer(to: newAmbient)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 4)
{
glLightModelfv(GLenum(GL_LIGHT_MODEL_AMBIENT), $0)
}
}
_lightModelAmbient = newAmbient
}
}
}
enum LightModelColourControl { case single, separateSpecular }
var _lightModelColourControl: LightModelColourControl = .single
var lightModelColourControl: LightModelColourControl
{
get { _lightModelColourControl }
set(newControl)
{
if newControl != _lightModelColourControl
{
let param = switch newControl
{
case .single: GL_SINGLE_COLOR
case .separateSpecular: GL_SEPARATE_SPECULAR_COLOR
}
glLightModelf(GLenum(GL_LIGHT_MODEL_COLOR_CONTROL), GLfloat(param))
_lightModelColourControl = newControl
}
}
}
var _lightModelLocalViewer = false
var lightModelLocalViewer: Bool
{
get { _lightModelLocalViewer }
set(newMode)
{
if newMode != _lightModelLocalViewer
{
glLightModeli(GLenum(GL_LIGHT_MODEL_LOCAL_VIEWER), newMode ? 1 : 0)
_lightModelLocalViewer = newMode
}
}
}
var _lightModeTwoSide = false
var lightModeTwoSide: Bool
{
get { _lightModeTwoSide }
set(newMode)
{
if newMode != _lightModeTwoSide
{
glLightModeli(GLenum(GL_LIGHT_MODEL_TWO_SIDE), newMode ? 1 : 0)
_lightModeTwoSide = newMode
}
}
}
enum MatrixMode { case modelView, projection, texture, colour }
private var _matrixMode: MatrixMode = .modelView
var matrixMode: MatrixMode
{
get { _matrixMode }
set(newMode)
{
if newMode != _matrixMode
{
let modeEnum = switch newMode
{
case .modelView: GL_MODELVIEW
case .projection: GL_PROJECTION
case .texture: GL_TEXTURE
case .colour: GL_COLOR
}
glMatrixMode(GLenum(modeEnum))
_matrixMode = newMode
}
}
}
static let defaultBuffer: UInt32 = 0
private var _arrayBuffer = GLuint(defaultBuffer)
var arrayBuffer: UInt32
{
get { UInt32(_arrayBuffer) }
set(newBuf)
{
if newBuf != _arrayBuffer
{
_arrayBuffer = GLuint(newBuf)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), _arrayBuffer)
}
}
}
private var _elementArrayBuffer = GLuint(defaultBuffer)
var elementArrayBuffer: UInt32
{
get { UInt32(_elementArrayBuffer) }
set(newBuf)
{
if newBuf != _elementArrayBuffer
{
_elementArrayBuffer = GLuint(newBuf)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), _elementArrayBuffer)
}
}
}
}

View File

@ -0,0 +1,134 @@
import Foundation
import OpenGL.GL
public protocol Renderer
{
func create(sdlWindow: OpaquePointer) throws
func delete()
func newFrame()
func resize(width: Int32, height: Int32)
var clearColour: Colour { get set }
func setVsync(mode: VSyncMode) throws
func createMesh(mesh: Mesh) throws -> RenderMesh
func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
func createTexture(data: UnsafeRawPointer, width: Int, height: Int,
filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
func deleteMesh(_ mesh: RenderMesh)
func deleteTexture(_ texture: RenderTexture2D)
func setProjection(matrix: Mat4f)
func setView(matrix: Mat4f)
func setMaterial(_ mat: Material)
func draw(mesh: RenderMesh, model: Mat4f, environment: Environment)
func draw(mesh: RenderMesh, environment: Environment)
func drawGizmos(lines: [Line])
}
public enum RendererError: Error
{
case sdlError(message: String)
}
public enum VSyncMode
{
case off
case on
case adaptive
}
public enum FilterMode
{
case point
case linear
}
public enum MipMode
{
case off
case nearest
case linear
}
public enum WrapMode
{
case clamping
case clampBorder
case clampEdge
case mirrored
case repeating
}
public protocol RendererResource
{
associatedtype T: Resource
static var empty: Self { get }
var isValid: Bool { get }
}
public struct RenderMesh: RendererResource
{
public typealias T = Mesh
public static var empty: RenderMesh { .init() }
public var isValid: Bool { _valid }
private let _valid: Bool;
let vboHnd: Int, iboHnd: Int
let subMeshes: [Mesh.SubMesh]
private init()
{
self._valid = false
self.vboHnd = 0
self.iboHnd = 0
self.subMeshes = []
}
init(vbo: Int, ibo: Int, subMeshes: [Mesh.SubMesh])
{
self._valid = true
self.vboHnd = vbo
self.iboHnd = ibo
self.subMeshes = subMeshes
}
}
public struct RenderTexture2D: RendererResource
{
public typealias T = Texture2D
public static var empty: Self { .init(0) }
public var isValid: Bool { id != 0 }
let id: UInt32
init(_ id: UInt32)
{
self.id = id
}
}
public struct Line
{
let from: Vec3f, to: Vec3f, colour: Colour
public init(from: Vec3f, to: Vec3f, colour: Colour)
{
self.from = from
self.to = to
self.colour = colour
}
}

View File

@ -0,0 +1,31 @@
import Foundation
public struct Texture2D: Resource
{
public let id: RenderTexture2D
public let width: Int, height: Int
}
extension Texture2D
{
public static let empty = Self(id: .empty, width: 0, height: 0)
}
public struct Texture2DParameters: ContentLoaderParametersProtocol
{
public typealias T = Texture2D
var minFilter: FilterMode, magFilter: FilterMode
var wrapMode: WrapMode
var mipMode: MipMode
public init(minFilter: FilterMode = .linear, magFilter: FilterMode = .linear, wrapMode: WrapMode = .repeating, mipMode: MipMode = .off)
{
self.minFilter = minFilter
self.magFilter = magFilter
self.wrapMode = wrapMode
self.mipMode = mipMode
}
}

View File

@ -0,0 +1,20 @@
import Foundation
extension FileHandle
{
// FixedWidthInteger or BinaryFloatingPoint
func read<T: FixedWidthInteger>(as: T.Type) throws -> T
{
let size = MemoryLayout<T>.size
guard let data = try self.read(upToCount: size)
else { throw NSError(domain: "FileHandle.read", code: NSFileReadUnknownError, userInfo: [
NSLocalizedFailureReasonErrorKey: "Read error",
NSLocalizedDescriptionKey: "Read error"]) }
guard data.count == size
else { throw NSError(domain: "FileHandle.read", code: NSFileReadUnknownError, userInfo: [
NSLocalizedFailureReasonErrorKey: "Value underread",
NSLocalizedDescriptionKey: "Value underread"]) }
return data.withUnsafeBytes { p -> T in p.load(as: T.self) }
}
}

View File

@ -0,0 +1,52 @@
import Foundation
class TextFile
{
private var _hnd: UnsafeMutablePointer<FILE>
private let _maxLength: Int
init(fileURL: URL, maxLineLength: Int = 1024) throws
{
guard let f = fopen(fileURL.path, "r")
else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) }
_hnd = f
_maxLength = maxLineLength
}
deinit
{
fclose(_hnd)
}
struct LinesSequence: Sequence, IteratorProtocol
{
typealias Element = String
private var _hnd: UnsafeMutablePointer<FILE>
private var _buffer: [CChar]
fileprivate init(_ handle: UnsafeMutablePointer<FILE>, _ maxLength: Int)
{
_hnd = handle
_buffer = [CChar](repeating: 0, count: maxLength)
}
mutating func next() -> String?
{
guard fgets(&_buffer, Int32(_buffer.count), _hnd) != nil
else
{
if feof(_hnd) != 0 { return nil }
else { return nil }
}
let length = strcspn(_buffer, "\r\n")
_buffer[length] = "\0".utf8CString[0]
//if let pos = strchr(_buffer, Int32("\n".utf8CString[0])) { pos[0] = "\0".utf8CString[0] }
return String(cString: _buffer)
}
}
public var lines: LinesSequence { LinesSequence(_hnd, _maxLength) }
}

View File

@ -0,0 +1,248 @@
import Foundation
struct UBJsonReader
{
private var file: FileHandle
init(file: FileHandle)
{
self.file = file
}
func read() throws -> UBJsonToken
{
guard try readCharacter() == "{"
else { throw UBReaderError.badFormat("Stream does not start with an object") }
return try readObject()
}
private func parse(type: Character) throws -> UBJsonToken
{
let oldFormat = true
return switch type
{
case "{": try readObject()
case "[": try readArray()
case "Z": .null
case "N": .noop
case "T": .bool(true)
case "F": .bool(false)
case "i": oldFormat
? .int16(try file.read(as: Int16.self).bigEndian)
: .int8(try file.read(as: Int8.self))
case "U": .uint8(try file.read(as: UInt8.self))
case "I": oldFormat
? .int32(try file.read(as: Int32.self).bigEndian)
: .int16(try file.read(as: Int16.self).bigEndian)
case "l": .int32(try file.read(as: Int32.self).bigEndian)
case "L": .int64(try file.read(as: Int64.self).bigEndian)
case "d": .float32(try readBeFloatPiss())
case "D": .float64(try readBeFloat())
case "H": throw UBReaderError.badFormat("High-precision numbers are unsupported")
case "C": .char(try readCharacter())
case "S": .string(try readString())
default: throw UBReaderError.badToken("Unexpected token \"\(type)\"")
}
}
private func readObject() throws -> UBJsonToken
{
var items = Dictionary<String, UBJsonToken>()
while true
{
let type = try readCharacter()
var name: String
switch type
{
case "S", "i":
name = try readString(type)
case "}":
return .object(items)
default:
throw UBReaderError.badToken("Unexpected token while reading object field key")
}
if items.keys.contains(name) { throw UBReaderError.badFormat("Object contains overlapping keys") }
items[name] = try parse(type: try readCharacter())
}
}
private func readArray() throws -> UBJsonToken
{
var array = [UBJsonToken]()
while true
{
let type = try readCharacter()
switch type
{
case "]":
return .array(array)
default:
array.append(try parse(type: type))
}
}
}
private func readBeFloatPiss() throws -> Float
{
guard var bytes = try? file.read(upToCount: 4)
else { throw UBReaderError.readError("Read failure while reading float data") }
return Float(bitPattern: bytes.withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian)
}
private func readBeFloat<T: BinaryFloatingPoint, U: UnsignedInteger>() throws -> T where T.RawSignificand == U
{
guard var bytes = try? file.read(upToCount: MemoryLayout<U>.size)
else { throw UBReaderError.readError("Read failure while reading float data") }
bytes.reverse()
return T(bytes.withUnsafeBytes { $0.load(as: U.self) })
}
private func readCharacter() throws -> Character
{
guard let raw = try? file.read(as: UInt8.self),
let uni = UnicodeScalar(Int(raw))
else { throw UBReaderError.readError("Read failure while reading character") }
return Character(uni)
}
private func readString(_ optType: Character? = nil) throws -> String
{
let type = optType == nil ? try readCharacter() : optType!
var length: Int
switch type
{
case "S":
guard try readCharacter() == "i"
else { throw UBReaderError.badToken("Malformed string") }
fallthrough
case "i":
length = Int(try file.read(as: Int8.self))
case "s":
length = Int(try file.read(as: UInt8.self))
default: throw UBReaderError.badToken("Unexpected token while reading string")
}
if length < 0 { throw UBReaderError.badToken("Negative string length") }
if length == 0 { return "" }
guard let data = try file.read(upToCount: length)
else { throw UBReaderError.readError("Error reading string") }
return String(decoding: data, as: UTF8.self)
}
}
enum UBReaderError: Error
{
case badFormat(_ message: String)
case badToken(_ message: String)
case readError(_ message: String)
case valueError(_ message: String)
}
enum UBJsonToken
{
case object(_ fields: Dictionary<String, UBJsonToken>)
case array(_ items: [UBJsonToken])
case null
case noop
case bool(_ value: Bool)
case int8(_ value: Int8)
case uint8(_ value: UInt8)
case int16(_ value: Int16)
case int32(_ value: Int32)
case int64(_ value: Int64)
case highPrecision
case float32(_ value: Float32)
case float64(_ value: Float64)
case char(_ value: Character)
case string(_ value: String)
}
extension UBJsonToken
{
var array: [UBJsonToken]
{
get throws
{
if case .array(let items) = self { return items }
throw UBReaderError.valueError("Not an array")
}
}
func getArray(key: String) throws -> [UBJsonToken]
{
if case .object(let fields) = self
{
guard let child = fields[key]
else { throw UBReaderError.valueError("Field \"\(key)\" not found") }
return try child.array
}
throw UBReaderError.valueError("Not an object")
}
var int16: Int16
{
get throws
{
if case .int16(let value) = self { return value }
throw UBReaderError.valueError("Not an int16")
}
}
func getInt16Array(key: String) throws -> [Int16]
{
try getArray(key: key).map(
{ i in
if case .int8(let value) = i { return Int16(value) }
else if case .uint8(let value) = i { return Int16(value) }
else if case .int16(let value) = i { return value }
else if case .int32(let value) = i
{
if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") }
return Int16(truncatingIfNeeded: value)
}
else if case .int64(let value) = i
{
if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") }
return Int16(truncatingIfNeeded: value)
}
else { throw UBReaderError.valueError("Can't read array as int16s") }
})
}
var float: Float
{
get throws
{
if case .float32(let value) = self { return value }
throw UBReaderError.valueError("Not a float32")
}
}
func getFloatArray(key: String) throws -> [Float]
{
try getArray(key: key).map({ try $0.float })
}
var string: String
{
get throws
{
if case .string(let value) = self { return value }
throw UBReaderError.valueError("Not a string")
}
}
func getString(key: String, default defStr: String? = nil) throws -> String
{
if case .object(let fields) = self
{
guard let child = fields[key] else
{
if defStr == nil { throw UBReaderError.valueError("Field \"\(key)\" not found") }
return defStr!
}
return try child.string
}
throw UBReaderError.valueError("Not an object")
}
}