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

This commit is contained in:
a dinosaur 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/Mesh.swift
Renderer/ModelBatch.swift
Renderer/ChunkRenderer.swift
Renderer/Renderer.swift
# Input wrappers

View File

@ -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<Float>) -> SIMD3<Float> {
#if true
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 localReadyMeshes = ConcurrentDictionary<ChunkID, RendererMesh?>()
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)
}
}
}
}

View File

@ -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<Double>.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))
}

View File

@ -76,8 +76,9 @@ struct Player {
func checkCollisionRaycast(_ world: World, _ position: SIMD3<Float>, top: Bool) -> Optional<RaycastHit> {
let dir: SIMD3<Float> = !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<Float>
#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..<iterations {
if self._onGround {
self.moveGround(iterDT, world, moveDir: movement)
} else {
self.moveAir(iterDT, world, moveDir: movement)
}
// Limit maximum velocity
let velocityLen = simd_length(self._velocity)
if velocityLen > Self.maxVelocity {
self._velocity = self._velocity / velocityLen * Self.maxVelocity
}
}
// Jumping

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._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
}

View File

@ -23,6 +23,7 @@ public class Renderer {
private var depthTextures: [MTLTexture]
//private var _instances: [MTLBuffer?]
private var _cameraPos: SIMD3<Float> = .zero, _directionalDir: SIMD3<Float> = .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<VertexShaderUniforms>.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<VertexShaderInstance>.stride,
index: VertexShaderInputIdx.instance.rawValue)
self._encoder.setFragmentBytes(&fragUniforms,
length: MemoryLayout<FragmentShaderUniforms>.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<VertexShaderInstance>.stride,
index: VertexShaderInputIdx.instance.rawValue)
self._encoder.setFragmentBytes(&fragUniforms,
length: MemoryLayout<FragmentShaderUniforms>.stride,
index: FragmentShaderInputIdx.uniforms.rawValue)
self._encoder.drawIndexedPrimitives(
type: .triangle,