• 若想用swift语言生成库则只能生成动态库,若是纯swift代码脚本,问题不大,按一般流程就行。 若是编译的库中有shader文件 则需要先编译出Metal库。具体方法可采用如下的方式。

    若想用swift语言生成库则只能生成动态库,若是纯swift代码脚本,问题不大,按一般流程就行。
    若是编译的库中有shader文件则需要先编译出Metal库。具体方法可采用如下的方式。

    1. 把shader文件和swift文件放在一起进行framework库的编译。编译成功后会自动生成一个default.metallib文件,该文件为shader生成的库文件。
    2. 把生成的framework导入需要调用的地方,注意使用库时需要单独把default.metallib取出来放到工程中的Resource下面。
    3. 使用let library = device.newDefaultLibrary()直接加载Metal库。

    上面的方式是最简单的,另外,对shader文件,也可以不在xcode中编译库,而是采取手动的编译方式。参考苹果官方文档:https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Dev-Technique/Dev-Technique.html
    手动编译Metal库后,加载库不能采用上面步骤3中的方式,需要采用如下的方式:

    guard let metallibpath = Bundle(for: type(of: self)).path(forResource: "YourShader", ofType: "metallib") else {
                    fatalError("Metal library not found")
                }
    let library = try device.makeLibrary(filepath: metallibpath)
    if library == nil{
                    print("metal library is null")
                }
    
    展开全文
  • Apple Metal2 Swift尝试

    2019-07-20 21:51:16
    写在前面的话 我个人并不是iOS或者macOS的开发工程师,只是之前用GLES2.0做过一些项目,前段时间知道苹果...虽然GLES还没有完全被苹果彻底弃用,但是还是想学习一下这个新的图形底层API—Metal 学习前的准备 Appl...

    写在前面的话

    我个人并不是iOS或者macOS的开发工程师,只是之前用GLES2.0做过一些项目,前段时间知道苹果公司已经决定弃用OpenGL和OpenCL。以前做图形相关的项目,只需要维护一套GLES的代码就可以支持Apple、Linux、Android以及市面几乎所有的嵌入式厂商。虽然GLES还没有完全被苹果彻底弃用,但是还是想学习一下这个新的图形底层API—Metal

    学习前的准备

    Apple目前推荐的官方开发语言是Swift,而且Metal也没有提供C++的API,不过网上有大神用C++封装了一层,可以参考一下。链接地址:https://github.com/naleksiev/mtlpp。不过还是决定试试使用Swift,代码中的各种?和!都是系统提示我的,所以肯定有不完善的地方。 另外手边没有iPhone,所以demo程序是运行在Mac上的。

    正题

    工程创建

    用Xcode创建一个macOS的Cocoa App项目,项目名字可以随便填一个,我这面填的是MyCode。语言选择Swift。Team那个地方选择自己的账号。其他的根据需要填写。
    PS:需要提前注册一个Apple ID,去Apple的开发者网站激活一下,不需要付费就可以在mac上调试自己的程序了。具体方法就不详细说明了。

    想做什么

    因为在GLES中FBO这个东西被用到的次数非常多,所以想看看这个东西在Metal中怎么实现。我是按照GLES的思路先渲染到一个中间Texture上,再渲染到屏幕。不过貌似Metal推荐的做法是使用Compute(就是替代原来OpenCL的东西)。

    开始写代码

    先实现Texture直接渲染到屏幕上

    基本代码结构

    Metal貌似支持渲染在MTKView和CAMetalLayer上,我采用MTKView,并且添加一个渲染类来实现真正的渲染工作。

    在系统为我们创建好的ViewController.swift中的viewDidLoad函数中添加MTKView的初始化工作。这部分的实现非常简单就一个init函数就搞定了,感觉这部分相当于原来EGL的各种操作,只不过系统帮你实现了。然后我想添加一个渲染类来处理描画。

    • 创建TextureRender 类
      在构造函数将MTKView作为参数传进来,并加一个render函数用来描画。代码大概是下面的样子:
    import Foundation
    import MetalKit
    
    class TextureRender {
        //MARK: Properties
        var _mtkView: MTKView!
        //MARK: Initialization
        init?( mtkView:MTKView ) {
            self._mtkView = mtkView
        }
        // MARK : render method
        func render() -> Void {
        }
    
    • ViewController中的实现
    1. 在ViewDidLoad函数中实例MTKView和TextureRender,并修改ViewController的View为MTKView
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let mtkView = MTKView.init(frame: self.view.bounds)
            mtkView.delegate = self
            self.view = mtkView
            
            self._render = TextureRender(mtkView: mtkView)
            // Do any additional setup after loading the view.
        }
    

    因为要设置mtkView的代理实例,所以要继承MTKViewDelegate以及实现代理方法。最后ViewController的代码如下:
    (到目前为止,ViewController中所有工作以及完成了)

    //
    //  ViewController.swift
    //  MyCode
    //
    //  Created by larry-kof on 2018/11/26.
    //  Copyright © 2018 larry-kof. All rights reserved.
    //
    
    import Cocoa
    import MetalKit
    
    class ViewController: NSViewController, MTKViewDelegate {
        
        var _render:TextureRender!
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let mtkView = MTKView.init(frame: self.view.bounds)
            mtkView.delegate = self
            self.view = mtkView
            
            self._render = TextureRender(mtkView: mtkView)
            // Do any additional setup after loading the view.
        }
    
        override var representedObject: Any? {
            didSet {
            // Update the view, if already loaded.
            }
        }
        
        
        // MARK: delegate
        func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    
        }
        
        func draw(in view: MTKView) {
            self._render.render()
        }
    
    }
    
    TextureRender实现

    根据GLES中的经验,下一步就要编译vertex shader和fragment shader生成glProgram。不过在metal中需要一个device就是选择一个硬件设备(GPU或者CPU),和OpenCL中的clGetDeviceIDs一样。所以在构造函数中获取device,同时记住mtkView的大小以后来设置描画Viewport的大小。

        init?( mtkView:MTKView ) {
            self._mtkView = mtkView
            
            self._mtkView.device = MTLCreateSystemDefaultDevice()
            self._portViewSize = self._mtkView.drawableSize
    
        }
    
    创建MTLRenderPipelineState

    这一步相当于GLES中glCreateProgram + glLinkProgram
    和GLES类似,需要些vertex shader和fragment shader
    在工程里添加新文件选择Metal File

    1. shader
      Meta shader代码如下:
    //
    //  shader.metal
    //  MyCode
    //
    //  Created by larry-kof on 2018/11/26.
    //  Copyright © 2018 larry-kof. All rights reserved.
    //
    
    #include <metal_stdlib>
    #include "ShaderTypes.h"
    using namespace metal;
    
    struct RasterizerData{
        float4 position [[position]];
        float2 texCoord;
    };
    
    vertex RasterizerData
    texVertexShader(uint vid [[vertex_id]],
                 constant Vertex* vertexArray [[ buffer(0) ]])
    {
        RasterizerData out;
        out.position = vertexArray[vid].position;
        out.texCoord = vertexArray[vid].texCoord;
        
        return out;
    }
    
    fragment float4
    texFragmentShader(RasterizerData input [[stage_in]],
                      texture2d<float> inputTexture [[ texture(0) ]])
    {
        constexpr sampler textureFilter (mag_filter::linear,
                                          min_filter::linear);
        
        float4 colorSample = inputTexture.sample(textureFilter, input.texCoord);
        
        return colorSample;
    }
    

    ShaderTypes.h的代码如下:

    //
    //  ShaderTypes.h
    //  MyCode
    //
    //  Created by larry-kof on 2018/11/26.
    //  Copyright © 2018 larry-kof. All rights reserved.
    //
    
    #ifndef ShaderTypes_h
    #define ShaderTypes_h
    
    #include <simd/simd.h>
    
    struct Vertex
    {
        vector_float4 position;
        vector_float2 texCoord;
    };
    
    #endif /* ShaderTypes_h */
    
    • 说明
      ShadeTypes.h中的position和texCoord:我们要从swift程序传过来的值
      float4 position [[position]]; :告诉shader去哪找vertex坐标,相当于gl_Position的作用
      uint vid [[vertex_id]] :因为传过来的vertex坐标最少三个,vid是index
      constant Vertex* vertexArray [[ buffer(0) ]]:
      (1) constant: 当变量是pointer或者ref,metal规定前面需要加constant或者device 来修饰
      (2) [[ buffer(0) ]]:相当于glVertexAttribPointer中的index
      RasterizerData input [[stage_in]] :stage_in表示从vertex shader传到fragment shader的数据,类似于GLES2.0 shader的varying关键字或者GLES3.0 的 in/out 关键字

    另外关于float4,half4等等变量从代码角度代替了vec4。区别应该是精度。

    1. Swift
      追加一个setupPipe的private方法,代码如下:
        private func setupPipe() {
            let defaultLibrary = self._mtkView.device?.makeDefaultLibrary()
            
            let vextexFunction = defaultLibrary?.makeFunction(name: "texVertexShader")
            let fragFunction = defaultLibrary?.makeFunction(name: "texFragmentShader")
            
            let pipelineStateDesc = MTLRenderPipelineDescriptor.init()
            pipelineStateDesc.vertexFunction = vextexFunction
            pipelineStateDesc.fragmentFunction = fragFunction
            
            pipelineStateDesc.colorAttachments[0].pixelFormat = self._mtkView.colorPixelFormat
            
            do {
                try self._pipelineState = self._mtkView.device?.makeRenderPipelineState(descriptor: pipelineStateDesc)
            } catch {
                print(error)
            }
        }
    
    • 说明
      let defaultLibrary = self._mtkView.device?.makeDefaultLibrary()
      Metal会去寻找工程下所有的metal文件并加载进来。

    makeFunction中的name就是对应shader的函数名,这一步相当于glCreateshader+ glShaderSource+ glCompileShader

    makeRenderPipelineState 相当于glCreaterProgram+ glAttachShader+ glLinkProgram

    创建顶点Buffer

    创建setupVertex的private方法,因为想直接使用ShaderTypes.h中的Vertex结构体,Swift不支持直接引用头文件,只能通过bridge的方式,也是好麻烦,突然觉得这个语言不是很好用了。。。

    1. 构建XXX-Bridge-Header.h
      右键工程文件夹(是黄色的那个) -> New File -> macOS -> Header File ,文件名一般是XXX-Bridge-Header.h,
      其中XXX是你的工程名,我的话就是MyCode-Bridge-Header.h

    2. 配置 Bridge Header
      点击工程(蓝色的) -> Build Settings -> 找到Objective-C Bridging Header -> 输入

    $(PRODUCT_NAME)/$(PRODUCT_NAME)-Bridging-Header.h
    
    1. XXX-Bridge-Header.h代码
      #import "ShaderTypes.h"

    2. setupVertex代码
      其中坐标的范围和GLES相同 vertex是 [-1,1 ], texCoord是[0,1]
      这一步相当于glGenBuffers + glBindBuffer + glBufferData

        private func setupVertex() {
            
            let qVertices = [Vertex(position: vector_float4([-1.0, -1.0, 0.0, 1.0]), texCoord: vector_float2([0.0, 0.0])),
                             Vertex(position: vector_float4([ -1.0, 1.0, 0.0, 1.0]), texCoord: vector_float2([0.0, 1.0])),
                             Vertex(position: vector_float4([1.0,  -1.0, 0.0, 1.0]), texCoord: vector_float2([1.0, 0.0])),
                             Vertex(position: vector_float4([1.0, 1.0, 0.0, 1.0]), texCoord: vector_float2([1.0, 1.0])),
                             ]
            self._vertice = self._mtkView.device?.makeBuffer(bytes: qVertices, length: qVertices.count * MemoryLayout<Vertex>.size , options: .storageModeShared)
            
            self._numVertice = qVertices.count
        }
    
    创建InputTexture
    1. 资源
      这里面采用TAG文件,因为想用官方Hello Compute提供的AAPLImage来解码。
    • 找到一个TGA文件
      简书好像没法插入TGA文件格式的图片,大家去网络找一个,或者我最后分享的整个工程代码中找到,或者使用其他的解码方式吧。。。
    1. 导入AAPLImage.m 和 AAPLImage.h文件

    2. 生成texture
      整体来说就是glTexImage2D
      其中Swift要想获得数据的指针真的有点麻烦,要使用withUnsafeBytes 这个方法,可能是为了指针使用的安全吧

        private func setupTexture() {
            let url = Bundle.main.url(forResource: "miami_beach", withExtension: "tga")
            let image = AAPLImage.init(tgaFileAtLocation: url!)
            
            let textureDesc = MTLTextureDescriptor.init()
            textureDesc.width = (image?.width)!
            textureDesc.height = (image?.height)!
            textureDesc.pixelFormat = .bgra8Unorm
            textureDesc.usage = .shaderRead
            textureDesc.textureType = .type2D
            
            self._inputTexture = self._mtkView.device?.makeTexture(descriptor: textureDesc)
            
            let region = MTLRegionMake2D(0, 0, textureDesc.width, textureDesc.height)
            image?.data.withUnsafeBytes {
                ( bytes:UnsafePointer<UInt8> ) in
                let rawPtr = UnsafeRawPointer(bytes)
                self._inputTexture.replace(region: region, mipmapLevel: 0, withBytes: rawPtr, bytesPerRow: 4 * textureDesc.width)
            }
        }
    
    1. 在构造函数添加setup方法
      //MARK: Initialization
        init?( mtkView:MTKView ) {
            self._mtkView = mtkView
            
            self._mtkView.device = MTLCreateSystemDefaultDevice()
            self._portViewSize = self._mtkView.drawableSize
    
            self.customInit()
        }
        
        // MARK: private function
        private func customInit() {
            self.setupPipe()
            self.setupVertex()
            self.setupTexture()
            self._commandQueue = self._mtkView.device?.makeCommandQueue()
        }
    
    render 方法的实现

    首先追加一个draw方法

    func draw(commandBuffer:MTLCommandBuffer, texture: MTLTexture, desDrawble:CAMetalDrawable) {
        }
    

    根据GLES的描画步骤是

    1. Clear Color, Depth Stencil等 -> commandBuffer.makeRenderCommandEncoder
    2. glSetViewport -> renderEncoder?.setViewport
    3. glUseProgram -> renderEncoder?.setRenderPipelineState
    4. glVertexAttribPointer -> renderEncoder?.setVertexBuffer
    5. 设置其他 -> renderEncoder?.setFragmentTexture
    6. glDrawArray -> renderEncoder?.drawPrimitives
      所以有以下的代码
        func draw(commandBuffer:MTLCommandBuffer, texture: MTLTexture, desDrawble:CAMetalDrawable) {
            let renderPassDesc = self._mtkView.currentRenderPassDescriptor
            if renderPassDesc != nil {
                renderPassDesc?.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 0.0)
                renderPassDesc?.colorAttachments[0].loadAction = .clear
                
                let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc!)
                renderEncoder?.setViewport(MTLViewport.init(originX: 0.0, originY: 0.0, width: Double(self._portViewSize!.width), height: Double(self._portViewSize!.height), znear: -1.0, zfar: 1.0))
                renderEncoder?.setRenderPipelineState(self._pipelineState)
                
                renderEncoder?.setFragmentTexture(texture, index: 0)
                renderEncoder?.setVertexBuffer(self._vertice, offset: 0, index: 0)
                renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: self._numVertice)
                
                renderEncoder?.endEncoding()
                
                commandBuffer.present(desDrawble)
            }
        }
    

    最后完成render函数

        // MARK : render method
        func render() -> Void {
            let commandBuffer = self._commandQueue?.makeCommandBuffer()
            draw(commandBuffer: commandBuffer!, texture: self._inputTexture, desDrawble: self._mtkView.currentDrawable!)
            commandBuffer!.commit()
        }
    

    最后的commit,类似glFinish + eglSwapBuffers

    最后的执行画面是
    Screenshot 2018-12-01 at 2.06.47 PM.png

    FBO

    创建FBORender类,继承TextureRender

    创建新的shader将原图改变成灰度图

    constant float3 kRec709Luma = float3(0.2126, 0.7152, 0.0722);
    fragment float4
    fboFragmentShader(RasterizerData input [[stage_in]],
                      texture2d<float> inputTexture [[ texture(0) ]])
    {
        constexpr sampler textureFilter (mag_filter::linear,
                                         min_filter::linear);
        
        float4 colorSample = inputTexture.sample(textureFilter, input.texCoord);
        
        float gray = dot(colorSample.rgb, kRec709Luma);
        return float4( gray, gray, gray, 1.0 );
    }
    

    在FBORender中,和之前创建新的PipelineState和middleTexture用来接收中间处理结果,并作为渲染到屏幕上的源。不同的是生成middleTexture的时候,需要添加renderTarget属性。

    textureDesc.usage = [.renderTarget, .shaderRead]
    

    添加drawToTexture函数,不同的是需要设置

            renderPassDesc.colorAttachments[0].texture = self._middleTexture
    

    感觉相当于glBindFramebuffer, 最后FBORender.swift的代码如下

    //
    //  FBORender.swift
    //  MyCode
    //
    //  Created by larry-kof on 2018/11/29.
    //  Copyright © 2018 larry-kof. All rights reserved.
    //
    
    import Foundation
    
    import MetalKit
    
    class FBORender:TextureRender {
        
        private var _fboPipelineState: MTLRenderPipelineState!
        private var _middleTexture: MTLTexture!
        
        override init?(mtkView: MTKView) {
            super.init(mtkView: mtkView)
            
            self.setupFBOPipe()
            self.setupMiddleTexture()
        }
        
        // MARK : private func
        private func setupFBOPipe() {
            let defaultLibrary = self._mtkView.device?.makeDefaultLibrary()
            
            let vextexFunction = defaultLibrary?.makeFunction(name: "texVertexShader")
            let fragFunction = defaultLibrary?.makeFunction(name: "fboFragmentShader")
            
            let pipelineStateDesc = MTLRenderPipelineDescriptor.init()
            pipelineStateDesc.vertexFunction = vextexFunction
            pipelineStateDesc.fragmentFunction = fragFunction
            
            pipelineStateDesc.colorAttachments[0].pixelFormat = self._inputTexture.pixelFormat
            
            do {
                try self._fboPipelineState = self._mtkView.device?.makeRenderPipelineState(descriptor: pipelineStateDesc)
            } catch {
                print(error)
            }
        }
        
        private func setupMiddleTexture() {
            let textureDesc = MTLTextureDescriptor.init()
            textureDesc.width = self._inputTexture.width
            textureDesc.height = self._inputTexture.height
            textureDesc.pixelFormat = self._inputTexture.pixelFormat
            textureDesc.usage = [.renderTarget, .shaderRead]
            textureDesc.textureType = .type2D
            
            self._middleTexture = self._mtkView.device?.makeTexture(descriptor: textureDesc)
        }
        
        private func drawToMiddleTexture(command: MTLCommandBuffer, texture: MTLTexture) {
            let renderPassDesc = MTLRenderPassDescriptor.init()
            renderPassDesc.colorAttachments[0].clearColor =  MTLClearColorMake(0.0, 0.0, 0.0, 0.0)
            renderPassDesc.colorAttachments[0].loadAction = .clear
            renderPassDesc.colorAttachments[0].texture = self._middleTexture
            
            let renderEncoder = command.makeRenderCommandEncoder(descriptor: renderPassDesc)
            
            renderEncoder?.setViewport(MTLViewport.init(originX: 0.0, originY: 0.0, width: Double(self._middleTexture.width), height: Double(self._middleTexture.height), znear: -1.0, zfar: 1.0))
            
            renderEncoder?.setRenderPipelineState(self._fboPipelineState)
            
    //        renderEncoder?.setVertexBuffer(self._vertice, offset: 0, index: 0)
            renderEncoder?.setFragmentTexture(texture, index: 0)
            renderEncoder?.setVertexBuffer(self._vertice, offset: 0, index: 0)
            renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: self._numVertice)
            
            renderEncoder?.endEncoding()
        }
        
        // MARK override method
        override func render() -> Void {
            let commanBuffer = self._commandQueue?.makeCommandBuffer()
            
            self.drawToMiddleTexture(command: commanBuffer!, texture: self._inputTexture)
            
            super.draw(commandBuffer: commanBuffer!, texture: self._middleTexture, desDrawble: self._mtkView.currentDrawable!)
            commanBuffer!.commit()
        }
        
        
    }
    

    最后的最后将ViewController.swift中的
    self._render = TextureRender(mtkView: mtkView)
    变成
    self._render = FBORender.init(mtkView: mtkView)

    执行结果:(不知道为什么上下反了,直接渲染的话是正常的,坐标我看了几遍感觉没问题,不知道哪里错了。还请大神指教。也许以后应该使用Compute做中间渲染吧)
    Screenshot 2018-12-01 at 7.45.55 PM.png

    工程整体代码

    https://github.com/larry-kof/Metal_Study_Demo

    展开全文
  • - iOS 8 Metal Swift教程(一) :开始学习 在本篇教程中,你将应用到3D图形中的一系列矩阵变换,并会学习到如下内容: 如何使用模型(model),视图(view)以及投影变换(projection transformations)。 如何...

    在开始之前,你可以先参考一下本系列的第一篇教程:

    - iOS 8 Metal Swift教程(一) :开始学习

    在本篇教程中,你将应用到3D图形中的一系列矩阵变换,并会学习到如下内容:

    如何使用模型(model),视图(view)以及投影变换(projection transformations)。

    如何使用矩阵运算变换几何图形

    如何在着色器(shader)间传递统一数据

    如何使用背面剔除(backface culling)来优化渲染

    开篇

    首先,你需要的下载一个工程,这和此前的教程中用到的是一样的。

    构建并运行,请注意你的测试设备需要兼容Metal,然后确认你能看到下面这个三角形。

    BeautifulTriangle-240x320.png

    现在你需要下载一个Matrix4类,这是事先写好的,然后你需要将其加入你的工程中。这个时候因为你交叉使用了Swift和Objective-C,Xcode会提示你是否要配置一个桥接头文件(Bridging Header),这时候只要选择YES就行。

    待会儿要在很多地方用到矩阵,所以你最好先看一遍Matrix4.m和Matrix4.h文件,对这个类有个清晰的认识。

    iOS的内建库GLKMatrix中包含了一个用于常见3D运算的GLKMath库,其中包括可以矩阵运算的GLKMatrix4类。

    本教程涉及大量的矩阵运算,使用这个库会很方便许多,不过GLKMatrix4是一个C语言结构体,所以在Swift中你不能直接调用它。

    呐,所以呢,我就给各位用Objective-C封装了一下C的结构体,这样我们就能愉快的在Swift中使用GLKMatrix4了,下面是这一调用封装过程的图解:

    Screen-Shot-2014-09-05-at-4.23.05-PM-480x275.png

    再次提醒一下,下面内容真的会有很多矩阵运算,所以你现在还是好好看一下Matrix4类的代码吧。

    重构一个节点类(Node Class)

    所有的内容在一开始的工程里面的ViewController.swift文件中已经都设置好了,这的确是最简便上手的方式,不过等到你的App变得越来越大越来越复杂那可就说不定了。

    在这一小节中,你需要通过以下步骤来重构项目:

    1.创建顶点结构(Vertex Structure)

    2.创建节点类(Node Class)

    3.创建三角形子类(Triangle Subclass)

    4.重构视图控制器(View Controller)

    5.重构着色器(Shader)

    提示:这一节是可以不看的,因为到本节最后只是让你对整个工程有个清晰的认识,如果你想要直接看3D处理的部分,那么可以直接跳过此节,而在下一节的开头你可以下载全新的开始项目——当然那是完全配好了的。

    1.创建顶点结构(Vertex Structure)

    新建一个文件并以 iOS/Source/Swift File为模板创建一个类,命名为Vertex.swift.

    打开该文件,用下面代码覆盖它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Vertex{
      
      var x,y,z: Float     // position data
      var r,g,b,a: Float   // color data
      
      func floatBuffer() -> [Float] {
        return [x,y,z,r,g,b,a]
      }
     };

    这个结构体会存储每一个顶点的颜色信息和位置信息。其中floatBuffer()方法会按照规定的顺序返回一个float型数组,其中包含的是结构体的位置和颜色的信息。

    2.创建节点类(Node Class)

    同上创建一个Swif文件命名为Node.swift.

    同上步骤,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import Foundationimport Metalimport QuartzCore 
    class Node {
      
      let name: String
      var vertexCount: Int
      var vertexBuffer: MTLBuffer  var device: MTLDevice 
      init(name: String, vertices: Array<Vertex>, device: MTLDevice){
        // 遍历每个顶点并将其序列化为一堆float数据放在一个buffer中
        var vertexData = Array<Float>()
        for vertex in vertices{
          vertexData += vertex.floatBuffer()
        }
      
        // 用上面的buffer中的数据来创建一个新的顶点buffer
        let dataSize = vertexData.count * sizeofValue(vertexData[0])
        vertexBuffer = device.newBufferWithBytes(vertexData, length: dataSize, options: nil)
      
        // 为实例中的各变量赋值
        self.name = name    self.device = device
        vertexCount = vertices.count
      }
     }

    作为要渲染的基本单位,每个节点中的顶点都需要包含有一个名字以便使用,之后设备会为其创建buffer并渲染顶点,buffer的结构大致是这样的:

    Screen-Shot-2014-09-05-at-5.28.41-PM-480x54.png

    接下来,你需要把当前视图控制器中的部分渲染代码移动到Node中去,这些代码会为特定的顶点渲染起作用。

    这样你需要在Node.swift里面添加一个新方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func render(commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, clearColor: MTLClearColor?){
      
      let renderPassDescriptor = MTLRenderPassDescriptor()
      renderPassDescriptor.colorAttachments[0].texture = drawable.texture
      renderPassDescriptor.colorAttachments[0].loadAction = .Clear
      renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)
      renderPassDescriptor.colorAttachments[0].storeAction = .Store 
      let commandBuffer = commandQueue.commandBuffer()
      
      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: vertexCount, instanceCount: vertexCount/3)
        renderEncoder.endEncoding()
      }
      
      commandBuffer.presentDrawable(drawable)
      commandBuffer.commit()
    }

    在上一篇教程里面你也可以找到这段代码,你会发现这段代码来源于ViewController类的render()方法,不过对于Node的顶点渲染有所改动。

    3.创建三角形子类(Triangle Subclass)

    同上创建文件,命名为Triangle.swift.

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import Foundationimport Metal 
    class Triangle: Node {
      
      init(device: MTLDevice){
      
        let V0 = Vertex(x:  0.0, y:   1.0, z:   0.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
        let V1 = Vertex(x: -1.0, y:  -1.0, z:   0.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
        let V2 = Vertex(x:  1.0, y:  -1.0, z:   0.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
      
        var verticesArray = [V0,V1,V2]
        super.init(name: "Triangle", vertices: verticesArray, device: device)
      }
     }

    这里的Triangle继承于刚刚创建的Node类,在构造函数里有三个关于三角形顶点的常量,最后打包为一个数组并传递给了父类的构造函数中。

    4.重构视图控制器(View Controller)

    打开ViewController.swift并删除下面这行:

    1
    var vertexBuffer: MTLBuffer! = nil

    因为Node对象需要用到vertextBuffer所以这行不要了。

    然后把下面这段代码:

    1
    2
    let vertexData:[Float] = [
        0.0, 1.0, 0.0,    -1.0, -1.0, 0.0,    1.0, -1.0, 0.0]

    替换为:

    1
    var objectToDraw: Triangle!

    之后再将这段:

    1
    2
    3
    4
    // 1
    let dataSize = vertexData.count * sizeofValue(vertexData[0])
    // 2
    vertexBuffer = device.newBufferWithBytes(vertexData, length: dataSize, options: nil)

    替换为:

    1
    objectToDraw = Triangle(device: device)

    好了,现在obejectToDraw初始化之后就可以用了,现在唯一还没做的事就是在ViewCotroller的render()方法中调用objetToDraw的draw()方法了。

    所以最后我们要修改的代码是render()方法,将其改为下面这样:

    1
    2
    3
    4
    func render() {
      var drawable = metalLayer.nextDrawable()
      objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable, clearColor: nil)
      }

    构建并运行…呃……好像可爱的三角形不见了……

    bkvtq-480x269.jpg

    这是为什么呢。这其实是因为你的顶点结构体里面现在包含了颜色数据了,而在你的顶点着色器里面仍旧还只是传递进位置信息,这就需要进行下一步。

    5.重构着色器(Shader)

    打开Shaders.metal然后仔细看看顶点着色器的代码,你会发现它返回的了一个float4型数据,只包括了每个顶点的位置信息。并且在参数列表里面的顶点Buffer里的数据是作为packed_float3型传入的。

    接着,我们需要创建两个结构体,以便将顶点数据完整的传入着色器中,其中一个会作为着色器的返回值,这样你在看代码的时候思路会清晰得多。

    在Shaders.metal中的using namespace metal下面加上如下代码:

    1
    2
    3
    4
    5
    6
    struct VertexIn{
      packed_float3 position;
      packed_float4 color;}; 
    struct VertexOut{
      float4 position [[position]];  //1
      float4 color;};

    使用VertexOut代替了原本的float4返回类型。

    注意着色器必须返回位置信息,在VertexOut中,你需要使用特定的修饰符[[position]]来指明位置信息。

    现在把着色器的代码改成下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    vertex VertexOut basic_vertex(                           // 返回值修改为VertexOut类型
      const device VertexIn* vertex_array [[ buffer(0) ]],   // 将传入的第一个参数从float3改为VertexIn类型,注意这里的VertexIn会映射到之前创建的Vertex结构体
      unsigned int vid [[ vertex_id ]]) {
      
      VertexIn VertexIn = vertex_array[vid];                 // 从数组中获取顶点
      
      VertexOut VertexOut;
      VertexOut.position = float4(VertexIn.position,1);
      VertexOut.color = VertexIn.color;                       // 将VertexIn传递到最后返回的VertexOut中
      
      return VertexOut;}

    所以,在这里为何不直接返回VertexIn就好了?

    因为如果这么做,后面经过变换之后,整个顶点的数据就会变化——你再也找不回原来的数据了。

    现在再来构建工程并运行,结果如下:

    Untitledqw-180x320.png

    不过你还没有向着色器传进颜色值信息,现在我们来实现它。

    将fragment着色器修改成这样:

    1
    2
    3
    4
    5
    fragment half4 basic_fragment(VertexOut interpolated [[stage_in]]) {  
        //这个顶点着色器会传入VertexOut,不过它会根据你正在渲染的部分的位置信息来插入,稍后才会讨论这个
      return half4(interpolated.color[0], interpolated.color[1], interpolated.color[2], interpolated.color[3]); 
         //这样你会返回当前渲染部分的颜色值,而不是干巴巴的硬编码才有的白色
    }

    构建并运行你就会看到这样的结果了:

    IMG_2420-180x320.png

    现在你可能会很奇怪为什么这个三角形变得五颜六色的,而你只定义了三个颜色值。

    因为程序会根据你设定的片段自动进行差值,比如三角形的最底部里两边的顶点的距离都是一样的,这时候它的颜色值就会是左边的绿色和右边的蓝色各取50%,这一过程完全是自动的,而且对你在着色器里加上的任何颜色值都适用。

    创建正方体

    提示:如果你之前跳过了上一节,那么你需要下载的源码在这里。这份代码和上一节最后完成的代码一样,你可以随便看看代码有助理解。

    下一步,我们将创建一个正方体来替换之前的三角形,同样的,你仍旧要把它新建为Node类的子类。

    仍旧是新建一个文件并以 iOS/Source/Swift File为模板创建一个类,命名为Cube.swift.

    打开这个文件,将其中内容更改为下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import UIKitimport Metal 
    class Cube: Node {
      
      init(device: MTLDevice){
      
        let A = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
        let B = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
        let C = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
        let D = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
      
        let Q = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
        let R = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
        let S = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
        let T = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
      
        var verticesArray:Array<Vertex> = [
          A,B,C ,A,C,D,   //Front
          R,T,S ,Q,R,S,   //Back
      
          Q,S,B ,Q,B,A,   //Left
          D,C,T ,D,T,R,   //Right
      
          Q,A,D ,Q,D,R,   //Top
          B,S,T ,B,T,C    //Bot
        ]
      
        super.init(name: "Cube", vertices: verticesArray, device: device)
      }
    }

    是不是觉得代码有些熟悉?其实跟三角形差不多,只不过有八个顶点需要绘制。

    也就是说,每一个面是由两个三角形组成的,为了便于理解,我们可以画个草图:

    Cube__PSF_-331x320.png

    接下来修改ViewController.swift中的objectToDraw为Cube:

    1
    var objectToDraw: Cube!

    同样的,修改构造函数,将objectToDraw初始化为Cube类型:

    1
    objectToDraw = Cube(device: device)

    构建并运行之后,你会发现结果跟下面一样,不管你信不信,反正下面这个是个正方体:

    IMG_2435-180x320.png

    你所看到的是正方体的正面——而且是非常大的特写。而且这货还按比例缩放展示了……

    所以你还是不愿意相信自己看到的是个正方体是吧,还是觉得我在逗你是吧。

    好的我就是在逗你,现在教你正确的方式就是修改Cube的大小,弄成下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    let A = Vertex(x: -0.3, y:   0.3, z:   0.3, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
    let B = Vertex(x: -0.3, y:  -0.3, z:   0.3, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
    let C = Vertex(x:  0.3, y:  -0.3, z:   0.3, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
    let D = Vertex(x:  0.3, y:   0.3, z:   0.3, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
     let Q = Vertex(x: -0.3, y:   0.3, z:  -0.3, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
    let R = Vertex(x:  0.3, y:   0.3, z:  -0.3, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
    let S = Vertex(x: -0.3, y:  -0.3, z:  -0.3, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
    let T = Vertex(x:  0.3, y:  -0.3, z:  -0.3, r:  0.1, g:  0.6, b:  0.4, a:  1.0)

    构建并运行。

    IMG_2436-180x320.png

    啊哈,正方体变小了~不过感觉有什么怪怪的地方。对了,是不是每次为了看到这个怪怪的正方体都需要修改那个可恶的顶点类呢……

    当然不是,所以我们需要说一下矩阵的用法。

    矩阵简介

    什么是矩阵?所谓矩阵,就是矩形的数组,在3D游戏里面,你经常会看到行列都为4的4×4矩阵。

    注意你使用的GLKit是以纵列为准的GLKMatrix4,所以矩阵的布局是像这样的:

    Screen-Shot-2014-09-03-at-1.43.20-PM.png

    通过使用矩阵你可以进行下面三种操作:

    1.平移:沿着X,Y,Z轴移动。

    2.旋转:绕任一坐标轴旋转。

    3.缩放:沿着在任一坐标轴方向上改变大小(注意在这篇教程里面你会在所有的坐标轴上等比例进行缩放)。

    Screen-Shot-2014-09-03-at-1.36.17-PM-332x320.png

    那么具体怎么做呢。首先……你需要创建一个Matrix4的实例,就像下面这样(在本节你不需要添加这些代码,贴出来仅仅是为了教程说明):

    1
    var modelTransformationMatrix = Matrix4()

    然后使用这些方法来变换图形:

    1
    modelTransformationMatrix.translate(positionX, y: positionY, z: positionZ)modelTransformationMatrix.rotateAroundX(rotationX, y: rotationY, z: rotationZ)modelTransformationMatrix.scale(scale, y: scale, z: scale)

    关于这些东西你可以在线性代数里学到,如果能理解原理那是最好不过,但是在这篇教程里面我们并不要求。

    在继续深入之前,需要打开HelloMetal-BridgingHeader.h然后导入Matrix4类,加入下面这行:

    1
    #import "Matrix4.h"

    模型变换

    需要用的第一个变换是模型变换,这会将你的节点坐标从本地坐标系转入世界坐标系,也就是说,你可以在广阔的范围内移动你的模型。

    我们来看一下具体怎么实现,打开Node.swift然后添加下列代码:

    1
    2
    3
    4
    5
    6
    7
    var positionX:Float = 0.0
    var positionY:Float = 0.0
    var positionZ:Float = 0.0
     var rotationX:Float = 0.0
    var rotationY:Float = 0.0
    var rotationZ:Float = 0.0
    var scale:Float     = 1.0

    这些属性会便于你在世界坐标系中来进行位置、角度和缩放比例的设置。你还需要构建一个模型矩阵(Model Matrix)来进行稍后的矩阵变换。

    在Node中加入下面这个方法:

    1
    2
    3
    4
    5
    6
    7
    func modelMatrix() -> Matrix4 {
        var matrix = Matrix4()
        matrix.translate(positionX, y: positionY, z: positionZ)
        matrix.rotateAroundX(rotationX, y: rotationY, z: rotationZ)
        matrix.scale(scale, y: scale, z: scale)
        return matrix
        }

    在这个方法中,你会根据这些参数生成一个矩阵。

    现在要做的就是把这个矩阵传递到着色器里然后用于变换,在此之前你要先弄明白统一数据(Uniform Data)这个概念。

    统一数据(Uniform Data)

    那么现在我们已经把不同的数据通过数组形式传递到着色器了。而模型矩阵会与整个模型进行相同的叉乘,如果为每一个顶点都拷贝一份模型矩阵来进行运算的话,在空间复杂度上会有很大消耗。

    当你想要使用相同的数据与整个模型进行运算的时候,完全可以使用统一数据来完成。

    首先需要把所有数据放进一个buffer对象中去,作为CPU和GPU都能访问的内存数据。

    在Node.swift中,找到var vertexBuffer:MTLBuffer然后在下面加这一行:

    1
    var uniformBuffer: MTLBuffer?

    接着在renderEncoder.setVertexBuffer(self.vertexBuffer, offset : 0, atIndex : 0)之后加入下面这段:

    1
    2
    3
    4
    5
    6
    7
    // 调用之前写的modelMatrix()方法将节点里面包括位置和角度这些值传递到一个模型矩阵里
    var nodeModelMatrix = self.modelMatrix()
    // 向设备申请一个内存区作为Buffer并共享给CPU和GPUuniformBuffer = device.newBufferWithLength(sizeof(Float) * Matrix4.numberOfElements(), options: nil)
    // 生成Buffer区的初始指针(类似于OC中的void *)
    var bufferPointer = uniformBuffer?.contents()
    // 将矩阵中的数据拷贝进Buffermemcpy(bufferPointer!, nodeModelMatrix.raw(), UInt(sizeof(Float)*Matrix4.numberOfElements()))
    // 将uniformBuffer传递给着色器(以及所指数据),有点类似于把buffer传进特殊的顶点数据一样,只不过在这里索引atIndex的值是1而不是0renderEncoder.setVertexBuffer(self.uniformBuffer, offset: 0, atIndex: 1)

    代码写到这里有一个问题:在理想情况下,你每秒大概会调用60次render()方法,也就意味着你每秒要创建60次这样的Buffer区。

    连续不断的分配内存是相当奢侈的事,并且开发App的时候我们也不推崇这种做法,以后的教程里我会提供更好的解决方案(而且你也会看到iOS Metal游戏的模板),不过限于本篇教程的难度控制我们在此就先使用这种消耗颇大的方式。

    现在你已经能够把矩阵传给顶点着色器了,剩下的问题就是如何使用矩阵。现在你需要在Shared.metal文件中的VetexOut结构的后面加入这个结构体:

    1
    2
    3
    struct Uniforms{
      float4x4 modelMatrix;
      };

    现在这个结构体仅包含了一个成员,不过之后我们会让它再包括进一个矩阵。

    紧接着将下面的顶点着色器修改为这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    vertex VertexOut basic_vertex(  const device VertexIn* vertex_array [[ buffer(0) ]],  const device Uniforms&  uniforms    [[ buffer(1) ]],           //这里添加了Uniform引用类型的参数并且标记将其置于slot 1里面(和之前的代码相匹配)  
    unsigned int vid [[ vertex_id ]]) {  
     float4x4 mv_Matrix = uniforms.modelMatrix;                     
        //从uniforms中获取模型矩阵的数据  
         VertexIn VertexIn = vertex_array[vid];   
         VertexOut VertexOut;  
         VertexOut.position = mv_Matrix * float4(VertexIn.position,1); 
         //让一个顶点进行矩阵变换,只需要用这个顶点的位置矩阵和变换矩阵相乘  
         VertexOut.color = VertexIn.color;   
         return VertexOut;
    }

    到这一步之后,下面的工作就是对Cube立方体进行一些小修改了。

    打开Cube.swift文件然后把正方体的值改为如下:

    1
    2
    3
    4
    5
    6
    7
    8
    let A = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
    let B = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
    let C = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
    let D = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
    let Q = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
    let R = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
    let S = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
    let T = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)

    在ViewController.swift中的objectToDraw = Cube(device : device)后面加入:

    1
    2
    objectToDraw.positionX = -0.25objectToDraw.rotationZ = Matrix4.degreesToRad(45);
    objectToDraw.scale = 0.5

    构建并运行:

    IMG_2438-180x320.png

    很明显之前的正方体按比例缩放了,且向左平移并绕Z轴旋转了45度。

    这里至少证明了一个事:数学上矩阵比起其他什么黑客帝国的矩阵不知道要高到哪里去了。

    Screen-Shot-2014-09-08-at-2.34.05-PM-700x244.png

    好的,下面进入科普时间——我们可以让正方体沿着任意坐标轴平移,所以在objectToDraw.positionX = -0.25之后加入下面代码:

    1
    2
    objectToDraw.positionY =  0.25
    objectToDraw.positionZ = -0.25

    构建并运行:

    IMG_2439-180x320.png

    接着你会很困惑,你不是已经把这货往Z轴方向平移了-0.25个单位么,为什么它看上去只在X和Y轴方向有感觉但是丝毫没有像预期那样远离你呢?这不科学啊!

    你可能会想,是不是矩阵传错了,不过我想说其实这里的矩阵传参没有任何问题,问题只是正方体的确移动了但是你看不出来。

    如果你想知道这个问题如何解决的话,那么需要先科普一下投影矩阵(Projection Matrix)这个概念。

    投影矩阵(Projection Matrix)

    所谓投影变换,就是将你的视觉坐标系转换到标准坐标系,使用不同的投影变换,所得出的效果也不尽相同。

    我们要介绍的是两种投影变换:正交法(Orthographic)透视法(Perspective)

    下边的示意图中,左边的是透视法,右边是正交法,观察点位于坐标系远点。

    zyGF1-480x215.gif

    理解透视法比较容易,因为透视就是我们平时眼睛看到事物的基本方式。正交法虽然会难一点,但是不用担心,因为你刚刚做完的“正方体”其实就是正交投影的观察结果。

    想象一下你站在铁轨上,沿着铁轨看过去,你所看到的景象就是透视,如下:

    tracks_3-463x320.jpg

    而如果是在正交视角下看的话,上面这张图会变得畸形而且铁路的两侧永远都是平行的。

    在下面的这张图里面,你会看到另一种投影透视法,它是一个被削尖的金字塔形,金字塔中的是你视觉感受到的实物场景,这一场景会投影到金字塔的横截面上,而这个横截面其实就是你的设备屏幕。

    Screen-Shot-2014-09-04-at-2.07.06-PM-480x256.png

    好了,现在我们知道Metal对所有的物体都采用了正交投影法,所以我们要做的就是将其转换为透视投影,同样的,这种转换我们也需要使用到矩阵。

    为了简化整个将你的正方体放进金字塔模型中的过程。我们需要创建一个投影矩阵来描述上面说的金字塔模型,并且将其绘制到你统一的代码框架中。

    Matrix4已经提供了创建透视投影矩阵的方法,所以我们只需尽情使用就好了。

    在ViewController.swift中加入下面属性:

    1
    var projectionMatrix: Matrix4!

    接下来在viewDidLoad()方法的最开始加入下面这段:

    1
    2
    projectionMatrix = Matrix4.makePerspectiveViewAngle(Matrix4.degreesToRad(85.0), aspectRatio: 
    Float(self.view.bounds.size.width / self.view.bounds.size.height), nearZ: 0.01, farZ: 100.0)

    这里设置的视角为85度,不需要使用水平宽度值,因为本来你已经传入了高宽比以及视野的远端和近端的距离。

    也就是说,任何比这块区域更近或者更远的物体都不会显示。

    现在,修改Node.swift中的render()方法,特别说明一下,你需要添加一个额外的参数,这样render()的方法名可以改成这样:

    1
    func render(commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, projectionMatrix: Matrix4, clearColor: MTLClearColor?)

    你要添加的就是projectionMatrix参数。

    然后你需要在你的uniform buffer中加入投影矩阵projectionMatrix,以便于它能传递到着色器。因为这块buffer需要传入两个矩阵的数据,所以你需要对它进行扩容:

    将下面这段:

    1
    uniformBuffer = device.newBufferWithLength(sizeof(Float) * Matrix4.numberOfElements(), options: nil)

    替换成:

    1
    uniformBuffer = device.newBufferWithLength(sizeof(Float) * Matrix4.numberOfElements() * 2, options: nil)

    然后找到这段:

    1
    memcpy(bufferPointer!, nodeModelMatrix.raw(), UInt(sizeof(Float)*Matrix4.numberOfElements()))

    修改为:

    1
    memcpy(bufferPointer! + sizeof(Float)*Matrix4.numberOfElements(), projectionMatrix.raw(), UInt(sizeof(Float)*Matrix4.numberOfElements()))

    现在两个矩阵都能传入uniform buffer了。接下来你所需要做的就是在你的着色器中使用投影矩阵了。

    打开Shaders.metal文件,然后按照我们之前提到过的在Uniforms中加入投影矩阵projectionMatrix:

    1
    2
    3
    4
    struct Uniforms{  
        float4x4 modelMatrix;  
        float4x4 projectionMatrix;
    };

    接着在顶点着色器中,你要获取投影矩阵来进行渲染,所以要找到代码:

    1
    float4x4 mv_Matrix = uniforms.modelMatrix;

    在后面添加:

    1
    float4x4 proj_Matrix = uniforms.projectionMatrix;

    进行投影变换,这时候你只需要像之前做的一样将位置数据与矩阵想乘。所以把下面这段代码:

    1
    VertexOut.position = mv_Matrix * float4(VertexIn.position,1);

    替换为:

    1
    VertexOut.position = proj_Matrix * mv_Matrix * float4(VertexIn.position,1);

    最后,从ViewController.swift中的render()方法中传入投影矩阵。

    将下面代码:

    1
    objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable, clearColor: nil)

    替换为:

    1
    objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable,projectionMatrix: projectionMatrix, clearColor: nil)

    然后将objectToDraw的变换参数改成如下:

    1
    2
    objectToDraw.positionX = 0.0objectToDraw.positionY =  0.0objectToDraw.positionZ = -2.0objectToDraw.rotationZ = Matrix4.degreesToRad(45);
    objectToDraw.scale = 0.5

    现在构建并运行,下面的看上去就真的很想一个正方体了,不过感觉做得还是有些不够过瘾。

    IMG_2440-180x320.png

    我们来简要的看看刚刚都做了什么:

    1.添加了一个模型矩阵,用于修改模型的位置,大小和角度。

    2.添加了一个投影矩阵,将正交视角切换为正常的透视视角。

    事实上前面这种超过两次以上的变换在3D渲染管线流程里面是这样实现的:

    1.视图变换(View Transformation):如果你想从不同的位置观察场景中物体怎么办?修改所有场景物体的模型矩阵是可行的,不过那样做的话就太低效了。通常情况下简便的方法就是改变观察点的位置,改变视角,也就是在场景中的摄像机(camera)。

    2.视口变换(Viewport Transformation):这个东西基本上就是等于在你的标准坐标系的抠出来一个小的部分作为世界然后再屏幕上绘制出来,在Metal里面这都是自动完成的——你只需要了解下就好。

    下面我们要做的事情有:

    1.添加视图变换。

    2.让立方体旋转。

    3.修改立方体的透明度。

    视图变换(View Transformation)

    一次视图变换就是将节点的在世界坐标系中的坐标转换到摄像机坐标系(观察者坐标系),换言之,你可以在世界坐标系中随时调整你的摄像机位置(观察点位置)。

    添加视图变换相当的简单,只需要将Node.swift中的render()方法的声明改成这样:

    1
    func render(commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, parentModelViewMatrix: Matrix4, projectionMatrix: Matrix4, clearColor: MTLClearColor?)

    上面的代码添加了一个parentModelViewMatrix参数,它代表的是摄像机的位置,并将会被用来进行转换。

    在render()方法中找到下面这个:

    1
    var nodeModelMatrix = self.modelMatrix()

    在它后面加上:

    1
    nodeModelMatrix.multiplyLeft(parentModelViewMatrix)

    请注意你无需将这个矩阵传入着色器,因为此前做那两个矩阵的时候已经传过了。你要做的是创建一个模型视图矩阵,方法是用模型矩阵和视图矩阵相乘。通常为了效率我们会预先求出他们的相乘结果。

    现在打开ViewController.swift修改其中的render()方法,传进一个参数,将下面的代码:

    1
    objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable,projectionMatrix: projectionMatrix, clearColor: nil)

    变为这样:

    1
    2
    var worldModelMatrix = Matrix4()worldModelMatrix.translate(0.0, y: 0.0, z: -7.0)
     objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable, parentModelViewMatrix: worldModelMatrix, projectionMatrix: projectionMatrix ,clearColor: nil)

    另外,删掉下面这些:

    1
    2
    3
    4
    5
    objectToDraw.positionX = 0.0
    objectToDraw.positionY =  0.0
    objectToDraw.positionZ = -2.0
    objectToDraw.rotationZ = Matrix4.degreesToRad(45);
    objectToDraw.scale = 0.5

    现在你无需将物体往后移动了,因为你的视角已经移动过了。

    构建并运行:

    IMG_2441.png

    为了加深理解,你可以进行更多的修改来测试。

    仍然是修改ViewController中的render()方法,找到下面这段:

    1
    worldModelMatrix.translate(0.0, y: 0.0, z: -7.0)

    然后在其后面添加:

    1
    worldModelMatrix.rotateAroundX(Matrix4.degreesToRad(25), y: 0.0, z: 0.0)

    运行一下:

    IMG_2445-180x320.png

    结果是你把物体绕X轴旋转了,或者说你的摄像机方向改变了——你高兴怎么想就怎么想吧。

    旋转的立方体

    现在修改代码让你的立方体能自动随着时间流逝旋转。

    打开Node.swift,加入一个新属性:

    1
    var time:CFTimeInterval = 0.0

    这一属性用来监测节点旋转了多长时间。

    然后在节点类的最后加入这个方法:

    1
    2
    3
    func updateWithDelta(delta: CFTimeInterval){    
        time += delta
    }

    然后打开ViewController.swift然后添加一个属性:

    1
    var lastFrameTimestamp: CFTimeInterval = 0.0

    然后将下面这行:

    1
    timer = CADisplayLink(target: self, selector: Selector("gameloop"))

    改成:

    1
    timer = CADisplayLink(target: self, selector: Selector("newFrame:"))

    另外将这段:

    1
    2
    3
    4
    5
    func gameloop() {
      autoreleasepool {
        self.render()
      }
    }

    修改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
      //每次DisplayLink链接的显示器屏幕刷新的时候都会调用newFrame方法,这里的DisplayLink是作为参数传入的
    func newFrame(displayLink: CADisplayLink){
      
      if lastFrameTimestamp == 0.0
      {
        lastFrameTimestamp = displayLink.timestamp  }
      
      // 计算当前帧与前一帧间的时间差,这个时间差并不总是一致的,因为有的帧可能会被跳过
      var elapsed:CFTimeInterval = displayLink.timestamp - lastFrameTimestamp
      lastFrameTimestamp = displayLink.timestamp 
      // 调用gameloop()不过将最后一次更新的时间到现在的时间差作为参数
      gameloop(timeSinceLastUpdate: elapsed)}
     func gameloop(#timeSinceLastUpdate: CFTimeInterval) {
      
      // 在渲染前使用updateWithDelta()来更新节点信息
      objectToDraw.updateWithDelta(timeSinceLastUpdate)
      
      // 当节点更新之后调用渲染render() 
      autoreleasepool {
        self.render()
      }
    }

    最后,需要重写一下updateWithDelta()方法,这个方法在Cube类里面,所以打开Cube.swift,然后加入该方法:

    1
    2
    3
    4
    5
    6
    7
    8
    override func updateWithDelta(delta: CFTimeInterval) {
      
      super.updateWithDelta(delta)
      
      var secsPerMove: Float = 6.0
      rotationY = sinf( Float(time) * 2.0 * Float(M_PI) / secsPerMove)
      rotationX = sinf( Float(time) * 2.0 * Float(M_PI) / secsPerMove)
    }

    这里没什么好说的,你所做的就是调用super()来更新时间属性,然后使用正弦函数来设置正方体的旋转特性,主要就是让你的正方体转到一定角度之后又转回来。

    构建并运行,现在你有一个会动的正方体了:

    IMG_2456-180x320.png

    这样看上去有活力多了,这货反反复复的旋转就像在跳小苹果。

    修改透明度

    最后的一部分我们来修改正方体的透明度,不过首先你需要知道我们为什么要这么做——因为Metal默认会先绘制正方体的背面才会来绘制前面……

    所以你要怎么解决这个问题呢。

    让我来给你指两条明路:

    1.你可以进行深度测试(Depth Testing),使用这个方法你需要存储每一个点的深度信息,这样的话当两点投影到屏幕同一点的时候,只会显示深度较低的。

    2.另一方法就是背面剔除(Backface Culling),这就是说,每一个三角面片其实都只绘制了能看得见的那一面,所以事实上每一个背面的点只有当它转向摄像机的时候才会被绘制。这些都是依据你为三角面片指定顶点的顺序来决定的。

    现在我们来用第二种方法也就是背面剔除来解决这个问题,当只有一个模型的时候这一方法会有效得多。唯一要坚守的原则就是:所有的三角面片必须按照逆时针方向来指定,否则就不会被绘制。

    祝你幸福。我设置顶点的时候,我都对这些三角面片已经很熟悉了所以不会出错……所以你还是更关注学习如何进行背面剔除吧。

    打开Node.swift然后找到这行:

    1
    if let renderEncoder = renderEncoderOpt {

    在下面添加:

    1
    2
    //For now cull mode is used instead of depth buffer
    renderEncoder.setCullMode(MTLCullMode.Front)

    构建并运行:

    IMG_24471-180x320.png

    现在你的正方体应该不透明了。

    拓展与学习

    你可以下载最终的项目文件

    现在你已经学习完有关Metal API 3D图形的不少知识了。现在对很多概念都有了大致的了解,需要消化一下。

    如果兴趣足够的话,在以后的教程里面我们会讨论有关纹理、照明、以及模型导入。

    下面依旧是一些参考文档:

    Metal Framework基础使用教程

    Metal基本图像处理实例

    苹果Metal的开发者页面

    苹果Metal的开发指南

    苹果Metal的Shared Language指南

    展开全文
  • 学习使用苹果GPU加速3D绘图的新API:Metal!   在iOS 8里,苹果发布了一个新的接口叫做Metal,它是一个支持GPU加速3D绘图的API。   Metal和OpenGL ES相似,它也是一个底层API,负责和3D绘图硬件交互。...
    (via:泰然网,译者:Hero Kingsley)
     
    学习使用苹果GPU加速3D绘图的新API:Metal!
     
    在iOS 8里,苹果发布了一个新的接口叫做Metal,它是一个支持GPU加速3D绘图的API。
     
    Metal和OpenGL ES相似,它也是一个底层API,负责和3D绘图硬件交互。它们之间的不同在于,Metal不是跨平台的。与之相反的,它设计的在苹果硬件上运行得极其高效,与OpenGL ES相比,它提供了更快的速度和更低的开销。
     
    在这篇教程里,你将会获得亲身的经历,使用Metal和Swift来创建一个有基本脉络的应用:画一个简单的三角形。在这个过程中,你将会学习一些Metal里最重要的类,比如devices、command queues,等等。
     
    这篇教程是设计为任何人可以阅读明白,无论你是否学习过3D绘图。但是,我们会过得很快。如果你之前有过3D编程或者是OpenGL编程的经历,你会发现它非常简单,因为里面的很多概念你已经很熟悉了。
     
    这篇教程假设你已经熟悉Swift了。如果你还是个Swift新手,先学习这些教程吧,苹果Swift站点、一些Swift教程。
     
    注意:Metal应用不能跑在iOS模拟器上,它们需要一个设备,设备上装载着苹果A7芯片或者更新的芯片。所以要学习这篇教程,你需要一台这样的设备(iPhone 5S,iPad Air,iPad mini2)来完成代码的测试。
     
    Metal vs. Sprite Kit, Scene Kit, or Unity
    在我们开始之前,我想要讨论怎样比较Metal和一些没那么底层的框架,比如:Sprite Kit,Scene Kit或者Unity。
     
    Metal是一个底层3D绘图API,和OpenGL类似,但是它的开销更低。它是一个GPU上一个简单的封装,所以能够完成几乎所有事情,像在屏幕上渲染一个精灵(sprite)或者是一个3D模型。但你要编写完成这些事情的所有代码。这样麻烦的代价是,你拥有了GPU的力量和控制。
     
    没那么底层的游戏框架,像Sprite Kit、Scene Kit或者Unity都是在底层3D绘图API(像是Metal或是OpenGL ES)的基础上构建的。它们提供大部分你需要在游戏中编写的底层封装代码,比如在屏幕上渲染一个精灵(sprite)或者一个3D模型。
    如果你所想要做的就是制作一个游戏,大多数情况下我会推荐你使用一个没那么底层的库,像Sprite Kit、Scene Kit或者Unity,因为它会让你的工作更轻松。如果你喜欢这样,我们有很多教程来帮助你学习这些框架。
     
    但是,还是有两个很好的原因来学习Metal: 
    1.使硬件达到运行效率的峰值:因为Metal非常底层,它允许你使硬件达到运行效率的峰值,对你的游戏如何运行有着完全的控制。
    2.这是一个很好的学习经历:学习Metal教导你很多关于3D绘图编程的概念,编写你自己的游戏引擎,以及高层(higher level)游戏框架如何运作。
     
    如果以上任何一点对你来说是个好的理由,继续读下去!
     
    Metal vs OpenGL ES
    下面让我们来对比一下Metal和OpenGL ES的不同之处。
     
    OpenGL ES被设计成跨平台的。那意味着你可以用C++OpenGL ES的代码,在大部分情况下只要作少许改动就能让它在另一个平台上运行,比如Android。
     
    苹果意识到尽管OpenGL ES对跨平台的支持很赞,但是它缺少了一些苹果设计产品的基本理念:苹果把操作系统、硬件、软件整合在了一起。
     
    所以苹果认真考虑了如果他们设计一套特定基于他们硬件的绘图API,会是怎样呢?它的目标是极速运行、低开销以及支持最新最好的特性。
     
    于是Metal诞生了。它对比OpenGL ES,能为你的应用单位时间内提高最高10倍的绘图调用次数。这能够产生超赞的特效,就像WWDC 2014 keynote上zen花园样例。
     
    让我们开始看看一些Metal代码吧!
     
    开头
    Xcode的iOS游戏模板有一个Metal选项,但是你不要在这里选择。这是因为我想要向你一步步展示如何编写一个Metal应用,所以你能够理解这过程中的每一步骤。
     
    打开Xcode 6通过iOS\Application\Single View Application template创建一个新的项目。使用HelloMetal作为项目名称,设置开发语言为Swift,设置设备为通用设备(Universal)。点击Next,选择一个目录,点击Create。
     
    有七个步骤来设置metal:
    1.创建一个MTLDevice
    2.创建一个CAMetalLayer
    3.创建一个Vertex Buffer
    4.创建一个Vertex Shader
    5.创建一个Fragment Shader
    6.创建一个Render Pipeline
    7.创建一个Command Queue
     
    让我们一个个看它们。
     
    1)创建一个MTLDevice 
    使用Metal你要做的第一件事就是获取一个MTLDevice的引用。
     
    你可以把一个MTLDevice想象成是你和CPU的直接连接。你将通过使用MTLDevice创建所有其他你需要的Metal对象(像是command queues,buffers,textures)。
     
    为了完成这点,打开ViewController.swift 并添加下面的import语句到文件最上方:
    1. import Metal 
     
    这导入了Metal框架,所以你能够使用Metal的类(像这文件中的MTLDevice)。接着,在ViewController类中添加以下属性:
    1. var device: MTLDevice! = nil 
     
    你将要在viewDidLoad函数内初始化这个属性,而不是在一个init函数里,所以它不得不是一个optional。既然你知道你一定会在使用它前初始化它,你为了方便,把它标记为一个隐式不包裹的optional。最后,添加这一行到viewDidLoad函数的最后。
    1. device = MTLCreateSystemDefaultDevice() 
    这个函数返回一个默认MTLDevice引用,你的代码将会用到它。
     
    2)创建一个CAMetalLayer
    在iOS里,你在屏幕上看见的所有东西,被一个CALayer所承载。存在不同特效的CALayer的子类,比如:渐变层(gradient layers)、形状层(shape layers)、重复层(replicator layers) 等等。
     
    好的,如果你想要用Metal在屏幕上画一些东西,你需要使用一个特别的CALayer子类,CAMetalLayer。所以在你的viewcontroller中添加一个。
     
    首先在这个文件的上方添加import语句。
    1. import QuartzCore 
    你需要它因为CAMetalLayer是QuartzCore框架的部分,而不是Metal框架里的。
     
    然后把新属性添加到类中:
    1. var metalLayer: CAMetalLayer! = nil 
    这将会存储你新layer的引用。
     
    最后,把这行代码添加到viewDidLoad方法最后。
    1. metalLayer = CAMetalLayer()          // 1 
    2. metalLayer.device = device           // 2 
    3. metalLayer.pixelFormat = .BGRA8Unorm // 3 
    4. metalLayer.framebufferOnly = true    // 4 
    5. metalLayer.frame = view.layer.frame  // 5 
    6. view.layer.addSublayer(metalLayer)   // 6 
    让我们一行行来看:
     
    a.你创建了一个CAMetalLayer
    b.你必须明确layer使用的MTLDevice,你简单地设置你早前获取的device。
    c.你把像素格式(pixel format)设置为BGRA8Unorm,它代表”8字节代表蓝色、绿色、红色和透明度,通过在0到1之间单位化的值来表示”。这次两种用在CAMetalLayer的像素格式之一,一般情况下你这样写就可以了。
    d.苹果鼓励你设置framebufferOnly为true,来增强表现效率。除非你需要对从layer生成的纹理(textures)取样,或者你需要在layer绘图纹理(drawable textures)激活一些计算内核,否则你不需要设置。(大部分情况下你不用设置)
    e.你把layer的frame设置为view的frame。
    f.你把layer作为view.layer下的子layer添加。
     
    3)创建一个Vertex Buffer
    在Metal里每一个东西都是三角形。在这个应用里,你只需要画一个三角形,不过即使是极其复杂的3D形状也能被解构为一系列的三角形。
     
    在Metal里,默认的坐标系是向量坐标系,这意味着默认的时候,一个2x2x1的立方体,中心点是(0,0,0.5)。
     
    如果你认为z=0是平面,那么(-1,-1,0)就是左下角,(0,0,0)就是中心,(1,1,0)是右上角。在这篇教程中,你想要在这些点上画三角形:
     
    让我们创建一个缓冲区。在你的类中添加下列的常量属性:
    1. let vertexData:[Float] = [ 
    2.   0.0, 1.0, 0.0, 
    3.   -1.0, -1.0, 0.0, 
    4.   1.0, -1.0, 0.0] 
    这在CPU创建一个浮点数数组——你需要通过把它移动到一个叫MTLBuffer的东西,来发送这些数据到GPU。
     
    添加另一个新的属性:
    1. var vertexBuffer: MTLBuffer! = nil 
    然后在 viewDidLoad 方法的最后添加以下代码:
    1. let dataSize = vertexData.count * sizeofValue(vertexData[0]) // 1 
    2. vertexBuffer = device.newBufferWithBytes(vertexData, length: dataSize, options: nil) // 2 
    让我们一行行来看:
    a.你需要获取vertex data的字节大小。你通过把第一个元素的大小和数组元素个数相乘来得到。
    b.你在MTLDevice上调用newBufferWithBytes(length:options:) ,在GPU创建一个新的buffer,从CPU里输送data。你传递nil来接受默认的选项。
     
    4)创建一个Vertex Shader
    你之前创建的顶点将成为你接下来写的一个叫vertext shader的小程序的输入。
     
    一个vertex shader 是一个在GPU上运行的小程序,它由像c++的一门语言编写,那门语言叫做Metal Shading Language
     
    一个vertex shader被每个顶点调用,它的工作是接受顶点的信息(如:位置和颜色、纹理坐标),返回一个潜在的修正位置(可能还有别的相关信息)。
     
    为了把事情保持简单,你的vertex shader将会返回一个和传递位置相同的位置。
    最简单的了解 vertex shader 的方法是,自己体验。点击File\New\File,选择iOS\Source\Metal File,然后点击Next。输入Shader.metal作为文件名上按回车,然后点击Create。
     
    注意:在Metal里,你能够在一个Metal文件里包含多个shaders。你也能把你的shader 分散在多个Metal文件中。Metal会从任意Metal文件中加载你项目包含的shaders。
    在Shaders.metal底部添加下列代码:
    1. vertex float4 basic_vertex(                           // 1 
    2.   const device packed_float3* vertex_array [[ buffer(0) ]], // 2 
    3.   unsigned int vid [[ vertex_id ]]) {                 // 3 
    4.   return float4(vertex_array[vid], 1.0);              // 4 
    让我们一行行来看:
    a.所有的vertex shaders必须以关键字vertex开头。函数必须至少返回顶点的最终位置——你通过指定float4(一个元素为4个浮点数的向量)。然后你给一个名字给vetex shader,以后你将用这个名字来访问这个vertex shader。
    b.第一个参数是一个指向一个元素为packed_float3(一个向量包含3个浮点数)的数组的指针,如:每个顶点的位置。这个 [[ ... ]] 语法被用在声明那些能被用作特定额外信息的属性,像是资源位置,shader输入,内建变量。这里你把这个参数用 [[ buffer(0) ]] 标记,来指明这个参数将会被在你代码中你发送到你的vertex shader的第一块buffer data所遍历。
    c.vertex shader会接受一个名叫vertex_id的属性的特定参数,它意味着它会被vertex数组里特定的顶点所装入。
    d.现在你基于vertex id来检索vertex数组中对应位置的vertex并把它返回。同时你把这个向量转换为一个float4类型,最后的value设置为1.0(简单的来说,这是3D数学要求的)。
     
    5)创建一个Fragment Shader
    完成我们的vertex shader后,另一个shader,它被每个在屏幕上的fragment(think pixel)调用,它就是fragment shader。
     
    fragment shader通过内插(interpolating)vertex shader的输出还获得自己的输入。比如:思考在三角形两个底顶点之间的fragment:
     
    fragment的输入值将会由50%的左下角顶点和50%的右下角顶点组成。
     
    fragment shader的工作是给每个fragment返回最后的颜色。为了简便,你将会把每个fragment返回白色。
     
    在Shader.metal的底部添加下列代码:
    1. fragment half4 basic_fragment() { // 1 
    2.   return half4(1.0);              // 2 
    让我们一行行来看:
    a. 所有fragment shaders必须以fragment关键字开始。这个函数必须至少返回fragment的最终颜色——你通过指定half4(一个颜色的RGBA值)来完成这个任务。注意,half4比float4在内存上更有效率,因为,你写入了更少的GPU内存。
    b. 这里你返回(1,1,1,1)的颜色,也就是白色。
     
    6)创建一个Render Pipeline
    现在你已经创建了一个vertex shader和一个fragment shader,你需要组合它们(加上一些配置数据)到一个特殊的对象,它名叫render pipeline。Metal一个很酷的地方是,渲染器(shaders)是预编译的,render pipeline 配置会在你第一次设置它的时候被编译,所以所有事情都极其高效。
     
    首先在ViewController.swift里添加一个属性:
    1. var pipelineState: MTLRenderPipelineState! = nil 
    这会对你即将要创建的render pipeline ,在它被编译后进行跟踪。
     
    接着,在 viewDidLoad 方法最后添加如下代码:
    1. // 1 
    2. let defaultLibrary = device.newDefaultLibrary() 
    3. let fragmentProgram = defaultLibrary.newFunctionWithName("basic_fragment"
    4. let vertexProgram = defaultLibrary.newFunctionWithName("basic_vertex"
    5.   
    6. // 2 
    7. let pipelineStateDescriptor = MTLRenderPipelineDescriptor() 
    8. pipelineStateDescriptor.vertexFunction = vertexProgram 
    9. pipelineStateDescriptor.fragmentFunction = fragmentProgram 
    10. pipelineStateDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm 
    11.   
    12. // 3 
    13. var pipelineError : NSError? 
    14. pipelineState = device.newRenderPipelineStateWithDescriptor(pipelineStateDescriptor, error: &pipelineError) 
    15. if !pipelineState { 
    16.   println("Failed to create pipeline state, error \(pipelineError)"
    让我们分部分看这些代码:
    a.你可以通过调用device.newDefaultLibrary方法获得的MTLibrary对象访问到你项目中的预编译shaders。然后你能够通过名字检索每个shader。
    b.你在这里设置你的render pipeline。它包含你想要使用的shaders、颜色附件(color attachment)的像素格式(pixel format)。(例如:你渲染到的输入缓冲区,也就是CAMetalLayer)。
    c.最后,你把这个pipeline 配置编译到一个pipeline 状态(state)中,让它使用起来有效率。
     
    7)创建一个Command Queue
    你需要做的最终的一次性设置步骤,是创建一个MTLCommandQueue。
     
    把这个想象成是一个列表装载着你告诉GPU一次要执行的命令。
     
    要创建一个command queue,简单地添加一个属性:
    1. var commandQueue: MTLCommandQueue! = nil 
     
    把下面这行添加到 viewDidLoad 的最后:
    1. commandQueue = device.newCommandQueue() 
    恭喜,你的预设置的代码完成了。
     
    渲染三角形
    现在,是时候学习每帧执行的代码,来渲染这个三角形!
     
    它将在五个步骤中被完成:
    1.创建一个Display link。
    2.创建一个Render Pass Descriptor
    3.创建一个Command Buffer
    4.创建一个Render Command Encoder
    5.提交你Command Buffer的内容。
     
    让我们深入来看!
     
    注意:理论上这个应用实际上不需要每帧渲染,因为三角形被绘制之后不会动。但是,大部分应用会有物体的移动,所以我们会那样做。同时也为将来的教程打下基础。
     
    1)创建一个Display Link
    你想要一个函数,在每次设备屏幕刷新的时候被调用,这样你就可以重绘屏幕。
     
    在iOS平台上,你通过CADisplayLink 类来实现。
     
    为了使用它,在类里添加一个新的属性:
    1. var timer: CADisplayLink! = nil 
    然后在 viewDidLoad 方法的末尾像这样初始化它:
    1. timer = CADisplayLink(target: self, selector: Selector("gameloop")) 
    2. timer.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode) 
    这会设置你的代码,让它每次刷新屏幕的时候调用一个名叫gameloop的方法。
    1. func render() { 
    2.   // TODO 
    3.   
    4. func gameloop() { 
    5.   autoreleasepool { 
    6.     self.render() 
    7.   } 
    这里 gameloop 函数简单地调用 render 函数,这时 render 函数只有一个空实现。让我们来实现它!
     
    2)创建一个Render Pass Descriptor
    下一步是创建一个MTLRenderPassDescriptor,它能配置什么纹理会被渲染到、什么是clear color,以及其他的配置。
     
    简单地在 render 函数里添加以下行:
    1. var drawable = metalLayer.nextDrawable() 
    2.   
    3. let renderPassDescriptor = MTLRenderPassDescriptor() 
    4. renderPassDescriptor.colorAttachments[0].texture = drawable.texture 
    5. renderPassDescriptor.colorAttachments[0].loadAction = .Clear 
    6. renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0) 
    首先你在之前的metal layer上调用nextDrawable() ,它会返回你需要绘制到屏幕上的纹理(texture)。接下来,你配置你的render pass descriptor 来使用它。你设置load action为clear,也就是说在绘制之前,把纹理清空。然后你把绘制的背景颜色设置为绿色。
     
    3)创建一个Command Buffer
    下一步是创建一个command buffer。你可以把它想象为一系列这一帧想要执行的渲染命令。酷的是在你提交command buffer之前,没有事情会真正发生,这样给你对事物在何时发生有一个很好的控制。创建一个command buffer很简单,只要在render函数末尾加上这行代码:
    1. let commandBuffer = commandQueue.commandBuffer() 
    一个command buffer包含一个或多个渲染指令(render commands)。让我们下面创建一个。
     
    4)创建一个渲染命令编码器(Render Command Encoder)
    为了创建一个渲染命令(render command),你使用一个名叫render command encoder的对象。在render函数的最后添加以下代码:
    1. let renderEncoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor) 
    2. renderEncoder.setRenderPipelineState(pipelineState) 
    3. renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0) 
    4. renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) 
    5. renderEncoder.endEncoding() 
    这里你创建一个command encoder,并指定你之前创建的pipeline和顶点。最重要的部分是,调用drawPrimitives(vertexStart:vertexCount:instanceCount:)。
     
    这里你你告诉GPU,让它基于vertex buffer画一系列的三角形。每个三角形由三个顶点组成,从vertex buffer 下标为0的顶点开始,总共有一个三角形。
     
    当你完成后,你只要调用 endEncoding()。
     
    5)提交你的Command Buffer
    最后一步是提交command buffer。在render函数最后添加这些代码:
    1. commandBuffer.presentDrawable(drawable) 
    2. commandBuffer.commit() 
    第一行需要保证新纹理会在绘制完成后立即出现。然后你把事务(transaction)提交,把任务交给GPU。过去我们敲了不少代码,不过现在终于结束了。编译并运行这个应用:
    我见过最赞的三角形!
     
    注意:如果你的应用崩溃了,请确定你在一台拥有A7芯片真机(iPhone 5S,iPad Air,iPad mini2 ,非模拟器)运行。

    最后
    这是我们教程最终的项目
     
    恭喜你,你学到了很多关于Metal API的知识!你现在对Metal的一些重要的概念有了了解,比如:shaders、devices、command buffers,pipeline等等。
     
    我可能会写更多这系列的教程,覆盖uniforms,3D,纹理,光照,以及导入模型。如果你感到有兴趣、并想看到更多教程的话,请留下你的评论。同时,确定查看苹果一些很好的资源:
     
    苹果Metal开发者文档,有很多文档、录像、样例代码的链接。
    展开全文
  • metal是苹果端设备的一个底层图形API,功能与opengl类似,支持图形渲染和GPU通用计算。而且苹果ios已经弃用opengles,metal将会成为ios的图形开发的唯一选择。metal的基础知识入门,首推Metal By Example系列:...

            metal是苹果端设备的一个底层图形API,功能与opengl类似,支持图形渲染和GPU通用计算。而且苹果ios已经弃用opengles,metal将会成为ios的图形开发的唯一选择。metal的基础知识入门,首推Metal By Example系列:http://metalbyexample.com/。博主此后的相关文章,主要给出工程实际遇到的典型问题及其解决方案。

            网络教程往往只关注基础知识的讲解,对于工程化的框架设计没有涉及。自行开发metal相关的图形渲染项目,需要设计一个合适的框架组织metal图形API的代码。这里给出一个简易的框架设计项目,方便大家进行下一步的学习和实际开发,源码地址:https://github.com/sjy234sjy234/Learn-Metal/tree/master/HelloMetal

            与opengl类似,metal的实现代码主要包括3个部分:context, view, renderer。其中,context类用于分配和维护device及其相应的library和commandQueue。view类提供一个继承于UIView的视图,提供支持metal渲染的layer,并提供触摸事件代理,方便图形界面交互的拓展。renderer类用来执行实际的绘制操作,所有的renderer实例都需要一个context实例进行初始化。

            此外,context中可以考虑加入一些图形相关的通用方法作为类方法,例如pixelBuffer转texture的方法等等。renderer是具体完成绘制操作的类,因此可以做很多的拓展,支持各种各样的绘制操作。可以考虑编写一个统一的基类renderer,而实现具体功能的renderer类都由该基类继承而来。

            源码地址已经给出,就不贴代码进行说明了,renderer的基类,博主有时间的时候会修改添加上去的。下面是绘制的两个三角形的效果。

                       

     

     

    展开全文
  • 看这篇文章之前,你得先确保你知道Swift如何与Objective-C混编,也知道Objective-C如何与C++混编,如果不知道,请百度...swift调用OC是不难的,你只要在swift的工程里创建OC的文件,系统就会提问帮你生成一个bridging-
  • iOS的Metal框架是一个类似OpenGL的框架,通过编写shaders(类c代码)运行在GPU上,利用GPU的高并行能力执行并行操作,比如图像处理,卷积神经网络。而MPS就是一套基于Metal框架的库,用户不需要理解Metal的细节,直接...
  • 本文不讲述图形变换的理论(可参考计算机图形学和线性代数),图形编程(场景物体剔除、光照等)涉及到很多的数学基础,本文直接从Shader语法阐述如何用Metal API渲染3D物体。
  • Metal 系列教程

    2018-08-25 11:13:27
    从 2014 年,Apple 正式推出 Metal 到现在,这个 Metal 系列教程,酝酿了很久,却迟迟没有进展。 直到 WWDC 2018,Apple 宣布 iOS 12 将弃用 OpenGL / CL,我想,这或许是个机会。 Apps built using OpenGL ES ...
  • Metal 入门
  • float4 vTextureCoord=float4(0.0); vTextureCoord=refract(-vert.eyePosition,vert.normal,0.95); finalColor=diffuseTexture.sample(samplr,vTextureCoord.xzy); vTextureCoord.xyz在我显示图像时,垂直方向旋转...
  • 教程 1 绘制第一个三角形 教程源码下载地址: https://github.com/jiangxh1992/MetalTutorialDemos ...一、知识点 Metal渲染管线 顶点缓冲 Metal着色器(顶点着色器和片段着色器) 顶点坐标系 Metal Shading...
  • GitHub上Swift开源项目!

    2017-02-04 19:10:16
    swift-open-project这里汇集了目前为止最为流行的Swift开源项目,选取SwiftGuide中的开源部分,这里将每周对项目进行总结,如果大家有推荐的可以联系thinkloki@gmail.com,或者issues,欢迎Star、Fork。感谢...
  • 什么是Metal ? 2014年,Apple为iOS引入了新的底层GPU编程框架:Metal。一年后,Metal进入了macOS,随后是watchOS和tvOS。苹果设备有两个可以进行编程以创建应用程序的“大脑”: 中