2017-07-25 12:21:45 computerme 阅读数 1146

若想用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")
            }
2019-07-20 21:51:16 kof0101 阅读数 31

写在前面的话

我个人并不是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

2018-10-29 23:07:47 Xoxo_x 阅读数 248
用于3D绘图和高效并发处理。
OpenGL 的跨平台特性,导致了其没有办法充分发挥GPU的性能,冗余的代码太多,更多的API需要维护,Metal就是为解决这一目的。
Metal 同其他框架协同工作,MetalKit可以简化获取屏幕Metal内容的步骤,使用Metal Performance Shaders 可以自定义函数,或者直接使用现有的函数库。

主题:

GPU Devices :GPU设备

Command Setup:创建命令

Graphics:图像

Parallel Computation:并发计算

Custom Functions:自定义功能 shader

Resource Management:资源管理

Tools:工具

Cookbook:说明书,多是Demo的意思,操作样本

Interoperability:与OpenGL ES的交互

2014-11-07 10:24:36 sunyazhou13 阅读数 1272

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

- 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指南

2018-08-18 19:55:11 sjy234sjy234 阅读数 1183

        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的基类,博主有时间的时候会修改添加上去的。下面是绘制的两个三角形的效果。

                   

 

 

Metal入门概念

阅读数 1390

没有更多推荐了,返回首页