The Skung Cave commit

This commit is contained in:
2024-05-09 20:52:01 +10:00
parent 446c444728
commit 9461fe08cf
35 changed files with 11988 additions and 9097 deletions

View File

@ -23,23 +23,21 @@ public struct ApplicationConfiguration
let vSync: VSyncMode
let windowWidth: Int32
let windowHeight: Int32
let windowTitle: String
let bundle: Bundle
public init(resizable: Bool, vSync: VSyncMode, windowWidth: Int32, windowHeight: Int32, bundle: Bundle)
public init(resizable: Bool, vSync: VSyncMode, windowWidth: Int32, windowHeight: Int32, title: String, bundle: Bundle)
{
self.resizable = resizable
self.vSync = vSync
self.windowWidth = windowWidth
self.windowHeight = windowHeight
self.windowTitle = title
self.bundle = bundle
}
}
//FIXME: Wrap these
public var sdlPad: OpaquePointer? = nil
var joyId: Int32 = -1
public class Application
{
private let implementation: ApplicationImplementation
@ -52,14 +50,6 @@ public class Application
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
@ -77,7 +67,7 @@ public class Application
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)
window = SDL_CreateWindow(configuration.windowTitle, winPos, winPos, winWidth, winHeight, winFlags)
guard window != nil else { fatalError("SDL_CreateWindow: \(String(cString: SDL_GetError()))") }
do
@ -91,15 +81,10 @@ public class Application
}
catch { fatalError("piss") }
for i in 0...SDL_NumJoysticks()
for idx in 0...SDL_NumJoysticks()
{
if SDL_IsGameController(i) != SDL_TRUE { continue }
if let open = openController(index: i)
{
sdlPad = open.pad
joyId = open.joy
break
}
if SDL_IsGameController(idx) != SDL_TRUE { continue }
Input.instance.padConnectedEvent(index: idx)
}
do
@ -131,13 +116,6 @@ public class Application
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))
{
@ -145,28 +123,27 @@ public class Application
resize = displaySize
default: break
}
case SDL_KEYDOWN:
Input.instance.pressEvent(scan: event.key.keysym.scancode, repeat: event.key.repeat != 0)
case SDL_KEYUP:
Input.instance.releaseEvent(scan: event.key.keysym.scancode)
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
}
}
if SDL_IsGameController(event.cdevice.which) != SDL_TRUE { break }
Input.instance.padConnectedEvent(index: event.cdevice.which)
case SDL_CONTROLLERDEVICEREMOVED:
if event.cdevice.which == joyId
{
SDL_GameControllerClose(sdlPad)
sdlPad = nil
joyId = -1
}
Input.instance.padRemovedEvent(index: event.cdevice.which)
case SDL_CONTROLLERBUTTONDOWN:
Input.instance.padButtonPressEvent(id: event.cbutton.which, btn: event.cbutton.button)
case SDL_CONTROLLERBUTTONUP:
Input.instance.padButtonReleaseEvent(id: event.cbutton.which, btn: event.cbutton.button)
case SDL_CONTROLLERAXISMOTION:
Input.instance.padAxisEvent(id: event.caxis.which, axis: event.caxis.axis, value: event.caxis.value)
default: break
}
}
let time = SDL_GetPerformanceCounter();
let deltaTime = Float(Double(time - prevTime) * timeMul)
let deltaTime = Float(Double(time &- prevTime) * timeMul)
prevTime = time
implementation.update(deltaTime: min(deltaTime, 1.0 / 15.0))

View File

