From fbff9b77fd7559ebc6cc0bf3d4744a5f88aaad4c Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Wed, 7 Aug 2024 16:36:23 +1000 Subject: [PATCH] depth buffer implementation --- Sources/Voxelotl/Application.swift | 27 ++++---- Sources/Voxelotl/FloatExtensions.swift | 3 + Sources/Voxelotl/Matrix4x4.swift | 40 ++++++++---- Sources/Voxelotl/Renderer.swift | 88 +++++++++++++++++++++++--- 4 files changed, 125 insertions(+), 33 deletions(-) diff --git a/Sources/Voxelotl/Application.swift b/Sources/Voxelotl/Application.swift index 641636c..1b24305 100644 --- a/Sources/Voxelotl/Application.swift +++ b/Sources/Voxelotl/Application.swift @@ -36,26 +36,25 @@ public class Application { return .exitFailure } - // Create Metal renderer - view = SDL_Metal_CreateView(window) - do { - let layer = unsafeBitCast(SDL_Metal_GetLayer(view), to: CAMetalLayer.self) - layer.displaySyncEnabled = cfg.vsyncMode == .off ? false : true - self.renderer = try Renderer(layer: layer) - } catch RendererError.initFailure(let message) { - printErr("Renderer init error: \(message)") - return .exitFailure - } catch { - printErr("Renderer init error: unexpected error") - } - // Get window metrics var backBufferWidth: Int32 = 0, backBufferHeight: Int32 = 0 guard SDL_GetWindowSizeInPixels(window, &backBufferWidth, &backBufferHeight) >= 0 else { printErr("SDL_GetWindowSizeInPixels() error: \(String(cString: SDL_GetError()))") return .exitFailure } - renderer!.resize(size: SIMD2(Int(backBufferWidth), Int(backBufferHeight))) + + // Create Metal renderer + view = SDL_Metal_CreateView(window) + do { + let layer = unsafeBitCast(SDL_Metal_GetLayer(view), to: CAMetalLayer.self) + layer.displaySyncEnabled = cfg.vsyncMode == .off ? false : true + self.renderer = try Renderer(layer: layer, size: SIMD2(Int(backBufferWidth), Int(backBufferHeight))) + } catch RendererError.initFailure(let message) { + printErr("Renderer init error: \(message)") + return .exitFailure + } catch { + printErr("Renderer init error: unexpected error") + } lastCounter = SDL_GetPerformanceCounter() return .running diff --git a/Sources/Voxelotl/FloatExtensions.swift b/Sources/Voxelotl/FloatExtensions.swift index 7f09d78..8f3fa59 100644 --- a/Sources/Voxelotl/FloatExtensions.swift +++ b/Sources/Voxelotl/FloatExtensions.swift @@ -1,4 +1,7 @@ public extension FloatingPoint { @inline(__always) var degrees: Self { self * (180 / Self.pi) } @inline(__always) var radians: Self { self * (Self.pi / 180) } + + @inline(__always) func lerp(_ a: Self, _ b: Self) -> Self { b * self + a * (1 - self) } + @inline(__always) func mlerp(_ a: Self, _ b: Self) -> Self { a + (b - a) * self } } diff --git a/Sources/Voxelotl/Matrix4x4.swift b/Sources/Voxelotl/Matrix4x4.swift index cdd57ee..532d45e 100644 --- a/Sources/Voxelotl/Matrix4x4.swift +++ b/Sources/Voxelotl/Matrix4x4.swift @@ -43,18 +43,36 @@ public extension simd_float4x4 { .init(0, 0, 0, 1)) } - static func perspective(verticalFov: T, aspect: T, near: T, far: T) -> Self { - let h = 1 / tan(verticalFov * T(0.5)) - let w = h / aspect - - let invClipRange = 1 / (far - near) - let z = -(far + near) * invClipRange - let z2 = -(2 * far * near) * invClipRange + static func orthographic(left: T, right: T, bottom: T, top: T, near: T, far: T) -> Self { + let + invWidth = 1 / (right - left), + invHeight = 1 / (top - bottom), + invDepth = 1 / (far - near) + let + tx = -(right + left) * invWidth, + ty = -(top + bottom) * invHeight, + tz = -near * invDepth + let x = 2 * invWidth, y = 2 * invHeight, z = invDepth return .init( - .init(w, 0, 0, 0), - .init(0, h, 0, 0), - .init(0, 0, z, -1), - .init(0, 0, z2, 0)) + .init( x, 0, 0, 0), + .init( 0, y, 0, 0), + .init( 0, 0, z, 0), + .init(tx, ty, tz, 1)) + } + + static func perspective(verticalFov fovY: T, aspect: T, near: T, far: T) -> Self { + let tanHalfFovY = tan(fovY * T(0.5)) + let invClipRange = 1 / (near - far) + + let y = 1 / tanHalfFovY + let x = y / aspect + let z = far * invClipRange + let w = near * z // (far * near) * invClipRange + return .init( + .init(x, 0, 0, 0), + .init(0, y, 0, 0), + .init(0, 0, z, -1), + .init(0, 0, w, 0)) } } diff --git a/Sources/Voxelotl/Renderer.swift b/Sources/Voxelotl/Renderer.swift index ba54bcb..fdd0263 100644 --- a/Sources/Voxelotl/Renderer.swift +++ b/Sources/Voxelotl/Renderer.swift @@ -41,13 +41,18 @@ fileprivate let cubeIndices: [UInt16] = [ ] class Renderer { + private let depthFormat: MTLPixelFormat = .depth16Unorm + private var device: MTLDevice private var layer: CAMetalLayer private var viewport = MTLViewport() + private var aspectRatio: Float private var queue: MTLCommandQueue private var lib: MTLLibrary private let passDescription = MTLRenderPassDescriptor() private var pso: MTLRenderPipelineState + private var depthStencilState: MTLDepthStencilState + private var depthStencilTexture: MTLTexture private var vtxBuffer: MTLBuffer, idxBuffer: MTLBuffer private var defaultTexture: MTLTexture @@ -62,7 +67,7 @@ class Renderer { }) } - init(layer metalLayer: CAMetalLayer) throws { + init(layer metalLayer: CAMetalLayer, size: SIMD2) throws { self.layer = metalLayer // Select best Metal device @@ -79,9 +84,36 @@ class Renderer { throw RendererError.initFailure("Failed to create command queue") } self.queue = queue - passDescription.colorAttachments[0].loadAction = MTLLoadAction.clear - passDescription.colorAttachments[0].storeAction = MTLStoreAction.store + + self.viewport = MTLViewport( + originX: 0.0, + originY: 0.0, + width: Double(size.x), + height: Double(size.y), + znear: 1.0, + zfar: -1.0) + self.aspectRatio = Float(size.x) / Float(size.y) + + passDescription.colorAttachments[0].loadAction = .clear + passDescription.colorAttachments[0].storeAction = .store passDescription.colorAttachments[0].clearColor = MTLClearColorMake(0.1, 0.1, 0.1, 1.0) + passDescription.depthAttachment.loadAction = .clear + passDescription.depthAttachment.storeAction = .dontCare + passDescription.depthAttachment.clearDepth = 1.0 + + guard let depthStencilTexture = Self.createDepthTexture(device, size, format: depthFormat) else { + throw RendererError.initFailure("Failed to create depth buffer") + } + self.depthStencilTexture = depthStencilTexture + passDescription.depthAttachment.texture = self.depthStencilTexture + + let stencilDepthDescription = MTLDepthStencilDescriptor() + stencilDepthDescription.depthCompareFunction = .less // OpenGL default + stencilDepthDescription.isDepthWriteEnabled = true + guard let depthStencilState = device.makeDepthStencilState(descriptor: stencilDepthDescription) else { + throw RendererError.initFailure("Failed to create depth stencil state") + } + self.depthStencilState = depthStencilState // Create shader library & grab functions do { @@ -97,6 +129,7 @@ class Renderer { pipeDescription.vertexFunction = vertexProgram pipeDescription.fragmentFunction = fragmentProgram pipeDescription.colorAttachments[0].pixelFormat = layer.pixelFormat + pipeDescription.depthAttachmentPixelFormat = depthFormat do { self.pso = try device.makeRenderPipelineState(descriptor: pipeDescription) } catch { @@ -207,7 +240,37 @@ class Renderer { return newTexture } + private static func createDepthTexture(_ device: MTLDevice, _ size: SIMD2, format: MTLPixelFormat + ) -> MTLTexture? { + let texDescriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: format, + width: size.x, + height: size.y, + mipmapped: false) + texDescriptor.depth = 1 + texDescriptor.sampleCount = 1 + texDescriptor.usage = [ .renderTarget, .shaderRead ] +#if !NDEBUG + texDescriptor.storageMode = .private +#else + texDescriptor.storageMode = .memoryless +#endif + + guard let depthStencilTexture = device.makeTexture(descriptor: texDescriptor) else { return nil } + depthStencilTexture.label = "Depth buffer" + + return depthStencilTexture + } + func resize(size: SIMD2) { + if Int(self.viewport.width) != size.x || Int(self.viewport.height) != size.y { + if let depthStencilTexture = Self.createDepthTexture(device, size, format: depthFormat) { + self.depthStencilTexture = depthStencilTexture + passDescription.depthAttachment.texture = self.depthStencilTexture + } + } + + self.aspectRatio = Float(size.x) / Float(size.y) self.viewport = MTLViewport( originX: 0.0, originY: 0.0, @@ -220,14 +283,21 @@ class Renderer { var time: Float = 0 //FIXME: temp func paint() throws { +#if true let projection = matrix_float4x4.perspective( verticalFov: Float(90.0).radians, - aspect: Float(self.viewport.width / self.viewport.height), - near: 0.1, - far: 10) + aspect: aspectRatio, + near: 0.003, + far: 4) +#else + let projection = matrix_float4x4.orthographic( + left: -aspectRatio, right: aspectRatio, + bottom: -1, top: 1, + near: 0, far: -4) +#endif let view = matrix_float4x4.identity let model: matrix_float4x4 = - .translate(.init(0, sin(time * 0.5) * 0.5, -2)) * + .translate(.init(0, sin(time * 0.5) * 0.75, -2)) * .scale(0.5) * .rotate(y: time) @@ -248,9 +318,11 @@ class Renderer { throw RendererError.drawFailure("Failed to make render encoder from command buffer") } + encoder.setCullMode(.back) + encoder.setFrontFacing(.counterClockwise) // OpenGL default encoder.setViewport(viewport) - encoder.setCullMode(MTLCullMode.none) encoder.setRenderPipelineState(pso) + encoder.setDepthStencilState(depthStencilState) encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0) encoder.setVertexBuffer(vtxBuffer,