mirror of
				https://github.com/GayPizzaSpecifications/voxelotl-engine.git
				synced 2025-11-04 10:59:39 +00:00 
			
		
		
		
	break up gameplay stuff
This commit is contained in:
		@ -4,16 +4,17 @@ import QuartzCore.CAMetalLayer
 | 
			
		||||
 | 
			
		||||
public class Application {
 | 
			
		||||
  private let cfg: ApplicationConfiguration
 | 
			
		||||
  private let del: GameDelegate
 | 
			
		||||
 | 
			
		||||
  private var window: OpaquePointer? = nil
 | 
			
		||||
  private var view: SDL_MetalView? = nil
 | 
			
		||||
  private var renderer: Renderer? = nil
 | 
			
		||||
  private var lastCounter: UInt64 = 0
 | 
			
		||||
  private var fpsCalculator = FPSCalculator()
 | 
			
		||||
  private var time: Duration = .zero
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  public init(configuration: ApplicationConfiguration) {
 | 
			
		||||
  public init(delegate: GameDelegate, configuration: ApplicationConfiguration) {
 | 
			
		||||
    self.cfg = configuration
 | 
			
		||||
    self.del = delegate
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private func initialize() -> ApplicationExecutionState {
 | 
			
		||||
@ -48,7 +49,7 @@ public class Application {
 | 
			
		||||
    do {
 | 
			
		||||
      let layer = unsafeBitCast(SDL_Metal_GetLayer(view), to: CAMetalLayer.self)
 | 
			
		||||
      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) {
 | 
			
		||||
      printErr("Renderer init error: \(message)")
 | 
			
		||||
      return .exitFailure
 | 
			
		||||
@ -103,8 +104,9 @@ public class Application {
 | 
			
		||||
      return .running
 | 
			
		||||
 | 
			
		||||
    case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
 | 
			
		||||
      let backBufferSize = SIMD2(Int(event.window.data1), Int(event.window.data2))
 | 
			
		||||
      renderer!.resize(size: backBufferSize)
 | 
			
		||||
      let backBufferSize = Size(Int(event.window.data1), Int(event.window.data2))
 | 
			
		||||
      self.renderer!.resize(size: backBufferSize)
 | 
			
		||||
      self.del.resize(backBufferSize)
 | 
			
		||||
      return .running
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
@ -112,13 +114,17 @@ public class Application {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private func update(_ deltaTime: Double) -> ApplicationExecutionState {
 | 
			
		||||
    fpsCalculator.frame(deltaTime: deltaTime) { fps in
 | 
			
		||||
      print("FPS: \(fps)")
 | 
			
		||||
    }
 | 
			
		||||
  private func update() -> ApplicationExecutionState {
 | 
			
		||||
    let deltaTime = getDeltaTime()
 | 
			
		||||
    time += deltaTime
 | 
			
		||||
    let gameTime = GameTime(total: time, delta: deltaTime)
 | 
			
		||||
 | 
			
		||||
    del.update(gameTime)
 | 
			
		||||
 | 
			
		||||
    do {
 | 
			
		||||
      try renderer!.paint()
 | 
			
		||||
      try renderer!.beginFrame()
 | 
			
		||||
      del.draw(renderer!, gameTime)
 | 
			
		||||
      renderer!.endFrame()
 | 
			
		||||
    } catch RendererError.drawFailure(let message) {
 | 
			
		||||
      printErr("Renderer draw error: \(message)")
 | 
			
		||||
      return .exitFailure
 | 
			
		||||
@ -130,11 +136,14 @@ public class Application {
 | 
			
		||||
    return .running
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private func getDeltaTime() -> Double {
 | 
			
		||||
  private func getDeltaTime() -> Duration {
 | 
			
		||||
    let counter = SDL_GetPerformanceCounter()
 | 
			
		||||
    let divisor = 1.0 / Double(SDL_GetPerformanceFrequency())
 | 
			
		||||
    defer { lastCounter = counter }
 | 
			
		||||
    return Double(counter &- lastCounter) * divisor
 | 
			
		||||
    defer {
 | 
			
		||||
      lastCounter = counter
 | 
			
		||||
    }
 | 
			
		||||
    let difference = Double(counter &- lastCounter)
 | 
			
		||||
    let divisor = Double(SDL_GetPerformanceFrequency())
 | 
			
		||||
    return Duration.seconds(difference / divisor)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func run() -> Int32 {
 | 
			
		||||
@ -149,9 +158,7 @@ public class Application {
 | 
			
		||||
          break quit
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let deltaTime = getDeltaTime()
 | 
			
		||||
      res = update(deltaTime)
 | 
			
		||||
      res = update()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return res == .exitSuccess ? 0 : 1
 | 
			
		||||
 | 
			
		||||
@ -8,13 +8,19 @@ add_executable(Voxelotl MACOSX_BUNDLE
 | 
			
		||||
 | 
			
		||||
  FloatExtensions.swift
 | 
			
		||||
  Matrix4x4.swift
 | 
			
		||||
  Rectangle.swift
 | 
			
		||||
 | 
			
		||||
  NSImageLoader.swift
 | 
			
		||||
  Renderer.swift
 | 
			
		||||
  GameController.swift
 | 
			
		||||
  Camera.swift
 | 
			
		||||
  Player.swift
 | 
			
		||||
  FPSCalculator.swift
 | 
			
		||||
  GameDelegate.swift
 | 
			
		||||
  Application.swift
 | 
			
		||||
 | 
			
		||||
  Game.swift
 | 
			
		||||
 | 
			
		||||
  main.swift)
 | 
			
		||||
 | 
			
		||||
set_source_files_properties(
 | 
			
		||||
 | 
			
		||||
@ -1,37 +1,113 @@
 | 
			
		||||
import simd
 | 
			
		||||
 | 
			
		||||
struct Camera {
 | 
			
		||||
  private var position = SIMD3<Float>.zero
 | 
			
		||||
  private var rotation = SIMD2<Float>.zero
 | 
			
		||||
public final class Camera {
 | 
			
		||||
  private struct Dirty: OptionSet {
 | 
			
		||||
    let rawValue: UInt8
 | 
			
		||||
 | 
			
		||||
  var view: matrix_float4x4 {
 | 
			
		||||
    .rotate(yawPitch: rotation) * .translate(-position)
 | 
			
		||||
    static let projection = Self(rawValue: 1 << 0)
 | 
			
		||||
    static let view       = Self(rawValue: 1 << 1)
 | 
			
		||||
    static let viewProj   = Self(rawValue: 1 << 2)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  mutating func update(deltaTime: Float) {
 | 
			
		||||
    if let pad = GameController.current?.state {
 | 
			
		||||
      let turning = pad.rightStick.radialDeadzone(min: 0.1, max: 1)
 | 
			
		||||
      rotation += turning * deltaTime
 | 
			
		||||
      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)
 | 
			
		||||
  private var _position: SIMD3<Float>
 | 
			
		||||
  private var _rotation: simd_quatf
 | 
			
		||||
  private var _fieldOfView: Float
 | 
			
		||||
  private var _aspectRatio: Float
 | 
			
		||||
  private var _zNearFar: ClosedRange<Float>
 | 
			
		||||
 | 
			
		||||
      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)
 | 
			
		||||
      position += .init(
 | 
			
		||||
        movement.x * rotc - movement.y * rots,
 | 
			
		||||
        0,
 | 
			
		||||
        movement.y * rotc + movement.x * rots
 | 
			
		||||
      ) * deltaTime
 | 
			
		||||
 | 
			
		||||
      if pad.pressed(.back) {
 | 
			
		||||
        position = .zero
 | 
			
		||||
        rotation = .zero
 | 
			
		||||
  public var position: SIMD3<Float> {
 | 
			
		||||
    get { self._position }
 | 
			
		||||
    set(position) {
 | 
			
		||||
      if self._position != position {
 | 
			
		||||
        self._position = position
 | 
			
		||||
        self._dirty.insert(.view)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
public struct FPSCalculator {
 | 
			
		||||
  private var _accumulator = 0.0
 | 
			
		||||
  private var _accumulator = Duration.zero
 | 
			
		||||
  private var _framesCount = 0
 | 
			
		||||
 | 
			
		||||
  public mutating func frame(deltaTime: Double, result: (_ fps: Int) -> Void) {
 | 
			
		||||
    _framesCount += 1
 | 
			
		||||
    _accumulator += deltaTime
 | 
			
		||||
  public mutating func frame(deltaTime: Duration, result: (_ fps: Int) -> Void) {
 | 
			
		||||
    self._framesCount += 1
 | 
			
		||||
    self._accumulator += deltaTime
 | 
			
		||||
 | 
			
		||||
    if _accumulator >= 1.0 {
 | 
			
		||||
      result(_framesCount)
 | 
			
		||||
    if self._accumulator >= Duration.seconds(1) {
 | 
			
		||||
      result(self._framesCount)
 | 
			
		||||
 | 
			
		||||
      _framesCount = 0
 | 
			
		||||
      _accumulator = fmod(_accumulator, 1.0)
 | 
			
		||||
      self._framesCount = 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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -68,20 +68,20 @@ public extension simd_float4x4 {
 | 
			
		||||
    let x = 2 * invWidth, y = 2 * invHeight, z = invDepth
 | 
			
		||||
 | 
			
		||||
    return .init(
 | 
			
		||||
    .init( x,  0,  0, 0),
 | 
			
		||||
    .init( 0,  y,  0, 0),
 | 
			
		||||
    .init( 0,  0,  z, 0),
 | 
			
		||||
    .init(tx, ty, tz, 1))
 | 
			
		||||
      .init( x,  0,  0, 0),
 | 
			
		||||
      .init( 0,  y,  0, 0),
 | 
			
		||||
      .init( 0,  0,  z, 0),
 | 
			
		||||
      .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 invClipRange = 1 / (near - far)
 | 
			
		||||
    let invClipRange = 1 / (zNear - zFar)
 | 
			
		||||
 | 
			
		||||
    let y = 1 / tanHalfFovY
 | 
			
		||||
    let x = y / aspect
 | 
			
		||||
    let z = far * invClipRange
 | 
			
		||||
    let w = near * z // (far * near) * invClipRange
 | 
			
		||||
    let z = zFar * invClipRange
 | 
			
		||||
    let w = zNear * z
 | 
			
		||||
    return .init(
 | 
			
		||||
      .init(x, 0, 0,  0),
 | 
			
		||||
      .init(0, y, 0,  0),
 | 
			
		||||
@ -89,3 +89,7 @@ public extension simd_float4x4 {
 | 
			
		||||
      .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 depthFormat: MTLPixelFormat = .depth16Unorm
 | 
			
		||||
 | 
			
		||||
class Renderer {
 | 
			
		||||
public class Renderer {
 | 
			
		||||
  private var device: MTLDevice
 | 
			
		||||
  private var layer: CAMetalLayer
 | 
			
		||||
  private var viewport: MTLViewport
 | 
			
		||||
  private var aspectRatio: Float
 | 
			
		||||
  private var backBufferSize: Size<Int>
 | 
			
		||||
  private var _aspectRatio: Float
 | 
			
		||||
  private var queue: MTLCommandQueue
 | 
			
		||||
  private var lib: MTLLibrary
 | 
			
		||||
  private let passDescription = MTLRenderPassDescriptor()
 | 
			
		||||
@ -55,12 +55,19 @@ class Renderer {
 | 
			
		||||
  private var depthStencilState: MTLDepthStencilState
 | 
			
		||||
  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 defaultTexture: MTLTexture
 | 
			
		||||
  private var cubeTexture: MTLTexture? = nil
 | 
			
		||||
 | 
			
		||||
  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? {
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
    // Select best Metal device
 | 
			
		||||
@ -89,8 +96,8 @@ class Renderer {
 | 
			
		||||
    }
 | 
			
		||||
    self.queue = queue
 | 
			
		||||
 | 
			
		||||
    self.viewport = Self.makeViewport(size: size)
 | 
			
		||||
    self.aspectRatio = Float(size.x) / Float(size.y)
 | 
			
		||||
    self.backBufferSize = size
 | 
			
		||||
    self._aspectRatio = Float(self.backBufferSize.w) / Float(self.backBufferSize.w)
 | 
			
		||||
 | 
			
		||||
    passDescription.colorAttachments[0].loadAction  = .clear
 | 
			
		||||
    passDescription.colorAttachments[0].storeAction = .store
 | 
			
		||||
@ -239,12 +246,12 @@ class Renderer {
 | 
			
		||||
    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? {
 | 
			
		||||
    let texDescriptor = MTLTextureDescriptor.texture2DDescriptor(
 | 
			
		||||
      pixelFormat: format,
 | 
			
		||||
      width:       size.x,
 | 
			
		||||
      height:      size.y,
 | 
			
		||||
      width:       size.w,
 | 
			
		||||
      height:      size.h,
 | 
			
		||||
      mipmapped:   false)
 | 
			
		||||
    texDescriptor.depth = 1
 | 
			
		||||
    texDescriptor.sampleCount = 1
 | 
			
		||||
@ -261,111 +268,110 @@ class Renderer {
 | 
			
		||||
    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(
 | 
			
		||||
      originX: 0.0, originY: 0.0,
 | 
			
		||||
      width:  Double(size.x),
 | 
			
		||||
      height: Double(size.y),
 | 
			
		||||
      znear: 0, zfar: 1)
 | 
			
		||||
      originX: Double(rect.x),
 | 
			
		||||
      originY: Double(rect.y),
 | 
			
		||||
      width:   Double(rect.w),
 | 
			
		||||
      height:  Double(rect.h),
 | 
			
		||||
      znear: znear, zfar: zfar)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func resize(size: SIMD2<Int>) {
 | 
			
		||||
    if Int(self.viewport.width) != size.x || Int(self.viewport.height) != size.y {
 | 
			
		||||
  func resize(size: Size<Int>) {
 | 
			
		||||
    if self.backBufferSize.w != size.w || self.backBufferSize.h != size.h {
 | 
			
		||||
      self.depthTextures = (0..<numFramesInFlight).map { _ in
 | 
			
		||||
        Self.createDepthTexture(device, size, format: depthFormat)!
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.aspectRatio = Float(size.x) / Float(size.y)
 | 
			
		||||
    self.viewport = Self.makeViewport(size: size)
 | 
			
		||||
    self.backBufferSize = size
 | 
			
		||||
    self._aspectRatio = Float(self.backBufferSize.w) / Float(self.backBufferSize.h)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  //FIXME: temp
 | 
			
		||||
  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)
 | 
			
		||||
 | 
			
		||||
  func beginFrame() throws {
 | 
			
		||||
    // Lock the semaphore here if too many frames are "in flight"
 | 
			
		||||
    _ = inFlightSemaphore.wait(timeout: .distantFuture)
 | 
			
		||||
 | 
			
		||||
    guard let rt = layer.nextDrawable() else {
 | 
			
		||||
      throw RendererError.drawFailure("Failed to get next drawable render target")
 | 
			
		||||
    }
 | 
			
		||||
    self._rt = rt
 | 
			
		||||
 | 
			
		||||
    passDescription.colorAttachments[0].texture = rt.texture
 | 
			
		||||
    passDescription.depthAttachment.texture = self.depthTextures[self.frame]
 | 
			
		||||
    passDescription.colorAttachments[0].texture = self._rt.texture
 | 
			
		||||
    passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame]
 | 
			
		||||
 | 
			
		||||
    guard let commandBuf: MTLCommandBuffer = queue.makeCommandBuffer() else {
 | 
			
		||||
      throw RendererError.drawFailure("Failed to make command buffer from queue")
 | 
			
		||||
    }
 | 
			
		||||
    commandBuf.addCompletedHandler { _ in
 | 
			
		||||
    self._commandBuf = commandBuf
 | 
			
		||||
    self._commandBuf.addCompletedHandler { _ in
 | 
			
		||||
      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")
 | 
			
		||||
    }
 | 
			
		||||
    self._encoder = encoder
 | 
			
		||||
 | 
			
		||||
    encoder.setCullMode(.back)
 | 
			
		||||
    encoder.setFrontFacing(.counterClockwise)  // OpenGL default
 | 
			
		||||
    encoder.setViewport(viewport)
 | 
			
		||||
    encoder.setRenderPipelineState(pso)
 | 
			
		||||
    encoder.setDepthStencilState(depthStencilState)
 | 
			
		||||
    self._encoder.setCullMode(.back)
 | 
			
		||||
    self._encoder.setFrontFacing(.counterClockwise)  // OpenGL default
 | 
			
		||||
    self._encoder.setViewport(Self.makeViewport(rect: self.frame))
 | 
			
		||||
    self._encoder.setRenderPipelineState(pso)
 | 
			
		||||
    self._encoder.setDepthStencilState(depthStencilState)
 | 
			
		||||
 | 
			
		||||
    encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0)
 | 
			
		||||
    encoder.setVertexBuffer(vtxBuffer,
 | 
			
		||||
    self._encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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,
 | 
			
		||||
      index: ShaderInputIdx.vertices.rawValue)
 | 
			
		||||
 | 
			
		||||
    // Ideal as long as our uniforms total 4 KB or less
 | 
			
		||||
    encoder.setVertexBytes(instances,
 | 
			
		||||
    self._encoder.setVertexBytes(instances,
 | 
			
		||||
      length: instances.count * MemoryLayout<ShaderInstance>.stride,
 | 
			
		||||
      index: ShaderInputIdx.instance.rawValue)
 | 
			
		||||
    encoder.setVertexBytes(&uniforms,
 | 
			
		||||
    self._encoder.setVertexBytes(&uniforms,
 | 
			
		||||
      length: MemoryLayout<ShaderUniforms>.stride,
 | 
			
		||||
      index: ShaderInputIdx.uniforms.rawValue)
 | 
			
		||||
 | 
			
		||||
    encoder.drawIndexedPrimitives(
 | 
			
		||||
    self._encoder.drawIndexedPrimitives(
 | 
			
		||||
      type: .triangle,
 | 
			
		||||
      indexCount: cubeIndices.count,
 | 
			
		||||
      indexType: .uint16,
 | 
			
		||||
      indexBuffer: idxBuffer,
 | 
			
		||||
      indexBufferOffset: 0,
 | 
			
		||||
      instanceCount: instances.count)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    encoder.endEncoding()
 | 
			
		||||
    commandBuf.present(rt)
 | 
			
		||||
    commandBuf.commit()
 | 
			
		||||
  func endFrame() {
 | 
			
		||||
    self._encoder.endEncoding()
 | 
			
		||||
    self._commandBuf.present(self._rt)
 | 
			
		||||
    self._commandBuf.commit()
 | 
			
		||||
 | 
			
		||||
    self.frame &+= 1
 | 
			
		||||
    if self.frame == numFramesInFlight {
 | 
			
		||||
      self.frame = 0
 | 
			
		||||
    self._rt = nil
 | 
			
		||||
    self._encoder = nil
 | 
			
		||||
    self._commandBuf = nil
 | 
			
		||||
 | 
			
		||||
    self.currentFrame &+= 1
 | 
			
		||||
    if self.currentFrame == numFramesInFlight {
 | 
			
		||||
      self.currentFrame = 0
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,10 @@
 | 
			
		||||
import Darwin
 | 
			
		||||
 | 
			
		||||
var rect = Rect(origin: .init(0, 0), size: .init(32, 32))
 | 
			
		||||
rect.origin += Point(10, 10)
 | 
			
		||||
 | 
			
		||||
let app = Application(
 | 
			
		||||
  delegate: Game(),
 | 
			
		||||
  configuration: ApplicationConfiguration(
 | 
			
		||||
  width: 1280,
 | 
			
		||||
  height: 720,
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ vertex FragmentInput vertexMain(
 | 
			
		||||
 | 
			
		||||
  FragmentInput out;
 | 
			
		||||
  out.position = ndc;
 | 
			
		||||
  out.color    = half4(i[instanceID].color);
 | 
			
		||||
  out.color    = half4(i[instanceID].color) / 255.0;
 | 
			
		||||
  out.normal   = vtx[vertexID].normal;
 | 
			
		||||
  out.texCoord = vtx[vertexID].texCoord;
 | 
			
		||||
  return out;
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ typedef struct {
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
  matrix_float4x4 model;
 | 
			
		||||
  vector_float4 color;
 | 
			
		||||
  vector_uchar4 color;
 | 
			
		||||
} ShaderInstance;
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user