From 477ce10e6802d00f5962e55d479ad24f8f0f4fb1 Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Thu, 8 Aug 2024 16:05:30 +1000 Subject: [PATCH] use controller for moving around a test plane --- Sources/Voxelotl/Application.swift | 26 ++- Sources/Voxelotl/CMakeLists.txt | 2 + Sources/Voxelotl/Camera.swift | 37 ++++ Sources/Voxelotl/FloatExtensions.swift | 3 + Sources/Voxelotl/GameController.swift | 224 +++++++++++++++++++++++++ Sources/Voxelotl/Matrix4x4.swift | 13 ++ Sources/Voxelotl/Renderer.swift | 18 +- 7 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 Sources/Voxelotl/Camera.swift create mode 100644 Sources/Voxelotl/GameController.swift diff --git a/Sources/Voxelotl/Application.swift b/Sources/Voxelotl/Application.swift index 1b24305..05aee21 100644 --- a/Sources/Voxelotl/Application.swift +++ b/Sources/Voxelotl/Application.swift @@ -17,13 +17,13 @@ public class Application { } private func initialize() -> ApplicationExecutionState { - guard SDL_Init(SDL_INIT_VIDEO) >= 0 else { + guard SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD) >= 0 else { printErr("SDL_Init() error: \(String(cString: SDL_GetError()))") return .exitFailure } // Create SDL window - var windowFlags = SDL_WindowFlags(0) + var windowFlags = SDL_WindowFlags(SDL_WINDOW_METAL) if cfg.flags.contains(.resizable) { windowFlags |= SDL_WindowFlags(SDL_WINDOW_RESIZABLE) } @@ -67,6 +67,10 @@ public class Application { SDL_Quit() } + private func beginHandleEvents() { + GameController.instance.newFrame() + } + private func handleEvent(_ event: SDL_Event) -> ApplicationExecutionState { switch SDL_EventType(event.type) { case SDL_EVENT_QUIT: @@ -81,6 +85,23 @@ public class Application { } return .running + case SDL_EVENT_GAMEPAD_ADDED: + if SDL_IsGamepad(event.gdevice.which) != SDL_FALSE { + GameController.instance.connectedEvent(id: event.gdevice.which) + } + return .running + case SDL_EVENT_GAMEPAD_REMOVED: + GameController.instance.removedEvent(id: event.gdevice.which) + return .running + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + GameController.instance.axisEvent(id: event.gaxis.which, + axis: SDL_GamepadAxis(Int32(event.gaxis.axis)), value: event.gaxis.value) + return .running + case SDL_EVENT_GAMEPAD_BUTTON_DOWN, SDL_EVENT_GAMEPAD_BUTTON_UP: + GameController.instance.buttonEvent(id: event.gbutton.which, + btn: SDL_GamepadButton(Int32(event.gbutton.button)), state: event.gbutton.state) + return .running + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: let backBufferSize = SIMD2(Int(event.window.data1), Int(event.window.data2)) renderer!.resize(size: backBufferSize) @@ -120,6 +141,7 @@ public class Application { var res = initialize() quit: while res == .running { + beginHandleEvents() var event = SDL_Event() while SDL_PollEvent(&event) > 0 { res = handleEvent(event) diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index c9d2e17..309ba6d 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -11,6 +11,8 @@ add_executable(Voxelotl MACOSX_BUNDLE NSImageLoader.swift Renderer.swift + GameController.swift + Camera.swift FPSCalculator.swift Application.swift main.swift) diff --git a/Sources/Voxelotl/Camera.swift b/Sources/Voxelotl/Camera.swift new file mode 100644 index 0000000..08467e1 --- /dev/null +++ b/Sources/Voxelotl/Camera.swift @@ -0,0 +1,37 @@ +import simd + +struct Camera { + private var position = SIMD3.zero + private var rotation = SIMD2.zero + + var view: matrix_float4x4 { + .rotate(yawPitch: rotation) * .translate(-position) + } + + 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) + + 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 + + if pad.pressed(.back) { + position = .zero + rotation = .zero + } + } + } +} diff --git a/Sources/Voxelotl/FloatExtensions.swift b/Sources/Voxelotl/FloatExtensions.swift index 8f3fa59..628fe77 100644 --- a/Sources/Voxelotl/FloatExtensions.swift +++ b/Sources/Voxelotl/FloatExtensions.swift @@ -4,4 +4,7 @@ public extension FloatingPoint { @inline(__always) func lerp(_ a: Self, _ b: Self) -> Self { b * self + a * (1 - self) } @inline(__always) func mlerp(_ a: Self, _ b: Self) -> Self { a + (b - a) * self } + + @inline(__always) func clamp(_ a: Self, _ b: Self) -> Self { min(max(self, a), b) } + @inline(__always) func saturate() -> Self { self.clamp(0, 1) } } diff --git a/Sources/Voxelotl/GameController.swift b/Sources/Voxelotl/GameController.swift new file mode 100644 index 0000000..f8bcec5 --- /dev/null +++ b/Sources/Voxelotl/GameController.swift @@ -0,0 +1,224 @@ +import SDL3 + +public class GameController { + public struct Pad { + public enum Axes { + case leftStickX, leftStickY + case rightStickX, rightStickY + case leftTrigger, rightTrigger + + internal var sdlEnum: SDL_GamepadAxis { + switch self { + case .leftStickX: SDL_GAMEPAD_AXIS_LEFTX + case .leftStickY: SDL_GAMEPAD_AXIS_LEFTY + case .rightStickX: SDL_GAMEPAD_AXIS_RIGHTX + case .rightStickY: SDL_GAMEPAD_AXIS_RIGHTY + case .leftTrigger: SDL_GAMEPAD_AXIS_LEFT_TRIGGER + case .rightTrigger: SDL_GAMEPAD_AXIS_RIGHT_TRIGGER + } + } + } + + public struct Buttons: OptionSet { + public let rawValue: Int + public init(rawValue: Int) { self.rawValue = rawValue } + + static let east = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_EAST.rawValue) + static let south = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_SOUTH.rawValue) + static let north = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_NORTH.rawValue) + static let west = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_WEST.rawValue) + static let back = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_BACK.rawValue) + static let start = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_START.rawValue) + static let guide = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_GUIDE.rawValue) + static let leftStick = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_STICK.rawValue) + static let rightStick = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK.rawValue) + static let leftBumper = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER.rawValue) + static let rightBumper = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER.rawValue) + static let dpadLeft = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_LEFT.rawValue) + static let dpadRight = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_RIGHT.rawValue) + static let dpadUp = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_UP.rawValue) + static let dpadDown = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_DOWN.rawValue) + static let misc1 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC1.rawValue) + static let misc2 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC2.rawValue) + static let misc3 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC3.rawValue) + static let misc4 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC4.rawValue) + static let misc5 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC5.rawValue) + static let misc6 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC6.rawValue) + static let paddle1 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1.rawValue) + static let paddle2 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1.rawValue) + static let paddle3 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2.rawValue) + static let paddle4 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE2.rawValue) + static let touchPad = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_TOUCHPAD.rawValue) + } + + public struct State { + private let _axes: [Int16] + private let _btns: Buttons, _btnImpulse: Buttons + + internal init(axes: [Int16], btns: Buttons, btnImpulse: Buttons) { + self._axes = axes + self._btns = btns + self._btnImpulse = btnImpulse + } + + public func axis(_ axis: Axes) -> Float { + let raw = rawAxis(axis) + let rescale = raw < 0 ? 1 / Float(-Int(Int16.min)) : 1 / Float(Int16.max) + return Float(raw) * rescale + } + @inline(__always) func rawAxis(_ axis: Axes) -> Int16 { + _axes[Int(axis.sdlEnum.rawValue)] + } + + public func down(_ btn: Buttons) -> Bool { + btn.isSubset(of: _btns) + } + public func pressed(_ btn: Buttons) -> Bool { + btn.isSubset(of: _btns.intersection(_btnImpulse)) + } + public func released(_ btn: Buttons) -> Bool { + btn.isSubset(of: _btnImpulse.subtracting(_btns)) + } + } + + public var name: String { String(cString: SDL_GetGamepadName(_sdlPad)) } + public var state: State { + .init( + axes: self._axes, + btns: self._btnCur, + btnImpulse: self._btnCur.symmetricDifference(self._btnPrv)) + } + + //MARK: - Private + + private var _joyInstance: SDL_JoystickID, _sdlPad: OpaquePointer + private var _axes = [Int16](repeating: 0, count: Int(SDL_GAMEPAD_AXIS_MAX.rawValue)) + private var _btnCur: Buttons = [], _btnPrv: Buttons = [] + + internal var instanceID: SDL_JoystickID { _joyInstance } + + private init(instance: SDL_JoystickID, pad: OpaquePointer) { + self._joyInstance = instance + self._sdlPad = pad + } + + internal static func open(joystickID: SDL_JoystickID) -> Self? { + return if let sdlPad = SDL_OpenGamepad(joystickID) { + .init(instance: joystickID, pad: sdlPad) + } else { nil } + } + + internal func close() { + SDL_CloseGamepad(self._sdlPad) + } + + internal mutating func buttonEvent(_ btn: SDL_GamepadButton, _ down: Bool) { + if down { + self._btnCur.formUnion(.init(rawValue: 1 << btn.rawValue)) + } else { + self._btnCur.subtract(.init(rawValue: 1 << btn.rawValue)) + } + } + + internal mutating func axisEvent(_ axis: SDL_GamepadAxis, _ value: Int16) { + self._axes[Int(axis.rawValue)] = value + } + + internal mutating func newTick() { + self._btnPrv = self._btnCur + } + } + + public static func getPad(id: Int32) -> Pad? { + _instance._pads[SDL_JoystickID(id)] ?? nil + } + + @inline(__always) public static var current: Pad? { + getPad(id: Int32(_instance._firstID)) + } + + //MARK: - Private + + private static let _instance = GameController() + public static var instance: GameController { _instance } + + private var _pads = Dictionary() + private var _firstID: SDL_JoystickID = 0 + + internal func connectedEvent(id: SDL_JoystickID) { + if _pads.keys.contains(id) { + return + } + if let pad = Pad.open(joystickID: id) { + printErr("Using gamepad #\(pad.instanceID), \"\(pad.name)\"") + if self._firstID == 0 { + self._firstID = id + } + self._pads[id] = pad + } + } + + internal func removedEvent(id: SDL_JoystickID) { + if let pad = self._pads.removeValue(forKey: id) { + pad.close() + } + if id == _firstID { + _firstID = _pads.keys.first ?? 0 + } + } + + internal func buttonEvent(id: SDL_JoystickID, btn: SDL_GamepadButton, state: UInt8) { + _pads[id]?.buttonEvent(btn, state == SDL_PRESSED) + } + + internal func axisEvent(id: SDL_JoystickID, axis: SDL_GamepadAxis, value: Int16) { + _pads[id]?.axisEvent(axis, value) + } + + internal func newFrame() { + for idx in _pads.values.indices { + _pads.values[idx].newTick() + } + } +} + + +//MARK: - Stick convenience functions + +public extension GameController.Pad.State { + var leftStick: SIMD2 { + .init(axis(.leftStickX), axis(.leftStickY)) + } + var rightStick: SIMD2 { + .init(axis(.rightStickX), axis(.rightStickY)) + } +} + +public extension FloatingPoint { + @inline(__always) internal func axisDeadzone(_ min: Self, _ max: Self) -> Self { + let x = abs(self) + return if x <= min { 0 } else if x >= max { + .init(signOf: self, magnitudeOf: 1) + } else { + .init(signOf: self, magnitudeOf: x - min) / (max - min) + } + } +} + +public extension SIMD2 where Scalar: FloatingPoint { + func cardinalDeadzone(min: Scalar, max: Scalar) -> Self { + .init(self.x.axisDeadzone(min, max), self.y.axisDeadzone(min, max)) + } + + func radialDeadzone(min: Scalar, max: Scalar) -> Self { + let magnitude = (x * x + y * y).squareRoot() + if magnitude == .zero || magnitude < min { + return .zero + } else if magnitude > max { + return self / magnitude + } else { + let rescale = (magnitude - min) / (max - min) + return self / magnitude * rescale + } + } +} diff --git a/Sources/Voxelotl/Matrix4x4.swift b/Sources/Voxelotl/Matrix4x4.swift index 532d45e..975dc1c 100644 --- a/Sources/Voxelotl/Matrix4x4.swift +++ b/Sources/Voxelotl/Matrix4x4.swift @@ -43,6 +43,19 @@ public extension simd_float4x4 { .init(0, 0, 0, 1)) } + @inline(__always) static func rotate(yawPitch yp: SIMD2) -> Self { rotate(yaw: yp.x, pitch: yp.y) } + + static func rotate(yaw ytheta: T, pitch xtheta: T) -> Self { + let xc = cos(xtheta), xs = sin(xtheta) + let yc = cos(ytheta), ys = sin(ytheta) + + return .init( + .init(yc, ys * xs, -ys * xc, 0), + .init( 0, xc, xs, 0), + .init(ys, yc * -xs, yc * xc, 0), + .init( 0, 0, 0, 1)) + } + static func orthographic(left: T, right: T, bottom: T, top: T, near: T, far: T) -> Self { let invWidth = 1 / (right - left), diff --git a/Sources/Voxelotl/Renderer.swift b/Sources/Voxelotl/Renderer.swift index fdd0263..4439fcb 100644 --- a/Sources/Voxelotl/Renderer.swift +++ b/Sources/Voxelotl/Renderer.swift @@ -280,26 +280,30 @@ class Renderer { zfar: -1.0) } - var time: Float = 0 //FIXME: temp + //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(90.0).radians, + verticalFov: Float(60.0).radians, aspect: aspectRatio, near: 0.003, - far: 4) + far: 100) #else let projection = matrix_float4x4.orthographic( left: -aspectRatio, right: aspectRatio, bottom: -1, top: 1, near: 0, far: -4) #endif - let view = matrix_float4x4.identity + let view = camera.view let model: matrix_float4x4 = - .translate(.init(0, sin(time * 0.5) * 0.75, -2)) * - .scale(0.5) * - .rotate(y: time) + .translate(.init(0, -1, 0)) * .scale(.init(10, 0.1, 10)) + //.translate(.init(0, sin(time * 0.5) * 0.75, -2)) * + //.scale(0.5) * + //.rotate(y: time) time += 0.025