[MetalKit]3-Using-MetalKit-part-2使用MetalKit2

1,504 阅读5分钟

本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.

MetalKit系统文章目录


在本系列的第一部分中我们介绍了MetalKit框架.让我们回到part1的项目中并继续.再看一遍render() 函数,他应该看起来这样:

func render() {
    let device = MTLCreateSystemDefaultDevice()!
    self.device = device
    let rpd = MTLRenderPassDescriptor()
    let bleen = MTLClearColor(red: 0, green: 0.5, blue: 0.5, alpha: 1)
    rpd.colorAttachments[0].texture = currentDrawable!.texture
    rpd.colorAttachments[0].clearColor = bleen
    rpd.colorAttachments[0].loadAction = .Clear
    let commandQueue = device.newCommandQueue()
    let commandBuffer = commandQueue.commandBuffer()
    let encoder = commandBuffer.renderCommandEncoderWithDescriptor(rpd)
    encoder.endEncoding()
    commandBuffer.presentDrawable(currentDrawable!)
    commandBuffer.commit()
}

让我们改进一下这段代码.首先,既然我们的类已经是MTKView的子类,它就已经有了自己的device,所以没有必要再声明一个.这就可以把头两行变成一行:

device = MTLCreateSystemDefaultDevice()

第二步,上周我们说到我们应该确保currentDrawablecurrentRenderPassDescriptor不为空否则应用会崩溃.为了简单点,我们在第1部分时没做,所以现在来添加一下.这将让我们能再减少几行代码.最终版看起来会像这样:

func render() {
    device = MTLCreateSystemDefaultDevice()
    if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
        rpd.colorAttachments[0].clearColor = MTLClearColorMake(0, 0.5, 0.5, 1.0)
        let command_buffer = device!.newCommandQueue().commandBuffer()
        let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
        command_encoder.endEncoding()
        command_buffer.presentDrawable(drawable)
        command_buffer.commit()
    }
}

你可能会好奇colorAttachments[0] 是什么.为了设置rendering pipeline state渲染管线状态,Metal框架提供了3种类型的附件,来让我们写入:

  • colorAttachments颜色附件
  • depthAttachmentPixelFormat像素格式的深度附件
  • stencilAttachmentPixelFormat像素格式的模板附件 我们目前只关心如何储存颜色数据,colorAttachments是一个纹理数组,里面包含了绘制结果并将他们展示到屏幕上.我们目前只有一个这样的纹理-数组中的第一个元素(数组下标为0). OK,现在是时候运行程序了,确保你仍然能看到像上次一样背景颜色.很棒!只用9行代码我们就创建了一个安全运行在GPU上的Metal代码.

接下来,让我们深入研究一个新的Metal话题-在屏幕上绘制几何体.所有的图形学教程比如和OpenGL相关的都会以Hello,Triangle类型程序开始,因为三角形是能绘制在屏幕上几何体中最简单的一个.它是2D图形学基本元素,图形学中其他所有对象都是三角形组成的,所以它是个很好的入门切入点.想象屏幕坐标系统拥有自己的贯穿屏幕中心的坐标轴,中心点坐标为 (0,0).相应的屏幕边缘应该为 -11 .让我们创建一组浮点数和一个缓冲器来保存三角形的顶点.在初始化device后插入下面几行代码:

let vertex_data:[Float] = [-1.0, -1.0, 0.0, 1.0,
                            1.0, -1.0, 0.0, 1.0,
                            0.0,  1.0, 0.0, 1.0]
let data_size = vertex_data.count * sizeof(Float)
let vertex_buffer = device!.newBufferWithBytes(vertex_data, length: data_size, options: [])

上面的顶点数据是按顺序排列的:左下,右下,中上.我们注意到每个顶点使用4个浮点数来表示坐标.前两个是xy轴.本次用不到的浮点数是:第三个深度(z轴)和第四个w坐标用来使坐标系齐次化.我们将在下一节讨论他们.然后我们计算这个数组的size大小为12个浮点数长度,最后我们用数组及其长度来创建一个缓冲器.现在我们已经储存好了我们的顶点,还需要找个办法将他们发送到GPU以便能在屏幕上显示他们.让我们看看绘制到屏幕的整个处理过程(即管线):

chapter03_1.png

我们目前已经完成了第1阶段,储存顶点.下一步要求我们有一个新的构件称为shader着色器.一个shader就是程序员能够用自定义函数来干预图形管线的地方.Metal提供了几种类型的着色器,但今天我们只看其中两种:vertex shader顶点着色器负责点的位置,fragment shader片段着色器负责点的颜色.

Metal框架提供了一个函数,我们可以在device中调用,来创建一个函数(shader)组成的Library库,如下:

let library = device!.newDefaultLibrary()!
let vertex_func = library.newFunctionWithName("vertex_func")
let frag_func = library.newFunctionWithName("fragment_func")

我们创建两个新的函数,将其指向对应的着色器(稍后会创建).下一步是创建一个Render Pipeline Descriptor渲染管线描述符来使用着色器:

let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm

你可能会好奇 .BGRA8Unorm是什么意思.它设置了像素格式,所以渲染管线中出来的所有东西的颜色组件都会是同一顺序(本例中按Blue,Green,Red,Alpha顺序),同时大小也会一致(本例中是8-bit的颜色值,范围从0255).最后一步是根据上面的descriptor描述符创建一个Render Pipeline State渲染管线状态:

let rps = try! device!.newRenderPipelineStateWithDescriptor(rpld)

最后,我们只需要让命令编码器获取到我们的三角形就可以了,所以添加下面几行代码到创建encoder编码器之后:

command_encoder.setRenderPipelineState(rps)
command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) 

现在我们回到两个shaders那里,我们在创建Library库时说过需要创建的.为了创建shader,我们需要创建一个Xcode中的新文件.选择Metal File类型,命名为Shaders.metal或者其他类似名字,单击Create.你将看到代码似乎不是Swift的,因为Metal shading language着色语言其实是基于C++的.添加下面的代码:

#include <metal_stdlib>

using namespace metal;

struct Vertex {
    float4 position [[position]];
};

vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
    return vertices[vid];
}

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return float4(0.7, 1, 1, 1);
}

代码相当直白.我们首先创建一个struct结构体命名为Vertex,里面只有一个成员-一个包含位置数组的数组.我们注意到数组是float4类型,在着色语言中该类型和我们前面创建顶点时的4个浮点数是一样的.我们现在先不解释 [[...]] 语法的意思.然后我们看到vertex_func着色器返回当前顶点的location位置,fragment_func着色器返回当前顶点的color颜色.我们硬编码了一个特殊的颜色值,但是我们可以将color结构体成员添加到Vertex上,这样将每个顶点设置不同的颜色. 如果你运行应用,将会看到像这样的三角形:

chapter03_2.png

下一部分我们将学习Metal shading language也就是3D图形是怎样渲染到GPUs的.源代码source code 已发布在Github上.

下次见!