mirror of
https://github.com/GayPizzaSpecifications/voxelotl-engine.git
synced 2025-08-03 13:11:33 +00:00
break up gameplay stuff
This commit is contained in:
@ -4,16 +4,17 @@ import QuartzCore.CAMetalLayer
|
|||||||
|
|
||||||
public class Application {
|
public class Application {
|
||||||
private let cfg: ApplicationConfiguration
|
private let cfg: ApplicationConfiguration
|
||||||
|
private let del: GameDelegate
|
||||||
|
|
||||||
private var window: OpaquePointer? = nil
|
private var window: OpaquePointer? = nil
|
||||||
private var view: SDL_MetalView? = nil
|
private var view: SDL_MetalView? = nil
|
||||||
private var renderer: Renderer? = nil
|
private var renderer: Renderer? = nil
|
||||||
private var lastCounter: UInt64 = 0
|
private var lastCounter: UInt64 = 0
|
||||||
private var fpsCalculator = FPSCalculator()
|
private var time: Duration = .zero
|
||||||
|
|
||||||
|
public init(delegate: GameDelegate, configuration: ApplicationConfiguration) {
|
||||||
public init(configuration: ApplicationConfiguration) {
|
|
||||||
self.cfg = configuration
|
self.cfg = configuration
|
||||||
|
self.del = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initialize() -> ApplicationExecutionState {
|
private func initialize() -> ApplicationExecutionState {
|
||||||
@ -48,7 +49,7 @@ public class Application {
|
|||||||
do {
|
do {
|
||||||
let layer = unsafeBitCast(SDL_Metal_GetLayer(view), to: CAMetalLayer.self)
|
let layer = unsafeBitCast(SDL_Metal_GetLayer(view), to: CAMetalLayer.self)
|
||||||
layer.displaySyncEnabled = cfg.vsyncMode == .off ? false : true
|
layer.displaySyncEnabled = cfg.vsyncMode == .off ? false : true
|
||||||
self.renderer = try Renderer(layer: layer, size: SIMD2<Int>(Int(backBufferWidth), Int(backBufferHeight)))
|
self.renderer = try Renderer(layer: layer, size: .init(Int(backBufferWidth), Int(backBufferHeight)))
|
||||||
} catch RendererError.initFailure(let message) {
|
} catch RendererError.initFailure(let message) {
|
||||||
printErr("Renderer init error: \(message)")
|
printErr("Renderer init error: \(message)")
|
||||||
return .exitFailure
|
return .exitFailure
|
||||||
@ -103,8 +104,9 @@ public class Application {
|
|||||||
return .running
|
return .running
|
||||||
|
|
||||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||||
let backBufferSize = SIMD2(Int(event.window.data1), Int(event.window.data2))
|
let backBufferSize = Size(Int(event.window.data1), Int(event.window.data2))
|
||||||
renderer!.resize(size: backBufferSize)
|
self.renderer!.resize(size: backBufferSize)
|
||||||
|
self.del.resize(backBufferSize)
|
||||||
return .running
|
return .running
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -112,13 +114,17 @@ public class Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(_ deltaTime: Double) -> ApplicationExecutionState {
|
private func update() -> ApplicationExecutionState {
|
||||||
fpsCalculator.frame(deltaTime: deltaTime) { fps in
|
let deltaTime = getDeltaTime()
|
||||||
print("FPS: \(fps)")
|
time += deltaTime
|
||||||
}
|
let gameTime = GameTime(total: time, delta: deltaTime)
|
||||||
|
|
||||||
|
del.update(gameTime)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try renderer!.paint()
|
try renderer!.beginFrame()
|
||||||
|
del.draw(renderer!, gameTime)
|
||||||
|
renderer!.endFrame()
|
||||||
} catch RendererError.drawFailure(let message) {
|
} catch RendererError.drawFailure(let message) {
|
||||||
printErr("Renderer draw error: \(message)")
|
printErr("Renderer draw error: \(message)")
|
||||||
return .exitFailure
|
return .exitFailure
|
||||||
@ -130,11 +136,14 @@ public class Application {
|
|||||||
return .running
|
return .running
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getDeltaTime() -> Double {
|
private func getDeltaTime() -> Duration {
|
||||||
let counter = SDL_GetPerformanceCounter()
|
let counter = SDL_GetPerformanceCounter()
|
||||||
let divisor = 1.0 / Double(SDL_GetPerformanceFrequency())
|
defer {
|
||||||
defer { lastCounter = counter }
|
lastCounter = counter
|
||||||
return Double(counter &- lastCounter) * divisor
|
}
|
||||||
|
let difference = Double(counter &- lastCounter)
|
||||||
|
let divisor = Double(SDL_GetPerformanceFrequency())
|
||||||
|
return Duration.seconds(difference / divisor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() -> Int32 {
|
func run() -> Int32 {
|
||||||
@ -149,9 +158,7 @@ public class Application {
|
|||||||
break quit
|
break quit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
res = update()
|
||||||
let deltaTime = getDeltaTime()
|
|
||||||
res = update(deltaTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res == .exitSuccess ? 0 : 1
|
return res == .exitSuccess ? 0 : 1
|
||||||
|
@ -8,13 +8,19 @@ add_executable(Voxelotl MACOSX_BUNDLE
|
|||||||
|
|
||||||
FloatExtensions.swift
|
FloatExtensions.swift
|
||||||
Matrix4x4.swift
|
Matrix4x4.swift
|
||||||
|
Rectangle.swift
|
||||||
|
|
||||||
NSImageLoader.swift
|
NSImageLoader.swift
|
||||||
Renderer.swift
|
Renderer.swift
|
||||||
GameController.swift
|
GameController.swift
|
||||||
Camera.swift
|
Camera.swift
|
||||||
|
Player.swift
|
||||||
FPSCalculator.swift
|
FPSCalculator.swift
|
||||||
|
GameDelegate.swift
|
||||||
Application.swift
|
Application.swift
|
||||||
|
|
||||||
|
Game.swift
|
||||||
|
|
||||||
main.swift)
|
main.swift)
|
||||||
|
|
||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
|
@ -1,37 +1,113 @@
|
|||||||
import simd
|
import simd
|
||||||
|
|
||||||
struct Camera {
|
public final class Camera {
|
||||||
private var position = SIMD3<Float>.zero
|
private struct Dirty: OptionSet {
|
||||||
private var rotation = SIMD2<Float>.zero
|
let rawValue: UInt8
|
||||||
|
|
||||||
var view: matrix_float4x4 {
|
static let projection = Self(rawValue: 1 << 0)
|
||||||
.rotate(yawPitch: rotation) * .translate(-position)
|
static let view = Self(rawValue: 1 << 1)
|
||||||
|
static let viewProj = Self(rawValue: 1 << 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func update(deltaTime: Float) {
|
private var _position: SIMD3<Float>
|
||||||
if let pad = GameController.current?.state {
|
private var _rotation: simd_quatf
|
||||||
let turning = pad.rightStick.radialDeadzone(min: 0.1, max: 1)
|
private var _fieldOfView: Float
|
||||||
rotation += turning * deltaTime
|
private var _aspectRatio: Float
|
||||||
if rotation.x < 0.0 {
|
private var _zNearFar: ClosedRange<Float>
|
||||||
rotation.x += .pi * 2
|
|
||||||
} else if rotation.x > .pi * 2 {
|
|
||||||
rotation.x -= .pi * 2
|
|
||||||
}
|
|
||||||
rotation.y = rotation.y.clamp(-.pi * 0.5, .pi * 0.5)
|
|
||||||
|
|
||||||
let movement = pad.leftStick.cardinalDeadzone(min: 0.1, max: 1)
|
private var _dirty: Dirty
|
||||||
|
private var _projection: matrix_float4x4
|
||||||
|
private var _view: matrix_float4x4
|
||||||
|
private var _viewProjection: matrix_float4x4
|
||||||
|
|
||||||
let rotc = cos(rotation.x), rots = sin(rotation.x)
|
public var position: SIMD3<Float> {
|
||||||
position += .init(
|
get { self._position }
|
||||||
movement.x * rotc - movement.y * rots,
|
set(position) {
|
||||||
0,
|
if self._position != position {
|
||||||
movement.y * rotc + movement.x * rots
|
self._position = position
|
||||||
) * deltaTime
|
self._dirty.insert(.view)
|
||||||
|
|
||||||
if pad.pressed(.back) {
|
|
||||||
position = .zero
|
|
||||||
rotation = .zero
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var rotation: simd_quatf {
|
||||||
|
get { self._rotation }
|
||||||
|
set(rotation) {
|
||||||
|
if self._rotation != rotation {
|
||||||
|
self._rotation = rotation
|
||||||
|
self._dirty.insert(.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var fieldOfView: Float {
|
||||||
|
get { self._fieldOfView.degrees }
|
||||||
|
set(fov) {
|
||||||
|
let fovRad = fov.radians
|
||||||
|
self._fieldOfView = fovRad
|
||||||
|
self._dirty.insert(.projection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var aspectRatio: Float {
|
||||||
|
get { self._aspectRatio }
|
||||||
|
set(aspect) {
|
||||||
|
if self._aspectRatio != aspect {
|
||||||
|
self._aspectRatio = aspect
|
||||||
|
self._dirty.insert(.projection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setSize(_ size: Size<Int>) {
|
||||||
|
self.aspectRatio = Float(size.w) / Float(size.h)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var range: ClosedRange<Float> {
|
||||||
|
get { self._zNearFar }
|
||||||
|
set(range) {
|
||||||
|
self._zNearFar = range
|
||||||
|
self._dirty.insert(.projection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var projection: matrix_float4x4 {
|
||||||
|
if self._dirty.contains(.projection) {
|
||||||
|
self._projection = .perspective(
|
||||||
|
verticalFov: self._fieldOfView,
|
||||||
|
aspect: self._aspectRatio,
|
||||||
|
near: self._zNearFar.lowerBound,
|
||||||
|
far: self._zNearFar.upperBound)
|
||||||
|
self._dirty.remove(.projection)
|
||||||
|
self._dirty.insert(.viewProj)
|
||||||
|
}
|
||||||
|
return self._projection
|
||||||
|
}
|
||||||
|
public var view: matrix_float4x4 {
|
||||||
|
if self._dirty.contains(.view) {
|
||||||
|
self._view = matrix_float4x4(rotation) * .translate(-position)
|
||||||
|
self._dirty.remove(.view)
|
||||||
|
self._dirty.insert(.viewProj)
|
||||||
|
}
|
||||||
|
return self._view
|
||||||
|
}
|
||||||
|
public var viewProjection: matrix_float4x4 {
|
||||||
|
if !self._dirty.isEmpty {
|
||||||
|
self._viewProjection = self.projection * self.view
|
||||||
|
self._dirty.remove(.viewProj)
|
||||||
|
}
|
||||||
|
return self._viewProjection
|
||||||
|
}
|
||||||
|
|
||||||
|
init(fov: Float, size: Size<Int>, range: ClosedRange<Float>) {
|
||||||
|
self._position = .zero
|
||||||
|
self._rotation = .identity
|
||||||
|
self._fieldOfView = fov.radians
|
||||||
|
self._aspectRatio = Float(size.w) / Float(size.h)
|
||||||
|
self._zNearFar = range
|
||||||
|
self._dirty = [ .projection, .view, .viewProj ]
|
||||||
|
self._projection = .init()
|
||||||
|
self._view = .init()
|
||||||
|
self._viewProjection = .init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct FPSCalculator {
|
public struct FPSCalculator {
|
||||||
private var _accumulator = 0.0
|
private var _accumulator = Duration.zero
|
||||||
private var _framesCount = 0
|
private var _framesCount = 0
|
||||||
|
|
||||||
public mutating func frame(deltaTime: Double, result: (_ fps: Int) -> Void) {
|
public mutating func frame(deltaTime: Duration, result: (_ fps: Int) -> Void) {
|
||||||
_framesCount += 1
|
self._framesCount += 1
|
||||||
_accumulator += deltaTime
|
self._accumulator += deltaTime
|
||||||
|
|
||||||
if _accumulator >= 1.0 {
|
if self._accumulator >= Duration.seconds(1) {
|
||||||
result(_framesCount)
|
result(self._framesCount)
|
||||||
|
|
||||||
_framesCount = 0
|
self._framesCount = 0
|
||||||
_accumulator = fmod(_accumulator, 1.0)
|
self._accumulator = .init(
|
||||||
|
secondsComponent: 0,
|
||||||
|
attosecondsComponent: self._accumulator.components.attoseconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
Sources/Voxelotl/Game.swift
Normal file
53
Sources/Voxelotl/Game.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import simd
|
||||||
|
|
||||||
|
struct Instance {
|
||||||
|
var position: SIMD3<Float> = .zero
|
||||||
|
var scale: SIMD3<Float> = .one
|
||||||
|
var rotation: simd_quatf = .identity
|
||||||
|
var color: SIMD4<Float> = .one
|
||||||
|
}
|
||||||
|
|
||||||
|
class Game: GameDelegate {
|
||||||
|
|
||||||
|
private var fpsCalculator = FPSCalculator()
|
||||||
|
var camera = Camera(fov: 60, size: .one, range: 0.03...25)
|
||||||
|
var player = Player()
|
||||||
|
var projection: matrix_float4x4 = .identity
|
||||||
|
|
||||||
|
func fixedUpdate(_ time: GameTime) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(_ time: GameTime) {
|
||||||
|
let deltaTime = Float(time.delta.asFloat)
|
||||||
|
fpsCalculator.frame(deltaTime: time.delta) { fps in
|
||||||
|
print("FPS: \(fps)")
|
||||||
|
}
|
||||||
|
|
||||||
|
player.update(deltaTime: deltaTime)
|
||||||
|
camera.position = player.position
|
||||||
|
camera.rotation =
|
||||||
|
simd_quatf(angle: player.rotation.y, axis: .init(1, 0, 0)) *
|
||||||
|
simd_quatf(angle: player.rotation.x, axis: .init(0, 1, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(_ renderer: Renderer, _ time: GameTime) {
|
||||||
|
let totalTime = Float(time.total.asFloat)
|
||||||
|
|
||||||
|
let instances: [Instance] = [
|
||||||
|
Instance(
|
||||||
|
position: .init(0, sin(totalTime * 1.5) * 0.5, -2),
|
||||||
|
scale: .init(repeating: 0.25),
|
||||||
|
rotation: .init(angle: totalTime * 3.0, axis: .init(0, 1, 0)),
|
||||||
|
color: .init(0.5, 0.5, 1, 1)),
|
||||||
|
Instance(position: .init(0, -1, 0), scale: .init(10, 0.1, 10)),
|
||||||
|
Instance(position: .init(-2.5, 0, -3), color: .init(1, 0.5, 0.75, 1)),
|
||||||
|
Instance(position: .init(-2.5, -0.5, -5), color: .init(0.75, 1, 1, 1))
|
||||||
|
]
|
||||||
|
renderer.batch(instances: instances, camera: self.camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(_ size: Size<Int>) {
|
||||||
|
self.camera.setSize(size)
|
||||||
|
}
|
||||||
|
}
|
23
Sources/Voxelotl/GameDelegate.swift
Normal file
23
Sources/Voxelotl/GameDelegate.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
public protocol GameDelegate {
|
||||||
|
func fixedUpdate(_ time: GameTime)
|
||||||
|
func update(_ time: GameTime)
|
||||||
|
func draw(_ renderer: Renderer, _ time: GameTime)
|
||||||
|
func resize(_ size: Size<Int>)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension GameDelegate {
|
||||||
|
func fixedUpdate(_ time: GameTime) {}
|
||||||
|
func update(_ time: GameTime) {}
|
||||||
|
func resize(_ size: Size<Int>) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GameTime {
|
||||||
|
let total: Duration
|
||||||
|
let delta: Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Duration {
|
||||||
|
var asFloat: Double {
|
||||||
|
Double(components.seconds) + Double(components.attoseconds) * 1e-18
|
||||||
|
}
|
||||||
|
}
|
@ -74,14 +74,14 @@ public extension simd_float4x4 {
|
|||||||
.init(tx, ty, tz, 1))
|
.init(tx, ty, tz, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func perspective(verticalFov fovY: T, aspect: T, near: T, far: T) -> Self {
|
static func perspective(verticalFov fovY: T, aspect: T, near zNear: T, far zFar: T) -> Self {
|
||||||
let tanHalfFovY = tan(fovY * T(0.5))
|
let tanHalfFovY = tan(fovY * T(0.5))
|
||||||
let invClipRange = 1 / (near - far)
|
let invClipRange = 1 / (zNear - zFar)
|
||||||
|
|
||||||
let y = 1 / tanHalfFovY
|
let y = 1 / tanHalfFovY
|
||||||
let x = y / aspect
|
let x = y / aspect
|
||||||
let z = far * invClipRange
|
let z = zFar * invClipRange
|
||||||
let w = near * z // (far * near) * invClipRange
|
let w = zNear * z
|
||||||
return .init(
|
return .init(
|
||||||
.init(x, 0, 0, 0),
|
.init(x, 0, 0, 0),
|
||||||
.init(0, y, 0, 0),
|
.init(0, y, 0, 0),
|
||||||
@ -89,3 +89,7 @@ public extension simd_float4x4 {
|
|||||||
.init(0, 0, w, 0))
|
.init(0, 0, w, 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension simd_quatf {
|
||||||
|
static var identity: Self { .init(real: 1, imag: .zero) }
|
||||||
|
}
|
||||||
|
36
Sources/Voxelotl/Player.swift
Normal file
36
Sources/Voxelotl/Player.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import simd
|
||||||
|
|
||||||
|
struct Player {
|
||||||
|
private var _position = SIMD3<Float>.zero
|
||||||
|
private var _rotation = SIMD2<Float>.zero
|
||||||
|
|
||||||
|
public var position: SIMD3<Float> { self._position }
|
||||||
|
public var rotation: SIMD2<Float> { self._rotation }
|
||||||
|
|
||||||
|
mutating func update(deltaTime: Float) {
|
||||||
|
if let pad = GameController.current?.state {
|
||||||
|
let turning = pad.rightStick.radialDeadzone(min: 0.1, max: 1)
|
||||||
|
_rotation += turning * deltaTime * 3.0
|
||||||
|
if _rotation.x < 0.0 {
|
||||||
|
_rotation.x += .pi * 2
|
||||||
|
} else if _rotation.x > .pi * 2 {
|
||||||
|
_rotation.x -= .pi * 2
|
||||||
|
}
|
||||||
|
_rotation.y = _rotation.y.clamp(-.pi * 0.5, .pi * 0.5)
|
||||||
|
|
||||||
|
let movement = pad.leftStick.cardinalDeadzone(min: 0.1, max: 1)
|
||||||
|
|
||||||
|
let rotc = cos(_rotation.x), rots = sin(_rotation.x)
|
||||||
|
_position += .init(
|
||||||
|
movement.x * rotc - movement.y * rots,
|
||||||
|
0,
|
||||||
|
movement.y * rotc + movement.x * rots
|
||||||
|
) * deltaTime * 3.0
|
||||||
|
|
||||||
|
if pad.pressed(.back) {
|
||||||
|
_position = .zero
|
||||||
|
_rotation = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
Sources/Voxelotl/Rectangle.swift
Normal file
99
Sources/Voxelotl/Rectangle.swift
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
struct Point<T: AdditiveArithmetic>: Equatable {
|
||||||
|
var x: T, y: T
|
||||||
|
|
||||||
|
static var zero: Self { .init(.zero, .zero) }
|
||||||
|
|
||||||
|
init(_ x: T, _ y: T) {
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always) static func == (lhs: Self, rhs: Self) -> Bool { lhs.x == rhs.x && lhs.y == rhs.y }
|
||||||
|
@inline(__always) static func != (lhs: Self, rhs: Self) -> Bool { lhs.x != rhs.x || lhs.y != rhs.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Point where T: AdditiveArithmetic {
|
||||||
|
@inline(__always) static func + (lhs: Self, rhs: Self) -> Self { Self(lhs.x + rhs.x, lhs.y + rhs.y) }
|
||||||
|
@inline(__always) static func - (lhs: Self, rhs: Self) -> Self { Self(lhs.x - rhs.x, lhs.y - rhs.y) }
|
||||||
|
|
||||||
|
@inline(__always) static func += (lhs: inout Self, rhs: Self) { lhs.x += rhs.x; lhs.y += rhs.y }
|
||||||
|
@inline(__always) static func -= (lhs: inout Self, rhs: Self) { lhs.x -= rhs.x; lhs.y -= rhs.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SIMD2 where Scalar: AdditiveArithmetic {
|
||||||
|
init(_ point: Point<Scalar>) {
|
||||||
|
self.init(point.x, point.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Size<T: AdditiveArithmetic>: Equatable {
|
||||||
|
var w: T, h: T
|
||||||
|
|
||||||
|
static var zero: Self { .init(.zero, .zero) }
|
||||||
|
|
||||||
|
init(_ w: T, _ h: T) {
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always) public static func == (lhs: Self, rhs: Self) -> Bool { lhs.w == rhs.w && lhs.h == rhs.h }
|
||||||
|
@inline(__always) public static func != (lhs: Self, rhs: Self) -> Bool { lhs.w != rhs.w || lhs.h != rhs.h }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Size where T: BinaryInteger {
|
||||||
|
static var one: Self { .init(T(1), T(1)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Rect<T: AdditiveArithmetic>: Equatable {
|
||||||
|
var x: T, y: T, w: T, h: T
|
||||||
|
|
||||||
|
var origin: Point<T> {
|
||||||
|
get { .init(self.x, self.y) }
|
||||||
|
set(point) { self.x = point.x; self.y = point.y }
|
||||||
|
}
|
||||||
|
var size: Size<T> {
|
||||||
|
get { .init(self.w, self.h) }
|
||||||
|
set(size) { self.w = size.w; self.h = size.h }
|
||||||
|
}
|
||||||
|
|
||||||
|
static var zero: Self { .init(origin: .zero, size: .zero) }
|
||||||
|
|
||||||
|
init(x: T, y: T, width: T, height: T) {
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = width
|
||||||
|
self.h = height
|
||||||
|
}
|
||||||
|
|
||||||
|
init(origin: Point<T>, size: Size<T>) {
|
||||||
|
self.x = origin.x
|
||||||
|
self.y = origin.y
|
||||||
|
self.w = size.w
|
||||||
|
self.h = size.h
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always) static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.x == rhs.x && lhs.y == rhs.y && lhs.w == rhs.w && lhs.h == rhs.h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Rect where T: AdditiveArithmetic {
|
||||||
|
var left: T { x }
|
||||||
|
var right: T { x + w }
|
||||||
|
var up: T { y }
|
||||||
|
var down: T { y + h }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Extent<T: AdditiveArithmetic>: Equatable {
|
||||||
|
var top: T, bottom: T, left: T, right: T
|
||||||
|
|
||||||
|
@inline(__always) static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.left == rhs.left && lhs.right == rhs.right && lhs.top == rhs.top && lhs.bottom == rhs.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Extent where T: Comparable {
|
||||||
|
var size: Size<T> { .init(
|
||||||
|
right > left ? right - left : left - right,
|
||||||
|
bottom > top ? bottom - top : top - bottom) }
|
||||||
|
}
|
@ -43,11 +43,11 @@ fileprivate let cubeIndices: [UInt16] = [
|
|||||||
fileprivate let numFramesInFlight: Int = 3
|
fileprivate let numFramesInFlight: Int = 3
|
||||||
fileprivate let depthFormat: MTLPixelFormat = .depth16Unorm
|
fileprivate let depthFormat: MTLPixelFormat = .depth16Unorm
|
||||||
|
|
||||||
class Renderer {
|
public class Renderer {
|
||||||
private var device: MTLDevice
|
private var device: MTLDevice
|
||||||
private var layer: CAMetalLayer
|
private var layer: CAMetalLayer
|
||||||
private var viewport: MTLViewport
|
private var backBufferSize: Size<Int>
|
||||||
private var aspectRatio: Float
|
private var _aspectRatio: Float
|
||||||
private var queue: MTLCommandQueue
|
private var queue: MTLCommandQueue
|
||||||
private var lib: MTLLibrary
|
private var lib: MTLLibrary
|
||||||
private let passDescription = MTLRenderPassDescriptor()
|
private let passDescription = MTLRenderPassDescriptor()
|
||||||
@ -55,12 +55,19 @@ class Renderer {
|
|||||||
private var depthStencilState: MTLDepthStencilState
|
private var depthStencilState: MTLDepthStencilState
|
||||||
private var depthTextures: [MTLTexture]
|
private var depthTextures: [MTLTexture]
|
||||||
|
|
||||||
|
private var _commandBuf: MTLCommandBuffer!
|
||||||
|
private var _encoder: MTLRenderCommandEncoder!
|
||||||
|
private var _rt: (any CAMetalDrawable)!
|
||||||
|
|
||||||
private var vtxBuffer: MTLBuffer, idxBuffer: MTLBuffer
|
private var vtxBuffer: MTLBuffer, idxBuffer: MTLBuffer
|
||||||
private var defaultTexture: MTLTexture
|
private var defaultTexture: MTLTexture
|
||||||
private var cubeTexture: MTLTexture? = nil
|
private var cubeTexture: MTLTexture? = nil
|
||||||
|
|
||||||
private let inFlightSemaphore = DispatchSemaphore(value: numFramesInFlight)
|
private let inFlightSemaphore = DispatchSemaphore(value: numFramesInFlight)
|
||||||
private var frame = 0
|
private var currentFrame = 0
|
||||||
|
|
||||||
|
var frame: Rect<Int> { .init(origin: .zero, size: self.backBufferSize) }
|
||||||
|
var aspectRatio: Float { self._aspectRatio }
|
||||||
|
|
||||||
fileprivate static func createMetalDevice() -> MTLDevice? {
|
fileprivate static func createMetalDevice() -> MTLDevice? {
|
||||||
MTLCopyAllDevices().reduce(nil, { best, dev in
|
MTLCopyAllDevices().reduce(nil, { best, dev in
|
||||||
@ -71,7 +78,7 @@ class Renderer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
init(layer metalLayer: CAMetalLayer, size: SIMD2<Int>) throws {
|
internal init(layer metalLayer: CAMetalLayer, size: Size<Int>) throws {
|
||||||
self.layer = metalLayer
|
self.layer = metalLayer
|
||||||
|
|
||||||
// Select best Metal device
|
// Select best Metal device
|
||||||
@ -89,8 +96,8 @@ class Renderer {
|
|||||||
}
|
}
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
|
|
||||||
self.viewport = Self.makeViewport(size: size)
|
self.backBufferSize = size
|
||||||
self.aspectRatio = Float(size.x) / Float(size.y)
|
self._aspectRatio = Float(self.backBufferSize.w) / Float(self.backBufferSize.w)
|
||||||
|
|
||||||
passDescription.colorAttachments[0].loadAction = .clear
|
passDescription.colorAttachments[0].loadAction = .clear
|
||||||
passDescription.colorAttachments[0].storeAction = .store
|
passDescription.colorAttachments[0].storeAction = .store
|
||||||
@ -239,12 +246,12 @@ class Renderer {
|
|||||||
return newTexture
|
return newTexture
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func createDepthTexture(_ device: MTLDevice, _ size: SIMD2<Int>, format: MTLPixelFormat
|
private static func createDepthTexture(_ device: MTLDevice, _ size: Size<Int>, format: MTLPixelFormat
|
||||||
) -> MTLTexture? {
|
) -> MTLTexture? {
|
||||||
let texDescriptor = MTLTextureDescriptor.texture2DDescriptor(
|
let texDescriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||||
pixelFormat: format,
|
pixelFormat: format,
|
||||||
width: size.x,
|
width: size.w,
|
||||||
height: size.y,
|
height: size.h,
|
||||||
mipmapped: false)
|
mipmapped: false)
|
||||||
texDescriptor.depth = 1
|
texDescriptor.depth = 1
|
||||||
texDescriptor.sampleCount = 1
|
texDescriptor.sampleCount = 1
|
||||||
@ -261,111 +268,110 @@ class Renderer {
|
|||||||
return depthStencilTexture
|
return depthStencilTexture
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewport(size: SIMD2<Int>) -> MTLViewport {
|
static func makeViewport(rect: Rect<Int>, znear: Double = 0.0, zfar: Double = 1.0) -> MTLViewport {
|
||||||
MTLViewport(
|
MTLViewport(
|
||||||
originX: 0.0, originY: 0.0,
|
originX: Double(rect.x),
|
||||||
width: Double(size.x),
|
originY: Double(rect.y),
|
||||||
height: Double(size.y),
|
width: Double(rect.w),
|
||||||
znear: 0, zfar: 1)
|
height: Double(rect.h),
|
||||||
|
znear: znear, zfar: zfar)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resize(size: SIMD2<Int>) {
|
func resize(size: Size<Int>) {
|
||||||
if Int(self.viewport.width) != size.x || Int(self.viewport.height) != size.y {
|
if self.backBufferSize.w != size.w || self.backBufferSize.h != size.h {
|
||||||
self.depthTextures = (0..<numFramesInFlight).map { _ in
|
self.depthTextures = (0..<numFramesInFlight).map { _ in
|
||||||
Self.createDepthTexture(device, size, format: depthFormat)!
|
Self.createDepthTexture(device, size, format: depthFormat)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.aspectRatio = Float(size.x) / Float(size.y)
|
self.backBufferSize = size
|
||||||
self.viewport = Self.makeViewport(size: size)
|
self._aspectRatio = Float(self.backBufferSize.w) / Float(self.backBufferSize.h)
|
||||||
}
|
}
|
||||||
|
|
||||||
//FIXME: temp
|
func beginFrame() throws {
|
||||||
var camera = Camera()
|
|
||||||
var time: Float = 0
|
|
||||||
|
|
||||||
func paint() throws {
|
|
||||||
camera.update(deltaTime: 0.025)
|
|
||||||
#if true
|
|
||||||
let projection = matrix_float4x4.perspective(
|
|
||||||
verticalFov: Float(60.0).radians,
|
|
||||||
aspect: aspectRatio,
|
|
||||||
near: 0.03,
|
|
||||||
far: 25)
|
|
||||||
#else
|
|
||||||
let projection = matrix_float4x4.orthographic(
|
|
||||||
left: -aspectRatio, right: aspectRatio,
|
|
||||||
bottom: -1, top: 1,
|
|
||||||
near: -0.03, far: -25)
|
|
||||||
#endif
|
|
||||||
let view = camera.view
|
|
||||||
|
|
||||||
let instances: [ShaderInstance] = [
|
|
||||||
ShaderInstance(model: .translate(.init(0, sin(time * 0.5) * 0.5, -2)) * .rotate(y: time) * .scale(0.25), color: .init(0.5, 0.5, 1, 1)),
|
|
||||||
ShaderInstance(model: .translate(.init(0, -1, 0)) * .scale(.init(10, 0.1, 10)), color: .init(1, 1, 1, 1)),
|
|
||||||
ShaderInstance(model: .translate(.init(-2.5, 0, -3)), color: .init(1, 0.5, 0.75, 1)),
|
|
||||||
ShaderInstance(model: .translate(.init(-2.5, -0.5, -5)), color: .init(0.75, 1, 1, 1))
|
|
||||||
]
|
|
||||||
|
|
||||||
time += 0.025
|
|
||||||
|
|
||||||
var uniforms = ShaderUniforms(projView: projection * view)
|
|
||||||
|
|
||||||
// Lock the semaphore here if too many frames are "in flight"
|
// Lock the semaphore here if too many frames are "in flight"
|
||||||
_ = inFlightSemaphore.wait(timeout: .distantFuture)
|
_ = inFlightSemaphore.wait(timeout: .distantFuture)
|
||||||
|
|
||||||
guard let rt = layer.nextDrawable() else {
|
guard let rt = layer.nextDrawable() else {
|
||||||
throw RendererError.drawFailure("Failed to get next drawable render target")
|
throw RendererError.drawFailure("Failed to get next drawable render target")
|
||||||
}
|
}
|
||||||
|
self._rt = rt
|
||||||
|
|
||||||
passDescription.colorAttachments[0].texture = rt.texture
|
passDescription.colorAttachments[0].texture = self._rt.texture
|
||||||
passDescription.depthAttachment.texture = self.depthTextures[self.frame]
|
passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame]
|
||||||
|
|
||||||
guard let commandBuf: MTLCommandBuffer = queue.makeCommandBuffer() else {
|
guard let commandBuf: MTLCommandBuffer = queue.makeCommandBuffer() else {
|
||||||
throw RendererError.drawFailure("Failed to make command buffer from queue")
|
throw RendererError.drawFailure("Failed to make command buffer from queue")
|
||||||
}
|
}
|
||||||
commandBuf.addCompletedHandler { _ in
|
self._commandBuf = commandBuf
|
||||||
|
self._commandBuf.addCompletedHandler { _ in
|
||||||
self.inFlightSemaphore.signal()
|
self.inFlightSemaphore.signal()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let encoder = commandBuf.makeRenderCommandEncoder(descriptor: passDescription) else {
|
guard let encoder = self._commandBuf.makeRenderCommandEncoder(descriptor: passDescription) else {
|
||||||
throw RendererError.drawFailure("Failed to make render encoder from command buffer")
|
throw RendererError.drawFailure("Failed to make render encoder from command buffer")
|
||||||
}
|
}
|
||||||
|
self._encoder = encoder
|
||||||
|
|
||||||
encoder.setCullMode(.back)
|
self._encoder.setCullMode(.back)
|
||||||
encoder.setFrontFacing(.counterClockwise) // OpenGL default
|
self._encoder.setFrontFacing(.counterClockwise) // OpenGL default
|
||||||
encoder.setViewport(viewport)
|
self._encoder.setViewport(Self.makeViewport(rect: self.frame))
|
||||||
encoder.setRenderPipelineState(pso)
|
self._encoder.setRenderPipelineState(pso)
|
||||||
encoder.setDepthStencilState(depthStencilState)
|
self._encoder.setDepthStencilState(depthStencilState)
|
||||||
|
|
||||||
encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0)
|
self._encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0)
|
||||||
encoder.setVertexBuffer(vtxBuffer,
|
}
|
||||||
|
|
||||||
|
func batch(instances: [Instance], camera: Camera) {
|
||||||
|
assert(instances.count < 52)
|
||||||
|
|
||||||
|
var uniforms = ShaderUniforms(projView: camera.viewProjection)
|
||||||
|
let instances = instances.map { (instance: Instance) -> ShaderInstance in
|
||||||
|
ShaderInstance(
|
||||||
|
model:
|
||||||
|
.translate(instance.position) *
|
||||||
|
matrix_float4x4(instance.rotation) *
|
||||||
|
.scale(instance.scale),
|
||||||
|
color: .init(
|
||||||
|
UInt8(instance.color.x * 0xFF),
|
||||||
|
UInt8(instance.color.y * 0xFF),
|
||||||
|
UInt8(instance.color.z * 0xFF),
|
||||||
|
UInt8(instance.color.w * 0xFF)))
|
||||||
|
}
|
||||||
|
|
||||||
|
self._encoder.setVertexBuffer(vtxBuffer,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
index: ShaderInputIdx.vertices.rawValue)
|
index: ShaderInputIdx.vertices.rawValue)
|
||||||
|
|
||||||
// Ideal as long as our uniforms total 4 KB or less
|
// Ideal as long as our uniforms total 4 KB or less
|
||||||
encoder.setVertexBytes(instances,
|
self._encoder.setVertexBytes(instances,
|
||||||
length: instances.count * MemoryLayout<ShaderInstance>.stride,
|
length: instances.count * MemoryLayout<ShaderInstance>.stride,
|
||||||
index: ShaderInputIdx.instance.rawValue)
|
index: ShaderInputIdx.instance.rawValue)
|
||||||
encoder.setVertexBytes(&uniforms,
|
self._encoder.setVertexBytes(&uniforms,
|
||||||
length: MemoryLayout<ShaderUniforms>.stride,
|
length: MemoryLayout<ShaderUniforms>.stride,
|
||||||
index: ShaderInputIdx.uniforms.rawValue)
|
index: ShaderInputIdx.uniforms.rawValue)
|
||||||
|
|
||||||
encoder.drawIndexedPrimitives(
|
self._encoder.drawIndexedPrimitives(
|
||||||
type: .triangle,
|
type: .triangle,
|
||||||
indexCount: cubeIndices.count,
|
indexCount: cubeIndices.count,
|
||||||
indexType: .uint16,
|
indexType: .uint16,
|
||||||
indexBuffer: idxBuffer,
|
indexBuffer: idxBuffer,
|
||||||
indexBufferOffset: 0,
|
indexBufferOffset: 0,
|
||||||
instanceCount: instances.count)
|
instanceCount: instances.count)
|
||||||
|
}
|
||||||
|
|
||||||
encoder.endEncoding()
|
func endFrame() {
|
||||||
commandBuf.present(rt)
|
self._encoder.endEncoding()
|
||||||
commandBuf.commit()
|
self._commandBuf.present(self._rt)
|
||||||
|
self._commandBuf.commit()
|
||||||
|
|
||||||
self.frame &+= 1
|
self._rt = nil
|
||||||
if self.frame == numFramesInFlight {
|
self._encoder = nil
|
||||||
self.frame = 0
|
self._commandBuf = nil
|
||||||
|
|
||||||
|
self.currentFrame &+= 1
|
||||||
|
if self.currentFrame == numFramesInFlight {
|
||||||
|
self.currentFrame = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import Darwin
|
import Darwin
|
||||||
|
|
||||||
|
var rect = Rect(origin: .init(0, 0), size: .init(32, 32))
|
||||||
|
rect.origin += Point(10, 10)
|
||||||
|
|
||||||
let app = Application(
|
let app = Application(
|
||||||
|
delegate: Game(),
|
||||||
configuration: ApplicationConfiguration(
|
configuration: ApplicationConfiguration(
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
|
@ -22,7 +22,7 @@ vertex FragmentInput vertexMain(
|
|||||||
|
|
||||||
FragmentInput out;
|
FragmentInput out;
|
||||||
out.position = ndc;
|
out.position = ndc;
|
||||||
out.color = half4(i[instanceID].color);
|
out.color = half4(i[instanceID].color) / 255.0;
|
||||||
out.normal = vtx[vertexID].normal;
|
out.normal = vtx[vertexID].normal;
|
||||||
out.texCoord = vtx[vertexID].texCoord;
|
out.texCoord = vtx[vertexID].texCoord;
|
||||||
return out;
|
return out;
|
||||||
|
@ -24,7 +24,7 @@ typedef struct {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
matrix_float4x4 model;
|
matrix_float4x4 model;
|
||||||
vector_float4 color;
|
vector_uchar4 color;
|
||||||
} ShaderInstance;
|
} ShaderInstance;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
Reference in New Issue
Block a user