From da26773221a7f197efc7849da33b236a19ce155b Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Sun, 1 Sep 2024 21:16:05 +1000 Subject: [PATCH] prep for chunk meshing --- Sources/Voxelotl/CMakeLists.txt | 2 + Sources/Voxelotl/Chunk.swift | 15 +++- Sources/Voxelotl/Color.swift | 2 +- Sources/Voxelotl/CubeMeshBuilder.swift | 40 +++++++++ Sources/Voxelotl/Game.swift | 8 +- Sources/Voxelotl/Math/AABB.swift | 32 +++---- Sources/Voxelotl/Math/VectorExtensions.swift | 19 +++- Sources/Voxelotl/Raycast.swift | 12 +-- Sources/Voxelotl/Renderer/Mesh.swift | 16 ++++ Sources/Voxelotl/Renderer/Renderer.swift | 92 +++++++------------- Sources/Voxelotl/World.swift | 10 +++ 11 files changed, 157 insertions(+), 91 deletions(-) create mode 100644 Sources/Voxelotl/CubeMeshBuilder.swift create mode 100644 Sources/Voxelotl/Renderer/Mesh.swift diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index 27d666a..afcab8a 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable(Voxelotl MACOSX_BUNDLE # Renderer classes Renderer/Material.swift Renderer/Environment.swift + Renderer/Mesh.swift Renderer/Renderer.swift # Input wrappers @@ -44,6 +45,7 @@ add_executable(Voxelotl MACOSX_BUNDLE Input/Mouse.swift # Core utility classes + CubeMeshBuilder.swift Color.swift Camera.swift FPSCalculator.swift diff --git a/Sources/Voxelotl/Chunk.swift b/Sources/Voxelotl/Chunk.swift index 33e9e90..bec5e09 100644 --- a/Sources/Voxelotl/Chunk.swift +++ b/Sources/Voxelotl/Chunk.swift @@ -1,4 +1,4 @@ -public struct Chunk { +public struct Chunk: Hashable { public static let shift = 4 // 16 public static let size: Int = 1 << shift public static let mask: Int = size - 1 @@ -63,6 +63,15 @@ public struct Chunk { } } + public func forEach(_ body: @escaping (Block, SIMD3) throws -> Void) rethrows { + for i in 0..> Self.shift) & Self.mask, + z: (i &>> (Self.shift + Self.shift)) & Self.mask)) + } + } + public func map(block transform: (Block, SIMD3) throws -> T) rethrows -> [T] { assert(self.blocks.count == Self.blockCount) @@ -112,12 +121,12 @@ public struct Chunk { } } -public enum BlockType: Equatable { +public enum BlockType: Hashable { case air case solid(_ color: Color) } -public struct Block { +public struct Block: Hashable { public var type: BlockType public init(_ type: BlockType) { diff --git a/Sources/Voxelotl/Color.swift b/Sources/Voxelotl/Color.swift index 121853a..6aa3d72 100644 --- a/Sources/Voxelotl/Color.swift +++ b/Sources/Voxelotl/Color.swift @@ -1,6 +1,6 @@ import Foundation -public struct Color: Equatable { +public struct Color: Hashable { private var _values: SIMD4 internal var values: SIMD4 { self._values } diff --git a/Sources/Voxelotl/CubeMeshBuilder.swift b/Sources/Voxelotl/CubeMeshBuilder.swift new file mode 100644 index 0000000..0b1cfe7 --- /dev/null +++ b/Sources/Voxelotl/CubeMeshBuilder.swift @@ -0,0 +1,40 @@ +public struct CubeMeshBuilder { + public static func build(bound: AABB) -> Mesh { + let cubeVertices: [VertexPositionNormalTexcoord] = [ + .init(position: .init( bound.left, bound.bottom, bound.near), normal: .back, texCoord: .init(0, 0)), + .init(position: .init(bound.right, bound.bottom, bound.near), normal: .back, texCoord: .init(1, 0)), + .init(position: .init( bound.left, bound.top, bound.near), normal: .back, texCoord: .init(0, 1)), + .init(position: .init(bound.right, bound.top, bound.near), normal: .back, texCoord: .init(1, 1)), + .init(position: .init(bound.right, bound.bottom, bound.near), normal: .right, texCoord: .init(0, 0)), + .init(position: .init(bound.right, bound.bottom, bound.far), normal: .right, texCoord: .init(1, 0)), + .init(position: .init(bound.right, bound.top, bound.near), normal: .right, texCoord: .init(0, 1)), + .init(position: .init(bound.right, bound.top, bound.far), normal: .right, texCoord: .init(1, 1)), + .init(position: .init(bound.right, bound.bottom, bound.far), normal: .forward, texCoord: .init(0, 0)), + .init(position: .init( bound.left, bound.bottom, bound.far), normal: .forward, texCoord: .init(1, 0)), + .init(position: .init(bound.right, bound.top, bound.far), normal: .forward, texCoord: .init(0, 1)), + .init(position: .init( bound.left, bound.top, bound.far), normal: .forward, texCoord: .init(1, 1)), + .init(position: .init( bound.left, bound.bottom, bound.far), normal: .left, texCoord: .init(0, 0)), + .init(position: .init( bound.left, bound.bottom, bound.near), normal: .left, texCoord: .init(1, 0)), + .init(position: .init( bound.left, bound.top, bound.far), normal: .left, texCoord: .init(0, 1)), + .init(position: .init( bound.left, bound.top, bound.near), normal: .left, texCoord: .init(1, 1)), + .init(position: .init( bound.left, bound.bottom, bound.far), normal: .down, texCoord: .init(0, 0)), + .init(position: .init(bound.right, bound.bottom, bound.far), normal: .down, texCoord: .init(1, 0)), + .init(position: .init( bound.left, bound.bottom, bound.near), normal: .down, texCoord: .init(0, 1)), + .init(position: .init(bound.right, bound.bottom, bound.near), normal: .down, texCoord: .init(1, 1)), + .init(position: .init( bound.left, bound.top, bound.near), normal: .up, texCoord: .init(0, 0)), + .init(position: .init(bound.right, bound.top, bound.near), normal: .up, texCoord: .init(1, 0)), + .init(position: .init( bound.left, bound.top, bound.far), normal: .up, texCoord: .init(0, 1)), + .init(position: .init(bound.right, bound.top, bound.far), normal: .up, texCoord: .init(1, 1)) + ] + return .init(vertices: cubeVertices, indices: cubeIndices) + } +} + +fileprivate let cubeIndices: [UInt16] = [ + 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 +] diff --git a/Sources/Voxelotl/Game.swift b/Sources/Voxelotl/Game.swift index 8671526..5148d5c 100644 --- a/Sources/Voxelotl/Game.swift +++ b/Sources/Voxelotl/Game.swift @@ -25,11 +25,15 @@ class Game: GameDelegate { var player = Player() var projection: matrix_float4x4 = .identity var world = World() + var cubeMesh: RendererMesh? + var renderChunks = [SIMD3: Mesh]() func create(_ renderer: Renderer) { self.resetPlayer() self.generateWorld() + self.cubeMesh = renderer.createMesh(CubeMeshBuilder.build(bound: .fromUnitCube(position: .zero, scale: .one))) + renderer.clearColor = Color.black.mix(.white, 0.1).linear } @@ -104,8 +108,8 @@ class Game: GameDelegate { .init(angle: totalTime * 0.7, axis: .init(0, 0, 1)), color: .init(r: 0.5, g: 0.5, b: 1).linear)) } - if !instances.isEmpty { - renderer.batch(instances: instances, material: material, environment: env, camera: self.camera) + if self.cubeMesh != nil && !instances.isEmpty { + renderer.batch(instances: instances, mesh: self.cubeMesh!, material: material, environment: env, camera: self.camera) } } diff --git a/Sources/Voxelotl/Math/AABB.swift b/Sources/Voxelotl/Math/AABB.swift index 1bdb0bc..e4be5be 100644 --- a/Sources/Voxelotl/Math/AABB.swift +++ b/Sources/Voxelotl/Math/AABB.swift @@ -1,45 +1,45 @@ import simd -struct AABB { +public struct AABB { private var _bounds: simd_float2x3 - var lower: SIMD3 { + public var lower: SIMD3 { get { _bounds[0] } set(row) { self._bounds[0] = row } } - var upper: SIMD3 { + public var upper: SIMD3 { get { _bounds[1] } set(row) { self._bounds[1] = row } } - var center: SIMD3 { + public var center: SIMD3 { get { (self._bounds[0] + self._bounds[1]) / 2 } } - var size: SIMD3 { + public var size: SIMD3 { get { self._bounds[1] - self._bounds[0] } } - var left: Float { self._bounds[0].x } - var bottom: Float { self._bounds[0].y } - var far: Float { self._bounds[0].z } - var right: Float { self._bounds[1].x } - var top: Float { self._bounds[1].y } - var near: Float { self._bounds[1].z } + public var left: Float { self._bounds[0].x } + public var bottom: Float { self._bounds[0].y } + public var far: Float { self._bounds[0].z } + public var right: Float { self._bounds[1].x } + public var top: Float { self._bounds[1].y } + public var near: Float { self._bounds[1].z } private init(bounds: simd_float2x3) { self._bounds = bounds } - init(from: SIMD3, to: SIMD3) { + public init(from: SIMD3, to: SIMD3) { self.init(bounds: .init(from, to)) } - static func fromUnitCube(position: SIMD3 = .zero, scale: SIMD3 = .one) -> Self { + public static func fromUnitCube(position: SIMD3 = .zero, scale: SIMD3 = .one) -> Self { self.init( from: position - scale, to: position + scale) } - func touching(_ other: Self) -> Bool{ + public func touching(_ other: Self) -> Bool{ let distLower = other._bounds[0] - self._bounds[1] // x: left, y: bottom, z: far let distUpper = self._bounds[0] - other._bounds[1] // x: right, y: top, z: near @@ -51,8 +51,8 @@ struct AABB { } } -extension AABB { - static func + (lhs: Self, rhs: SIMD3) -> Self { +public extension AABB { + public static func + (lhs: Self, rhs: SIMD3) -> Self { .init(bounds: lhs._bounds + .init(rhs, rhs)) } } diff --git a/Sources/Voxelotl/Math/VectorExtensions.swift b/Sources/Voxelotl/Math/VectorExtensions.swift index d83755a..8542869 100644 --- a/Sources/Voxelotl/Math/VectorExtensions.swift +++ b/Sources/Voxelotl/Math/VectorExtensions.swift @@ -22,11 +22,13 @@ public extension SIMD3 { } } -public extension SIMD3 where Scalar: FloatingPoint { - @inline(__always) static var X: Self { Self(1, 0, 0) } - @inline(__always) static var Y: Self { Self(0, 1, 0) } - @inline(__always) static var Z: Self { Self(0, 0, 1) } +public extension SIMD3 where Scalar: Numeric { + @inline(__always) static var X: Self { Self(1, 0, 0) } + @inline(__always) static var Y: Self { Self(0, 1, 0) } + @inline(__always) static var Z: Self { Self(0, 0, 1) } +} +public extension SIMD3 where Scalar: FloatingPoint { @inline(__always) static var up: Self { Y } @inline(__always) static var down: Self { -Y } @inline(__always) static var left: Self { -X } @@ -35,6 +37,15 @@ public extension SIMD3 where Scalar: FloatingPoint { @inline(__always) static var back: Self { Z } } +public extension SIMD3 where Scalar: SignedInteger & FixedWidthInteger { + @inline(__always) static var up: Self { Y } + @inline(__always) static var down: Self { 0 &- Y } + @inline(__always) static var left: Self { 0 &- X } + @inline(__always) static var right: Self { X } + @inline(__always) static var forward: Self { 0 &- Z } + @inline(__always) static var back: Self { Z } +} + public extension SIMD3 where Scalar: Numeric & AdditiveArithmetic { @inline(__always) func dot(_ b: Self) -> Scalar { self.x * b.x + self.y * b.y + self.z * b.z } } diff --git a/Sources/Voxelotl/Raycast.swift b/Sources/Voxelotl/Raycast.swift index b73f8c9..077fe7c 100644 --- a/Sources/Voxelotl/Raycast.swift +++ b/Sources/Voxelotl/Raycast.swift @@ -90,12 +90,12 @@ public enum RaycastSide { public extension SIMD3 where Scalar == Int { func offset(by side: RaycastSide) -> Self { let ofs: Self = switch side { - case .right: .init( 1, 0, 0) - case .left: .init(-1, 0, 0) - case .up: .init( 0, 1, 0) - case .down: .init( 0, -1, 0) - case .back: .init( 0, 0, 1) - case .front: .init( 0, 0, -1) + case .right: .right + case .left: .left + case .up: .up + case .down: .down + case .back: .back + case .front: .forward } return self &+ ofs } diff --git a/Sources/Voxelotl/Renderer/Mesh.swift b/Sources/Voxelotl/Renderer/Mesh.swift new file mode 100644 index 0000000..4a49f22 --- /dev/null +++ b/Sources/Voxelotl/Renderer/Mesh.swift @@ -0,0 +1,16 @@ +public struct Mesh { + public let vertices: [VertexType] + public let indices: [IndexType] +} + +public extension Mesh { + static var empty: Self { .init(vertices: .init(), indices: .init()) } +} + +public protocol Vertex: Equatable {} + +public struct VertexPositionNormalTexcoord: Vertex { + var position: SIMD3 + var normal: SIMD3 + var texCoord: SIMD2 +} diff --git a/Sources/Voxelotl/Renderer/Renderer.swift b/Sources/Voxelotl/Renderer/Renderer.swift index ad90427..5d2125d 100644 --- a/Sources/Voxelotl/Renderer/Renderer.swift +++ b/Sources/Voxelotl/Renderer/Renderer.swift @@ -4,42 +4,6 @@ import QuartzCore.CAMetalLayer import simd import ShaderTypes -fileprivate let cubeVertices: [ShaderVertex] = [ - .init(position: .init(-1, -1, 1), normal: .back, texCoord: .init(0, 0)), - .init(position: .init( 1, -1, 1), normal: .back, texCoord: .init(1, 0)), - .init(position: .init(-1, 1, 1), normal: .back, texCoord: .init(0, 1)), - .init(position: .init( 1, 1, 1), normal: .back, texCoord: .init(1, 1)), - .init(position: .init( 1, -1, 1), normal: .right, texCoord: .init(0, 0)), - .init(position: .init( 1, -1, -1), normal: .right, texCoord: .init(1, 0)), - .init(position: .init( 1, 1, 1), normal: .right, texCoord: .init(0, 1)), - .init(position: .init( 1, 1, -1), normal: .right, texCoord: .init(1, 1)), - .init(position: .init( 1, -1, -1), normal: .forward, texCoord: .init(0, 0)), - .init(position: .init(-1, -1, -1), normal: .forward, texCoord: .init(1, 0)), - .init(position: .init( 1, 1, -1), normal: .forward, texCoord: .init(0, 1)), - .init(position: .init(-1, 1, -1), normal: .forward, texCoord: .init(1, 1)), - .init(position: .init(-1, -1, -1), normal: .left, texCoord: .init(0, 0)), - .init(position: .init(-1, -1, 1), normal: .left, texCoord: .init(1, 0)), - .init(position: .init(-1, 1, -1), normal: .left, texCoord: .init(0, 1)), - .init(position: .init(-1, 1, 1), normal: .left, texCoord: .init(1, 1)), - .init(position: .init(-1, -1, -1), normal: .down, texCoord: .init(0, 0)), - .init(position: .init( 1, -1, -1), normal: .down, texCoord: .init(1, 0)), - .init(position: .init(-1, -1, 1), normal: .down, texCoord: .init(0, 1)), - .init(position: .init( 1, -1, 1), normal: .down, texCoord: .init(1, 1)), - .init(position: .init(-1, 1, 1), normal: .up, texCoord: .init(0, 0)), - .init(position: .init( 1, 1, 1), normal: .up, texCoord: .init(1, 0)), - .init(position: .init(-1, 1, -1), normal: .up, texCoord: .init(0, 1)), - .init(position: .init( 1, 1, -1), normal: .up, texCoord: .init(1, 1)), -] - -fileprivate let cubeIndices: [UInt16] = [ - 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 -] - fileprivate let numFramesInFlight: Int = 3 fileprivate let colorFormat: MTLPixelFormat = .bgra8Unorm_srgb fileprivate let depthFormat: MTLPixelFormat = .depth32Float @@ -61,7 +25,6 @@ public class Renderer { private var _encoder: MTLRenderCommandEncoder! = nil - private var vtxBuffer: MTLBuffer, idxBuffer: MTLBuffer private var defaultTexture: MTLTexture private var cubeTexture: MTLTexture? = nil @@ -150,24 +113,6 @@ public class Renderer { throw RendererError.initFailure("Failed to create pipeline state: \(error.localizedDescription)") } - // Create cube mesh buffers - guard let vtxBuffer = device.makeBuffer( - bytes: cubeVertices, - length: cubeVertices.count * MemoryLayout.stride, - options: .storageModeManaged) - else { - throw RendererError.initFailure("Failed to create vertex buffer") - } - self.vtxBuffer = vtxBuffer - guard let idxBuffer = device.makeBuffer( - bytes: cubeIndices, - length: cubeIndices.count * MemoryLayout.stride, - options: .storageModeManaged) - else { - throw RendererError.initFailure("Failed to create index buffer") - } - self.idxBuffer = idxBuffer - // Create a default texture do { self.defaultTexture = try Self.loadTexture(device, queue, image2D: Image2D(Data([ @@ -192,6 +137,29 @@ public class Renderer { } + func createMesh(_ mesh: Mesh) -> RendererMesh? { + let vertices = mesh.vertices.map { + ShaderVertex(position: $0.position, normal: $0.normal, texCoord: $0.texCoord) + } + guard let vtxBuffer = self.device.makeBuffer( + bytes: vertices, + length: vertices.count * MemoryLayout.stride, + options: .storageModeManaged) + else { + printErr("Failed to create vertex buffer") + return nil + } + guard let idxBuffer = device.makeBuffer( + bytes: mesh.indices, + length: mesh.indices.count * MemoryLayout.stride, + options: .storageModeManaged) + else { + printErr("Failed to create index buffer") + return nil + } + return .init(_vertBuf: vtxBuffer, _idxBuf: idxBuffer, numIndices: mesh.indices.count) + } + static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, resourcePath path: String) throws -> MTLTexture { do { return try loadTexture(device, queue, url: Bundle.main.getResource(path)) @@ -329,7 +297,6 @@ public class Renderer { encoder.setRenderPipelineState(pso) encoder.setDepthStencilState(depthStencilState) encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0) - encoder.setVertexBuffer(vtxBuffer, offset: 0, index: VertexShaderInputIdx.vertices.rawValue) self._encoder = encoder frameFunc(self) @@ -346,7 +313,7 @@ public class Renderer { } } - func batch(instances: [Instance], material: Material, environment: Environment, camera: Camera) { + func batch(instances: [Instance], mesh: RendererMesh, material: Material, environment: Environment, camera: Camera) { assert(self._encoder != nil, "batch can't be called outside of a frame being rendered") var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection) @@ -391,6 +358,7 @@ public class Renderer { self._encoder.setCullMode(.init(environment.cullFace)) + self._encoder.setVertexBuffer(mesh._vertBuf, offset: 0, index: VertexShaderInputIdx.vertices.rawValue) self._encoder.setVertexBuffer(instanceBuffer, offset: 0, index: VertexShaderInputIdx.instance.rawValue) @@ -404,14 +372,20 @@ public class Renderer { self._encoder.drawIndexedPrimitives( type: .triangle, - indexCount: cubeIndices.count, + indexCount: mesh.numIndices, indexType: .uint16, - indexBuffer: idxBuffer, + indexBuffer: mesh._idxBuf, indexBufferOffset: 0, instanceCount: numInstances) } } +public struct RendererMesh { + fileprivate let _vertBuf: MTLBuffer + fileprivate let _idxBuf: MTLBuffer + public let numIndices: Int +} + extension MTLClearColor { init(_ color: Color) { self.init(red: color.r, green: color.g, blue: color.b, alpha: color.a) diff --git a/Sources/Voxelotl/World.swift b/Sources/Voxelotl/World.swift index 7bff672..352754a 100644 --- a/Sources/Voxelotl/World.swift +++ b/Sources/Voxelotl/World.swift @@ -19,6 +19,16 @@ public class World { self._chunks[position &>> Chunk.shift]?.setBlock(at: position, type: type) } + func getChunk(id chunkID: SIMD3) -> Chunk? { + self._chunks[chunkID] + } + + public func forEachChunk(_ body: @escaping (_ id: SIMD3, _ chunk: Chunk) throws -> Void) rethrows { + for i in self._chunks { + try body(i.key, i.value) + } + } + func generate(width: Int, height: Int, depth: Int, seed: UInt64) { self._generator.reset(seed: seed) let orig = SIMD3(width, height, depth) / 2