[MetalKit]4-Using-MetalKit-part-3使用MetalKit3

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

MetalKit系统文章目录git


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

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

func render() {
    device = MTLCreateSystemDefaultDevice()
    createBuffer()
    registerShaders()
    sendToGPU()
}
复制代码

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

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:post

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() 函数.咱们无需改变旧代码,只须要将它移动到新的地方:ui

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;
}
复制代码

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

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上.

下次见!

相关文章
相关标签/搜索