The basic Metal object/code structure

To finish off our talk about Apple Metal, let's look at an overview of the API's object and code structuring. We already briefly saw some shader code in the Metal Shading Language, so let's see how we can work with this API in our projects.

Objects

Purpose

Device

Reference to the GPU

Command queue

Serial sequence of command buffers

Command buffer

Contains GPU hardware commands

Command encoder

Translates API commands to GPU hardware commands

State

Framebuffer configuration, depth, samplers, blend, and so on

Code

Shaders (vertex, fragment, geometry, and tessellation)

Resources

Textures and Data Buffer Objects (vertices, constants, and so on)

The preceding table represents the various types of objects that we'd work with if writing a game directly in the Metal API. They are the Device, the State, the Command Buffer, our Shaders, Textures, and many more.

We can import the Metal API into ViewController.swift class with the following:

import Metal
import QuartzCore

This imports the Metal API. The QuartzCore API is needed as well since the CAMetalLayer object we will work with is a member of that library. Also, make sure that you set your target device to an actual iOS device as new or newer than the iPhone 5S, the Xcode simulator does not support Metal. Otherwise, Xcode will give you the Could Not Build Objective-C model Metal error. This is true as of the writing of this book with the Xcode 7 Beta. Over time and probably after the official public release of the El Capitan OS, this will no longer be needed. For now, to test your own custom Metal code, you will have to test on an actual device. Doing so will involve having to pay for your own Apple Development account. More on this is given in the next chapter.

Here's the order in which we'd have to work with the objects in the table shown previously as well as some code samples in Swift that accomplish these steps:

  1. Create the reference to the Device with the MTLDevice class as:
    let device: MTLDevice = MTLCreateSystemDefaultDevice()
  2. Create a CAMetalLayer object for these objects to be placed on the screen as:
    let metalLayer = CAMetalLayer()
  3. Create Vertex Data/Buffer Object(s) (VBOs) to send data to shaders as follows:
    /*Simple Vertex Data object, an array of floats that draws a simple triangle to the screen */
    let vertexData:[Float] = [
      0.0, 1.0, 0.0,
      -1.0, -1.0, 0.0,
      1.0, -1.0, 0.0]
  4. Create our shaders that will work with these VBOs.

    We did this in our previous shader code samples. The vertex data combined with our previously made shaders together create a simple white triangle to the screen.

  5. Set up a Render Pipeline as follows:
    //Library objects that reference our shaders we created
    let library = device.newDefaultLibrary()!
    //constant where we pass the vertex shader function
    let vertexFunction = library.newFunctionWithName("basic_vertex")
    //now the fragment shader 
    let fragmentFunction = library.newFunctionWithName("basic_fragment")
    
    /*Describes the Render Pipeline and sets the vertex and fragment shaders of the Render Pipeine*/
    let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
    //initiates the descriptor's vertex and fragment shader function properties with the constants we created prior
    pipelineStateDescriptor.vertexFunction = vertexFunction
    pipelineStateDescriptor.fragmentFunction = fragmentFunction
    
    //Makes the pixel format an 8bit color format
    pipelineStateDescriptor.colorAttachments.objectAtIndexedSubscript(0).
    pixelFormat = .BGRA8Unorm
     
    /*Checks if we described the Render Pipeline correctly, otherwise, throws an error. */
    var pipelineError : NSError?
    pipelineState = device.newRenderPipelineStateWithDescriptor(pipelineStateDescriptor, error: &pipelineError)
    if pipelineState == nil {
      println("Pipeline state not created, error (pipelineError)")
  6. Create a command queue as follows:
    var commandQueue = device.newCommandQueue()

To actually render these objects in our game, we'd have to do the following processes in our view controller:

  1. Create a display link. This is a timer that refreshes every time the screen refreshes. It's a member of the class CADisplayLink and at every screen refresh, we call the gameRenderLoop function.
    var timer = CADisplayLink(target: self, selector: Selector("gameRenderLoop"))
    timer.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

    The gameRenderLoop function can look like the following. It calls the soon-to-be filled in function, render():

    func gameRenderloop() {
      autoreleasepool {
        self.render()
      }
  2. Create a Render Pass Descriptor. For this example, a mostly red texture is to be created around our white triangle as shown here:
    let passDescriptor = MTLRenderPassDescriptor() 
    passDescriptor.colorAttachments[0].texture = drawable.texture
    passDescriptor.colorAttachments[0].loadAction = .Clear
    passDescriptor.colorAttachments[0].storeAction = .Store
    passDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.8, 0.0, 0.0, 1.0)
  3. Create a Command Buffer in our render() function:
    let commandBuffer = commandQueue.commandBuffer()
  4. Create a Render Command Encoder. In other words, a set of commands for commandQueue. In the code example given later, this tells the GPU to draw triangles with the VBO we created earlier. This is placed (in this example) in the render() function.
    let renderEncoderOpt = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor)
    if let renderEncoder = renderEncoderOpt {
      renderEncoder.setRenderPipelineState(pipelineState)
      renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
      renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
      renderEncoder.endEncoding()
    }
  5. Commit your Command Buffer. This essentially tells the GPU to do its draw call based on the commands that have been packed into the commandBuffer object. Place this after the past code's if statement in the render() function.
    commandBuffer.presentDrawable(drawable) 
    commandBuffer.commit()

That is the short of it. That's the general process of drawing a simple triangle to the screen and manually creating the render loop on the GPU.

Should you rather opt for SpriteKit and SceneKit to do all of this manual work for you? That would be understandable. Remember though, like when playing a game on hard mode, it comes with its rewards to take the harder route. Yes, as of iOS 9, the SpriteKit and SceneKit frameworks are default to Metal. Game engines, such as Unity and Unreal Engine, even implement Metal when converting projects to the platform. However, knowing how to build your games in a low-level graphics API, such as Metal or OpenGL, will give the developer the ability to have the potential for most lean/fast performing game for the device family. Be sure to check out some of the games created with Metal next time you search online. They can really give your players a great experience. At the same time, this can challenge your skills as a game developer since being a game developer is the combination of an artist, engineer, and computer scientist. Working directly in the GPU's basic functions will challenge all of that.

To dive more into the rabbit hole that is low-level graphics development with Metal, check out these links:

The first link is to the official Apple Developer page for Metal. The next link is Apple's list of data types used in the Metal API. The last two links are two separate tutorials to make simple Metal scenes in Swift. Some of the code we used can be found in these tutorials as well as full Xcode projects. The first of these two links are to the iOS tutorial site www.raywenderlich.com. The last link is to a page that has a great video presentation and full instructions on Swift and Metal 3D graphics by former Apple Engineer, Warren Moore.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.250.11