From 935aa3e765615264eb9aa998b354ec6d8cbc5d6c Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Mon, 9 Sep 2024 02:20:31 +1000 Subject: [PATCH] refactor chunk rendering and fix chunk rendering precision loss at large positions --- Sources/Voxelotl/CMakeLists.txt | 1 + Sources/Voxelotl/Camera.swift | 14 +++++ Sources/Voxelotl/ChunkMeshGeneration.swift | 33 +++++------- Sources/Voxelotl/Game.swift | 42 +++++++-------- Sources/Voxelotl/Player.swift | 34 +++++++----- Sources/Voxelotl/Renderer/ChunkRenderer.swift | 42 +++++++++++++++ Sources/Voxelotl/Renderer/ModelBatch.swift | 4 +- Sources/Voxelotl/Renderer/Renderer.swift | 53 +++++++++++++++---- 8 files changed, 158 insertions(+), 65 deletions(-) create mode 100644 Sources/Voxelotl/Renderer/ChunkRenderer.swift diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index a174b68..048a488 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -46,6 +46,7 @@ add_executable(Voxelotl MACOSX_BUNDLE Renderer/Environment.swift Renderer/Mesh.swift Renderer/ModelBatch.swift + Renderer/ChunkRenderer.swift Renderer/Renderer.swift # Input wrappers diff --git a/Sources/Voxelotl/Camera.swift b/Sources/Voxelotl/Camera.swift index 25f4d1c..f1ff65c 100644 --- a/Sources/Voxelotl/Camera.swift +++ b/Sources/Voxelotl/Camera.swift @@ -129,6 +129,20 @@ public final class Camera { self._dirty = [ .projection, .view, .viewProj ] } + //TODO: maybe make this a struct instead? + convenience init(_ copy: Camera) { + self.init(fov: copy._fieldOfView, size: copy.size, range: copy._zNearFar) + self._position = copy._position + self._rotation = copy._rotation + self._aspectRatio = copy._aspectRatio + self._viewport = copy._viewport + self._dirty = copy._dirty + self._projection = copy._projection + self._view = copy._view + self._viewProjection = copy._viewProjection + self._invViewProjection = copy._invViewProjection + } + public func screenRay(_ screen: SIMD2) -> SIMD3 { #if true simd_normalize(self.unproject(screen: SIMD3(screen, 1)) - self.unproject(screen: SIMD3(screen, 0))) diff --git a/Sources/Voxelotl/ChunkMeshGeneration.swift b/Sources/Voxelotl/ChunkMeshGeneration.swift index aea1cce..17505f4 100644 --- a/Sources/Voxelotl/ChunkMeshGeneration.swift +++ b/Sources/Voxelotl/ChunkMeshGeneration.swift @@ -4,10 +4,12 @@ public struct ChunkMeshGeneration { private let queue: OperationQueue private let localReadyMeshes = ConcurrentDictionary() - weak var game: Game? - weak var renderer: Renderer? + private weak var _world: World? + private weak var _renderer: Renderer? - init(queue: DispatchQueue) { + init(world: World, renderer: Renderer, queue: DispatchQueue) { + self._world = world + self._renderer = renderer self.queue = OperationQueue() self.queue.underlyingQueue = queue self.queue.maxConcurrentOperationCount = 8 @@ -20,28 +22,19 @@ public struct ChunkMeshGeneration { func queueGenerateJob(id chunkID: ChunkID, chunk: Chunk) { self.queue.addOperation { - guard let game = self.game else { - return - } - - guard let renderer = self.renderer else { - return - } - - let mesh = ChunkMeshBuilder.build(world: game.world, chunk: chunk) - self.localReadyMeshes[chunkID] = renderer.createMesh(mesh) + let mesh = ChunkMeshBuilder.build(world: self._world!, chunk: chunk) + self.localReadyMeshes[chunkID] = self._renderer!.createMesh(mesh) } } - public mutating func acceptReadyMeshes() { - guard let game = self.game else { - return - } - + public mutating func acceptReadyMeshes(_ chunkRenderer: inout ChunkRenderer) { queue.waitUntilAllOperationsAreFinished() - for (chunkID, mesh) in self.localReadyMeshes.take() { - game.renderChunks.updateValue(mesh, forKey: chunkID) + if let mesh = mesh { + chunkRenderer.addChunk(id: chunkID, mesh: mesh) + } else { + chunkRenderer.removeChunk(id: chunkID) + } } } } diff --git a/Sources/Voxelotl/Game.swift b/Sources/Voxelotl/Game.swift index f692d2d..b2693c2 100644 --- a/Sources/Voxelotl/Game.swift +++ b/Sources/Voxelotl/Game.swift @@ -7,11 +7,18 @@ class Game: GameDelegate { var projection: matrix_float4x4 = .identity var world = World(generator: StandardWorldGenerator()) var cubeMesh: RendererMesh? - var renderChunks = [ChunkID: RendererMesh?]() var chunkMeshGeneration: ChunkMeshGeneration! + var chunkRenderer: ChunkRenderer! var modelBatch: ModelBatch! func create(_ renderer: Renderer) { + self.chunkRenderer = ChunkRenderer(renderer: renderer) + self.chunkRenderer.material = .init( + ambient: Color(rgba8888: 0x4F4F4F00).linear, + diffuse: Color(rgba8888: 0xDFDFDF00).linear, + specular: Color(rgba8888: 0x2F2F2F00).linear, + gloss: 75) + self.resetPlayer() self.generateWorld() self.world.waitForActiveOperations() @@ -19,9 +26,9 @@ class Game: GameDelegate { self.cubeMesh = renderer.createMesh(CubeMeshBuilder.build(bound: .fromUnitCube(position: .zero, scale: .one))) renderer.clearColor = Color.black.mix(.white, 0.1).linear - self.chunkMeshGeneration = .init(queue: .global(qos: .userInitiated)) - self.chunkMeshGeneration.game = self - self.chunkMeshGeneration.renderer = renderer + self.chunkMeshGeneration = .init( + world: world, renderer: renderer, + queue: .global(qos: .userInitiated)) self.modelBatch = renderer.createModelBatch() } @@ -33,7 +40,7 @@ class Game: GameDelegate { private func generateWorld() { self.world.removeAllChunks() - self.renderChunks.removeAll() + self.chunkRenderer.removeAll() let seed = UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32 printErr(seed) #if DEBUG @@ -86,12 +93,6 @@ class Game: GameDelegate { self.world.update() } - public static let material = Material( - ambient: Color(rgba8888: 0x4F4F4F00).linear, - diffuse: Color(rgba8888: 0xDFDFDF00).linear, - specular: Color(rgba8888: 0x2F2F2F00).linear, - gloss: 75) - func draw(_ renderer: Renderer, _ time: GameTime) { let totalTime = Float(time.total.asFloat) @@ -103,24 +104,21 @@ class Game: GameDelegate { self.world.handleRenderDamagedChunks { id, chunk in self.chunkMeshGeneration.generate(id: id, chunk: chunk) } - self.chunkMeshGeneration.acceptReadyMeshes() + self.chunkMeshGeneration.acceptReadyMeshes(&self.chunkRenderer) + + self.chunkRenderer.draw(environment: env, camera: self.camera) self.modelBatch.begin(camera: camera, environment: env) - - for (id, chunk) in self.renderChunks { - if chunk == nil { - continue - } - let drawPos = id.getFloatPosition() - self.modelBatch.draw(.init(mesh: chunk!, material: Self.material), position: drawPos) - } - if let position = player.rayhitPos { let rotation: simd_quatf = .init(angle: totalTime * 3.0, axis: .Y) * .init(angle: totalTime * 1.5, axis: .X) * .init(angle: totalTime * 0.7, axis: .Z) - self.modelBatch.draw(.init(mesh: self.cubeMesh!, material: Self.material), + self.modelBatch.draw(.init(mesh: self.cubeMesh!, material: .init( + ambient: .black.mix(.green, 0.65).linear, + diffuse: .white.mix(.black, 0.20).linear, + specular: .magenta.linear, + gloss: 250)), position: position, scale: 0.0725 * 0.5, rotation: rotation, color: .init(r: 0.5, g: 0.5, b: 1)) } diff --git a/Sources/Voxelotl/Player.swift b/Sources/Voxelotl/Player.swift index 7dde219..5c8eeb8 100644 --- a/Sources/Voxelotl/Player.swift +++ b/Sources/Voxelotl/Player.swift @@ -76,8 +76,9 @@ struct Player { func checkCollisionRaycast(_ world: World, _ position: SIMD3, top: Bool) -> Optional { let dir: SIMD3 = !top ? .down : .up - var org = !top ? self._position + .up * Self.height : self._position - let max: Float = Self.height + Self.epsilon * 4 + let hHeight = Self.height * 0.5 + var org = self._position + .up * hHeight + let max: Float = hHeight + Self.epsilon * 4 org.x -= Self.radius org.z -= Self.radius @@ -92,6 +93,7 @@ struct Player { return nil } + var testPos: SIMD3 #if false self._position.y = newPosition.y if self._velocity.y <= 0, let hit = checkCollisionRaycast(world, self._position, top: false) @@ -109,7 +111,7 @@ struct Player { } #else self._position.y = newPosition.y - var testPos = self._position + testPos = self._position if self._velocity.y > 0 { testPos.y -= Self.epsilon } if let aabb = checkCollision(world, testPos) { if self._velocity.y <= 0 { @@ -246,6 +248,10 @@ struct Player { jumpInput = .held } + if Keyboard.pressed(.leftBracket, repeat: true) { + self._position *= 2 + } + // Read mouse input if Mouse.pressed(.left) { destroy = true } if Mouse.pressed(.right) { place = true } @@ -298,15 +304,19 @@ struct Player { // Flying and unflying self._velocity.y += Float(flying).clamp(-1, 1) * Self.flySpeedCoeff * deltaTime // Apply physics - if self._onGround { - self.moveGround(deltaTime, world, moveDir: movement) - } else { - self.moveAir(deltaTime, world, moveDir: movement) - } - // Limit maximum velocity - let velocityLen = simd_length(self._velocity) - if velocityLen > Self.maxVelocity { - self._velocity = self._velocity / velocityLen * Self.maxVelocity + let iterations = 1 + let iterDT = deltaTime / Float(iterations) + for _ in 0.. Self.maxVelocity { + self._velocity = self._velocity / velocityLen * Self.maxVelocity + } } // Jumping diff --git a/Sources/Voxelotl/Renderer/ChunkRenderer.swift b/Sources/Voxelotl/Renderer/ChunkRenderer.swift new file mode 100644 index 0000000..4d05a72 --- /dev/null +++ b/Sources/Voxelotl/Renderer/ChunkRenderer.swift @@ -0,0 +1,42 @@ +import simd + +public struct ChunkRenderer { + private weak var _renderer: Renderer? + private var _renderChunks = [ChunkID: RendererMesh]() + + public var material: Material + + public init(renderer: Renderer) { + self._renderer = renderer + self.material = .init(ambient: .black, diffuse: .white, specular: .white, gloss: 20.0) + } + + public mutating func draw(environment: Environment, camera globalCamera: Camera) { + let fChunkSz = Float(Chunk.size), divisor = 1 / fChunkSz + let origin = SIMD3(floor(globalCamera.position * divisor), rounding: .down) + + let localCamera = Camera(globalCamera) + localCamera.position = globalCamera.position - SIMD3(origin) * fChunkSz + + self._renderer!.setupBatch(environment: environment, camera: localCamera) + for (chunkID, mesh) in self._renderChunks { + let drawPos = SIMD3(SIMD3(chunkID) &- origin) * fChunkSz + self._renderer!.submit( + mesh: mesh, + instance: .init(world: .translate(drawPos)), + material: self.material) + } + } + + public mutating func addChunk(id chunkID: ChunkID, mesh: RendererMesh) { + self._renderChunks.updateValue(mesh, forKey: chunkID) + } + + public mutating func removeChunk(id chunkID: ChunkID) { + self._renderChunks.removeValue(forKey: chunkID) + } + + public mutating func removeAll() { + self._renderChunks.removeAll() + } +} diff --git a/Sources/Voxelotl/Renderer/ModelBatch.swift b/Sources/Voxelotl/Renderer/ModelBatch.swift index 1f4e399..8a46f91 100644 --- a/Sources/Voxelotl/Renderer/ModelBatch.swift +++ b/Sources/Voxelotl/Renderer/ModelBatch.swift @@ -18,12 +18,12 @@ public struct ModelBatch { self._cam = camera self._env = environment self._prev = nil - self._renderer.setupBatch(material: Game.material, environment: environment, camera: camera) + self._renderer.setupBatch(environment: environment, camera: camera) } private mutating func flush() { assert(self._instances.count > 0) - self._renderer.submitBatch(mesh: self._prev.mesh, instances: self._instances) + self._renderer.submitBatch(mesh: self._prev.mesh, instances: self._instances, material: self._prev.material) self._instances.removeAll(keepingCapacity: true) self._prev = nil } diff --git a/Sources/Voxelotl/Renderer/Renderer.swift b/Sources/Voxelotl/Renderer/Renderer.swift index 9733d32..9ab8b5b 100644 --- a/Sources/Voxelotl/Renderer/Renderer.swift +++ b/Sources/Voxelotl/Renderer/Renderer.swift @@ -23,6 +23,7 @@ public class Renderer { private var depthTextures: [MTLTexture] //private var _instances: [MTLBuffer?] + private var _cameraPos: SIMD3 = .zero, _directionalDir: SIMD3 = .zero private var _encoder: MTLRenderCommandEncoder! = nil @@ -381,17 +382,13 @@ public class Renderer { return ModelBatch(self) } - internal func setupBatch(material: Material, environment: Environment, camera: Camera) { + internal func setupBatch(environment: Environment, camera: Camera) { assert(self._encoder != nil, "startBatch 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: SIMD4(material.ambient), - diffuseColor: SIMD4(material.diffuse), - specularColor: SIMD4(material.specular), - specularIntensity: material.gloss) + + self._cameraPos = camera.position + self._directionalDir = simd_normalize(environment.lightDirection) self._encoder.setCullMode(.init(environment.cullFace)) @@ -399,12 +396,40 @@ public class Renderer { self._encoder.setVertexBytes(&vertUniforms, length: MemoryLayout.stride, index: VertexShaderInputIdx.uniforms.rawValue) + } + + internal func submit(mesh: RendererMesh, instance: ModelBatch.Instance, material: Material) { + assert(self._encoder != nil, "submit can't be called outside of a frame being rendered") + var instanceData = VertexShaderInstance( + model: instance.world, + normalModel: instance.world.inverse.transpose, + color: SIMD4(instance.color)) + var fragUniforms = FragmentShaderUniforms( + cameraPosition: self._cameraPos, + directionalLight: self._directionalDir, + ambientColor: SIMD4(material.ambient), + diffuseColor: SIMD4(material.diffuse), + specularColor: SIMD4(material.specular), + specularIntensity: material.gloss) + + 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(&instanceData, + length: MemoryLayout.stride, + index: VertexShaderInputIdx.instance.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) } - internal func submitBatch(mesh: RendererMesh, instances: [ModelBatch.Instance]) { + internal func submitBatch(mesh: RendererMesh, instances: [ModelBatch.Instance], material: Material) { assert(self._encoder != nil, "submitBatch can't be called outside of a frame being rendered") let numInstances = instances.count assert(numInstances > 0, "submitBatch called with zero instances") @@ -451,12 +476,22 @@ public class Renderer { normalModel: instance.world.inverse.transpose, color: SIMD4(instance.color)) } + var fragUniforms = FragmentShaderUniforms( + cameraPosition: self._cameraPos, + directionalLight: self._directionalDir, + ambientColor: SIMD4(material.ambient), + diffuseColor: SIMD4(material.diffuse), + specularColor: SIMD4(material.specular), + specularIntensity: material.gloss) 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(instanceData, length: numInstances * MemoryLayout.stride, index: VertexShaderInputIdx.instance.rawValue) + self._encoder.setFragmentBytes(&fragUniforms, + length: MemoryLayout.stride, + index: FragmentShaderInputIdx.uniforms.rawValue) self._encoder.drawIndexedPrimitives( type: .triangle,