[MetalKit]Using MetalKit part 3使用MetalKit3

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

MetalKit系统文章目录


上一节我说我们将学习Metal shading language.在学之前,我们先做一些代码清理和重构.从下载前一节的源代码 source code开始.我们将从重构render()函数开始.所以让我们取出vertex bufferrender pipeline state,并创建3个新的函数放进去,这样我们的旧函数就减少到这样:

var vertex_buffer: MTLBuffer!
var rps: MTLRenderPipelineState! = nil

func render() {
    device = MTLCreateSystemDefaultDevice()
    createBuffer()
    registerShaders()
    sendToGPU()
}

我们先对createBuffer()函数做一些改变.回忆上一节vertex dataFloat类型的数组,像这样:

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]

让我们把它转换成更好的格式,一个带有两个vector_float4类型成员的结构体,一个position另一个是color:

struct Vertex {
    var position: vector_float4
    var color: vector_float4
}

你可能会好奇vector_float4到底是什么样的数据类型.从苹果官方文档中我们发现,这种向量类型是一种clang基础类型,比传统的SIMD类型更适合向量-向量向量-标量的算术运算.它可以通过类似数组下标来访问向量的成员分量,具体作法是用.操作符和组件名称访问(x,y,z,w,或它们的组合).除了.xyzw组件名外,下面的子向量也能通过:.lo / .hi(向量的前半部分和后半部分)来轻松访问,还有奇偶位的.even / .odd子向量:

vector_float4 x = 1.0f;         // x = { 1, 1, 1, 1 }.

vector_float3 y = { 1, 2, 3 };  // y = { 1, 2, 3 }.

x.xyz = y.zyx;                  // x = { 1/3, 1/2, 1, 1 }.

x.w = 0;                        // x = { 1/4, 1/3, 1/2, 0 }.

让我们返回到createBuffer()用新的结构体来替换vertex-data:

func createBuffer() {
    let vertex_data = [Vertex(position: [-1.0, -1.0, 0.0, 1.0], color: [1, 0, 0, 1]),
                       Vertex(position: [ 1.0, -1.0, 0.0, 1.0], color: [0, 1, 0, 1]),
                       Vertex(position: [ 0.0,  1.0, 0.0, 1.0], color: [0, 0, 1, 1])]
    vertex_buffer = device!.newBufferWithBytes(vertex_data, length: sizeof(Vertex) * 3, options:[])
}

你看,通过简单地将它转成结构体数组,我们可以轻易创建顶点数据.

同时,我们保持顶点位置仍在上次的位置上,并且我们为每个顶点添加单独的颜色(红,绿,蓝).接下来,是registerShaders()函数.我们无需改变旧代码,只需要将它移动到新的地方:

func registerShaders() {
    let library = device!.newDefaultLibrary()!
    let vertex_func = library.newFunctionWithName("vertex_func")
    let frag_func = library.newFunctionWithName("fragment_func")
    let rpld = MTLRenderPipelineDescriptor()
    rpld.vertexFunction = vertex_func
    rpld.fragmentFunction = frag_func
    rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm
    do {
        try rps = device!.newRenderPipelineStateWithDescriptor(rpld)
    } catch let error {
        self.print("\(error)")
    }
}

最后,我们对sendToGPU()函数也做同样的操作,不改变旧代码只移动到新地方:

func sendToGPU() {
    if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
        rpd.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
        let command_buffer = device!.newCommandQueue().commandBuffer()
        let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
        command_encoder.setRenderPipelineState(rps)
        command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
        command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
        command_encoder.endEncoding()
        command_buffer.presentDrawable(drawable)
        command_buffer.commit()
    }
}

接下来让我们转移到Shaders.metal文件.这时我们做两处修改.首先,给我们的Vertex结构体添加一个color成员,这样我们就可以在CPUGPU之间来回传递数据:

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

其次,我们替换上次在fragment着色器中使用的硬编码的颜色:

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

替换为每个顶点自带的实际颜色(通过vertex_buffer传递到GPU):

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return vert.color;
}

如果你运行程序,你看到一个更漂亮的彩色三角形:

[MetalKit]Using MetalKit part 3使用MetalKit3_第1张图片
chapter04.png

你也许会奇怪,为什么我们只传递给三个顶点对应颜色,但顶点之间的颜色却是渐变的?要理解这些,就必须先理解两种着色器的不同及它们在图形管线中角色的不同.让我们看看任一个着色器的语法(这里先顶点着色器作例子):

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

第一个关键词,是函数限定符只能使用vertex, fragmentkernel.下一个关键词是返回值类型.接下来是带有圆括号参数的函数name名称.Metal shading language限定了指针的使用,必须用device,threadgroupconstant修饰符来声明,这些修饰符指定了函数变量或参数分配到的内存区域.[[...]]语法是用来声明属性,例如资源位置,着色器输入,以及在着色器与CPU之间来回传递的内置变量.

Metal使用[[ buffer(index) ]]属性来标识出位置,让deviceconstant buffer的参数类型能够区分.内置的输入变量和输出变量被用来在图形函数(顶点和片段)与固定图形管线流程之间传递数据.在我们例子中[[vertex_id]]是传递过程中每个顶点的标识符.Metal接收顶点函数和光栅产生的片段的输出,来产生输入到片段函数的各个片段.每个片段输入依靠[[stage_in]]属性修饰符来标识.

vertex shader用指向顶点列表的指针作为第一个参数.我们可以用第2个参数vid来索引vertices顶点,其中的vid被赋值成vertex_id,它告诉Metal插入当前正在被处理的顶点的索引作为第2个参数.然后只需传递每个顶点(包括位置和颜色)给fragment shader片段着色器去处理.fragment shader片段着色器所作的操作是,取出从vertex shader顶点着色器中传过来的顶点,直接传给每个像素而无需改变输入数据.顶点着色器运行频率不高(本例中只需3次-每个顶点1次),但fragment shader片段着色器运行几千次-每个需要绘制的像素一次.

所以你可能仍然会问:"ok,但是颜色渐变到底怎么回事呢?" 现在你理解了每个着色器的作用及运行频率,你可以认为任一个像素点的颜色都是它的附近像素颜色的平均值.例如,在red红green绿颜色像素正中间的像素颜色将会是yellow黄,只是因为fragment shader片段着色器用平均数来产生颜色插值:0.5 * red + 0.5 * green.同样的,在red红blue蓝正中间的颜色会是magenta品红,在blue蓝green绿正中间的颜色会是cyan青.就这样,剩余部分像素都是用初始颜色的插值,最终结果就是你看到的渐变范围.

源代码source code 已发布在Github上.
下次见!

你可能感兴趣的:([MetalKit]Using MetalKit part 3使用MetalKit3)