refactor chunk rendering and fix chunk rendering precision loss at large positions

This commit is contained in:
2024-09-09 02:20:31 +10:00
parent 6e99401474
commit 935aa3e765
8 changed files with 158 additions and 65 deletions

View File

@ -46,6 +46,7 @@ add_executable(Voxelotl MACOSX_BUNDLE
Renderer/Environment.swift Renderer/Environment.swift
Renderer/Mesh.swift Renderer/Mesh.swift
Renderer/ModelBatch.swift Renderer/ModelBatch.swift
Renderer/ChunkRenderer.swift
Renderer/Renderer.swift Renderer/Renderer.swift
# Input wrappers # Input wrappers

View File

@ -129,6 +129,20 @@ public final class Camera {
self._dirty = [ .projection, .view, .viewProj ] 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<Float>) -> SIMD3<Float> { public func screenRay(_ screen: SIMD2<Float>) -> SIMD3<Float> {
#if true #if true
simd_normalize(self.unproject(screen: SIMD3(screen, 1)) - self.unproject(screen: SIMD3(screen, 0))) simd_normalize(self.unproject(screen: SIMD3(screen, 1)) - self.unproject(screen: SIMD3(screen, 0)))

View File

@ -4,10 +4,12 @@ public struct ChunkMeshGeneration {
private let queue: OperationQueue private let queue: OperationQueue
private let localReadyMeshes = ConcurrentDictionary<ChunkID, RendererMesh?>() private let localReadyMeshes = ConcurrentDictionary<ChunkID, RendererMesh?>()
weak var game: Game? private weak var _world: World?
weak var renderer: Renderer? 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 = OperationQueue()
self.queue.underlyingQueue = queue self.queue.underlyingQueue = queue
self.queue.maxConcurrentOperationCount = 8 self.queue.maxConcurrentOperationCount = 8
@ -20,28 +22,19 @@ public struct ChunkMeshGeneration {
func queueGenerateJob(id chunkID: ChunkID, chunk: Chunk) { func queueGenerateJob(id chunkID: ChunkID, chunk: Chunk) {
self.queue.addOperation { self.queue.addOperation {
guard let game = self.game else { let mesh = ChunkMeshBuilder.build(world: self._world!, chunk: chunk)
return self.localReadyMeshes[chunkID] = self._renderer!.createMesh(mesh)
}
guard let renderer = self.renderer else {
return
}
let mesh = ChunkMeshBuilder.build(world: game.world, chunk: chunk)
self.localReadyMeshes[chunkID] = renderer.createMesh(mesh)
} }
} }
public mutating func acceptReadyMeshes() { public mutating func acceptReadyMeshes(_ chunkRenderer: inout ChunkRenderer) {
guard let game = self.game else {
return
}
queue.waitUntilAllOperationsAreFinished() queue.waitUntilAllOperationsAreFinished()
for (chunkID, mesh) in self.localReadyMeshes.take() { 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)
}
} }
} }
} }

View File

