mirror of
				https://github.com/GayPizzaSpecifications/voxelotl-engine.git
				synced 2025-11-04 10:59:39 +00:00 
			
		
		
		
	initial chunk render caching
This commit is contained in:
		@ -55,6 +55,7 @@ add_executable(Voxelotl MACOSX_BUNDLE
 | 
			
		||||
  # Game logic classes
 | 
			
		||||
  Chunk.swift
 | 
			
		||||
  WorldGenerator.swift
 | 
			
		||||
  ChunkMeshBuilder.swift
 | 
			
		||||
  World.swift
 | 
			
		||||
  Raycast.swift
 | 
			
		||||
  Player.swift
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@ public struct Chunk: Hashable {
 | 
			
		||||
 | 
			
		||||
  public func forEach(_ body: @escaping (Block, SIMD3<Int>) throws -> Void) rethrows {
 | 
			
		||||
    for i in 0..<Self.blockCount {
 | 
			
		||||
      try body(blocks[i], self.origin &+ SIMD3(
 | 
			
		||||
      try body(blocks[i], SIMD3(
 | 
			
		||||
        x: i & Self.mask,
 | 
			
		||||
        y: (i &>> 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 world = World()
 | 
			
		||||
  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) {
 | 
			
		||||
    self.resetPlayer()
 | 
			
		||||
@ -47,10 +50,19 @@ class Game: GameDelegate {
 | 
			
		||||
    let seed = UInt64(Arc4Random.instance.next()) | UInt64(Arc4Random.instance.next()) << 32
 | 
			
		||||
    printErr(seed)
 | 
			
		||||
#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
 | 
			
		||||
    self.world.generate(width: 5, height: 3, depth: 5, seed: seed)
 | 
			
		||||
#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) {
 | 
			
		||||
@ -64,13 +76,16 @@ class Game: GameDelegate {
 | 
			
		||||
 | 
			
		||||
    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 pad.pressed(.back) { reset = true }
 | 
			
		||||
      if pad.pressed(.start) { generate = true }
 | 
			
		||||
      if pad.pressed(.guide) { toggleRenderMode = true }
 | 
			
		||||
    }
 | 
			
		||||
    if Keyboard.pressed(.r) { reset = true }
 | 
			
		||||
    if Keyboard.pressed(.g) { generate = true }
 | 
			
		||||
    if Keyboard.pressed(.p, repeat: true) { toggleRenderMode = true }
 | 
			
		||||
    if Keyboard.pressed(.leftBracket) { self.rebuildChunkMeshes() }
 | 
			
		||||
 | 
			
		||||
    // Player reset
 | 
			
		||||
    if reset {
 | 
			
		||||
@ -80,6 +95,9 @@ class Game: GameDelegate {
 | 
			
		||||
    if generate {
 | 
			
		||||
      self.generateWorld()
 | 
			
		||||
    }
 | 
			
		||||
    if toggleRenderMode {
 | 
			
		||||
      self.renderMode = !self.renderMode
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.player.update(deltaTime: deltaTime, world: world, camera: &camera)
 | 
			
		||||
  }
 | 
			
		||||
@ -96,7 +114,32 @@ class Game: GameDelegate {
 | 
			
		||||
      specular: Color(rgba8888: 0x2F2F2F00).linear,
 | 
			
		||||
      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 {
 | 
			
		||||
      instances.append(
 | 
			
		||||
        Instance(
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import SDL3
 | 
			
		||||
public class Keyboard {
 | 
			
		||||
  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 leftBracket
 | 
			
		||||
    case right, left, up, down
 | 
			
		||||
    case space, tab
 | 
			
		||||
  }
 | 
			
		||||
@ -85,6 +86,7 @@ internal extension Keyboard.Keys {
 | 
			
		||||
    case .x:     SDLK_X
 | 
			
		||||
    case .y:     SDLK_Y
 | 
			
		||||
    case .z:     SDLK_Z
 | 
			
		||||
    case .leftBracket: SDLK_LEFTBRACKET
 | 
			
		||||
    case .left:  SDLK_LEFT
 | 
			
		||||
    case .right: SDLK_RIGHT
 | 
			
		||||
    case .up:    SDLK_UP
 | 
			
		||||
@ -122,6 +124,7 @@ internal extension Keyboard.Keys {
 | 
			
		||||
    case .x:     SDL_SCANCODE_X
 | 
			
		||||
    case .y:     SDL_SCANCODE_Y
 | 
			
		||||
    case .z:     SDL_SCANCODE_Z
 | 
			
		||||
    case .leftBracket: SDL_SCANCODE_LEFTBRACKET
 | 
			
		||||
    case .left:  SDL_SCANCODE_LEFT
 | 
			
		||||
    case .right: SDL_SCANCODE_RIGHT
 | 
			
		||||
    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 indices: [IndexType]
 | 
			
		||||
}
 | 
			
		||||
@ -14,3 +14,10 @@ public struct VertexPositionNormalTexcoord: Vertex {
 | 
			
		||||
  var normal:   SIMD3<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 {
 | 
			
		||||
      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(
 | 
			
		||||
      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) {
 | 
			
		||||
    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)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
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 {
 | 
			
		||||
  case initFailure(_ message: String)
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ vertex FragmentInput vertexMain(
 | 
			
		||||
  FragmentInput out;
 | 
			
		||||
  out.position = u.projView * world;
 | 
			
		||||
  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.texCoord = vtx[vertexID].texCoord;
 | 
			
		||||
  return out;
 | 
			
		||||
 | 
			
		||||
@ -26,13 +26,14 @@ typedef NS_ENUM(NSInteger, VertexShaderInputIdx) {
 | 
			
		||||
typedef struct {
 | 
			
		||||
  vector_float3 position;
 | 
			
		||||
  vector_float3 normal;
 | 
			
		||||
  color_half4   color;
 | 
			
		||||
  vector_float2 texCoord;
 | 
			
		||||
} ShaderVertex;
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
  matrix_float4x4 model;
 | 
			
		||||
  matrix_float4x4 normalModel;
 | 
			
		||||
  color_half4 color;
 | 
			
		||||
  color_half4     color;
 | 
			
		||||
} VertexShaderInstance;
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user