initial chunk render caching

This commit is contained in:
2024-09-01 23:34:32 +10:00
parent da26773221
commit 5f372a9398
9 changed files with 220 additions and 10 deletions

View File

@ -55,6 +55,7 @@ add_executable(Voxelotl MACOSX_BUNDLE
# Game logic classes # Game logic classes
Chunk.swift Chunk.swift
WorldGenerator.swift WorldGenerator.swift
ChunkMeshBuilder.swift
World.swift World.swift
Raycast.swift Raycast.swift
Player.swift Player.swift

View File

@ -65,7 +65,7 @@ public struct Chunk: Hashable {
public func forEach(_ body: @escaping (Block, SIMD3<Int>) throws -> Void) rethrows { public func forEach(_ body: @escaping (Block, SIMD3<Int>) throws -> Void) rethrows {
for i in 0..<Self.blockCount { for i in 0..<Self.blockCount {
try body(blocks[i], self.origin &+ SIMD3( try body(blocks[i], SIMD3(
x: i & Self.mask, x: i & Self.mask,
y: (i &>> Self.shift) & Self.mask, y: (i &>> Self.shift) & Self.mask,
z: (i &>> (Self.shift + Self.shift)) & Self.mask)) z: (i &>> (Self.shift + Self.shift)) & Self.mask))

View File

@ -0,0 +1,84 @@
struct ChunkMeshBuilder {
public static func build(world: World, chunkID: SIMD3<Int>) -> Mesh<VertexPositionNormalColorTexcoord, UInt16> {
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
}
}

View File

@ -26,7 +26,10 @@ class Game: GameDelegate {
var projection: matrix_float4x4 = .identity var projection: matrix_float4x4 = .identity
var world = World() var world = World()
var cubeMesh: RendererMesh? var cubeMesh: RendererMesh?
var renderChunks = [SIMD3<Int>: Mesh<VertexPositionNormalTexcoord, UInt16>]()
var renderMode: Bool = false
var damageChunks = [SIMD3<Int>: Mesh<VertexPositionNormalColorTexcoord, UInt16>]()
var renderChunks = [SIMD3<Int>: RendererMesh]()
func create(_ renderer: Renderer) { func create(_ renderer: Renderer) {
self.resetPlayer() self.resetPlayer()
@ -47,10 +50,19 @@ class Game: GameDelegate {
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
self.world.generate(width: 2, height: 1, depth: 1, seed: seed) self.world.generate(width: 2, height: 2, depth: 2, seed: seed)
#else #else
self.world.generate(width: 5, height: 3, depth: 5, seed: seed) self.world.generate(width: 5, height: 3, depth: 5, seed: seed)
#endif #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) { func fixedUpdate(_ time: GameTime) {
@ -64,13 +76,16 @@ class Game: GameDelegate {
let deltaTime = min(Float(time.delta.asFloat), 1.0 / 15) 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 let pad = GameController.current?.state {
if pad.pressed(.back) { reset = true } if pad.pressed(.back) { reset = true }
if pad.pressed(.start) { generate = true } if pad.pressed(.start) { generate = true }
if pad.pressed(.guide) { toggleRenderMode = true }
} }
if Keyboard.pressed(.r) { reset = true } if Keyboard.pressed(.r) { reset = true }
if Keyboard.pressed(.g) { generate = true } if Keyboard.pressed(.g) { generate = true }
if Keyboard.pressed(.p, repeat: true) { toggleRenderMode = true }
if Keyboard.pressed(.leftBracket) { self.rebuildChunkMeshes() }
// Player reset // Player reset
if reset { if reset {
@ -80,6 +95,9 @@ class Game: GameDelegate {
if generate { if generate {
self.generateWorld() self.generateWorld()
} }
if toggleRenderMode {
self.renderMode = !self.renderMode
}
self.player.update(deltaTime: deltaTime, world: world, camera: &camera) self.player.update(deltaTime: deltaTime, world: world, camera: &camera)
} }
@ -96,7 +114,32 @@ class Game: GameDelegate {
specular: Color(rgba8888: 0x2F2F2F00).linear, specular: Color(rgba8888: 0x2F2F2F00).linear,
gloss: 75) 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<Float>(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 { if let position = player.rayhitPos {
instances.append( instances.append(
Instance( Instance(

View File

@ -3,6 +3,7 @@ import SDL3
public class Keyboard { public class Keyboard {
public enum Keys { 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 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 right, left, up, down
case space, tab case space, tab
} }
@ -85,6 +86,7 @@ internal extension Keyboard.Keys {
case .x: SDLK_X case .x: SDLK_X
case .y: SDLK_Y case .y: SDLK_Y
case .z: SDLK_Z case .z: SDLK_Z
case .leftBracket: SDLK_LEFTBRACKET
case .left: SDLK_LEFT case .left: SDLK_LEFT
case .right: SDLK_RIGHT case .right: SDLK_RIGHT
case .up: SDLK_UP case .up: SDLK_UP
@ -122,6 +124,7 @@ internal extension Keyboard.Keys {
case .x: SDL_SCANCODE_X case .x: SDL_SCANCODE_X
case .y: SDL_SCANCODE_Y case .y: SDL_SCANCODE_Y
case .z: SDL_SCANCODE_Z case .z: SDL_SCANCODE_Z
case .leftBracket: SDL_SCANCODE_LEFTBRACKET
case .left: SDL_SCANCODE_LEFT case .left: SDL_SCANCODE_LEFT
case .right: SDL_SCANCODE_RIGHT case .right: SDL_SCANCODE_RIGHT
case .up: SDL_SCANCODE_UP case .up: SDL_SCANCODE_UP

View File

@ -1,4 +1,4 @@
public struct Mesh<VertexType: Vertex, IndexType: UnsignedInteger> { public struct Mesh<VertexType: Vertex, IndexType: UnsignedInteger>: Equatable {
public let vertices: [VertexType] public let vertices: [VertexType]
public let indices: [IndexType] public let indices: [IndexType]
} }
@ -14,3 +14,10 @@ public struct VertexPositionNormalTexcoord: Vertex {
var normal: SIMD3<Float> var normal: SIMD3<Float>
var texCoord: SIMD2<Float> var texCoord: SIMD2<Float>
} }
public struct VertexPositionNormalColorTexcoord: Vertex {
var position: SIMD3<Float>
var normal: SIMD3<Float>
var color: SIMD4<Float16>
var texCoord: SIMD2<Float>
}

View File

@ -137,9 +137,37 @@ public class Renderer {
} }
func createMesh(_ mesh: Mesh<VertexPositionNormalTexcoord, UInt16>) -> RendererMesh? { func createMesh(_ mesh: Mesh<VertexPositionNormalColorTexcoord, UInt16>) -> RendererMesh? {
if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil }
let vertices = mesh.vertices.map { 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<ShaderVertex>.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<UInt16>.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<VertexPositionNormalTexcoord, UInt16>) -> RendererMesh? {
if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil }
let color = Color<Float16>.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( guard let vtxBuffer = self.device.makeBuffer(
bytes: vertices, bytes: vertices,
@ -313,6 +341,44 @@ public class Renderer {
} }
} }
func draw(model: matrix_float4x4, color: Color<Float16>, 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<VertexShaderInstance>.stride,
index: VertexShaderInputIdx.instance.rawValue)
self._encoder.setVertexBytes(&vertUniforms,
length: MemoryLayout<VertexShaderUniforms>.stride,
index: VertexShaderInputIdx.uniforms.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)
}
func batch(instances: [Instance], mesh: RendererMesh, material: Material, environment: Environment, camera: Camera) { 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") 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) .init(self.r.bitPattern, self.g.bitPattern, self.b.bitPattern, self.a.bitPattern)
} }
} }
fileprivate extension SIMD4 where Scalar == Float16 {
var reinterpretUShort: SIMD4<UInt16> {
.init(self.x.bitPattern, self.y.bitPattern, self.z.bitPattern, self.w.bitPattern)
}
}
enum RendererError: Error { enum RendererError: Error {
case initFailure(_ message: String) case initFailure(_ message: String)

View File

@ -23,7 +23,7 @@ vertex FragmentInput vertexMain(
FragmentInput out; FragmentInput out;
out.position = u.projView * world; out.position = u.projView * world;
out.world = world.xyz; 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.normal = (i[instanceID].normalModel * float4(vtx[vertexID].normal, 0)).xyz;
out.texCoord = vtx[vertexID].texCoord; out.texCoord = vtx[vertexID].texCoord;
return out; return out;

View File

@ -26,6 +26,7 @@ typedef NS_ENUM(NSInteger, VertexShaderInputIdx) {
typedef struct { typedef struct {
vector_float3 position; vector_float3 position;
vector_float3 normal; vector_float3 normal;
color_half4 color;
vector_float2 texCoord; vector_float2 texCoord;
} ShaderVertex; } ShaderVertex;