@ -35,7 +35,7 @@ extension ContentManager
loaders[ext] = loader
}
public mutating func create(mesh: Mesh) throws -> RenderMesh
public mutating func create<V: Vertex>(mesh: Mesh<V>) throws -> RenderMesh<V>
{
let rendMesh = try renderer.createMesh(mesh: mesh)
resources.append(rendMesh)
@ -72,13 +72,18 @@ extension ContentManager
{
let resourceUrl = try getResource(path)
let loader = loaders[resourceUrl.pathExtension]
let loader = loaders[resourceUrl.pathExtension.lowercased()]
guard let resource = loader?.load(url: resourceUrl)
else { throw ContentError.loadFailure }
if T.self == Mesh.self
if T.self == Mesh<VertexPositionNormalTexcoord>.self
{
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
guard let mesh = resource as? Mesh<VertexPositionNormalTexcoord> else { throw ContentError.loadFailure }
return mesh as! T
}
if T.self == Mesh<VertexPositionNormalColourTexcoord>.self
{
guard let mesh = resource as? Mesh<VertexPositionNormalTexcoord> else { throw ContentError.loadFailure }
return mesh as! T
}
if T.self == Image.self
@ -100,13 +105,18 @@ extension ContentManager
{
let resourceUrl = try getResource(path)
let loader = loaders[resourceUrl.pathExtension]
let loader = loaders[resourceUrl.pathExtension.lowercased()]
guard let resource = loader?.load(url: resourceUrl)
else { throw ContentError.loadFailure }
if T.self == Mesh.self
if T.self == Mesh<VertexPositionNormalTexcoord>.self
{
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
guard let mesh = resource as? Mesh<VertexPositionNormalTexcoord> else { throw ContentError.loadFailure }
return mesh as! T
}
if T.self == Mesh<VertexPositionNormalColourTexcoord>.self
{
guard let mesh = resource as? Mesh<VertexPositionNormalTexcoord> else { throw ContentError.loadFailure }
return mesh as! T
}
if T.self == Image.self
@ -131,9 +141,15 @@ extension ContentManager
guard let resource = loader?.load(url: resourceUrl)
else { throw ContentError.loadFailure }
if T.self == RenderMesh.self
if T.self == RenderMesh<VertexPositionNormalTexcoord>.self
{
guard let mesh = resource as? Mesh else { throw ContentError.loadFailure }
guard let mesh = resource as? Mesh<VertexPositionNormalTexcoord> else { throw ContentError.loadFailure }
let renderResource = try self.create(mesh: mesh)
return renderResource as! T
}
if T.self == RenderMesh<VertexPositionNormalColourTexcoord>.self
{
guard let mesh = resource as? Mesh<VertexPositionNormalColourTexcoord> else { throw ContentError.loadFailure }
let renderResource = try self.create(mesh: mesh)
return renderResource as! T
}
@ -144,9 +160,13 @@ extension ContentManager
{
for resource in resources.reversed()
{
if resource is RenderMesh
if resource is RenderMesh<VertexPositionNormalTexcoord>
{
renderer.deleteMesh(resource as! RenderMesh)
renderer.deleteMesh(resource as! RenderMesh<VertexPositionNormalTexcoord>)
}
else if resource is RenderMesh<VertexPositionNormalColourTexcoord>
{
renderer.deleteMesh(resource as! RenderMesh<VertexPositionNormalColourTexcoord>)
}
else if resource is RenderTexture2D
{

View File

@ -6,4 +6,5 @@ public protocol LoaderProtocol
associatedtype T: Resource
func load(url: URL) -> T?
func load(url: URL, content: inout ContentManager) -> T?
}

View File

@ -1,16 +1,61 @@
import OrderedCollections
public struct Model: Resource
public struct Model<T: Vertex>: Resource
{
public let meshes: [Mesh]
public let meshes: [Mesh<T>]
}
public struct Mesh: Resource
public struct Mesh<T: Vertex>: Resource
{
public typealias Index = UInt16
public struct Vertex: Equatable
public struct SubMesh
{
public let start, length: Int
public let material: Int //hack
public init(start: Int, length: Int, material: Int = -1)
{
self.start = start
self.length = length
self.material = material
}
}
public let vertices: [T]
public let indices: [Index]
public let subMeshes: OrderedDictionary<String, SubMesh>
public let materials: [Material] // hack
}
public extension Mesh
{
static var empty: Self { Self(vertices: .init(), indices: .init(), subMeshes: .init(), materials: .init()) }
init(vertices: [T], indices: [Index])
{
self.init(
vertices: vertices,
indices: indices,
subMeshes: .init(),
materials: .init())
}
init(vertices: [T], indices: [Index], subMeshes: OrderedDictionary<String, SubMesh>)
{
self.init(
vertices: vertices,
indices: indices,
subMeshes: subMeshes,
materials: .init())
}
}
public protocol Vertex: Equatable {}
public struct VertexPositionNormalTexcoord: Vertex
{
public let position: Vec3f
public let normal: Vec3f
@ -24,31 +69,18 @@ public struct Mesh: Resource
}
}
public struct SubMesh
public struct VertexPositionNormalColourTexcoord: Vertex
{
public let start, length: Int
public let position: Vec3f
public let normal: Vec3f
public let colour: Colour
public let texCoord: Vec2f
public init(start: Int, length: Int)
public init(position: Vec3f, colour: Colour, normal: Vec3f, texCoord: Vec2f)
{
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())
self.position = position
self.colour = colour
self.normal = normal
self.texCoord = texCoord
}
}

View File

@ -1,15 +1,18 @@
import Foundation
import OrderedCollections
public struct ObjModel
{
public var positions = [Vec3f]()
public var colours = [Vec3f]()
public var normals = [Vec3f]()
public var texCoords = [Vec2f]()
public var objects = Dictionary<String, Object>()
public struct Object { public var faces = [Face]() }
public var objects = OrderedDictionary<String, Object>()
public var materials = Dictionary<String, ObjMaterial>()
public struct Object { public var meshes = [Mesh]() }
public struct Mesh { public var material = "", faces = [Face]() }
public struct Index { public let p: Int, n: Int, t: Int }
public enum Face
{

View File

@ -0,0 +1,157 @@
import Foundation
import SDL2
public class GamePad
{
public enum Axes
{
case leftStickX, leftStickY
case rightStickX, rightStickY
case leftTrigger, rightTrigger
}
public enum Buttons
{
case east, south, north, west
case select, start, guide, mic, touchPad
case leftStick, rightStick
case leftBumper, rightBumper
case dpadLeft, dpadRight, dpadUp, dpadDown
case paddle1, paddle2, paddle3, paddle4
}
public struct State
{
public func down(_ btn: Buttons) -> Bool { btnState(btn) & GamePad._DOWN == GamePad._DOWN }
public func pressed(_ btn: Buttons) -> Bool { btnState(btn) == GamePad._PRESS }
public func released(_ btn: Buttons) -> Bool { btnState(btn) == GamePad._RELEASE }
public func axis(_ axis: Axes) -> Float
{
let axisRescale = 1.0 / Float(Int16.max)
return Float(rawAxis(axis)) * axisRescale
}
@inline(__always) func rawAxis(_ axis: Axes) -> Int16
{
_axes[Int(axis.sdlEnum.rawValue)]
}
@inline(__always) private func btnState(_ btn: Buttons) -> UInt8
{
_btns[Int(btn.sdlEnum.rawValue)]
}
private let _btns: [UInt8], _axes: [Int16]
internal init(btns: [UInt8], axes: [Int16])
{
self._btns = btns
self._axes = axes
}
}
public var state: State
{
.init(btns: _btns, axes: _axes)
}
@inline(__always) public static var current: GamePad?
{
let input = Input.instance
return if let id = input.firstPadID { input.getPad(id: id) } else { nil }
}
private static let _UP = UInt8(0b00), _DOWN = UInt8(0b10), _IMPULSE = UInt8(0b01)
private static let _PRESS = _DOWN | _IMPULSE
private static let _RELEASE = _UP | _IMPULSE
private var _btns = [UInt8](repeating: 0, count: Int(SDL_CONTROLLER_BUTTON_MAX.rawValue))
private var _axes = [Int16](repeating: 0, count: Int(SDL_CONTROLLER_AXIS_MAX.rawValue))
private var _sdlPad: OpaquePointer
internal init(pad: OpaquePointer)
{
self._sdlPad = pad
}
internal func newTick()
{
_btns = _btns.map({ $0 & ~Self._IMPULSE })
}
internal func close()
{
SDL_GameControllerClose(_sdlPad)
}
internal func buttonPressEvent(_ btn: UInt8)
{
_btns[Int(btn)] = Self._PRESS
}
internal func buttonReleaseEvent(_ btn: UInt8)
{
_btns[Int(btn)] = Self._RELEASE
}
internal func axisEvent(_ axis: UInt8, _ value: Int16)
{
_axes[Int(axis)] = value
}
}
public extension GamePad.State
{
var leftStick: Vec2f { Vec2f(axis(.leftStickX), axis(.leftStickY)) }
var rightStick: Vec2f { Vec2f(axis(.rightStickX), axis(.rightStickY)) }
}
internal extension GamePad.Axes
{
var sdlEnum: SDL_GameControllerAxis
{
switch self
{
case .leftStickX: SDL_CONTROLLER_AXIS_LEFTX
case .leftStickY: SDL_CONTROLLER_AXIS_LEFTY
case .rightStickX: SDL_CONTROLLER_AXIS_RIGHTX
case .rightStickY: SDL_CONTROLLER_AXIS_RIGHTY
case .leftTrigger: SDL_CONTROLLER_AXIS_TRIGGERLEFT
case .rightTrigger: SDL_CONTROLLER_AXIS_TRIGGERRIGHT
}
}
}
internal extension GamePad.Buttons
{
var sdlEnum: SDL_GameControllerButton
{
switch self
{
case .east: SDL_CONTROLLER_BUTTON_B
case .south: SDL_CONTROLLER_BUTTON_A
case .west: SDL_CONTROLLER_BUTTON_Y
case .north: SDL_CONTROLLER_BUTTON_X
case .start: SDL_CONTROLLER_BUTTON_START
case .select: SDL_CONTROLLER_BUTTON_BACK
case .guide: SDL_CONTROLLER_BUTTON_GUIDE
case .mic: SDL_CONTROLLER_BUTTON_MISC1
case .touchPad: SDL_CONTROLLER_BUTTON_TOUCHPAD
case .leftStick: SDL_CONTROLLER_BUTTON_LEFTSTICK
case .rightStick: SDL_CONTROLLER_BUTTON_RIGHTSTICK
case .leftBumper: SDL_CONTROLLER_BUTTON_LEFTSHOULDER
case .rightBumper: SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
case .dpadUp: SDL_CONTROLLER_BUTTON_DPAD_UP
case .dpadDown: SDL_CONTROLLER_BUTTON_DPAD_DOWN
case .dpadLeft: SDL_CONTROLLER_BUTTON_DPAD_LEFT
case .dpadRight: SDL_CONTROLLER_BUTTON_DPAD_RIGHT
case .paddle1: SDL_CONTROLLER_BUTTON_PADDLE1
case .paddle2: SDL_CONTROLLER_BUTTON_PADDLE2
case .paddle3: SDL_CONTROLLER_BUTTON_PADDLE3
case .paddle4: SDL_CONTROLLER_BUTTON_PADDLE4
}
}
}

View File

@ -0,0 +1,62 @@
import SDL2
public class Input
{
private static let _instance = Input()
public static var instance: Input { _instance }
public var keyboard: Keyboard { _keyboard }
public func getPad(id: Int) -> GamePad? { _pads[Int32(id)] }
public var firstPadID: Int? { _firstJoyID >= 0 ? Int(_firstJoyID) : nil }
private var _keyboard = Keyboard()
private var _pads = Dictionary<Int32, GamePad>()
private var _firstJoyID: Int32 = -1
private init() {}
internal func pressEvent(scan: SDL_Scancode, repeat r: Bool) { keyboard.pressEvent(scan: scan, repeat: r) }
internal func releaseEvent(scan: SDL_Scancode) { keyboard.releaseEvent(scan: scan) }
internal func padConnectedEvent(index: Int32)
{
guard let sdlPad = SDL_GameControllerOpen(index)
else { return }
let id = SDL_JoystickGetDeviceInstanceID(index)
_pads[id] = GamePad(pad: sdlPad)
print("Using gamepad #\(id), \"\(String(cString: SDL_GameControllerName(sdlPad)))\"")
if _firstJoyID < 0 { _firstJoyID = id }
}
internal func padRemovedEvent(index: Int32)
{
let id = SDL_JoystickGetDeviceInstanceID(index)
_pads.removeValue(forKey: id)
if id == _firstJoyID
{
_firstJoyID = _pads.keys.first ?? -1
}
}
internal func padButtonPressEvent(id: Int32, btn: UInt8)
{
_pads[id]?.buttonPressEvent(btn)
}
internal func padButtonReleaseEvent(id: Int32, btn: UInt8)
{
_pads[id]?.buttonReleaseEvent(btn)
}
internal func padAxisEvent(id: Int32, axis: UInt8, value: Int16)
{
_pads[id]?.axisEvent(axis, value)
}
internal func newTick()
{
_keyboard.newTick()
for pad in _pads.values { pad.newTick() }
}
}

View File

@ -0,0 +1,137 @@
import SDL2
public class Keyboard
{
public enum Keys
{
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
}
public func keyDown(_ key: Keys) -> Bool
{
_keys[Int(key.sdlScancode.rawValue)] & Self._DOWN == Self._DOWN
}
public func keyPressed(_ key: Keys, repeat rep: Bool = false) -> Bool
{
var state = _keys[Int(key.sdlScancode.rawValue)]
if rep { state &= ~Self._REPEAT }
return state == Self._PRESS
}
public func keyReleased(_ key: Keys) -> Bool
{
_keys[Int(key.sdlKeycode.rawValue)] == Self._RELEASE
}
private static let _UP = UInt8(0b000), _DOWN = UInt8(0b010), _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))
internal func newTick()
{
_keys = _keys.map({ $0 & ~(Self._IMPULSE | Self._REPEAT) })
}
internal func pressEvent(scan: SDL_Scancode, repeat rep: Bool)
{
var newState = Self._PRESS
if rep { newState |= Self._REPEAT }
_keys[Int(scan.rawValue)] = newState
}
internal func releaseEvent(scan: SDL_Scancode)
{
_keys[Int(scan.rawValue)] = Self._RELEASE
}
}
extension Keyboard.Keys
{
internal var sdlKeycode: SDL_KeyCode
{
switch self
{
case .a: SDLK_a
case .b: SDLK_b
case .c: SDLK_c
case .d: SDLK_d
case .e: SDLK_e
case .f: SDLK_f
case .g: SDLK_g
case .h: SDLK_h
case .i: SDLK_i
case .j: SDLK_j
case .k: SDLK_k
case .l: SDLK_l
case .m: SDLK_m
case .n: SDLK_n
case .o: SDLK_o
case .p: SDLK_p
case .q: SDLK_q
case .r: SDLK_r
case .s: SDLK_s
case .t: SDLK_t
case .u: SDLK_u
case .v: SDLK_v
case .w: SDLK_w
case .x: SDLK_x
case .y: SDLK_y
case .z: SDLK_z
case .left: SDLK_LEFT
case .right: SDLK_RIGHT
case .up: SDLK_UP
case .down: SDLK_DOWN
}
}
internal var sdlScancode: SDL_Scancode
{
switch self
{
case .a: SDL_SCANCODE_A
case .b: SDL_SCANCODE_B
case .c: SDL_SCANCODE_C
case .d: SDL_SCANCODE_D
case .e: SDL_SCANCODE_E
case .f: SDL_SCANCODE_F
case .g: SDL_SCANCODE_G
case .h: SDL_SCANCODE_H
case .i: SDL_SCANCODE_I
case .j: SDL_SCANCODE_J
case .k: SDL_SCANCODE_K
case .l: SDL_SCANCODE_L
case .m: SDL_SCANCODE_M
case .n: SDL_SCANCODE_N
case .o: SDL_SCANCODE_O
case .p: SDL_SCANCODE_P
case .q: SDL_SCANCODE_Q
case .r: SDL_SCANCODE_R
case .s: SDL_SCANCODE_S
case .t: SDL_SCANCODE_T
case .u: SDL_SCANCODE_U
case .v: SDL_SCANCODE_V
case .w: SDL_SCANCODE_W
case .x: SDL_SCANCODE_X
case .y: SDL_SCANCODE_Y
case .z: SDL_SCANCODE_Z
case .left: SDL_SCANCODE_LEFT
case .right: SDL_SCANCODE_RIGHT
case .up: SDL_SCANCODE_UP
case .down: SDL_SCANCODE_DOWN
}
}
}
extension SDL_KeyCode
{
internal init(scancode: SDL_Scancode)
{
self.init(scancode.rawValue | UInt32(SDLK_SCANCODE_MASK))
}
}

View File

@ -4,7 +4,7 @@ import OrderedCollections
class G3DbLoader: LoaderProtocol
{
typealias T = Mesh
typealias T = Mesh<VertexPositionNormalTexcoord>
func load(url: URL) -> T?
{
@ -12,6 +12,8 @@ class G3DbLoader: LoaderProtocol
else { return Optional.none }
return try? reader.read()
}
func load(url: URL, content: inout ContentManager) -> T? { return load(url: url) }
}
@ -27,15 +29,15 @@ fileprivate struct G3DbReader
self.reader = UBJsonReader(file: file)
}
mutating func read() throws -> Mesh
mutating func read() throws -> Mesh<VertexPositionNormalTexcoord>
{
let model = try readModel()
let mesh = model.meshes[0]
var vertices = [Mesh.Vertex]()
var vertices = [VertexPositionNormalTexcoord]()
var indices = [UInt16]()
var subMeshes = OrderedDictionary<String, Mesh.SubMesh>()
var subMeshes = OrderedDictionary<String, Mesh<VertexPositionNormalTexcoord>.SubMesh>()
let attributeSize =
{ (attrib: G3DAttribute) in

View File

@ -16,6 +16,8 @@ struct NSImageLoader: LoaderProtocol
return image
}
func load(url: URL, content: inout ContentManager) -> T? { return load(url: url) }
static func loadImage(url: URL) throws -> Image
{
try autoreleasepool

View File

@ -2,10 +2,9 @@ import Foundation
import OrderedCollections
/*
extension ObjMaterial
{
func convert() -> Material
func convert(content: UnsafeMutablePointer<ContentManager>? = nil) -> Material
{
var m = Material()
m.diffuse = self.diffuse.setAlpha(self.alpha)
@ -14,37 +13,61 @@ extension ObjMaterial
m.specular = self.specular
m.specularExp = self.specularExp
}
if let content = content
{
if let albedo = self.diffuseMap
{
let filter = albedo.flags.contains(.blendHoriz) || albedo.flags.contains(.blendVert)
m.texture = (try? content.pointee.load(albedo.path,
params: Texture2DParameters(
minFilter: filter ? .linear : .point,
magFilter: filter ? .linear : .point,
wrapMode: albedo.flags.contains(.clamp) ? .clampBorder : .repeating,
mipMode: .linear)))?.id ?? RenderTexture2D.empty
}
}
return m
}
}
*/
public struct ObjLoader: LoaderProtocol
{
public typealias T = Mesh
typealias V = VertexPositionNormalColourTexcoord
public typealias T = Mesh<VertexPositionNormalColourTexcoord>
public init() {}
public func load(url: URL) -> T?
{
try? Self.read(url: url)
return try? Self.read(model: try ObjReader.read(url: url), content: nil)
}
private static func read(url: URL) throws -> Mesh
public func load(url: URL, content: inout ContentManager) -> T?
{
return try read(model: try ObjReader.read(url: url))
return try? Self.read(model: try ObjReader.read(url: url), content: &content)
}
public static func read(model: ObjModel) throws -> Mesh
public static func read(model: ObjModel, content: UnsafeMutablePointer<ContentManager>?) throws -> T
{
var subMeshes: OrderedDictionary<String, Mesh.SubMesh> = .init()
var vertices = [Mesh.Vertex]()
var subMeshes = OrderedDictionary<String, T.SubMesh>()
var materials = OrderedDictionary<String, Material>()
var vertices = [V]()
var indices = [UInt16]()
for (key, mtl) in model.materials
{
materials[key] = mtl.convert(content: content)
}
let readIndex =
{ (v: ObjModel.Index) -> UInt16 in
let vertex = Mesh.Vertex(
let colour = model.colours.isEmpty ? .white : Colour(
r: model.colours[v.p][0],
g: model.colours[v.p][1],
b: model.colours[v.p][2]).linear
let vertex = V(
position: model.positions[v.p],
colour: colour,
normal: model.normals[v.n],
texCoord: model.texCoords[v.t])
if let index = vertices.firstIndex(of: vertex)
@ -61,10 +84,13 @@ public struct ObjLoader: LoaderProtocol
}
}
for mesh in model.objects
for object in model.objects.filter({ $0.key != "Collision3D" })
{
var id = 0
for mesh: ObjModel.Mesh in object.value.meshes
{
let start = indices.count
for face: ObjModel.Face in mesh.value.faces
for face: ObjModel.Face in mesh.faces
{
switch face
{
@ -83,10 +109,14 @@ public struct ObjLoader: LoaderProtocol
let length = indices.count - start
if length > 0
{
subMeshes[mesh.key] = .init(start: start, length: length)
subMeshes["\(object.key)_\(id)"] = .init(
start: start, length: length,
material: materials.index(forKey: mesh.material) ?? -1)
}
id += 1
}
}
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes, materials: materials.values.map { $0 })
}
}

View File

@ -8,25 +8,46 @@ public struct ObjReader
var file = try ObjDocumentReader(filePath: url)
var model = ObjModel()
var materials = Dictionary<String, ObjMaterial>()
var name: String? = nil
var object = ObjModel.Object()
var mesh = ObjModel.Mesh()
file.string(tag: "mtllib") { s in
let mats = try ObjMtlLoader.read(url: url.deletingLastPathComponent().appendingPathComponent(s[0]))
materials.merge(mats, uniquingKeysWith: { (_, new) in new } )
model.materials.merge(mats, uniquingKeysWith: { (_, new) in new } )
}
file.string(tag: "o", count: 1) { s in
if !object.faces.isEmpty
if !mesh.faces.isEmpty { object.meshes.append(mesh) }
if !object.meshes.isEmpty
{
model.objects[name!] = object
object = .init()
mesh = .init()
}
name = String(s[0])
}
file.float(tag: "v", count: 3) { v in model.positions.append(Vec3f(v[0], v[1], v[2])) }
file.float(tag: "v") { v in
if v.count == 6
{
if model.colours.isEmpty && !model.positions.isEmpty
{
for _ in 0..<model.positions.count
{
model.colours.append(.one)
}
}
model.colours.append(Vec3f(v[3], v[4], v[5]))
}
else if !model.colours.isEmpty && v.count == 3
{
model.colours.append(.one)
}
else if v.count != 3 { throw ObjLoaderError.badTagParameters }
model.positions.append(Vec3f(v[0], v[1], v[2]))
}
file.float(tag: "vn", count: 3) { v in model.normals.append(Vec3f(v[0], v[1], v[2])) }
file.float(tag: "vt", count: 2) { v in model.texCoords.append(Vec2f(v[0], v[1])) }
file.int(tag: "l") { i in mesh.faces.append(.line(p1: i[0], p2: i[1])) }
file.raw(tag: "f") { raw in
let readIndex =
{ (raw: Substring) in
@ -40,14 +61,14 @@ public struct ObjReader
if raw.count == 3
{
for raw in raw { _ = try readIndex(raw) }
object.faces.append(.triangle(
mesh.faces.append(.triangle(
v1: try readIndex(raw[raw.startIndex]),
v2: try readIndex(raw[raw.startIndex + 1]),
v3: try readIndex(raw[raw.startIndex + 2])))
}
else if raw.count == 4
{
object.faces.append(.quad(
mesh.faces.append(.quad(
v1: try readIndex(raw[raw.startIndex]),
v2: try readIndex(raw[raw.startIndex + 1]),
v3: try readIndex(raw[raw.startIndex + 2]),
@ -55,17 +76,25 @@ public struct ObjReader
}
else if raw.count >= 5
{
object.faces.append(.ngon(try raw.map { try readIndex($0) }))
mesh.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])) }
file.string(tag: "usemtl", count: 1) { s in
if !mesh.faces.isEmpty
{
object.meshes.append(mesh)
mesh = .init()
}
mesh.material = s[0]
}
// file.int(tag: "s", count: 1) { i in } //TODO: Smoothing groups
try file.read()
if !object.faces.isEmpty
{
model.objects[name!] = object
}
if !mesh.faces.isEmpty { object.meshes.append(mesh) }
if !object.meshes.isEmpty { model.objects[name!] = object }
return model
}
}

View File

@ -39,6 +39,8 @@ public extension SIMD2 where Scalar: FloatingPoint
return Self(self.x * invX + b.x * x, self.y * invX + b.y * x)
}
@inline(__always) func distance(_ b: Self) -> Scalar { return (b - self).len }
func cardinalDeadzone(min: Scalar, max: Scalar) -> Self
{
Self(self.x.axisDeadzone(min, max), self.y.axisDeadzone(min, max))

View File

@ -76,13 +76,19 @@ class OpenGL: Renderer
}
}
var wireframe: Bool
{
get { state.polygonMode == .line }
set(mode) { state.polygonMode = mode ? .line : .fill }
}
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
func createMesh<V: Vertex>(mesh: Mesh<V>) throws -> RenderMesh<V>
{
var buffers = [GLuint](repeating: 0, count: 2)
buffers.withUnsafeMutableBufferPointer
@ -94,35 +100,36 @@ class OpenGL: Renderer
state.elementArrayBuffer = buffers[1]
glBufferData(GLenum(GL_ARRAY_BUFFER),
MemoryLayout<Mesh.Vertex>.stride * mesh.vertices.count,
MemoryLayout<V>.stride * mesh.vertices.count,
mesh.vertices, GLenum(GL_STATIC_DRAW))
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
MemoryLayout<Mesh.Index>.stride * mesh.indices.count,
MemoryLayout<V>.stride * mesh.indices.count,
mesh.indices, GLenum(GL_STATIC_DRAW))
state.elementArrayBuffer = OpenGLState.defaultBuffer
state.arrayBuffer = OpenGLState.defaultBuffer
var subMeshes = [Mesh.SubMesh]()
var subMeshes = [Mesh<V>.SubMesh]()
if mesh.subMeshes.isEmpty
{
subMeshes.append(Mesh.SubMesh(start: 0, length: mesh.indices.count))
subMeshes.append(Mesh<V>.SubMesh(start: 0, length: mesh.indices.count))
}
else
{
for subMesh in mesh.subMeshes
{
if ["Collision", "Collision3D"].contains(subMesh.key) { continue }
subMeshes.append(Mesh.SubMesh(
subMeshes.append(Mesh<V>.SubMesh(
start: subMesh.value.start,
length: subMesh.value.length))
length: subMesh.value.length,
material: subMesh.value.material))
}
}
return RenderMesh(
vbo: Int(buffers[0]),
ibo: Int(buffers[1]),
subMeshes: subMeshes)
subMeshes: subMeshes,
materials: mesh.materials)
}
func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
@ -172,7 +179,7 @@ class OpenGL: Renderer
}
func deleteMesh(_ mesh: RenderMesh)
func deleteMesh<V: Vertex>(_ mesh: RenderMesh<V>)
{
var buffers = [GLuint](repeating: 0, count: 2)
buffers[0] = GLuint(mesh.iboHnd)
@ -208,42 +215,64 @@ class OpenGL: Renderer
}
private func draw(subMesh: Mesh.SubMesh)
private func draw<V: Vertex>(subMesh: Mesh<V>.SubMesh)
{
glDrawElements(
GLenum(GL_TRIANGLES),
GLsizei(subMesh.length),
GLenum(GL_UNSIGNED_SHORT),
.init(bitPattern: MemoryLayout<Mesh.Index>.stride * subMesh.start));
.init(bitPattern: MemoryLayout<Mesh<V>.Index>.stride * subMesh.start));
}
private func draw(mesh: RenderMesh)
private func bindMesh<V: Vertex>(mesh: RenderMesh<V>)
{
state.enableClient([ .vertex, .normal, .textureCoord ])
state.enableClient([ .vertex, .colour, .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
let stride = GLsizei(MemoryLayout<V>.stride)
if V.self == VertexPositionNormalTexcoord.self
{
draw(subMesh: subMesh)
glVertexPointer(3, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.position)!))
glNormalPointer(GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.normal)!))
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.texCoord)!))
}
else if V.self == VertexPositionNormalColourTexcoord.self
{
glVertexPointer(3, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.position)!))
glColorPointer(4, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.colour)!))
glNormalPointer(GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.normal)!))
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.texCoord)!))
}
}
private func unbindMesh()
{
state.elementArrayBuffer = OpenGLState.defaultBuffer
state.arrayBuffer = OpenGLState.defaultBuffer
state.disableClient([ .vertex, .normal, .textureCoord ])
}
func draw(mesh: RenderMesh, model: Mat4f, environment env: Environment)
private func draw<V: Vertex>(mesh: RenderMesh<V>)
{
bindMesh(mesh: mesh)
for subMesh in mesh.subMeshes
{
draw(subMesh: subMesh)
}
unbindMesh()
}
func draw<V: Vertex>(mesh: RenderMesh<V>, model: Mat4f, environment env: Environment)
{
if (mesh.subMeshes.isEmpty) { return }
@ -255,7 +284,7 @@ class OpenGL: Renderer
glPopMatrix()
}
func draw(mesh: RenderMesh, environment env: Environment)
func draw<V: Vertex>(mesh: RenderMesh<V>, environment env: Environment)
{
if (mesh.subMeshes.isEmpty) { return }
@ -263,6 +292,14 @@ class OpenGL: Renderer
draw(mesh: mesh)
}
func draw<V: Vertex>(mesh: RenderMesh<V>, subMesh: Mesh<V>.SubMesh, environment env: Environment)
{
applyEnvironment(env)
bindMesh(mesh: mesh)
draw(subMesh: subMesh)
unbindMesh()
}
private func loadMatrix(_ matrix: Mat4f)
{
withUnsafePointer(to: matrix.columns)

View File

@ -793,4 +793,26 @@ struct OpenGLState
}
}
}
enum PolygonMode { case point, line, fill }
private var _polygonMode = PolygonMode.fill
var polygonMode: PolygonMode
{
get { _polygonMode }
set(newMode)
{
if newMode != _polygonMode
{
let modeEnum = switch newMode
{
case .point: GL_POINT
case .line: GL_LINE
case .fill: GL_FILL
}
glPolygonMode(GLenum(GL_FRONT_AND_BACK), GLenum(modeEnum));
_polygonMode = newMode
}
}
}
}

