Apple Metal2 Swift尝试

写在前面的话

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

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

最后的执行画面是


Apple Metal2 Swift尝试_第1张图片
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 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做中间渲染吧)


Apple Metal2 Swift尝试_第2张图片
Screenshot 2018-12-01 at 7.45.55 PM.png

工程整体代码

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

你可能感兴趣的:(Apple Metal2 Swift尝试)