From 5f372a9398615efe93c14083da9e4aa10d34305c Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Sun, 1 Sep 2024 23:34:32 +1000 Subject: [PATCH] initial chunk render caching --- Sources/Voxelotl/CMakeLists.txt | 1 + Sources/Voxelotl/Chunk.swift | 2 +- Sources/Voxelotl/ChunkMeshBuilder.swift | 84 ++++++++++++++++++++++++ Sources/Voxelotl/Game.swift | 51 ++++++++++++-- Sources/Voxelotl/Input/Keyboard.swift | 3 + Sources/Voxelotl/Renderer/Mesh.swift | 9 ++- Sources/Voxelotl/Renderer/Renderer.swift | 75 ++++++++++++++++++++- Sources/Voxelotl/shader.metal | 2 +- Sources/Voxelotl/shadertypes.h | 3 +- 9 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 Sources/Voxelotl/ChunkMeshBuilder.swift diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index afcab8a..ec4b1c8 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -55,6 +55,7 @@ add_executable(Voxelotl MACOSX_BUNDLE # Game logic classes Chunk.swift WorldGenerator.swift + ChunkMeshBuilder.swift World.swift Raycast.swift Player.swift diff --git a/Sources/Voxelotl/Chunk.swift b/Sources/Voxelotl/Chunk.swift index bec5e09..d0a1bea 100644 --- a/Sources/Voxelotl/Chunk.swift +++ b/Sources/Voxelotl/Chunk.swift @@ -65,7 +65,7 @@ public struct Chunk: Hashable { 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)) diff --git a/Sources/Voxelotl/ChunkMeshBuilder.swift b/Sources/Voxelotl/ChunkMeshBuilder.swift new file mode 100644 index 0000000..ff880b4 --- /dev/null +++ b/Sources/Voxelotl/ChunkMeshBuilder.swift @@ -0,0 +1,84 @@ +struct ChunkMeshBuilder { + public static func build(world: World, chunkID: SIMD3) -> Mesh { + guard let chunk = world.getChunk(id: chunkID) else { return .empty } + + var vertices = [VertexPositionNormalColorTexcoord]() + var indices = [UInt16]() + chunk.forEach { block, position in + if case .solid(let color) = block.type { + for side in [ Side.left, .right, .down, .up, .back, .front ] { + let globalPos = chunk.origin &+ position + if case .air = world.getBlock(at: globalPos.offset(by: side)).type { + let orig = UInt16(vertices.count) + vertices.append(contentsOf: cubeVertices[side]!.map { + .init( + position: SIMD3(position) + $0.position, + normal: $0.normal, + color: SIMD4(color), + texCoord: $0.texCoord) + }) + indices.append(contentsOf: sideIndices.map { orig + $0 }) + } + } + } + } + + return .init(vertices: vertices, indices: indices) + } +} + +fileprivate let cubeVertices: [Side: [VertexPositionNormalTexcoord]] = [ + .back: [ + .init(position: .init(0, 0, 1), normal: .back, texCoord: .init(0, 0)), + .init(position: .init(1, 0, 1), normal: .back, texCoord: .init(1, 0)), + .init(position: .init(0, 1, 1), normal: .back, texCoord: .init(0, 1)), + .init(position: .init(1, 1, 1), normal: .back, texCoord: .init(1, 1)) + ], .right: [ + .init(position: .init(1, 0, 1), normal: .right, texCoord: .init(0, 0)), + .init(position: .init(1, 0, 0), normal: .right, texCoord: .init(1, 0)), + .init(position: .init(1, 1, 1), normal: .right, texCoord: .init(0, 1)), + .init(position: .init(1, 1, 0), normal: .right, texCoord: .init(1, 1)) + ], .front: [ + .init(position: .init(1, 0, 0), normal: .forward, texCoord: .init(0, 0)), + .init(position: .init(0, 0, 0), normal: .forward, texCoord: .init(1, 0)), + .init(position: .init(1, 1, 0), normal: .forward, texCoord: .init(0, 1)), + .init(position: .init(0, 1, 0), normal: .forward, texCoord: .init(1, 1)) + ], .left: [ + .init(position: .init(0, 0, 0), normal: .left, texCoord: .init(0, 0)), + .init(position: .init(0, 0, 1), normal: .left, texCoord: .init(1, 0)), + .init(position: .init(0, 1, 0), normal: .left, texCoord: .init(0, 1)), + .init(position: .init(0, 1, 1), normal: .left, texCoord: .init(1, 1)) + ], .down: [ + .init(position: .init(0, 0, 0), normal: .down, texCoord: .init(0, 0)), + .init(position: .init(1, 0, 0), normal: .down, texCoord: .init(1, 0)), + .init(position: .init(0, 0, 1), normal: .down, texCoord: .init(0, 1)), + .init(position: .init(1, 0, 1), normal: .down, texCoord: .init(1, 1)) + ], .up: [ + .init(position: .init(0, 1, 1), normal: .up, texCoord: .init(0, 0)), + .init(position: .init(1, 1, 1), normal: .up, texCoord: .init(1, 0)), + .init(position: .init(0, 1, 0), normal: .up, texCoord: .init(0, 1)), + .init(position: .init(1, 1, 0), normal: .up, texCoord: .init(1, 1)) + ] +] + +fileprivate let sideIndices: [UInt16] = [ 0, 1, 2, 2, 1, 3 ] + +fileprivate enum Side { + case left, right + case down, up + case back, front +} + +fileprivate extension SIMD3 where Scalar: SignedInteger & FixedWidthInteger { + func offset(by side: Side) -> Self { + let ofs: Self = switch side { + 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/Game.swift b/Sources/Voxelotl/Game.swift index 5148d5c..0e10741 100644 --- a/Sources/Voxelotl/Game.swift +++ b/Sources/Voxelotl/Game.swift @@ -26,7 +26,10 @@ class Game: GameDelegate { var projection: matrix_float4x4 = .identity var world = World() var cubeMesh: RendererMesh? - var renderChunks = [SIMD3: Mesh]() + + var renderMode: Bool = false + var damageChunks = [SIMD3: Mesh]() + var renderChunks = [SIMD3: RendererMesh]() func create(_ renderer: Renderer) { self.resetPlayer() @@ -47,10 +50,19 @@ class Game: GameDelegate { let seed = UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32 printErr(seed) #if DEBUG - self.world.generate(width: 2, height: 1, depth: 1, seed: seed) + self.world.generate(width: 2, height: 2, depth: 2, seed: seed) #else self.world.generate(width: 5, height: 3, depth: 5, seed: seed) #endif + + // Build chunk meshes + self.rebuildChunkMeshes() + } + + private func rebuildChunkMeshes() { + self.world.forEachChunk { id, chunk in + self.damageChunks[id] = ChunkMeshBuilder.build(world: self.world, chunkID: id) + } } func fixedUpdate(_ time: GameTime) { @@ -64,13 +76,16 @@ class Game: GameDelegate { let deltaTime = min(Float(time.delta.asFloat), 1.0 / 15) - var reset = false, generate = false + var reset = false, generate = false, toggleRenderMode = false if let pad = GameController.current?.state { if pad.pressed(.back) { reset = true } if pad.pressed(.start) { generate = true } + if pad.pressed(.guide) { toggleRenderMode = true } } if Keyboard.pressed(.r) { reset = true } if Keyboard.pressed(.g) { generate = true } + if Keyboard.pressed(.p, repeat: true) { toggleRenderMode = true } + if Keyboard.pressed(.leftBracket) { self.rebuildChunkMeshes() } // Player reset if reset { @@ -80,6 +95,9 @@ class Game: GameDelegate { if generate { self.generateWorld() } + if toggleRenderMode { + self.renderMode = !self.renderMode + } self.player.update(deltaTime: deltaTime, world: world, camera: &camera) } @@ -96,7 +114,32 @@ class Game: GameDelegate { specular: Color(rgba8888: 0x2F2F2F00).linear, gloss: 75) - var instances = world.instances + if self.renderMode { + // Update chunk meshes if needed + if !self.damageChunks.isEmpty { + for i in self.damageChunks { + if let new = renderer.createMesh(i.1) { + self.renderChunks[i.0] = new + } else { + self.renderChunks.removeValue(forKey: i.0) + } + } + self.damageChunks = [:] + } + + for (id, chunk) in self.renderChunks { + let drawPos = SIMD3(id &<< Chunk.shift) + renderer.draw( + model: .translate(drawPos), + color: .white, + mesh: chunk, + material: material, + environment: env, + camera: self.camera) + } + } + + var instances = self.renderMode ? [Instance]() : world.instances if let position = player.rayhitPos { instances.append( Instance( diff --git a/Sources/Voxelotl/Input/Keyboard.swift b/Sources/Voxelotl/Input/Keyboard.swift index 1f76a7c..0d39882 100644 --- a/Sources/Voxelotl/Input/Keyboard.swift +++ b/Sources/Voxelotl/Input/Keyboard.swift @@ -3,6 +3,7 @@ import SDL3 public class Keyboard { public enum Keys { case a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z + case leftBracket case right, left, up, down case space, tab } @@ -85,6 +86,7 @@ internal extension Keyboard.Keys { case .x: SDLK_X case .y: SDLK_Y case .z: SDLK_Z + case .leftBracket: SDLK_LEFTBRACKET case .left: SDLK_LEFT case .right: SDLK_RIGHT case .up: SDLK_UP @@ -122,6 +124,7 @@ internal extension Keyboard.Keys { case .x: SDL_SCANCODE_X case .y: SDL_SCANCODE_Y case .z: SDL_SCANCODE_Z + case .leftBracket: SDL_SCANCODE_LEFTBRACKET case .left: SDL_SCANCODE_LEFT case .right: SDL_SCANCODE_RIGHT case .up: SDL_SCANCODE_UP diff --git a/Sources/Voxelotl/Renderer/Mesh.swift b/Sources/Voxelotl/Renderer/Mesh.swift index 4a49f22..4e00b21 100644 --- a/Sources/Voxelotl/Renderer/Mesh.swift +++ b/Sources/Voxelotl/Renderer/Mesh.swift @@ -1,4 +1,4 @@ -public struct Mesh { +public struct Mesh: Equatable { public let vertices: [VertexType] public let indices: [IndexType] } @@ -14,3 +14,10 @@ public struct VertexPositionNormalTexcoord: Vertex { var normal: SIMD3 var texCoord: SIMD2 } + +public struct VertexPositionNormalColorTexcoord: Vertex { + var position: SIMD3 + var normal: SIMD3 + var color: SIMD4 + var texCoord: SIMD2 +} diff --git a/Sources/Voxelotl/Renderer/Renderer.swift b/Sources/Voxelotl/Renderer/Renderer.swift index 5d2125d..2ff85c1 100644 --- a/Sources/Voxelotl/Renderer/Renderer.swift +++ b/Sources/Voxelotl/Renderer/Renderer.swift @@ -137,9 +137,37 @@ public class Renderer { } - func createMesh(_ mesh: Mesh) -> RendererMesh? { + func createMesh(_ mesh: Mesh) -> RendererMesh? { + if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil } + let vertices = mesh.vertices.map { - ShaderVertex(position: $0.position, normal: $0.normal, texCoord: $0.texCoord) + ShaderVertex(position: $0.position, normal: $0.normal, color: $0.color.reinterpretUShort, 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) + } + + func createMesh(_ mesh: Mesh) -> RendererMesh? { + if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil } + + let color = Color.white.reinterpretUShort + let vertices = mesh.vertices.map { + ShaderVertex(position: $0.position, normal: $0.normal, color: color, texCoord: $0.texCoord) } guard let vtxBuffer = self.device.makeBuffer( bytes: vertices, @@ -313,6 +341,44 @@ public class Renderer { } } + func draw(model: matrix_float4x4, color: Color, mesh: RendererMesh, material: Material, environment: Environment, camera: Camera) { + assert(self._encoder != nil, "draw can't be called outside of a frame being rendered") + + var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection) + var fragUniforms = FragmentShaderUniforms( + cameraPosition: camera.position, + directionalLight: normalize(environment.lightDirection), + ambientColor: material.ambient.reinterpretUShort, + diffuseColor: material.diffuse.reinterpretUShort, + specularColor: material.specular.reinterpretUShort, + specularIntensity: material.gloss) + var instance = VertexShaderInstance( + model: model, + normalModel: model.inverse.transpose, + color: color.reinterpretUShort) + + self._encoder.setCullMode(.init(environment.cullFace)) + + self._encoder.setVertexBuffer(mesh._vertBuf, offset: 0, index: VertexShaderInputIdx.vertices.rawValue) + // Ideal as long as our uniforms total 4 KB or less + self._encoder.setVertexBytes(&instance, + length: MemoryLayout.stride, + index: VertexShaderInputIdx.instance.rawValue) + self._encoder.setVertexBytes(&vertUniforms, + length: MemoryLayout.stride, + index: VertexShaderInputIdx.uniforms.rawValue) + self._encoder.setFragmentBytes(&fragUniforms, + length: MemoryLayout.stride, + index: FragmentShaderInputIdx.uniforms.rawValue) + + self._encoder.drawIndexedPrimitives( + type: .triangle, + indexCount: mesh.numIndices, + indexType: .uint16, + indexBuffer: mesh._idxBuf, + indexBufferOffset: 0) + } + 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") @@ -407,6 +473,11 @@ fileprivate extension Color where T == Float16 { .init(self.r.bitPattern, self.g.bitPattern, self.b.bitPattern, self.a.bitPattern) } } +fileprivate extension SIMD4 where Scalar == Float16 { + var reinterpretUShort: SIMD4 { + .init(self.x.bitPattern, self.y.bitPattern, self.z.bitPattern, self.w.bitPattern) + } +} enum RendererError: Error { case initFailure(_ message: String) diff --git a/Sources/Voxelotl/shader.metal b/Sources/Voxelotl/shader.metal index 02c99db..0efb259 100644 --- a/Sources/Voxelotl/shader.metal +++ b/Sources/Voxelotl/shader.metal @@ -23,7 +23,7 @@ vertex FragmentInput vertexMain( FragmentInput out; out.position = u.projView * world; out.world = world.xyz; - out.color = half4(i[instanceID].color); + out.color = vtx[vertexID].color * i[instanceID].color; out.normal = (i[instanceID].normalModel * float4(vtx[vertexID].normal, 0)).xyz; out.texCoord = vtx[vertexID].texCoord; return out; diff --git a/Sources/Voxelotl/shadertypes.h b/Sources/Voxelotl/shadertypes.h index 088ef00..ab16d91 100644 --- a/Sources/Voxelotl/shadertypes.h +++ b/Sources/Voxelotl/shadertypes.h @@ -26,13 +26,14 @@ typedef NS_ENUM(NSInteger, VertexShaderInputIdx) { typedef struct { vector_float3 position; vector_float3 normal; + color_half4 color; vector_float2 texCoord; } ShaderVertex; typedef struct { matrix_float4x4 model; matrix_float4x4 normalModel; - color_half4 color; + color_half4 color; } VertexShaderInstance; typedef struct {