01- Metal学习之基本概念

前言

今天,我们口袋里装着超级计算机,iPhone正在接近我们许多笔记本电脑的计算方能力。即使拥有所有这些额外的功能,我们仍然受限于OpenGL API的限制,因为它是跨平台方案,通用性是它最大的优点,但是它无法充分利用苹果对其所有产品的深度集成。而且OpenGL 还存在一些结构性问题,导致它无法实现高效率绘制。而且每次绘制调用时发生许多昂贵的操作,Metal改变了操作顺序,并把昂贵的工作移动到绘图调用之外,这样可以释放更多的处理器带宽。而且Metal可以让程序员完全控制GPU如何进行工作,可以更高的提高效率,总结下来,OpenGL是通用图形编程API,制订了行业标准,Metal是针对苹果设备进行高度优化的图形编程API。

Metal工作流程图

截屏2021-12-16 下午4.43.24.png

上图取自苹果官方文档,从这张图中,我们可以大致了解到metal的一个工作流程。
简单来说就是渲染工作 计算工作等实际操作被封装成命令编码器,然后多个编码器打包送给命令缓冲区对象,最后命令缓冲区对象被送入命令队列中,等待交由GPU执行。

我们先看一下Metal中常用的类


截屏2021-12-17 下午3.13.05.png
  • MTLDevice: 对GPU硬件设备的软件引用。

  • MTLCommandQueue: 命令队列,负责创建每帧的命令缓冲对象,并管理它们。

  • MTLLibrary:包含了你的顶点着色器和片段着色器的源代码。

  • MTLRenderPipelineState: 设置绘制的信息,例如要使用的着色器函数、要使用的深度和颜色设置以及如何读取顶点数据。

  • MTLBuffer: 以可以发送到 GPU 的形式保存数据,例如顶点信息.

下面我们来结合一个绘制三角形的例子来讲解代码流程。
这里我们先用Metal框架来构建这个流程。您可能想知道为什么这里不直接用轮子(MetalKit),因为从最基础的Metal框架入门,可以让你对这些部件有更好的了解。而且,使用模板的话代码往往包含一些你在项目中不需要的东西,刚开始的时候,最好还是从最基础的过程做起,这样更有助于你了解这个框架。

1.构建MTL设备

Metal的基础是GPU,要想于GPU交互,您需要创建一个类来引用它,这里就是MTLDevice对象,它是GPU的软件抽象。

 var device = MTLCreateSystemDefaultDevice()

2. 创建Metal显示图层

func setupMetal() {
        metalLayer = CAMetalLayer.init()
        metalLayer.frame = view.bounds
        metalLayer.device = device
        metalLayer.pixelFormat = .bgra8Unorm
        metalLayer.framebufferOnly = true
        view.layer.addSublayer(metalLayer)
    }

最终metal的渲染效果是呈现在CAMetalLayer类图层的。所以我们需要创建这个图层,并把它添加到视图控制器的图层中。

3.加载数据

func loadData() {
        
        //命令队列
        commandQueue = device?.makeCommandQueue()
        
        
        //创建顶点数据缓冲对象
        let vertexData: [Float] = [0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0]
        
        let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
    
        vertexBuffer = device?.makeBuffer(bytes: vertexData, length: dataSize, options: .storageModeShared)
        
        
        //加载着色器
        let defaultLibrary = device?.makeDefaultLibrary()
        
        let fragmentProgram = defaultLibrary?.makeFunction(name: "basic_fragment")
        let vertexProgram = defaultLibrary?.makeFunction(name: "basic_vertex")
        
        //创建管道状态描述器
        let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
        pipelineStateDescriptor.vertexFunction = vertexProgram
        pipelineStateDescriptor.fragmentFunction = fragmentProgram
        
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        
        
        //创建管道状态对象
        do {
            try pipelineState = device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
        } catch let error {
            print("Failed to create pipeline state, error \(error)")
        }
    }

这里我们创建一些渲染对象命令,做一些准备工作。注意:这里命令队列对象我们只需要创建一次,然后持有它就行了,因为这个创建这个对象开销很大,不需要每次渲染的时候都创建。

4. 渲染

这里我们没有使用模板,所以我们需要自己创建循环来渲染。这里我们采用的是CADisplaylLink类。代码如下:

创建循环

  timer = CADisplayLink(target: self, selector: #selector(ViewController.gameLoop))
        timer.add(to: .main, forMode: .default)

渲染过程

func render() {
         @objc func gameLoop() {
        autoreleasepool {
            self.render()
        }
    }
        //创建渲染命令描述其
        let renderPassDescriptor = MTLRenderPassDescriptor()
        
        guard let drawable = metalLayer.nextDrawable() else {
            return
        }
        
        renderPassDescriptor.colorAttachments[0].texture = drawable.texture
        renderPassDescriptor.colorAttachments[0].loadAction = .clear
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(221.0/255.0, 160.0/255.0, 221.0/255.0, 1.0)
        
        let commandBuffer = commandQueue.makeCommandBuffer()
        
        //创建渲染命令编码器
        let renderEncoder = commandBuffer!.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
        renderEncoder?.setRenderPipelineState(pipelineState)
        renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0 )
        renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
        
        renderEncoder?.endEncoding()
        
        //将命令编码器提交到命令缓冲对象中,由命令缓冲对象提交给命令队列,等待GPU执行
        commandBuffer?.present(drawable)
        commandBuffer?.commit()
    }

关于着色器,我们只简单的看一下写法,不做深入讨论,后续会介绍。

#include <metal_stdlib>
using namespace metal;

vertex float4 basic_vertex ( const device packed_float3 * vertex_array[[buffer(0)]], unsigned int vid [[vertex_id]]) {
    return  float4 (vertex_array[vid],1.0);
}

fragment half4 basic_fragment() {
    return  half4(1.0);
    
}

代码已上传至Github -> Metal仓库

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容