init dump
This commit is contained in:
205
Sources/JolkEngine/Application.swift
Normal file
205
Sources/JolkEngine/Application.swift
Normal 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)
|
||||
}
|
||||
}
|
95
Sources/JolkEngine/Colour.swift
Normal file
95
Sources/JolkEngine/Colour.swift
Normal 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)
|
||||
}
|
||||
|
165
Sources/JolkEngine/Content/ContentManager.swift
Normal file
165
Sources/JolkEngine/Content/ContentManager.swift
Normal 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)\"") }
|
||||
}
|
9
Sources/JolkEngine/Content/Image.swift
Normal file
9
Sources/JolkEngine/Content/Image.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct Image: Resource
|
||||
{
|
||||
let pixels: Data
|
||||
let width: Int
|
||||
let height: Int
|
||||
}
|
9
Sources/JolkEngine/Content/LoaderProtocol.swift
Normal file
9
Sources/JolkEngine/Content/LoaderProtocol.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public protocol LoaderProtocol
|
||||
{
|
||||
associatedtype T: Resource
|
||||
|
||||
func load(url: URL) -> T?
|
||||
}
|
54
Sources/JolkEngine/Content/Mesh.swift
Normal file
54
Sources/JolkEngine/Content/Mesh.swift
Normal 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())
|
||||
}
|
||||
}
|
120
Sources/JolkEngine/Content/ObjModel.swift
Normal file
120
Sources/JolkEngine/Content/ObjModel.swift
Normal 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 }
|
||||
}
|
||||
}
|
4
Sources/JolkEngine/Content/Resource.swift
Normal file
4
Sources/JolkEngine/Content/Resource.swift
Normal file
@ -0,0 +1,4 @@
|
||||
public protocol Resource
|
||||
{
|
||||
|
||||
}
|
155
Sources/JolkEngine/Input.swift
Normal file
155
Sources/JolkEngine/Input.swift
Normal 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))
|
||||
}
|
||||
}
|
249
Sources/JolkEngine/Loaders/G3DbLoader.swift
Normal file
249
Sources/JolkEngine/Loaders/G3DbLoader.swift
Normal file
@ -0,0 +1,249 @@
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
|
||||
class G3DbLoader: LoaderProtocol
|
||||
{
|
||||
typealias T = Mesh
|
||||
|
||||
func load(url: URL) -> T?
|
||||
{
|
||||
guard var reader = try? G3DbReader(url)
|
||||
else { return Optional.none }
|
||||
return try? reader.read()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct G3DbReader
|
||||
{
|
||||
private let version: [Int16] = [0, 1]
|
||||
private var reader: UBJsonReader
|
||||
|
||||
init(_ url: URL) throws
|
||||
{
|
||||
guard let file = try? FileHandle(forReadingFrom: url)
|
||||
else { throw G3DbReaderError.fileNotFound }
|
||||
self.reader = UBJsonReader(file: file)
|
||||
}
|
||||
|
||||
mutating func read() throws -> Mesh
|
||||
{
|
||||
let model = try readModel()
|
||||
|
||||
let mesh = model.meshes[0]
|
||||
|
||||
var vertices = [Mesh.Vertex]()
|
||||
var indices = [UInt16]()
|
||||
var subMeshes = OrderedDictionary<String, Mesh.SubMesh>()
|
||||
|
||||
let attributeSize =
|
||||
{ (attrib: G3DAttribute) in
|
||||
switch attrib
|
||||
{
|
||||
case .position, .normal, .colour, .tangent, .bitangent:
|
||||
return 3
|
||||
case .texCoord(_), .blendWeight(_):
|
||||
return 2
|
||||
case .colourPacked:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
var stride = 0
|
||||
for a in mesh.attributes
|
||||
{
|
||||
stride += attributeSize(a)
|
||||
}
|
||||
let numVerts = mesh.vertices.count / stride
|
||||
vertices.reserveCapacity(numVerts)
|
||||
for i in 0..<numVerts
|
||||
{
|
||||
var srcIdx = i * stride
|
||||
var position: Vec3f = .zero, normal: Vec3f = .zero
|
||||
var texCoord: Vec2f = .zero
|
||||
for a in mesh.attributes
|
||||
{
|
||||
switch a
|
||||
{
|
||||
case .position:
|
||||
position = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
mesh.vertices[srcIdx + 2],
|
||||
-mesh.vertices[srcIdx + 1])
|
||||
case .normal:
|
||||
normal = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
mesh.vertices[srcIdx + 2],
|
||||
-mesh.vertices[srcIdx + 1])
|
||||
case .texCoord(id: 0):
|
||||
texCoord = .init(
|
||||
mesh.vertices[srcIdx],
|
||||
1.0 - mesh.vertices[srcIdx + 1])
|
||||
default: break
|
||||
}
|
||||
srcIdx += attributeSize(a)
|
||||
}
|
||||
vertices.append(.init(position: position, normal: normal, texCoord: texCoord))
|
||||
}
|
||||
for part in mesh.parts
|
||||
{
|
||||
subMeshes[part.key] = .init(start: indices.count, length: part.value.indices.count)
|
||||
indices += part.value.indices.map { UInt16($0) }
|
||||
}
|
||||
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
|
||||
}
|
||||
|
||||
mutating func readModel() throws -> G3DModel
|
||||
{
|
||||
let root = try reader.read()
|
||||
let version = try root.getArray(key: "version")
|
||||
guard try version.count == 2 && (try version.map({ try $0.int16 }) == self.version)
|
||||
else { throw G3DbReaderError.versionMismatch }
|
||||
|
||||
var model = G3DModel()
|
||||
model.id = try root.getString(key: "id", default: "")
|
||||
try readMeshes(root, &model)
|
||||
try readMaterials(root, &model)
|
||||
return model
|
||||
}
|
||||
|
||||
mutating func readMeshes(_ root: UBJsonToken, _ model: inout G3DModel) throws
|
||||
{
|
||||
let meshes = try root.getArray(key: "meshes")
|
||||
for obj in meshes
|
||||
{
|
||||
var mesh = G3DModelMesh()
|
||||
|
||||
mesh.id = try obj.getString(key: "id", default: "")
|
||||
for attrib in try obj.getArray(key: "attributes")
|
||||
{
|
||||
mesh.attributes.append(try .resolve(try attrib.string))
|
||||
}
|
||||
mesh.vertices = try obj.getFloatArray(key: "vertices")
|
||||
for partObj in try obj.getArray(key: "parts")
|
||||
{
|
||||
let id = try partObj.getString(key: "id")
|
||||
if mesh.parts.keys.contains(id) { throw G3DbReaderError.duplicateIDs }
|
||||
|
||||
var part = G3dModelMeshPart()
|
||||
part.mode = try .resolve(try partObj.getString(key: "type"))
|
||||
part.indices = try partObj.getInt16Array(key: "indices")
|
||||
|
||||
mesh.parts[id] = part
|
||||
}
|
||||
model.meshes.append(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func readMaterials(_ root: UBJsonToken, _ model: inout G3DModel) throws
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct G3DModel
|
||||
{
|
||||
var id: String = .init()
|
||||
var meshes: [G3DModelMesh] = .init()
|
||||
}
|
||||
|
||||
fileprivate struct G3DModelMesh
|
||||
{
|
||||
var id: String = .init()
|
||||
var attributes: [G3DAttribute] = .init()
|
||||
var vertices: [Float] = .init()
|
||||
var parts: Dictionary<String, G3dModelMeshPart> = .init()
|
||||
}
|
||||
|
||||
fileprivate struct G3dModelMeshPart
|
||||
{
|
||||
var mode: G3DPrimativeType = .invalid
|
||||
var indices: [Int16] = .init()
|
||||
}
|
||||
|
||||
fileprivate enum G3DAttribute: Equatable
|
||||
{
|
||||
case position
|
||||
case normal
|
||||
case tangent
|
||||
case bitangent
|
||||
case colour
|
||||
case colourPacked
|
||||
case texCoord(id: UInt8)
|
||||
case blendWeight(id: UInt8)
|
||||
|
||||
static let order = [ .position, .colour, .colourPacked, .normal,
|
||||
texCoord(id: 0), .blendWeight(id: 0), .tangent, .bitangent ]
|
||||
}
|
||||
|
||||
extension G3DAttribute
|
||||
{
|
||||
static func resolve(_ attrib: String) throws -> Self
|
||||
{
|
||||
let getAttributeId =
|
||||
{ (attrib: String, offset: Int) throws -> UInt8 in
|
||||
let idIdx = attrib.index(attrib.startIndex, offsetBy: offset)
|
||||
let idStr = attrib.suffix(from: idIdx)
|
||||
guard let id = UInt8(idStr)
|
||||
else { throw G3DbReaderError.badAttribute }
|
||||
return id
|
||||
}
|
||||
|
||||
return switch attrib
|
||||
{
|
||||
case "POSITION": .position
|
||||
case "NORMAL": .normal
|
||||
case "COLOR": .colour
|
||||
case "COLORPACKED": .colourPacked
|
||||
case "TANGENT": .tangent
|
||||
case "BINORMAL": .bitangent
|
||||
default:
|
||||
if attrib.starts(with: "TEXCOORD")
|
||||
{
|
||||
.texCoord(id: try getAttributeId(attrib, 8))
|
||||
}
|
||||
else if attrib.starts(with: "BLENDWEIGHT")
|
||||
{
|
||||
.blendWeight(id: try getAttributeId(attrib, 11))
|
||||
}
|
||||
else { throw G3DbReaderError.badAttribute }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum G3DPrimativeType
|
||||
{
|
||||
case invalid
|
||||
case triangles
|
||||
case lines
|
||||
case points
|
||||
case triangleStrip
|
||||
case lineStrip
|
||||
}
|
||||
|
||||
extension G3DPrimativeType
|
||||
{
|
||||
static func resolve(_ key: String) throws -> Self
|
||||
{
|
||||
switch key
|
||||
{
|
||||
case "TRIANGLES": .triangles
|
||||
case "LINES": .lines
|
||||
case "POINTS": .points
|
||||
case "TRIANGLE_STRIP": .triangleStrip
|
||||
case "LINE_STRIP": .lineStrip
|
||||
default: throw G3DbReaderError.invalidFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum G3DbReaderError: Error
|
||||
{
|
||||
case fileNotFound
|
||||
case versionMismatch
|
||||
case unsupportedFormat
|
||||
case invalidFormat
|
||||
case badAttribute
|
||||
case duplicateIDs
|
||||
}
|
50
Sources/JolkEngine/Loaders/NSImageLoader.swift
Normal file
50
Sources/JolkEngine/Loaders/NSImageLoader.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import AppKit
|
||||
|
||||
|
||||
enum ImageLoaderError: Error
|
||||
{
|
||||
case loadFailed(_ message: String)
|
||||
}
|
||||
|
||||
struct NSImageLoader: LoaderProtocol
|
||||
{
|
||||
typealias T = Image
|
||||
|
||||
func load(url: URL) -> T?
|
||||
{
|
||||
guard let image = try? NSImageLoader.loadImage(url: url) else { return nil }
|
||||
return image
|
||||
}
|
||||
|
||||
static func loadImage(url: URL) throws -> Image
|
||||
{
|
||||
try autoreleasepool
|
||||
{
|
||||
// Open as a Core Graphics image
|
||||
guard let nsImg = NSImage(contentsOf: url),
|
||||
let image = nsImg.cgImage(forProposedRect: nil, context: nil, hints: nil)
|
||||
else { throw ImageLoaderError.loadFailed("Failed to open image \"\(url.absoluteString)\"") }
|
||||
|
||||
// Convert to 8-bit ARGB (SRGB) w/ premultiplied alpha
|
||||
let alphaInfo = image.alphaInfo == .none ? CGImageAlphaInfo.noneSkipLast : CGImageAlphaInfo.premultipliedLast
|
||||
guard let colourspace = CGColorSpace(name: CGColorSpace.sRGB),
|
||||
let context = CGContext(data: nil,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: image.width * 4,
|
||||
space: colourspace,
|
||||
bitmapInfo: alphaInfo.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
|
||||
else { throw ImageLoaderError.loadFailed("Coudn't create graphics context") }
|
||||
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(image.height))
|
||||
context.concatenate(flipVertical)
|
||||
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
|
||||
|
||||
guard let data = context.data else { throw ImageLoaderError.loadFailed("what") }
|
||||
return Image(
|
||||
pixels: Data(bytes: data, count: 4 * image.width * image.height),
|
||||
width: image.width,
|
||||
height: image.height)
|
||||
}
|
||||
}
|
||||
}
|
92
Sources/JolkEngine/Loaders/ObjLoader.swift
Normal file
92
Sources/JolkEngine/Loaders/ObjLoader.swift
Normal file
@ -0,0 +1,92 @@
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
|
||||
/*
|
||||
extension ObjMaterial
|
||||
{
|
||||
func convert() -> Material
|
||||
{
|
||||
var m = Material()
|
||||
m.diffuse = self.diffuse.setAlpha(self.alpha)
|
||||
if ![ .colour, .lambert, .shadowOnly ].contains(self.model)
|
||||
{
|
||||
m.specular = self.specular
|
||||
m.specularExp = self.specularExp
|
||||
}
|
||||
return m
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public struct ObjLoader: LoaderProtocol
|
||||
{
|
||||
public typealias T = Mesh
|
||||
|
||||
public init() {}
|
||||
|
||||
public func load(url: URL) -> T?
|
||||
{
|
||||
try? Self.read(url: url)
|
||||
}
|
||||
|
||||
private static func read(url: URL) throws -> Mesh
|
||||
{
|
||||
return try read(model: try ObjReader.read(url: url))
|
||||
}
|
||||
|
||||
public static func read(model: ObjModel) throws -> Mesh
|
||||
{
|
||||
var subMeshes: OrderedDictionary<String, Mesh.SubMesh> = .init()
|
||||
var vertices = [Mesh.Vertex]()
|
||||
var indices = [UInt16]()
|
||||
|
||||
let readIndex =
|
||||
{ (v: ObjModel.Index) -> UInt16 in
|
||||
let vertex = Mesh.Vertex(
|
||||
position: model.positions[v.p],
|
||||
normal: model.normals[v.n],
|
||||
texCoord: model.texCoords[v.t])
|
||||
if let index = vertices.firstIndex(of: vertex)
|
||||
{
|
||||
indices.append(UInt16(index))
|
||||
return UInt16(index)
|
||||
}
|
||||
else
|
||||
{
|
||||
let index = UInt16(vertices.count)
|
||||
indices.append(index)
|
||||
vertices.append(vertex)
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
for mesh in model.objects
|
||||
{
|
||||
let start = indices.count
|
||||
for face: ObjModel.Face in mesh.value.faces
|
||||
{
|
||||
switch face
|
||||
{
|
||||
case .triangle(let v1, let v2, let v3):
|
||||
for v in [ v1, v2, v3 ] { _ = readIndex(v) }
|
||||
case .quad(let v1, let v2, let v3, let v4):
|
||||
let n1 = readIndex(v1)
|
||||
_ = readIndex(v2)
|
||||
indices.append(readIndex(v3))
|
||||
_ = readIndex(v4)
|
||||
indices.append(n1)
|
||||
case .ngon(_): fallthrough
|
||||
default: break
|
||||
}
|
||||
}
|
||||
let length = indices.count - start
|
||||
if length > 0
|
||||
{
|
||||
subMeshes[mesh.key] = .init(start: start, length: length)
|
||||
}
|
||||
}
|
||||
|
||||
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
|
||||
}
|
||||
}
|
372
Sources/JolkEngine/Loaders/ObjReader.swift
Normal file
372
Sources/JolkEngine/Loaders/ObjReader.swift
Normal file
@ -0,0 +1,372 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct ObjReader
|
||||
{
|
||||
public static func read(url: URL) throws -> ObjModel
|
||||
{
|
||||
var file = try ObjDocumentReader(filePath: url)
|
||||
|
||||
var model = ObjModel()
|
||||
var materials = Dictionary<String, ObjMaterial>()
|
||||
var name: String? = nil
|
||||
var object = ObjModel.Object()
|
||||
|
||||
file.string(tag: "mtllib") { s in
|
||||
let mats = try ObjMtlLoader.read(url: url.deletingLastPathComponent().appendingPathComponent(s[0]))
|
||||
materials.merge(mats, uniquingKeysWith: { (_, new) in new } )
|
||||
}
|
||||
file.string(tag: "o", count: 1) { s in
|
||||
if !object.faces.isEmpty
|
||||
{
|
||||
model.objects[name!] = object
|
||||
object = .init()
|
||||
}
|
||||
name = String(s[0])
|
||||
}
|
||||
file.float(tag: "v", count: 3) { v in model.positions.append(Vec3f(v[0], v[1], v[2])) }
|
||||
file.float(tag: "vn", count: 3) { v in model.normals.append(Vec3f(v[0], v[1], v[2])) }
|
||||
file.float(tag: "vt", count: 2) { v in model.texCoords.append(Vec2f(v[0], v[1])) }
|
||||
file.raw(tag: "f") { raw in
|
||||
let readIndex =
|
||||
{ (raw: Substring) in
|
||||
let face = raw.split(separator: "/")
|
||||
guard face.count == 3,
|
||||
let posIdx = Int(face[0]), let coordIdx = Int(face[1]), let normIdx = Int(face[2])
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
return ObjModel.Index(p: posIdx - 1, n: normIdx - 1, t: coordIdx - 1)
|
||||
}
|
||||
|
||||
if raw.count == 3
|
||||
{
|
||||
for raw in raw { _ = try readIndex(raw) }
|
||||
object.faces.append(.triangle(
|
||||
v1: try readIndex(raw[raw.startIndex]),
|
||||
v2: try readIndex(raw[raw.startIndex + 1]),
|
||||
v3: try readIndex(raw[raw.startIndex + 2])))
|
||||
}
|
||||
else if raw.count == 4
|
||||
{
|
||||
object.faces.append(.quad(
|
||||
v1: try readIndex(raw[raw.startIndex]),
|
||||
v2: try readIndex(raw[raw.startIndex + 1]),
|
||||
v3: try readIndex(raw[raw.startIndex + 2]),
|
||||
v4: try readIndex(raw[raw.startIndex + 3])))
|
||||
}
|
||||
else if raw.count >= 5
|
||||
{
|
||||
object.faces.append(.ngon(try raw.map { try readIndex($0) }))
|
||||
}
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}
|
||||
file.int(tag: "l") { i in object.faces.append(.line(p1: i[0], p2: i[1])) }
|
||||
try file.read()
|
||||
|
||||
if !object.faces.isEmpty
|
||||
{
|
||||
model.objects[name!] = object
|
||||
}
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ObjMtlLoader
|
||||
{
|
||||
static func read(url: URL) throws -> Dictionary<String, ObjMaterial>
|
||||
{
|
||||
var file = try ObjDocumentReader(filePath: url)
|
||||
|
||||
var materials = Dictionary<String, ObjMaterial>()
|
||||
var name: String = ""
|
||||
var mat: ObjMaterial? = nil
|
||||
|
||||
file.string(tag: "newmtl", count: 1)
|
||||
{ s in
|
||||
if mat != nil
|
||||
{
|
||||
materials[name] = mat!
|
||||
}
|
||||
mat = .init()
|
||||
name = String(s[0])
|
||||
}
|
||||
|
||||
file.preHandle { s in if s != "newmtl" && mat == nil { throw ObjLoaderError.unexpectedTag } }
|
||||
file.float(tag: "Ka", count: 3) { f in mat!.ambient = Colour(r: f[0], g: f[1], b: f[2]) } // "Ambient colour"
|
||||
file.float(tag: "Kd", count: 3) { f in mat!.diffuse = Colour(r: f[0], g: f[1], b: f[2]) } // "Diffuse colour"
|
||||
file.float(tag: "Ks", count: 3) { f in mat!.specular = Colour(r: f[0], g: f[1], b: f[2]) } // "Specular colour"
|
||||
file.float(tag: "Ns", count: 1) { f in mat!.specularExp = f[0] } // "Specular exponent/shininess"
|
||||
file.float(tag: "Ni", count: 1) { f in mat!.refraction = f[0] } // "Optical density/refraction index"
|
||||
file.float(tag: "d", count: 1) { f in mat!.alpha = f[0] } // "Dissolve" 0.0 = transparent, 1.0 = opaque
|
||||
file.float(tag: "Tr", count: 1) { f in mat!.alpha = 1.0 - f[0] } // "Transparent" 0.0 = opaque, 1.0 = transparent
|
||||
file.int(tag: "illum", count: 1) { i in mat!.model = try .resolve(i[0]) } // "Illumination model"
|
||||
file.textureMap(tag: "map_Ka") { s in } // "Ambient colourmap"
|
||||
file.textureMap(tag: "map_Kd") { t in mat!.diffuseMap = t } // "Albedo map"
|
||||
file.textureMap(tag: "map_Ks") { t in mat!.specularMap = t } // "Specular colourmap"
|
||||
file.textureMap(tag: "map_Ns") { t in mat!.specularExpMap = t } // "Specular exponent map"
|
||||
file.textureMap(tag: "map_d") { t in mat!.dissolveMap = t } // "Dissolve map"
|
||||
//file.string(tag: "map_Tr") { t in } // "Translucency map"
|
||||
for t in ["map_Bump", "bump"] { file.textureMap(tag: t) { t in mat!.bumpMap = t } } // "Bump map"
|
||||
file.textureMap(tag: "disp") { t in mat!.displacementMap = t } // "Displacement map"
|
||||
file.textureMap(tag: "decal") { t in mat!.decalMap = t } // ?
|
||||
// PBR extensions
|
||||
file.float(tag: "Pr", count: 1) { f in mat!.roughness = f[0] } // "Roughness"
|
||||
file.float(tag: "Pm", count: 1) { f in mat!.metallic = f[0] } // "Metallic"
|
||||
file.float(tag: "Ps", count: 1) { f in mat!.sheen = f[0] } // "Sheen"
|
||||
file.float(tag: "Pc", count: 1) { f in } // "Clearcoat thickness"
|
||||
file.float(tag: "Pcr", count: 1) { f in } // "Clearcoat roughness"
|
||||
file.float(tag: "Ke", count: 3) { f in } // "Emmission colour"
|
||||
file.float(tag: "aniso", count: 1) { f in } // "Anisotropy"
|
||||
file.float(tag: "anisor", count: 1) { f in } // "Anisotropy rotation"
|
||||
file.textureMap(tag: "map_Pr") { t in mat!.roughnessMap = t } // "Roughness texturemap"
|
||||
file.textureMap(tag: "map_Pm") { t in mat!.metallicMap = t } // "Metallic texturemap"
|
||||
file.textureMap(tag: "map_Ps") { t in mat!.sheenMap = t } // "Sheen texturemap"
|
||||
file.textureMap(tag: "map_Ke") { t in mat!.emmisionMap = t } // "Emmision texturemap"
|
||||
file.textureMap(tag: "norm") { t in mat!.normalMap = t } // "Normal map"
|
||||
|
||||
try file.read()
|
||||
|
||||
if let material = mat
|
||||
{
|
||||
materials[name] = material
|
||||
}
|
||||
return materials
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension ObjMaterial.IlluminationModel
|
||||
{
|
||||
static func resolve(_ id: Int) throws -> Self
|
||||
{
|
||||
switch id
|
||||
{
|
||||
case 0: .colour
|
||||
case 1: .lambert
|
||||
case 2: .blinnPhong
|
||||
case 3: .reflectionRaytrace
|
||||
case 4: .transparentGlassReflectionRaytrace
|
||||
case 5: .reflectionRaytraceFresnel
|
||||
case 6: .transparentRefractionReflectionRaytrace
|
||||
case 7: .transparentRefractionReflectionRaytraceFresnel
|
||||
case 8: .reflection
|
||||
case 9: .transparentGlassReflection
|
||||
case 10: .shadowOnly
|
||||
default: throw ObjLoaderError.badTagParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension ObjMaterial.TextureMap
|
||||
{
|
||||
static func parse(_ argstr: Substring) throws -> Self
|
||||
{
|
||||
var map = Self()
|
||||
var args = [Substring]()
|
||||
|
||||
let parseFlag =
|
||||
{ (flag: Flags) in
|
||||
let arg = args[0].lowercased()
|
||||
if arg == "on" { map.flags.insert(flag) }
|
||||
else if arg == "off" { map.flags.remove(flag) }
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}
|
||||
let parseInt =
|
||||
{ (arg: Int) in
|
||||
guard let result = Int(args[arg]) else { throw ObjLoaderError.badTagParameters }
|
||||
return result
|
||||
}
|
||||
let parseFloat =
|
||||
{ (arg: Int) in
|
||||
guard let result = Float(args[arg]) else { throw ObjLoaderError.badTagParameters }
|
||||
return result
|
||||
}
|
||||
let parseVector = { Vec3f(try parseFloat(0), try parseFloat(1), try parseFloat(2)) }
|
||||
|
||||
typealias Option = () throws -> Void
|
||||
let options: Dictionary<String, (Int, Option)> =
|
||||
[
|
||||
"blendu": (1, { try parseFlag(.blendHoriz) }),
|
||||
"blendv": (1, { try parseFlag(.blendVert) }),
|
||||
"bm": (1, { map.blendMul = try parseFloat(0) }),
|
||||
"cc": (1, { try parseFlag(.colourCorrection) }),
|
||||
"clamp": (1, { try parseFlag(.clamp) }),
|
||||
"imfchan": (1, { map.imfChan = try .resolve(args[0]) }),
|
||||
"mm": (2, { map.mmBaseGain = (try parseFloat(0), try parseFloat(1)) }),
|
||||
"o": (3, { map.offset = try parseVector() }),
|
||||
"s": (3, { map.scale = try parseVector() }),
|
||||
"t": (3, { map.turbulence = try parseVector() }),
|
||||
"texres": (1, { map.textureResolution = try parseInt(0) })
|
||||
]
|
||||
|
||||
var expectArgs = 0, option: Option? = nil
|
||||
var index = argstr.startIndex
|
||||
repeat
|
||||
{
|
||||
if expectArgs > 0, let callback = option
|
||||
{
|
||||
let start = index
|
||||
index = argstr[start...].firstIndex(where: \.isWhitespace) ?? argstr.endIndex
|
||||
args.append(argstr[start..<index])
|
||||
expectArgs -= 1
|
||||
if expectArgs == 0
|
||||
{
|
||||
try callback()
|
||||
option = nil
|
||||
}
|
||||
}
|
||||
else if argstr[index] == "-"
|
||||
{
|
||||
let start = argstr.index(after: index)
|
||||
index = argstr[start...].firstIndex(where: \.isWhitespace) ?? argstr.endIndex
|
||||
let name = String(argstr[start..<index])
|
||||
guard let params = options[name] else { throw ObjLoaderError.badTagParameters }
|
||||
(expectArgs, option) = params
|
||||
}
|
||||
else
|
||||
{
|
||||
break
|
||||
}
|
||||
index = argstr[index...].firstIndex { !$0.isWhitespace } ?? argstr.endIndex
|
||||
}
|
||||
while index < argstr.endIndex
|
||||
if index >= argstr.endIndex || expectArgs > 0 { throw ObjLoaderError.badTagParameters }
|
||||
|
||||
map.path = String(argstr[index...])
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension ObjMaterial.TextureMap.ImfChan
|
||||
{
|
||||
static func resolve(_ token: Substring) throws -> Self
|
||||
{
|
||||
switch token
|
||||
{
|
||||
case "r": .r
|
||||
case "g": .g
|
||||
case "b": .b
|
||||
case "m": .m
|
||||
case "l": .l
|
||||
case "z": .z
|
||||
default: throw ObjLoaderError.badTagParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ObjDocumentReader
|
||||
{
|
||||
private enum Handler
|
||||
{
|
||||
case string(closure: ([String]) throws -> Void, count: Int? = nil)
|
||||
case float(closure: ([Float]) throws -> Void, count: Int? = nil)
|
||||
case int(closure: ([Int]) throws -> Void, count: Int? = nil)
|
||||
case textureMap(closure: (ObjMaterial.TextureMap) throws -> Void)
|
||||
case raw(closure: ([Substring]) throws -> Void)
|
||||
}
|
||||
|
||||
private var file: TextFile
|
||||
typealias Callback = (Substring) throws -> Void
|
||||
private var _prehandler: Callback? = nil
|
||||
private var handlers = Dictionary<String, Handler>()
|
||||
|
||||
init(filePath: URL) throws
|
||||
{
|
||||
file = try TextFile(fileURL: filePath)
|
||||
}
|
||||
|
||||
mutating func string(tag: String, count: Int? = nil, closure: @escaping ([String]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .string(closure: closure, count: count)
|
||||
}
|
||||
|
||||
mutating func float(tag: String, count: Int? = nil, closure: @escaping ([Float]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .float(closure: closure, count: count)
|
||||
}
|
||||
|
||||
mutating func int(tag: String, count: Int? = nil, closure: @escaping ([Int]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .int(closure: closure, count: count)
|
||||
}
|
||||
|
||||
mutating func textureMap(tag: String, closure: @escaping (ObjMaterial.TextureMap) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .textureMap(closure: closure)
|
||||
}
|
||||
|
||||
mutating func raw(tag: String, closure: @escaping ([Substring]) throws -> Void)
|
||||
{
|
||||
handlers[tag] = .raw(closure: closure)
|
||||
}
|
||||
|
||||
mutating func preHandle(closure: @escaping Callback) { _prehandler = closure }
|
||||
|
||||
private func handle(_ handler: Handler, command: Substring, arg: Substring) throws
|
||||
{
|
||||
switch handler
|
||||
{
|
||||
case .string(let closure, let count):
|
||||
let args = arg.split(separator: " ")
|
||||
if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters }
|
||||
try closure(args.map({ String($0) }))
|
||||
case .float(let closure, let count):
|
||||
let args = arg.split(separator: " ")
|
||||
if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters }
|
||||
try closure(args.map(
|
||||
{
|
||||
if let value = Float($0) { return value }
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}))
|
||||
case .int(let closure, let count):
|
||||
let args = arg.split(separator: " ")
|
||||
if count != nil && args.count != count! { throw ObjLoaderError.badTagParameters }
|
||||
try closure(args.map(
|
||||
{
|
||||
if let value = Int($0) { return value }
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
}))
|
||||
case .textureMap(let closure):
|
||||
try closure(ObjMaterial.TextureMap.parse(arg))
|
||||
case .raw(let closure):
|
||||
try closure(arg.split(separator: " "))
|
||||
}
|
||||
}
|
||||
|
||||
private func parseLine(_ line: String) throws -> (Substring, Substring)?
|
||||
{
|
||||
// Trim comment if present
|
||||
var trimmed = if let index = line.firstIndex(of: "#") { line[..<index] } else { line[...] }
|
||||
|
||||
// Trim leading and trailing whitespace
|
||||
guard let start = trimmed.firstIndex(where: { !$0.isWhitespace }),
|
||||
let end = trimmed.lastIndex(where: { !$0.isWhitespace })
|
||||
else { return nil }
|
||||
trimmed = trimmed[start...end]
|
||||
|
||||
// Split command & rest of string as arguments
|
||||
guard let cmdEnd = trimmed.firstIndex(where: \.isWhitespace),
|
||||
let argStart = trimmed.suffix(from: trimmed.index(after: cmdEnd)).firstIndex(where: { !$0.isWhitespace })
|
||||
else { throw ObjLoaderError.badTagParameters }
|
||||
return (trimmed.prefix(upTo: cmdEnd), trimmed.suffix(from: argStart))
|
||||
}
|
||||
|
||||
func read() throws
|
||||
{
|
||||
for line in file.lines
|
||||
{
|
||||
if !line.isEmpty,
|
||||
let (cmd, argstr) = try parseLine(line),
|
||||
let handler = handlers[String(cmd)]
|
||||
{
|
||||
if let prehandler = _prehandler { try prehandler(cmd) }
|
||||
try handle(handler, command: cmd, arg: argstr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ObjLoaderError: Error
|
||||
{
|
||||
case badTagParameters
|
||||
case unexpectedTag
|
||||
}
|
291
Sources/JolkEngine/Maths.swift
Normal file
291
Sources/JolkEngine/Maths.swift
Normal 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
|
81
Sources/JolkEngine/Renderer/Environment.swift
Normal file
81
Sources/JolkEngine/Renderer/Environment.swift
Normal 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))
|
||||
}
|
||||
}
|
20
Sources/JolkEngine/Renderer/Material.swift
Normal file
20
Sources/JolkEngine/Renderer/Material.swift
Normal 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
|
||||
}
|
||||
}
|
440
Sources/JolkEngine/Renderer/OpenGL.swift
Normal file
440
Sources/JolkEngine/Renderer/OpenGL.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
796
Sources/JolkEngine/Renderer/OpenGLState.swift
Normal file
796
Sources/JolkEngine/Renderer/OpenGLState.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
134
Sources/JolkEngine/Renderer/Renderer.swift
Normal file
134
Sources/JolkEngine/Renderer/Renderer.swift
Normal 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
|
||||
}
|
||||
}
|
31
Sources/JolkEngine/Renderer/Texture2D.swift
Normal file
31
Sources/JolkEngine/Renderer/Texture2D.swift
Normal 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
|
||||
}
|
||||
}
|
20
Sources/JolkEngine/Util/FileHandle.swift
Normal file
20
Sources/JolkEngine/Util/FileHandle.swift
Normal 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) }
|
||||
}
|
||||
}
|
52
Sources/JolkEngine/Util/TextFile.swift
Normal file
52
Sources/JolkEngine/Util/TextFile.swift
Normal 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) }
|
||||
}
|
248
Sources/JolkEngine/Util/UBJsonReader.swift
Normal file
248
Sources/JolkEngine/Util/UBJsonReader.swift
Normal 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")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user