// // Renderer.swift // Terrain2 // // Created by Eryn Wells on 11/3/18. // Copyright © 2018 Eryn Wells. All rights reserved. // // Our platform independent renderer class import Metal import MetalKit import simd // The 256 byte aligned size of our uniform structure let alignedUniformsSize = (MemoryLayout.size & ~0xFF) + 0x100 let maxBuffersInFlight = 3 enum RendererError: Error { case badVertexDescriptor } class Renderer: NSObject, MTKViewDelegate { public let device: MTLDevice let commandQueue: MTLCommandQueue var dynamicUniformBuffer: MTLBuffer var pipelineState: MTLRenderPipelineState var depthState: MTLDepthStencilState var colorMap: MTLTexture let inFlightSemaphore = DispatchSemaphore(value: maxBuffersInFlight) var uniformBufferOffset = 0 var uniformBufferIndex = 0 var uniforms: UnsafeMutablePointer var projectionMatrix: matrix_float4x4 = matrix_float4x4() var rotation: Float = 0 var terrain: Terrain init?(metalKitView: MTKView) { self.device = metalKitView.device! self.commandQueue = self.device.makeCommandQueue()! let uniformBufferSize = alignedUniformsSize * maxBuffersInFlight self.dynamicUniformBuffer = self.device.makeBuffer(length:uniformBufferSize, options:[MTLResourceOptions.storageModeShared])! self.dynamicUniformBuffer.label = "UniformBuffer" uniforms = UnsafeMutableRawPointer(dynamicUniformBuffer.contents()).bindMemory(to:Uniforms.self, capacity:1) metalKitView.depthStencilPixelFormat = MTLPixelFormat.depth32Float_stencil8 metalKitView.colorPixelFormat = MTLPixelFormat.bgra8Unorm_srgb metalKitView.sampleCount = 1 terrain = Terrain(dimensions: float2(8, 8), segments: uint2(20, 20), device: device)! do { pipelineState = try Renderer.buildRenderPipelineWithDevice(device: device, metalKitView: metalKitView, mtlVertexDescriptor: terrain.vertexDescriptor) } catch { print("Unable to compile render pipeline state. Error info: \(error)") return nil } let depthStateDesciptor = MTLDepthStencilDescriptor() depthStateDesciptor.depthCompareFunction = MTLCompareFunction.less depthStateDesciptor.isDepthWriteEnabled = true self.depthState = device.makeDepthStencilState(descriptor:depthStateDesciptor)! do { colorMap = try Renderer.loadTexture(device: device, textureName: "ColorMap") } catch { print("Unable to load texture. Error info: \(error)") return nil } super.init() } class func buildRenderPipelineWithDevice(device: MTLDevice, metalKitView: MTKView, mtlVertexDescriptor: MTLVertexDescriptor) throws -> MTLRenderPipelineState { /// Build a render state pipeline object let library = device.makeDefaultLibrary() let vertexFunction = library?.makeFunction(name: "vertexShader") let fragmentFunction = library?.makeFunction(name: "fragmentShader") let pipelineDescriptor = MTLRenderPipelineDescriptor() pipelineDescriptor.label = "RenderPipeline" pipelineDescriptor.sampleCount = metalKitView.sampleCount pipelineDescriptor.vertexFunction = vertexFunction pipelineDescriptor.fragmentFunction = fragmentFunction pipelineDescriptor.vertexDescriptor = mtlVertexDescriptor pipelineDescriptor.colorAttachments[0].pixelFormat = metalKitView.colorPixelFormat pipelineDescriptor.depthAttachmentPixelFormat = metalKitView.depthStencilPixelFormat pipelineDescriptor.stencilAttachmentPixelFormat = metalKitView.depthStencilPixelFormat return try device.makeRenderPipelineState(descriptor: pipelineDescriptor) } class func loadTexture(device: MTLDevice, textureName: String) throws -> MTLTexture { /// Load texture data with optimal parameters for sampling let textureLoader = MTKTextureLoader(device: device) let textureLoaderOptions = [ MTKTextureLoader.Option.textureUsage: NSNumber(value: MTLTextureUsage.shaderRead.rawValue), MTKTextureLoader.Option.textureStorageMode: NSNumber(value: MTLStorageMode.`private`.rawValue) ] return try textureLoader.newTexture(name: textureName, scaleFactor: 1.0, bundle: nil, options: textureLoaderOptions) } private func updateDynamicBufferState() { /// Update the state of our uniform buffers before rendering uniformBufferIndex = (uniformBufferIndex + 1) % maxBuffersInFlight uniformBufferOffset = alignedUniformsSize * uniformBufferIndex uniforms = UnsafeMutableRawPointer(dynamicUniformBuffer.contents() + uniformBufferOffset).bindMemory(to:Uniforms.self, capacity:1) } private func updateGameState() { /// Update any game state before rendering uniforms[0].projectionMatrix = projectionMatrix let rotationAxis = float3(1, 1, 0) let modelMatrix = matrix4x4_rotation(radians: rotation, axis: rotationAxis) let viewMatrix = matrix4x4_translation(0.0, 0.0, -8.0) uniforms[0].modelViewMatrix = simd_mul(viewMatrix, modelMatrix) rotation += 0.01 } func draw(in view: MTKView) { /// Per frame updates hare _ = inFlightSemaphore.wait(timeout: DispatchTime.distantFuture) if let commandBuffer = commandQueue.makeCommandBuffer() { let semaphore = inFlightSemaphore commandBuffer.addCompletedHandler { (_ commandBuffer)-> Swift.Void in semaphore.signal() } self.updateDynamicBufferState() self.updateGameState() /// Delay getting the currentRenderPassDescriptor until we absolutely need it to avoid /// holding onto the drawable and blocking the display pipeline any longer than necessary let renderPassDescriptor = view.currentRenderPassDescriptor if let renderPassDescriptor = renderPassDescriptor { /// Final pass rendering code here if let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) { renderEncoder.label = "Primary Render Encoder" renderEncoder.pushDebugGroup("Draw Plane") renderEncoder.setCullMode(.none) renderEncoder.setFrontFacing(.counterClockwise) renderEncoder.setRenderPipelineState(pipelineState) renderEncoder.setDepthStencilState(depthState) renderEncoder.setTriangleFillMode(.lines) renderEncoder.setVertexBuffer(dynamicUniformBuffer, offset:uniformBufferOffset, index: BufferIndex.uniforms.rawValue) renderEncoder.setFragmentBuffer(dynamicUniformBuffer, offset:uniformBufferOffset, index: BufferIndex.uniforms.rawValue) for (index, element) in terrain.mesh.vertexDescriptor.layouts.enumerated() { guard let layout = element as? MDLVertexBufferLayout else { return } if layout.stride != 0 { let buffer = terrain.mesh.vertexBuffers[index] renderEncoder.setVertexBuffer(buffer.buffer, offset:buffer.offset, index: index) } } renderEncoder.setFragmentTexture(colorMap, index: TextureIndex.color.rawValue) for submesh in terrain.mesh.submeshes { renderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset) } renderEncoder.popDebugGroup() renderEncoder.endEncoding() if let drawable = view.currentDrawable { commandBuffer.present(drawable) } } } commandBuffer.commit() } } func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { /// Respond to drawable size or orientation changes here let aspect = Float(size.width) / Float(size.height) projectionMatrix = matrix_perspective_right_hand(fovyRadians: radians_from_degrees(65), aspectRatio:aspect, nearZ: 0.1, farZ: 100.0) } } // Generic matrix math utility functions func matrix4x4_rotation(radians: Float, axis: float3) -> matrix_float4x4 { let unitAxis = normalize(axis) let ct = cosf(radians) let st = sinf(radians) let ci = 1 - ct let x = unitAxis.x, y = unitAxis.y, z = unitAxis.z return matrix_float4x4.init(columns:(vector_float4( ct + x * x * ci, y * x * ci + z * st, z * x * ci - y * st, 0), vector_float4(x * y * ci - z * st, ct + y * y * ci, z * y * ci + x * st, 0), vector_float4(x * z * ci + y * st, y * z * ci - x * st, ct + z * z * ci, 0), vector_float4( 0, 0, 0, 1))) } func matrix4x4_translation(_ translationX: Float, _ translationY: Float, _ translationZ: Float) -> matrix_float4x4 { return matrix_float4x4.init(columns:(vector_float4(1, 0, 0, 0), vector_float4(0, 1, 0, 0), vector_float4(0, 0, 1, 0), vector_float4(translationX, translationY, translationZ, 1))) } func matrix_perspective_right_hand(fovyRadians fovy: Float, aspectRatio: Float, nearZ: Float, farZ: Float) -> matrix_float4x4 { let ys = 1 / tanf(fovy * 0.5) let xs = ys / aspectRatio let zs = farZ / (nearZ - farZ) return matrix_float4x4.init(columns:(vector_float4(xs, 0, 0, 0), vector_float4( 0, ys, 0, 0), vector_float4( 0, 0, zs, -1), vector_float4( 0, 0, zs * nearZ, 0))) } func radians_from_degrees(_ degrees: Float) -> Float { return (degrees / 180) * .pi }