View File

@ -11,15 +11,16 @@ public protocol Renderer
func resize(width: Int32, height: Int32)
var clearColour: Colour { get set }
var wireframe: Bool { get set }
func setVsync(mode: VSyncMode) throws
func createMesh(mesh: Mesh) throws -> RenderMesh
func createMesh<V: Vertex>(mesh: Mesh<V>) throws -> RenderMesh<V>
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 deleteMesh<V: Vertex>(_ mesh: RenderMesh<V>)
func deleteTexture(_ texture: RenderTexture2D)
func setProjection(matrix: Mat4f)
@ -27,8 +28,9 @@ public protocol Renderer
func setMaterial(_ mat: Material)
func draw(mesh: RenderMesh, model: Mat4f, environment: Environment)
func draw(mesh: RenderMesh, environment: Environment)
func draw<V: Vertex>(mesh: RenderMesh<V>, model: Mat4f, environment: Environment)
func draw<V: Vertex>(mesh: RenderMesh<V>, environment: Environment)
func draw<V: Vertex>(mesh: RenderMesh<V>, subMesh: Mesh<V>.SubMesh, environment: Environment)
func drawGizmos(lines: [Line])
}
@ -76,9 +78,9 @@ public protocol RendererResource
var isValid: Bool { get }
}
public struct RenderMesh: RendererResource
public struct RenderMesh<V: Vertex>: RendererResource
{
public typealias T = Mesh
public typealias T = Mesh<V>
public static var empty: RenderMesh { .init() }
@ -86,7 +88,8 @@ public struct RenderMesh: RendererResource
private let _valid: Bool;
let vboHnd: Int, iboHnd: Int
let subMeshes: [Mesh.SubMesh]
public let subMeshes: [Mesh<V>.SubMesh]
public let materials: [Material] // HACK !!!!
private init()
{
@ -94,14 +97,16 @@ public struct RenderMesh: RendererResource
self.vboHnd = 0
self.iboHnd = 0
self.subMeshes = []
self.materials = []
}
init(vbo: Int, ibo: Int, subMeshes: [Mesh.SubMesh])
init(vbo: Int, ibo: Int, subMeshes: [Mesh<V>.SubMesh], materials: [Material] = .init())
{
self._valid = true
self.vboHnd = vbo
self.iboHnd = ibo
self.subMeshes = subMeshes
self.materials = materials
}
}

