terrain/Terrain2/Renderer.swift
Eryn Wells 11b9cba8ac Add a Uniform object for RandomAlgorithm
This object contains several random numbers for the GPU kernel to use when generating random points. This isn't right yet (there are still patterns in the generated data) but it's time to move on...
2018-11-07 16:47:07 -05:00

292 lines
12 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 library: MTLLibrary
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
var iterateTerrainAlgorithm = true
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
guard let library = device.makeDefaultLibrary() else {
print("Unable to create default library")
return nil
}
self.library = library
terrain = Terrain(dimensions: float2(10, 10), segments: uint2(100, 100), device: device, library: library)!
do {
pipelineState = try Renderer.buildRenderPipelineWithDevice(device: device,
library: library,
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,
library: MTLLibrary,
metalKitView: MTKView,
mtlVertexDescriptor: MTLVertexDescriptor) throws -> MTLRenderPipelineState {
/// Build a render state pipeline object
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
if iterateTerrainAlgorithm {
terrain.algorithm.updateUniforms()
}
uniforms[0].projectionMatrix = projectionMatrix
let rotationAxis = float3(0, 1, 0)
let modelMatrix = matrix4x4_rotation(radians: rotation, axis: rotationAxis)
let viewMatrix = matrix4x4_translation(0.0, -2.0, -8.0)
uniforms[0].modelViewMatrix = simd_mul(viewMatrix, modelMatrix)
rotation += 0.0025
}
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
self.iterateTerrainAlgorithm = false
semaphore.signal()
}
self.updateDynamicBufferState()
self.updateGameState()
if iterateTerrainAlgorithm, let computeEncoder = commandBuffer.makeComputeCommandEncoder() {
print("Scheduling terrain generator iteration with \(terrain.algorithm.name) algorithm")
computeEncoder.label = "Generator Encoder"
computeEncoder.pushDebugGroup("Generate Terrain: \(terrain.algorithm.name)")
terrain.algorithm.encode(in: computeEncoder)
computeEncoder.popDebugGroup()
computeEncoder.endEncoding()
}
/// 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.algorithm.outTexture, 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
}