mirror of
https://github.com/GayPizzaSpecifications/voxelotl-engine.git
synced 2025-08-02 13:00:53 +00:00
refactor chunk rendering and fix chunk rendering precision loss at large positions
This commit is contained in:
parent
6e99401474
commit
935aa3e765
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
42
Sources/Voxelotl/Renderer/ChunkRenderer.swift
Normal file
42
Sources/Voxelotl/Renderer/ChunkRenderer.swift
Normal 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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user