Binary file not shown.

View File

@ -14,6 +14,7 @@ import JolkEngine
vSync: .on,
windowWidth: 1280,
windowHeight: 720,
title: "Caves of Swift",
bundle: Bundle.module)
).run()
}
@ -21,105 +22,20 @@ import JolkEngine
class CavesOfJolk: ApplicationImplementation
{
private lazy var scene = Scene1()
//private lazy var scene = CaveScene()
private var aspect: Float = 0
private var drawEdges = false
var env = Environment()
var worldMesh = RenderMesh.empty, cube = RenderMesh.empty, suzanne = RenderMesh.empty, toybox = RenderMesh.empty
var texture = Texture2D.empty, jolkTex = Texture2D.empty, suzanneDiffuse = Texture2D.empty
private lazy var world = Collision()
private lazy var colin = Colin()
private lazy var jolkCube = JolkCube(position: .init(0, 1, -4))
private var lightTheta: Float = 0
private var frameCount = 0
private var frameTimer: Float = 1
func create(render: inout Renderer)
{
render.clearColour = XnaColour.CornflowerBlue
env.setFog(
colour: XnaColour.CornflowerBlue,
//mode: .depth, falloff: .range(type: .linear, start: 4, end: 38))
//mode: .depth, falloff: .range(type: .linear, start: 1.25, end: 24.5))
//mode: .distance, falloff: .factor(type: .exp2, density: 0.1))
mode: .depth, falloff: .factor(type: .exp2, density: 0.0222))
env.addAmbientLight(colour: XnaColour.DarkSlateGray.lighten(by: -0.666))
env.addDirectionalLight(
direction: -Vec3f(1, -1, -1).normalised,
colour: XnaColour.BlanchedAlmond
.lighten(by: -0.02)
.mix(with: XnaColour.LightSlateGray, 0.5)
.mix(with: XnaColour.White, 0.125))
env.addPointLight(
position: Vec3f(3, 0.33, -5),
colour: XnaColour.Green.mix(with: XnaColour.Gray, 0.5),
intensity: 2)
env.addPointLight(
position: Vec3f(5.5, 0.33, -6),
colour: XnaColour.Red.mix(with: XnaColour.Gray, 0.5),
intensity: 2)
env.addPointLight(
position: Vec3f(4, 0.33, -7),
colour: XnaColour.Blue.mix(with: XnaColour.Gray, 0.5),
intensity: 2)
scene.setup(render: &render)
}
func loadContent(content: inout ContentManager) throws
{
try loadWorld(&content)
cube = try content.create(mesh: .init(
vertices: [
.init(position: Vec3f(-1, -1, 1), normal: .forward, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, -1, 1), normal: .forward, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, 1, 1), normal: .forward, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, 1, 1), normal: .forward, texCoord: Vec2f(1, 1)),
.init(position: Vec3f( 1, -1, 1), normal: .right, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, -1, -1), normal: .right, texCoord: Vec2f(1, 0)),
.init(position: Vec3f( 1, 1, 1), normal: .right, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, 1, -1), normal: .right, texCoord: Vec2f(1, 1)),
.init(position: Vec3f( 1, -1, -1), normal: .back, texCoord: Vec2f(0, 0)),
.init(position: Vec3f(-1, -1, -1), normal: .back, texCoord: Vec2f(1, 0)),
.init(position: Vec3f( 1, 1, -1), normal: .back, texCoord: Vec2f(0, 1)),
.init(position: Vec3f(-1, 1, -1), normal: .back, texCoord: Vec2f(1, 1)),
.init(position: Vec3f(-1, -1, -1), normal: .left, texCoord: Vec2f(0, 0)),
.init(position: Vec3f(-1, -1, 1), normal: .left, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, 1, -1), normal: .left, texCoord: Vec2f(0, 1)),
.init(position: Vec3f(-1, 1, 1), normal: .left, texCoord: Vec2f(1, 1)),
.init(position: Vec3f(-1, -1, -1), normal: .down, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, -1, -1), normal: .down, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, -1, 1), normal: .down, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, -1, 1), normal: .down, texCoord: Vec2f(1, 1)),
.init(position: Vec3f(-1, 1, 1), normal: .up, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, 1, 1), normal: .up, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, 1, -1), normal: .up, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, 1, -1), normal: .up, texCoord: Vec2f(1, 1))
],
indices: [
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
8, 9, 10, 10, 9, 11,
12, 13, 14, 14, 13, 15,
16, 17, 18, 18, 17, 19,
20, 21, 22, 22, 21, 23 ]))
suzanne = try content.load("suzanne.g3db")
toybox = try content.load("toybox.g3db")
let linearMipped = Texture2DParameters(
minFilter: .linear, magFilter: .linear,
wrapMode: .repeating,
mipMode: .linear)
let linearClamp = Texture2DParameters(
minFilter: .linear, magFilter: .linear,
wrapMode: .clampEdge)
texture = try content.load("cobblestone.png", params: linearMipped)
jolkTex = try content.load("jolkmeup.jpg", params: linearClamp)
suzanneDiffuse = try content.load("suzanne_diffuse.png", params: linearMipped)
try scene.loadContent(content: &content)
}
func resize(width: Int, height: Int)
@ -130,10 +46,7 @@ class CavesOfJolk: ApplicationImplementation
func update(deltaTime: Float)
{
colin.update(deltaTime: deltaTime, world: world)
jolkCube.update(deltaTime: deltaTime, world: world)
lightTheta += deltaTime
scene.update(deltaTime: deltaTime)
frameCount += 1
frameTimer -= deltaTime
@ -145,132 +58,10 @@ class CavesOfJolk: ApplicationImplementation
}
}
private func loadWorld(_ content: inout ContentManager) throws
{
//let world: Mesh = try content.load("World.obj")
let obj = try ObjReader.read(url: try content.getResource("World.obj"))
let mesh: Mesh = try ObjLoader.read(model: obj)
worldMesh = try content.create(mesh: mesh)
if let collision = obj.objects["Collision3D"]
{
world.build(obj: obj, collision: collision)
}
}
/*
if let collision = world.subMeshes["Collision"]
{
edges.reserveCapacity(collision.length / 6)
let get2dVertex =
{ i in
let vertex = world.vertices[Int(world.indices[i])]
let position = Vec2f(vertex.position.x, vertex.position.z)
let normal = Vec2f(vertex.normal.x, vertex.normal.z)
return (position, normal)
}
for i in stride(from: collision.start, to: collision.start + collision.length, by: 3)
{
let (v0p, v0n) = get2dVertex(i)
let (v1p, v1n) = get2dVertex(i + 1)
let (v2p, v2n) = get2dVertex(i + 2)
let (p0, p1) = if v0p == v2p || v1p == v2p { (v0p, v1p) }
else if v0p == v1p { (v0p, v2p) }
else { throw NSError() }
let edge = Edge(p: p0.lerp(p1, 0.5), n: (v0n + v1n + v2n).normalised, w: (p0 - p1).len)
if !edges.contains(where: { edge.p == $0.p && edge.w == $0.w })
{
edges.append(edge)
}
}
}
*/
func draw(render: inout Renderer, deltaTime: Float)
{
// Setup camera
render.setProjection(matrix:
.perspective(fovY: Float.rad(fromDeg: colin.fov), aspect: aspect, zNear: 0.1, zFar: 100))
render.setView(matrix: colin.transform)
if Input.instance.keyboard.keyPressed(.w, repeat: true) { render.wireframe = !render.wireframe }
// Update point lights
var theta = Vec3f(repeating: lightTheta) * Vec3f(0.12, 0.011, 0.056) * 6
var i = env.lights.startIndex
while i != env.lights.endIndex
{
if case .point(let colour, _, _) = env.lights[i]
{
env.lights[i] = .point(
colour: colour,
position: Vec3f(
4 + 6 * cos(theta[0]),
0.33 + 0.33 * sin(theta[1]),
-6 + 3 * sin(theta[0] * 2)),
intensity: 3.1 + 1.53 * cos(theta[2]))
let spacing = 0.5 * Float.pi * (2 / 3.0)
theta += spacing * Vec3f(1, 0.98, 0.5566)
}
i = env.lights.index(after: i)
}
// Draw world
render.setMaterial(Material(
specular: XnaColour.BlanchedAlmond.mix(with: .black, 0.12),
texture: texture.id))
render.draw(mesh: worldMesh, environment: env)
// Draw jolked up shit
render.setMaterial(Material(
specular: XnaColour.Gray,
gloss: 20.0,
texture: jolkTex.id))
render.draw(mesh: cube, model: jolkCube.transform, environment: env)
render.setMaterial(Material(texture: suzanneDiffuse.id))
render.draw(mesh: suzanne, model: .translate(.up + Vec3f(3.0, 0.0, -3.5) * 2.5), environment: env)
render.draw(mesh: toybox,
model: .translate(Vec3f(6.0, 0.667, -3.5) * Vec3f(2.5, 1, 2.5))
* .rotate(y: lightTheta * 0.5) * .scale(scalar: 0.25), environment: env)
if Input.instance.keyPressed(.c) { drawEdges = !drawEdges }
if drawEdges
{
/*
var lines = [Line](
repeating: .init(from: .init(), to: .init(), colour: .zero),
count: edges.count * 3)
var i: Int = 0
for edge in edges
{
let tangent = Vec2f(edge.n.y, -edge.n.x)
let a = edge.p - tangent * edge.w * 0.5
let b = edge.p + tangent * edge.w * 0.5
lines[i] = Line(
from: Vec3f(a.x, 0, a.y),
to: Vec3f(b.x, 0, b.y),
colour: Colour(r: 0.1, g: 0.9, b: 0.1))
lines[i + 1] = Line(
from: Vec3f(edge.p.x, 0, edge.p.y),
to: Vec3f(edge.p.x + edge.n.x * 0.25, 0, edge.p.y + edge.n.y * 0.25),
colour: Colour(r: 0.9, g: 0.1, b: 0.1))
let deltaPos = Vec2f(colin.position.x, colin.position.z) - edge.p
let something = tangent * deltaPos.cross(edge.n)
lines[i + 2] = Line(
from: Vec3f(edge.p.x + something.x, 0.0, edge.p.y + something.y),
to: Vec3f(edge.p.x + something.x + edge.n.x * 0.5, 0.0, edge.p.y + something.y + edge.n.y * 0.5),
colour: XnaColour.Azure)
i += 3
}
render.drawGizmos(lines: lines)
*/
world.draw(render, position: colin.position)
}
scene.draw(render: &render, deltaTime: deltaTime, aspectRatio: aspect)
}
}

