MetalTexturedMesh/Renderer.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
The Renderer class. This is the reason for the sample. Here you'll find all the detail about how to setup and interact with Metal types to render content to the screen. This type conforms to MTKViewDelegate and performs the rendering in the appropriate call backs. It is created in the ViewController.viewDidLoad() method. |
*/ |
import Metal |
import simd |
import MetalKit |
struct Constants { |
var modelViewProjectionMatrix = matrix_identity_float4x4 |
var normalMatrix = matrix_identity_float3x3 |
} |
@objc |
class Renderer : NSObject, MTKViewDelegate |
{ |
weak var view: MTKView! |
let device: MTLDevice |
let commandQueue: MTLCommandQueue |
let renderPipelineState: MTLRenderPipelineState |
let depthStencilState: MTLDepthStencilState |
let sampler: MTLSamplerState |
let texture: MTLTexture |
let mesh: Mesh |
var time = TimeInterval(0.0) |
var constants = Constants() |
init?(mtkView: MTKView) { |
view = mtkView |
// Use 4x MSAA multisampling |
view.sampleCount = 4 |
// Clear to solid white |
view.clearColor = MTLClearColorMake(1, 1, 1, 1) |
// Use a BGRA 8-bit normalized texture for the drawable |
view.colorPixelFormat = .bgra8Unorm |
// Use a 32-bit depth buffer |
view.depthStencilPixelFormat = .depth32Float |
// Ask for the default Metal device; this represents our GPU. |
if let defaultDevice = MTLCreateSystemDefaultDevice() { |
device = defaultDevice |
} |
else { |
print("Metal is not supported") |
return nil |
} |
// Create the command queue we will be using to submit work to the GPU. |
commandQueue = device.makeCommandQueue() |
// Compile the functions and other state into a pipeline object. |
do { |
renderPipelineState = try Renderer.buildRenderPipelineWithDevice(device, view: mtkView) |
} |
catch { |
print("Unable to compile render pipeline state") |
return nil |
} |
mesh = Mesh(cubeWithSize: 1.0, device: device)! |
do { |
texture = try Renderer.buildTexture(name: "checkerboard", device) |
} |
catch { |
print("Unable to load texture from main bundle") |
return nil |
} |
// Make a depth-stencil state that passes when fragments are nearer to the camera than previous fragments |
depthStencilState = Renderer.buildDepthStencilStateWithDevice(device, compareFunc: .less, isWriteEnabled: true) |
// Make a texture sampler that wraps in both directions and performs bilinear filtering |
sampler = Renderer.buildSamplerStateWithDevice(device, addressMode: .repeat, filter: .linear) |
super.init() |
// Now that all of our members are initialized, set ourselves as the drawing delegate of the view |
view.delegate = self |
view.device = device |
} |
class func buildRenderPipelineWithDevice(_ device: MTLDevice, view: MTKView) throws -> MTLRenderPipelineState { |
// The default library contains all of the shader functions that were compiled into our app bundle |
let library = device.newDefaultLibrary()! |
// Retrieve the functions that will comprise our pipeline |
let vertexFunction = library.makeFunction(name: "vertex_transform") |
let fragmentFunction = library.makeFunction(name: "fragment_lit_textured") |
// A render pipeline descriptor describes the configuration of our programmable pipeline |
let pipelineDescriptor = MTLRenderPipelineDescriptor() |
pipelineDescriptor.label = "Render Pipeline" |
pipelineDescriptor.sampleCount = view.sampleCount |
pipelineDescriptor.vertexFunction = vertexFunction |
pipelineDescriptor.fragmentFunction = fragmentFunction |
pipelineDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat |
pipelineDescriptor.depthAttachmentPixelFormat = view.depthStencilPixelFormat |
return try device.makeRenderPipelineState(descriptor: pipelineDescriptor) |
} |
class func buildTexture(name: String, _ device: MTLDevice) throws -> MTLTexture { |
let textureLoader = MTKTextureLoader(device: device) |
let asset = NSDataAsset.init(name: name) |
if let data = asset?.data { |
return try textureLoader.newTexture(with: data, options: [:]) |
} else { |
fatalError("Could not load image \(name) from an asset catalog in the main bundle") |
} |
} |
class func buildSamplerStateWithDevice(_ device: MTLDevice, |
addressMode: MTLSamplerAddressMode, |
filter: MTLSamplerMinMagFilter) -> MTLSamplerState |
{ |
let samplerDescriptor = MTLSamplerDescriptor() |
samplerDescriptor.sAddressMode = addressMode |
samplerDescriptor.tAddressMode = addressMode |
samplerDescriptor.minFilter = filter |
samplerDescriptor.magFilter = filter |
return device.makeSamplerState(descriptor: samplerDescriptor) |
} |
class func buildDepthStencilStateWithDevice(_ device: MTLDevice, |
compareFunc: MTLCompareFunction, |
isWriteEnabled: Bool) -> MTLDepthStencilState |
{ |
let desc = MTLDepthStencilDescriptor() |
desc.depthCompareFunction = compareFunc |
desc.isDepthWriteEnabled = isWriteEnabled |
return device.makeDepthStencilState(descriptor: desc) |
} |
func updateWithTimestep(_ timestep: TimeInterval) |
{ |
// We keep track of time so we can animate the various transformations |
time = time + timestep |
let modelToWorldMatrix = matrix4x4_rotation(Float(time) * 0.5, vector_float3(0.7, 1, 0)) |
// So that the figure doesn't get distorted when the window changes size or rotates, |
// we factor the current aspect ration into our projection matrix. We also select |
// sensible values for the vertical view angle and the distances to the near and far planes. |
let viewSize = self.view.bounds.size |
let aspectRatio = Float(viewSize.width / viewSize.height) |
let verticalViewAngle = radians_from_degrees(65) |
let nearZ: Float = 0.1 |
let farZ: Float = 100.0 |
let projectionMatrix = matrix_perspective(verticalViewAngle, aspectRatio, nearZ, farZ) |
let viewMatrix = matrix_look_at(0, 0, 2.5, 0, 0, 0, 0, 1, 0) |
// The combined model-view-projection matrix moves our vertices from model space into clip space |
let mvMatrix = matrix_multiply(viewMatrix, modelToWorldMatrix); |
constants.modelViewProjectionMatrix = matrix_multiply(projectionMatrix, mvMatrix) |
constants.normalMatrix = matrix_inverse_transpose(matrix_upper_left_3x3(mvMatrix)) |
} |
func render(_ view: MTKView) { |
// Our animation will be dependent on the frame time, so that regardless of how |
// fast we're animating, the speed of the transformations will be roughly constant. |
let timestep = 1.0 / TimeInterval(view.preferredFramesPerSecond) |
updateWithTimestep(timestep) |
// Our command buffer is a container for the work we want to perform with the GPU. |
let commandBuffer = commandQueue.makeCommandBuffer() |
// Ask the view for a configured render pass descriptor. It will have a loadAction of |
// MTLLoadActionClear and have the clear color of the drawable set to our desired clear color. |
let renderPassDescriptor = view.currentRenderPassDescriptor |
if let renderPassDescriptor = renderPassDescriptor { |
// Create a render encoder to clear the screen and draw our objects |
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) |
renderEncoder.pushDebugGroup("Draw Cube") |
// Since we specified the vertices of our triangles in counter-clockwise |
// order, we need to switch from the default of clockwise winding. |
renderEncoder.setFrontFacing(.counterClockwise) |
renderEncoder.setDepthStencilState(depthStencilState) |
// Set the pipeline state so the GPU knows which vertex and fragment function to invoke. |
renderEncoder.setRenderPipelineState(renderPipelineState) |
// Bind the buffer containing the array of vertex structures so we can |
// read it in our vertex shader. |
renderEncoder.setVertexBuffer(mesh.vertexBuffer, offset:0, at:0) |
// Bind the uniform buffer so we can read our model-view-projection matrix in the shader. |
renderEncoder.setVertexBytes(&constants, length: MemoryLayout<Constants>.size, at: 1) |
// Bind our texture so we can sample from it in the fragment shader |
renderEncoder.setFragmentTexture(texture, at: 0) |
// Bind our sampler state so we can use it to sample the texture in the fragment shader |
renderEncoder.setFragmentSamplerState(sampler, at: 0) |
// Issue the draw call to draw the indexed geometry of the mesh |
renderEncoder.drawIndexedPrimitives(type: mesh.primitiveType, |
indexCount: mesh.indexCount, |
indexType: mesh.indexType, |
indexBuffer: mesh.indexBuffer, |
indexBufferOffset: 0) |
renderEncoder.popDebugGroup() |
// We are finished with this render command encoder, so end it. |
renderEncoder.endEncoding() |
// Tell the system to present the cleared drawable to the screen. |
if let drawable = view.currentDrawable |
{ |
commandBuffer.present(drawable) |
} |
} |
// Now that we're done issuing commands, we commit our buffer so the GPU can get to work. |
commandBuffer.commit() |
} |
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { |
// respond to resize |
} |
@objc(drawInMTKView:) |
func draw(in metalView: MTKView) |
{ |
render(metalView) |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13