美文网首页MetalKit专题
[MetalKit]Using MetalKit part 3使

[MetalKit]Using MetalKit part 3使

作者: 苹果API搬运工 | 来源:发表于2017-07-16 16:56 被阅读42次

    本系列文章是对 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;
    }
    

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

    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使

        本文链接:https://www.haomeiwen.com/subject/ltmtkxtx.html