View File

@ -2,7 +2,7 @@ import Foundation
import JolkEngine
class Collision
struct Collision
{
struct Edge { let p: Vec2f, n: Vec2f, w: Float }
@ -66,16 +66,16 @@ class Collision
enum Winding { case none, cw, ccw }
func build(obj: ObjModel, collision: ObjModel.Object)
mutating func build(obj: ObjModel, collision: ObjModel.Object)
{
for face in collision.faces
for face in collision.meshes.flatMap({ $0.faces })
{
switch face
{
case .triangle(let v1, let v2, let v3):
let t = Triangle(obj.positions[v1.p], obj.positions[v2.p], obj.positions[v3.p])
let n = t.normal
if abs(n.y) < 0.25 { continue }
if abs(n.y) == 0 { continue }
edge3d.append(.triangle(normal: n, origin: (t.a + t.b + t.c) / 3.0, tri: t))
case .quad(let v1, let v2, let v3, let v4):
let q = Quad(obj.positions[v1.p], obj.positions[v2.p], obj.positions[v3.p], obj.positions[v4.p])

View File

@ -2,7 +2,7 @@ import JolkEngine
protocol Actor
{
func update(deltaTime: Float, world: Collision)
mutating func update(deltaTime: Float, world: Collision)
var position: Vec3f { get }
var transform: Mat4f { get }

View File

@ -4,9 +4,10 @@ import OpenGL.GL
import JolkEngine
class Colin: Actor
struct Colin: Actor
{
var position: Vec3f { _pos }
mutating func setPosition(_ new: Vec3f) { _pos = new }
var transform: Mat4f
{
@ -16,10 +17,10 @@ class Colin: Actor
}
var angle: Vec2f { return Vec2f(ofsAngle.x + _angle, ofsAngle.y) }
mutating func setAngle(_ new: Vec2f) { _angle = new.x; ofsAngle = .init(0.0, new.y) }
var fov: Float { return _fov }
private var time: Float = 0.0
//private var jumpVel: Float = 0.0
private var velocity: Vec3f = .zero
private var _pos: Vec3f = .zero
private var _pos2D: Vec2f { get { Vec2f(_pos.x, _pos.z) } set(new) { _pos.x = new.x; _pos.z = new.y } }
@ -27,13 +28,11 @@ class Colin: Actor
private var ofsAngle: Vec2f = .zero
private var _fov: Float = 60.0
private var nutted = false
private var colinMode = false
private var backPressed = false
private let colinWidth: Float = 0.2
private func move2D(new: Vec2f, edges: [Collision.Edge])
private mutating func move2D(new: Vec2f, edges: [Collision.Edge])
{
let velocity = new - _pos2D
var lastPos = _pos2D
@ -79,7 +78,7 @@ class Colin: Actor
}
}
private func response3D(plainPos o: Vec3f, plainNorm n: Vec3f)
private mutating func response3D(plainPos o: Vec3f, plainNorm n: Vec3f)
{
let height: Float = 1.12
//let diff = _pos - lastPos
@ -97,7 +96,7 @@ class Colin: Actor
}
}
private func move3D(_ deltaTime: Float, world: Collision)
private mutating func move3D(_ deltaTime: Float, world: Collision)
{
var lastPos = _pos
_pos += velocity * deltaTime
@ -169,91 +168,73 @@ class Colin: Actor
}
}
func update(deltaTime: Float, world: Collision)
mutating func update(deltaTime: Float, world: Collision)
{
time += deltaTime
let axisRescale = 1.0 / Float(INT16_MAX)
let getAxis = { (axis: SDL_GameControllerAxis) in
Float(SDL_GameControllerGetAxis(sdlPad, axis)) * axisRescale }
let getButton = { (btn: SDL_GameControllerButton) in
SDL_GameControllerGetButton(sdlPad, btn) == SDL_PRESSED }
let pad = GamePad.current?.state
let stick = Vec2f(
getAxis(SDL_CONTROLLER_AXIS_LEFTX),
getAxis(SDL_CONTROLLER_AXIS_LEFTY))
.cardinalDeadzone(min: 0.1, max: 1)
let stick = pad?.leftStick.cardinalDeadzone(min: 0.1, max: 1)
let turnSpeed = Float.pi * 2 * 0.1
if Input.instance.keyDown(.left)
let turnSpeed = Float.pi * 2 * 0.2
if Input.instance.keyboard.keyDown(.left)
{
_angle -= turnSpeed * deltaTime
}
if Input.instance.keyDown(.right)
if Input.instance.keyboard.keyDown(.right)
{
_angle += turnSpeed * deltaTime
}
if stick != .zero
if let stick = stick
{
_angle += stick.x * 2.0 * turnSpeed * deltaTime
}
var moveVec: Vec2f = .zero
let speed: Float = 2
let speed: Float = 4
let forward = Vec2f(sin(self._angle), -cos(self._angle))
if Input.instance.keyDown(.up)
if Input.instance.keyboard.keyDown(.up)
{
moveVec += forward * speed
}
if Input.instance.keyDown(.down)
if Input.instance.keyboard.keyDown(.down)
{
moveVec -= forward * speed
}
if stick != .zero
if let stick = stick
{
moveVec -= forward * stick.y * speed
}
if Input.instance.keyDown(.c)
if Input.instance.keyboard.keyDown(.c)
{
colinMode = !colinMode
}
let lookCone = Float.pi / 2.0
let dst = Vec2f(
getAxis(SDL_CONTROLLER_AXIS_RIGHTX),
getAxis(SDL_CONTROLLER_AXIS_RIGHTY))
.radialDeadzone(min: 0.1, max: 1) * lookCone
ofsAngle = ofsAngle.lerp(dst, 16 * deltaTime)
let dst = pad?.rightStick.radialDeadzone(min: 0.1, max: 1) ?? .zero
ofsAngle = ofsAngle.lerp(dst * lookCone, 16 * deltaTime)
let targetFov = Float.lerp(60, 20, getAxis(SDL_CONTROLLER_AXIS_TRIGGERRIGHT))
let targetFov = Float.lerp(60, 20, pad?.axis(.rightTrigger) ?? .zero)
_fov = Float.lerp(_fov, targetFov, 20 * deltaTime)
if let pad = pad
{
let right = Vec2f(cos(self._angle), sin(self._angle))
if getButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER)
{
moveVec -= right * speed
}
if getButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)
{
moveVec += right * speed
if pad.down(.leftBumper) { moveVec -= right * speed }
if pad.down(.rightBumper) { moveVec += right * speed }
if pad.pressed(.select) { colinMode = !colinMode }
}
let back = getButton(SDL_CONTROLLER_BUTTON_BACK)
if !backPressed && back { colinMode = !colinMode }
backPressed = back
if getButton(SDL_CONTROLLER_BUTTON_A) || Input.instance.keyDown(.n)
if pad?.pressed(.south) ?? false || Input.instance.keyboard.keyDown(.n)
{
// play nut.wav
nutted = true
}
else { nutted = false }
//move2D(new: _pos2D + velocity, edges: edges)
if Input.instance.keyDown(.z) || getButton(SDL_CONTROLLER_BUTTON_B) { velocity.y = 2.0 }
if Input.instance.keyboard.keyDown(.z) || pad?.down(.east) ?? false { velocity.y = 2.0 }
//_pos.y += jumpVel * deltaTime
//if _pos.y > 0.0 { jumpVel -= 5.4 * deltaTime }
velocity.y -= 5.4 * deltaTime

View File

@ -3,7 +3,7 @@ import simd
import JolkEngine
class JolkCube: Actor
struct JolkCube: Actor
{
private var _pos: Vec3f
private var theta: Float = 0.0
@ -15,7 +15,7 @@ class JolkCube: Actor
var position: Vec3f { _pos }
func update(deltaTime: Float, world: Collision)
mutating func update(deltaTime: Float, world: Collision)
{
theta += 15 * deltaTime
_pos.y = 1 + sin(theta * 0.25) * 0.25

View File

@ -0,0 +1,46 @@
# Blender 3.6.2 MTL File: 'CaveScene.blend'
# www.blender.org
newmtl Grass
Ns 360.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd grass.png
newmtl GravelFloor
Ns 360.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd PackedDirt.JPG
newmtl Material
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd SkyBox-Clouds-Med-MidMorning.PNG
newmtl RockWall
Ns 360.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd 1024x1024_seamless_texture_10.jpg

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,16 @@ map_Kd cobblestone.png
map_Ks cobblestone_specular.png
map_Bump -bm 1.000000 cobblestone_normal.png
newmtl Grass
Ns 30.581900
Ka 1.000000 1.000000 1.000000
Ks 0.007389 0.007389 0.007389
Ke 0.000000 0.000000 0.000000
Ni 11.849999
d 1.000000
illum 2
map_Kd grass.png
newmtl Material
Ns 167.965591
Ka 1.000000 1.000000 1.000000

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -0,0 +1,58 @@
import Foundation
import simd
import JolkEngine
struct CaveScene: Scene
{
private var colin = Colin()
private var world = Collision()
private var worldModel = RenderMesh<VertexPositionNormalColourTexcoord>.empty
private var drawEdges = false
mutating func setup(render: inout any JolkEngine.Renderer)
{
colin.setPosition(Vec3f(3.55475903, 0.0667395443, 0.221960306))
colin.setAngle(Vec2f(-1.47447872, 0.0))
}
mutating func loadContent(content: inout JolkEngine.ContentManager) throws
{
let obj = try ObjReader.read(url: try content.getResource("CaveScene.obj"))
let mesh: Mesh = try ObjLoader.read(model: obj, content: &content)
worldModel = try content.create(mesh: mesh)
if let collision = obj.objects["Collision3D"]
{
world.build(obj: obj, collision: collision)
}
}
mutating func update(deltaTime: Float)
{
colin.update(deltaTime: deltaTime, world: world)
if Input.instance.keyboard.keyPressed(.c) { drawEdges = !drawEdges }
}
func draw(render: inout Renderer, deltaTime: Float, aspectRatio aspect: Float)
{
render.setProjection(matrix: .perspective(
fovY: Float.rad(fromDeg: colin.fov),
aspect: aspect, zNear: 0.1, zFar: 4000.0))
render.setView(matrix: colin.transform)
let env = Environment()
let drawRanges =
{ [render](range: any Sequence<Int>) in
for i in range
{
let subMesh = worldModel.subMeshes[i]
render.setMaterial(worldModel.materials[subMesh.material])
render.draw(mesh: worldModel, subMesh: subMesh, environment: env)
}
}
if colin.position.x < 14 { drawRanges([ 0, 1, 2, 6 ]) }
else { drawRanges(3...5) }
if drawEdges { world.draw(render, position: colin.position) }
}
}

View File

@ -0,0 +1,10 @@
import JolkEngine
protocol Scene
{
mutating func setup(render: inout Renderer)
mutating func loadContent(content: inout ContentManager) throws
mutating func update(deltaTime: Float)
func draw(render: inout Renderer, deltaTime: Float, aspectRatio aspect: Float)
}

View File

@ -0,0 +1,194 @@
import JolkEngine
import Foundation
import simd
struct Scene1: Scene
{
private var env = Environment()
private var worldMesh = RenderMesh<VertexPositionNormalColourTexcoord>.empty
private var cube = RenderMesh<VertexPositionNormalTexcoord>.empty
private var suzanne = RenderMesh<VertexPositionNormalTexcoord>.empty
private var toybox = RenderMesh<VertexPositionNormalTexcoord>.empty
private var jolkTex = Texture2D.empty, suzanneDiffuse = Texture2D.empty
private var drawEdges = false
private var world = Collision()
private var colin = Colin()
private var jolkCube = JolkCube(position: .init(0, 1, -4))
private var lightTheta: Float = 0
mutating func setup(render: inout any JolkEngine.Renderer)
{
render.clearColour = XnaColour.CornflowerBlue
env.setFog(
colour: XnaColour.CornflowerBlue,
//mode: .depth, falloff: .range(type: .linear, start: 4, end: 38))
//mode: .depth, falloff: .range(type: .linear, start: 1.25, end: 24.5))
//mode: .distance, falloff: .factor(type: .exp2, density: 0.1))
mode: .depth, falloff: .factor(type: .exp2, density: 0.0222))
env.addAmbientLight(colour: XnaColour.DarkSlateGray.lighten(by: -0.666))
env.addDirectionalLight(
direction: -Vec3f(1, -1, -1).normalised,
colour: XnaColour.BlanchedAlmond
.lighten(by: -0.02)
.mix(with: XnaColour.LightSlateGray, 0.5)
.mix(with: XnaColour.White, 0.125))
env.addPointLight(
position: Vec3f(3, 0.33, -5),
colour: XnaColour.Green.mix(with: XnaColour.Gray, 0.5),
intensity: 2)
env.addPointLight(
position: Vec3f(5.5, 0.33, -6),
colour: XnaColour.Red.mix(with: XnaColour.Gray, 0.5),
intensity: 2)
env.addPointLight(
position: Vec3f(4, 0.33, -7),
colour: XnaColour.Blue.mix(with: XnaColour.Gray, 0.5),
intensity: 2)
}
mutating func loadContent(content: inout JolkEngine.ContentManager) throws
{
try loadWorld(&content)
cube = try content.create(mesh: .init(
vertices: [
.init(position: Vec3f(-1, -1, 1), normal: .forward, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, -1, 1), normal: .forward, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, 1, 1), normal: .forward, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, 1, 1), normal: .forward, texCoord: Vec2f(1, 1)),
.init(position: Vec3f( 1, -1, 1), normal: .right, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, -1, -1), normal: .right, texCoord: Vec2f(1, 0)),
.init(position: Vec3f( 1, 1, 1), normal: .right, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, 1, -1), normal: .right, texCoord: Vec2f(1, 1)),
.init(position: Vec3f( 1, -1, -1), normal: .back, texCoord: Vec2f(0, 0)),
.init(position: Vec3f(-1, -1, -1), normal: .back, texCoord: Vec2f(1, 0)),
.init(position: Vec3f( 1, 1, -1), normal: .back, texCoord: Vec2f(0, 1)),
.init(position: Vec3f(-1, 1, -1), normal: .back, texCoord: Vec2f(1, 1)),
.init(position: Vec3f(-1, -1, -1), normal: .left, texCoord: Vec2f(0, 0)),
.init(position: Vec3f(-1, -1, 1), normal: .left, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, 1, -1), normal: .left, texCoord: Vec2f(0, 1)),
.init(position: Vec3f(-1, 1, 1), normal: .left, texCoord: Vec2f(1, 1)),
.init(position: Vec3f(-1, -1, -1), normal: .down, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, -1, -1), normal: .down, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, -1, 1), normal: .down, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, -1, 1), normal: .down, texCoord: Vec2f(1, 1)),
.init(position: Vec3f(-1, 1, 1), normal: .up, texCoord: Vec2f(0, 0)),
.init(position: Vec3f( 1, 1, 1), normal: .up, texCoord: Vec2f(1, 0)),
.init(position: Vec3f(-1, 1, -1), normal: .up, texCoord: Vec2f(0, 1)),
.init(position: Vec3f( 1, 1, -1), normal: .up, texCoord: Vec2f(1, 1))
],
indices: [
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
8, 9, 10, 10, 9, 11,
12, 13, 14, 14, 13, 15,
16, 17, 18, 18, 17, 19,
20, 21, 22, 22, 21, 23 ]))
suzanne = try content.load("suzanne.g3db")
toybox = try content.load("toybox.g3db")
let linearMipped = Texture2DParameters(
minFilter: .linear, magFilter: .linear,
wrapMode: .repeating,
mipMode: .linear)
let linearClamp = Texture2DParameters(
minFilter: .linear, magFilter: .linear,
wrapMode: .clampEdge)
jolkTex = try content.load("jolkmeup.jpg", params: linearClamp)
suzanneDiffuse = try content.load("suzanne_diffuse.png", params: linearMipped)
}
private mutating func loadWorld(_ content: inout ContentManager) throws
{
let name = "World.obj"
let obj = try ObjReader.read(url: try content.getResource(name))
let mesh: Mesh = try ObjLoader.read(model: obj, content: &content)
worldMesh = try content.create(mesh: mesh)
if let collision = obj.objects["Collision3D"]
{
world.build(obj: obj, collision: collision)
}
}
mutating func update(deltaTime: Float)
{
colin.update(deltaTime: deltaTime, world: world)
jolkCube.update(deltaTime: deltaTime, world: world)
// Update point lights
lightTheta += deltaTime
var theta = Vec3f(repeating: lightTheta) * Vec3f(0.12, 0.011, 0.056) * 6
var i = env.lights.startIndex
while i != env.lights.endIndex
{
if case .point(let colour, _, _) = env.lights[i]
{
env.lights[i] = .point(
colour: colour,
position: Vec3f(
4 + 6 * cos(theta[0]),
0.33 + 0.33 * sin(theta[1]),
-6 + 3 * sin(theta[0] * 2)),
intensity: 3.1 + 1.53 * cos(theta[2]))
let spacing = 0.5 * Float.pi * (2 / 3.0)
theta += spacing * Vec3f(1, 0.98, 0.5566)
}
i = env.lights.index(after: i)
}
if Input.instance.keyboard.keyPressed(.c) { drawEdges = !drawEdges }
}
func draw(render: inout Renderer, deltaTime: Float, aspectRatio aspect: Float)
{
// Setup camera
render.setProjection(matrix:
.perspective(fovY: Float.rad(fromDeg: colin.fov), aspect: aspect, zNear: 0.1, zFar: 100))
render.setView(matrix: colin.transform)
// Draw world
for s in worldMesh.subMeshes
{
//render.setMaterial(Material(
// specular: XnaColour.BlanchedAlmond.mix(with: .black, 0.12),
// texture: texture.id))
if s.material != -1
{
render.setMaterial(worldMesh.materials[s.material])
}
else
{
render.setMaterial(.init())
}
render.draw(mesh: worldMesh, subMesh: s, environment: env)
}
// Draw jolked up shit
render.setMaterial(Material(
specular: XnaColour.Gray,
gloss: 20.0,
texture: jolkTex.id))
render.draw(mesh: cube, model: jolkCube.transform, environment: env)
render.setMaterial(Material(texture: suzanneDiffuse.id))
render.draw(mesh: suzanne, model: .translate(.up + Vec3f(3.0, 0.0, -3.5) * 2.5), environment: env)
render.draw(mesh: toybox,
model: .translate(Vec3f(6.0, 0.667, -3.5) * Vec3f(2.5, 1, 2.5))
* .rotate(y: lightTheta * 0.5) * .scale(scalar: 0.25), environment: env)
if drawEdges
{
world.draw(render, position: colin.position)
}
}
}

Binary file not shown.