271 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			271 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  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<Uniforms>.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<Uniforms>
 | 
						|
 | 
						|
    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.setVertexTexture(terrain.heights, index: 0)
 | 
						|
                    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
 | 
						|
}
 |