@ -7,11 +7,18 @@ class Game: GameDelegate {
var projection: matrix_float4x4 = .identity var projection: matrix_float4x4 = .identity
var world = World(generator: StandardWorldGenerator()) var world = World(generator: StandardWorldGenerator())
var cubeMesh: RendererMesh? var cubeMesh: RendererMesh?
var renderChunks = [ChunkID: RendererMesh?]()
var chunkMeshGeneration: ChunkMeshGeneration! var chunkMeshGeneration: ChunkMeshGeneration!
var chunkRenderer: ChunkRenderer!
var modelBatch: ModelBatch! var modelBatch: ModelBatch!
func create(_ renderer: Renderer) { 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.resetPlayer()
self.generateWorld() self.generateWorld()
self.world.waitForActiveOperations() self.world.waitForActiveOperations()
@ -19,9 +26,9 @@ class Game: GameDelegate {
self.cubeMesh = renderer.createMesh(CubeMeshBuilder.build(bound: .fromUnitCube(position: .zero, scale: .one))) self.cubeMesh = renderer.createMesh(CubeMeshBuilder.build(bound: .fromUnitCube(position: .zero, scale: .one)))
renderer.clearColor = Color<Double>.black.mix(.white, 0.1).linear renderer.clearColor = Color<Double>.black.mix(.white, 0.1).linear
self.chunkMeshGeneration = .init(queue: .global(qos: .userInitiated)) self.chunkMeshGeneration = .init(
self.chunkMeshGeneration.game = self world: world, renderer: renderer,
self.chunkMeshGeneration.renderer = renderer queue: .global(qos: .userInitiated))
self.modelBatch = renderer.createModelBatch() self.modelBatch = renderer.createModelBatch()
} }
@ -33,7 +40,7 @@ class Game: GameDelegate {
private func generateWorld() { private func generateWorld() {
self.world.removeAllChunks() self.world.removeAllChunks()
self.renderChunks.removeAll() self.chunkRenderer.removeAll()
let seed = UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32 let seed = UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32
printErr(seed) printErr(seed)
#if DEBUG #if DEBUG
@ -86,12 +93,6 @@ class Game: GameDelegate {
self.world.update() 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) { func draw(_ renderer: Renderer, _ time: GameTime) {
let totalTime = Float(time.total.asFloat) let totalTime = Float(time.total.asFloat)
@ -103,24 +104,21 @@ class Game: GameDelegate {
self.world.handleRenderDamagedChunks { id, chunk in self.world.handleRenderDamagedChunks { id, chunk in
self.chunkMeshGeneration.generate(id: id, chunk: chunk) 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) 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 { if let position = player.rayhitPos {
let rotation: simd_quatf = let rotation: simd_quatf =
.init(angle: totalTime * 3.0, axis: .Y) * .init(angle: totalTime * 3.0, axis: .Y) *
.init(angle: totalTime * 1.5, axis: .X) * .init(angle: totalTime * 1.5, axis: .X) *
.init(angle: totalTime * 0.7, axis: .Z) .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, position: position, scale: 0.0725 * 0.5, rotation: rotation,
color: .init(r: 0.5, g: 0.5, b: 1)) color: .init(r: 0.5, g: 0.5, b: 1))
} }

View File

@ -76,8 +76,9 @@ struct Player {
func checkCollisionRaycast(_ world: World, _ position: SIMD3<Float>, top: Bool) -> Optional<RaycastHit> { func checkCollisionRaycast(_ world: World, _ position: SIMD3<Float>, top: Bool) -> Optional<RaycastHit> {
let dir: SIMD3<Float> = !top ? .down : .up let dir: SIMD3<Float> = !top ? .down : .up
var org = !top ? self._position + .up * Self.height : self._position let hHeight = Self.height * 0.5
let max: Float = Self.height + Self.epsilon * 4 var org = self._position + .up * hHeight
let max: Float = hHeight + Self.epsilon * 4
org.x -= Self.radius org.x -= Self.radius
org.z -= Self.radius org.z -= Self.radius
@ -92,6 +93,7 @@ struct Player {
return nil return nil
} }
var testPos: SIMD3<Float>
#if false #if false
self._position.y = newPosition.y self._position.y = newPosition.y
if self._velocity.y <= 0, let hit = checkCollisionRaycast(world, self._position, top: false) if self._velocity.y <= 0, let hit = checkCollisionRaycast(world, self._position, top: false)
@ -109,7 +111,7 @@ struct Player {
} }
#else #else
self._position.y = newPosition.y self._position.y = newPosition.y
var testPos = self._position testPos = self._position
if self._velocity.y > 0 { testPos.y -= Self.epsilon } if self._velocity.y > 0 { testPos.y -= Self.epsilon }
if let aabb = checkCollision(world, testPos) { if let aabb = checkCollision(world, testPos) {
if self._velocity.y <= 0 { if self._velocity.y <= 0 {
@ -246,6 +248,10 @@ struct Player {
jumpInput = .held jumpInput = .held
} }
if Keyboard.pressed(.leftBracket, repeat: true) {
self._position *= 2
}
// Read mouse input // Read mouse input
if Mouse.pressed(.left) { destroy = true } if Mouse.pressed(.left) { destroy = true }
if Mouse.pressed(.right) { place = true } if Mouse.pressed(.right) { place = true }
@ -298,16 +304,20 @@ struct Player {
// Flying and unflying // Flying and unflying
self._velocity.y += Float(flying).clamp(-1, 1) * Self.flySpeedCoeff * deltaTime self._velocity.y += Float(flying).clamp(-1, 1) * Self.flySpeedCoeff * deltaTime
// Apply physics // Apply physics
let iterations = 1
let iterDT = deltaTime / Float(iterations)
for _ in 0..<iterations {
if self._onGround { if self._onGround {
self.moveGround(deltaTime, world, moveDir: movement) self.moveGround(iterDT, world, moveDir: movement)
} else { } else {
self.moveAir(deltaTime, world, moveDir: movement) self.moveAir(iterDT, world, moveDir: movement)
} }
// Limit maximum velocity // Limit maximum velocity
let velocityLen = simd_length(self._velocity) let velocityLen = simd_length(self._velocity)
if velocityLen > Self.maxVelocity { if velocityLen > Self.maxVelocity {
self._velocity = self._velocity / velocityLen * Self.maxVelocity self._velocity = self._velocity / velocityLen * Self.maxVelocity
} }
}
// Jumping // Jumping
if self._onGround && willJump { if self._onGround && willJump {

View File

@ -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<Int>(floor(globalCamera.position * divisor), rounding: .down)
let localCamera = Camera(globalCamera)
localCamera.position = globalCamera.position - SIMD3<Float>(origin) * fChunkSz
self._renderer!.setupBatch(environment: environment, camera: localCamera)
for (chunkID, mesh) in self._renderChunks {
let drawPos = SIMD3<Float>(SIMD3<Int>(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()
}
}

View File

@ -18,12 +18,12 @@ public struct ModelBatch {
self._cam = camera self._cam = camera
self._env = environment self._env = environment
self._prev = nil self._prev = nil
self._renderer.setupBatch(material: Game.material, environment: environment, camera: camera) self._renderer.setupBatch(environment: environment, camera: camera)
} }
private mutating func flush() { private mutating func flush() {
assert(self._instances.count > 0) 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._instances.removeAll(keepingCapacity: true)
self._prev = nil self._prev = nil
} }

View File

@ -23,6 +23,7 @@ public class Renderer {
private var depthTextures: [MTLTexture] private var depthTextures: [MTLTexture]
//private var _instances: [MTLBuffer?] //private var _instances: [MTLBuffer?]
private var _cameraPos: SIMD3<Float> = .zero, _directionalDir: SIMD3<Float> = .zero
private var _encoder: MTLRenderCommandEncoder! = nil private var _encoder: MTLRenderCommandEncoder! = nil
@ -381,17 +382,13 @@ public class Renderer {
return ModelBatch(self) 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") assert(self._encoder != nil, "startBatch can't be called outside of a frame being rendered")
var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection) var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection)
var fragUniforms = FragmentShaderUniforms(
cameraPosition: camera.position, self._cameraPos = camera.position
directionalLight: normalize(environment.lightDirection), self._directionalDir = simd_normalize(environment.lightDirection)
ambientColor: SIMD4(material.ambient),
diffuseColor: SIMD4(material.diffuse),
specularColor: SIMD4(material.specular),
specularIntensity: material.gloss)
self._encoder.setCullMode(.init(environment.cullFace)) self._encoder.setCullMode(.init(environment.cullFace))
@ -399,12 +396,40 @@ public class Renderer {
self._encoder.setVertexBytes(&vertUniforms, self._encoder.setVertexBytes(&vertUniforms,
length: MemoryLayout<VertexShaderUniforms>.stride, length: MemoryLayout<VertexShaderUniforms>.stride,
index: VertexShaderInputIdx.uniforms.rawValue) 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<VertexShaderInstance>.stride,
index: VertexShaderInputIdx.instance.rawValue)
self._encoder.setFragmentBytes(&fragUniforms, self._encoder.setFragmentBytes(&fragUniforms,
length: MemoryLayout<FragmentShaderUniforms>.stride, length: MemoryLayout<FragmentShaderUniforms>.stride,
index: FragmentShaderInputIdx.uniforms.rawValue) 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") assert(self._encoder != nil, "submitBatch can't be called outside of a frame being rendered")
let numInstances = instances.count let numInstances = instances.count
assert(numInstances > 0, "submitBatch called with zero instances") assert(numInstances > 0, "submitBatch called with zero instances")
@ -451,12 +476,22 @@ public class Renderer {
normalModel: instance.world.inverse.transpose, normalModel: instance.world.inverse.transpose,
color: SIMD4(instance.color)) 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) self._encoder.setVertexBuffer(mesh._vertBuf, offset: 0, index: VertexShaderInputIdx.vertices.rawValue)
// Ideal as long as our uniforms total 4 KB or less // Ideal as long as our uniforms total 4 KB or less
self._encoder.setVertexBytes(instanceData, self._encoder.setVertexBytes(instanceData,
length: numInstances * MemoryLayout<VertexShaderInstance>.stride, length: numInstances * MemoryLayout<VertexShaderInstance>.stride,
index: VertexShaderInputIdx.instance.rawValue) index: VertexShaderInputIdx.instance.rawValue)
self._encoder.setFragmentBytes(&fragUniforms,
length: MemoryLayout<FragmentShaderUniforms>.stride,
index: FragmentShaderInputIdx.uniforms.rawValue)
self._encoder.drawIndexedPrimitives( self._encoder.drawIndexedPrimitives(
type: .triangle, type: .triangle,