mirror of
https://github.com/GayPizzaSpecifications/voxelotl-engine.git
synced 2025-08-03 13:11:33 +00:00
initial chunk render caching
This commit is contained in:
@ -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
|
||||||
|
@ -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))
|
||||||
|
84
Sources/Voxelotl/ChunkMeshBuilder.swift
Normal file
84
Sources/Voxelotl/ChunkMeshBuilder.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user