Metal 编程指南

Metal Programming Guide(编程指南)

文章分三部分组成
Metal 基础
Metal 进阶
Metal 实战

github: 项目示例工程和讨论

序章:Metal的世界

欢迎进入Metal编程的世界—— Metal是一个强大而精致的框架,它能将你的创意与苹果硬件的强劲性能无缝连接起来。本文旨在引导你踏上这一激动人心的旅程,从基本概念到高级技巧,最终让你能够自信地使用Metal来实现你的图形和计算愿景。

Metal简介
Metal是苹果公司专为其设备打造的先进图形和计算API。自2014年首次亮相以来,它已经成为iOS、macOS、tvOS和watchOS上性能关键型应用的核心。通过直接与GPU沟通,Metal极大地降低了软件和硬件之间的开销,使得开发者能够挖掘出设备的最大潜力。

Metal的重要性
随着移动设备和个人电脑在我们生活中的地位日益提高,对于高效能的图形和计算API的需求也随之增长。Metal不仅仅是游戏开发者的福音,它同样适用于需要大量图形处理的应用,如数据可视化、虚拟现实和增强现实等。掌握Metal,意味着你能够创造出更加丰富、更加流畅的用户体验。

文章目标和结构
文章面向所有对Metal感兴趣的读者——无论你是一名有抱负的游戏开发者,还是希望提升应用性能的软件工程师。我们将从Metal的基础开始,逐步深入到更为复杂的主题,如多线程渲染、高级着色器编程和性能优化。

预期成果
通过该博客的学习,你不仅会理解Metal的工作原理,还会通过实战项目掌握其应用。我们的目标是让你能够独立设计和实现复杂的图形和计算任务,让你的应用充分利用苹果设备的性能。

资源和社区

文中的示例代码、教程和讨论区都在 github: 项目示例工程和讨论

第1-4章预览

第1章 运行metal

本章的目的是通过最基础的metal代码来快速了解Metal中的配置。
首先使用Xcode新建一个工程,不区分iOS和MacOS在示例代码中两个工程都可以使用。创建完工程后请将下面代码复制到ViewController.m文件中,点击运行即可看到如图1-1显示的效果。

#import "ViewController.h"
#import <MetalKit/MetalKit.h>
#import <simd/simd.h>

NSString *shader = @"\
#include <metal_stdlib>\
\n\
using namespace metal;\
vertex float4 vertexShader(uint vid [[vertex_id]], constant float4 *vertices [[buffer(0)]]) {\
    return vertices[vid];\
}\
fragment float4 fragmentShader() {\
    return float4(1,0,0,1);\
}\
";

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MTKView *mtkView = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    mtkView.device = MTLCreateSystemDefaultDevice();
    mtkView.clearColor = MTLClearColorMake(1, 1, 1, 1);
    [self.view addSubview:mtkView];

    NSError *error;
    id<MTLLibrary> defaultLibrary = [mtkView.device newLibraryWithSource:shader options:nil error:&error];
    id <MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id <MTLFunction> fragmentFunction = 
[defaultLibrary newFunctionWithName:@"fragmentShader"];
    
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;

id <MTLRenderPipelineState> pipelineState = 
[mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];

id<MTLCommandBuffer> commandBuffer = 
[[mtkView.device newCommandQueue] commandBuffer];
    MTLRenderPassDescriptor *renderPassDescriptor = mtkView.currentRenderPassDescriptor;
    if (renderPassDescriptor != nil) {
        id<MTLRenderCommandEncoder> renderEncoder = 
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        [renderEncoder setRenderPipelineState:pipelineState];
        static const float vertices[] = {
             0.0, 1.0, 0, 1,
            -1.0, -1.0, 0, 1,
             1.0, -1.0, 0, 1,
        };
        [renderEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0];
        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
        [renderEncoder endEncoding];
        [commandBuffer presentDrawable:mtkView.currentDrawable];
    }
    [commandBuffer commit];
}

@end

从上面代码可以看出,创建并绘制一个三角形只需要不到70行的代码即可,本章主要会对这70行代码进行拆分和讲解,使读者在阅读完本章能对metal的基础api有所了解,后续章节会一一展开。

为了更好的理解本章所讲内容,请打开文章的配套工程示例代码并找到“01基础用法“。
图1-1 一个三角形

1.1着色器(Shader)

使用metal时,着色器代码可以通过多种方式来创建,本章中使用了字符串定义了着色器代码,代码定义了两个着色器方法:顶点着色器和片元着色器。

关于着色器在后续章节中都会使用,因此本节只是简单介绍用于绘制三角形的着色器,我们需要了解的是着色器代码是由GPU进行处理的,GPU处理的流程如图1-2所示。
图1-2 GPU处理流程
metal着色器语言是基于C++14设计的,在C++基础上多了一些扩展和限制,本文主要目的是帮助读者能掌握metal的开发能力,在使用期间也会标明着色器语言的扩展和限制,如果读者需要完整的了解metal着色器语言的语法规范可以在Apple官网中搜索“Metal Shading Language Specification“。

本章的着色器代码可以分为三部分:
1 引入 metal 标准库并设置命名空间。

#include <metal_stdlib>
using namespace metal;

metal库中包含了一些metal所需的基本函数和数据类型的定义。其次是对metal的命名空间做了引用,关于命名空间问题可以参考C++代码基础。

2 定义顶点着色器(Vertex Shader)。

vertex float4 vertexShader(uint vid [[vertex_id]], constant float4 *vertices [[buffer(0)]]) {
    return vertices[vid];
}

在着色器代码中,函数前面添加关键字‘vertex’即表明当前函数为顶点着色器函数。顶点着色器函数用于确认位置(坐标),因此该函数的返回值为float4类型,float4代表一个浮点数向量,它包含4个位置信息(x,y,z,w)。

该函数有两个参数:一个表示顶点索引的vid和一个指向顶点数据的vertices数组。

在介绍参数前需要先了解着色器代码中的‘[[ ]]’含义,双括号是属性限定符(attribute qualifiers)的语法,用于为编译器提供关于着色器函数参数或结构体成员的额外信息。这些属性限定符可以有效的帮助着色器执行和资源绑定。常见的属性限定符有很多,本章只使用了两个,为了能更好的理解属性限定符的作用,此处只说明这两个的含义,后续使用到新的再做介绍从而加深印象。

[[vertex_id]],用于表示当前顶点的索引。 当你在顶点着色器中使用这个修饰符时,它会自动为每个顶点提供一个唯一的索引值。这个值是由metal图形管线自动生成的,用于标识当前正在处理的顶点。如本章中绘制了一个三角形,三角形由3个顶点构成,因此vid的值是0-2。

[[buffer(index)]],用于指定一个缓冲区的绑定索引。 index是一个整数,表示该资源在渲染或计算命令中的绑定点。仔细查看示例代码,可以看出在62行中,我们为着色器传递顶点数组时标明了该数组所在的位置是0。
本章中的顶点着色器函数并没有去做进一步的数据处理,只是将接收到的位置信息进行返回。

3 定义片元着色器(Fragment Shader)。

fragment float4 fragmentShader() {
    return float4(1,0,0,1);
}

在着色器代码中,函数前面添加关键字‘fragment即表明当前函数为片元着色器函数。片元着色器函数主要负责确定最终渲染到屏幕上每个像素的颜色。

该函数返回值也是float4向量,代表一个RGBA的颜色,红色通道为100%,绿色和蓝色通道为 0%,alpha通道为100%。当我们使用该片元着色器函数后绘制的物体都为红色。

1.2 metal显示窗口

MTKView是MetalKit框架提供的一个专门用于metal渲染的视图类,它简化了metal渲染的设置和管理,提供了一个方便的接口来进行基本的metal渲染操作。MTKView继承自NSView(在macOS上)或UIView(在iOS上),因此它可以很容易地集成到UIKit或AppKit的视图层次结构中。

    MTKView *mtkView = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    mtkView.device = MTLCreateSystemDefaultDevice();
    mtkView.clearColor = MTLClearColorMake(1, 1, 1, 1);
    [self.view addSubview:mtkView];

上述代码中,我们创建了一个MTKView并设置其大小,随后配置该View的device属性。device属性表明MTKView要使用哪个GPU设备来执行metal渲染操作。

MTLCreateSystemDefaultDevice() 函数来获取系统默认的metal设备,该函数会返回一个代表当前设备的metal设备对象。此对象用于创建各种metal所需的实例。

需要注意的是MTKView设置背景颜色需要通过clearColor属性来设置,本章中MTKView的背景色为白色。clearColor是一个MTLClearColor类型,它包含红色、绿色、蓝色和透明度分量,这些分量的值通常在 0.0 到 1.0 之间,分别对应颜色分量的强度。

1.3 加载着色器

接下来我们需要将1.1节中定义的着色器函数加载到运行代码中,使其能被调用并执行。

NSError *error;
id<MTLLibrary> defaultLibrary = [mtkView.device newLibraryWithSource:shader options:nil error:&error];
id <MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id <MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

在metal中,编写的着色器代码需要被编译成GPU可执行的代码才能在设备上运行。这个过程就是将着色器代码编译成Metal shader library(Metal 着色器库)。metal着色器库包含了编译后的顶点着色器、片元着色器等函数,可以在运行时被metal应用程序加载和使用。

本章中,通过将着色器代码字符串传递给newLibraryWithSource:options:error: 方法,可以动态地从代码中创建 metal 着色器库(id)。

上述代码会将 shader 字符串中的着色器代码编译成metal着色器库,并将编译后的函数存储在 defaultLibrary变量中,以便后续使用。
之后我们从defaultLibrary中获取获取顶点和片段函数。从defaultLibrary中获取顶点和片段函数需要使用newFunctionWithName: 方法,该方法接受着色器函数的名称作为参数,并返回一个代表该函数的MTLFunction对象。

上述代码通过调用defaultLibrary的newFunctionWithName: 方法,传递着色器函数的名称作为参数,从而获取了顶点着色器和片元着色器的MTLFunction对象。在这里,顶点着色器的名称是 “vertexShader”,片段函数的名称是 “fragmentShader”,这与着色器代码中定义的函数名称相对应。

1.4 渲染管线

渲染管线状态对象是GPU执行渲染命令时的核心配置之一,它定义了如何处理顶点数据、如何执行顶点和片元着色器、以及如何将最终的片元颜色输出到屏幕或纹理。

MTLRenderPipelineDescriptor 类允许开发者在创建这个状态对象之前指定这些参数。

MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;

id <MTLRenderPipelineState> pipelineState = 
[mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];

作为metal渲染的核心配置之一,渲染管线有很多属性和方法,这些属性和方法在后续章节都会使用,本章的介绍只是作为引言。

首先需要创建一个渲染管线对象MTLRenderPipelineDescriptor将我们获取到的用于绘制三角形的顶点函数和片元函数设置到渲染管线对象中。

接着设置颜色附件colorAttachments,此属性用于配置渲染管线中的颜色附件。数组的长度表示渲染管线中可以配置的颜色附件的数量。通常情况下,渲染管线可以配置多个颜色附件,用于同时输出多个渲染目标的颜色数据。本章中我们的目标是将三角形渲染到MTKView中,因此选择与屏幕显示格式匹配的像素格式。

需要注意的是如果渲染管线的颜色输出格式与当前视图的颜色像素格式不匹配,可能会导致渲染结果显示异常或渲染失败。

最后使用metal设备对象的newRenderPipelineStateWithDescriptor:error:方法。这个方法会接受一个渲染管线描述符作为参数,并返回一个表示渲染管线状态的MTLRenderPipelineState对象。

渲染管线状态表示一个已配置好的渲染管线,可直接用于渲染操作。

1.5 命令缓冲区(MTLCommandBuffer)

如果渲染管线是metal渲染的关键配置,那么命令缓冲区则是metal执行时的核心。它代表一系列由GPU执行的命令的集合。这些命令可以包括渲染指令、计算指令、内存复制指令等。开发者将这些命令编码到命令缓冲区中,然后将它提交给命令队列(MTLCommandQueue),由GPU异步执行。

本节主要介绍命令缓冲区的创建和提交,命令编码在下一节中介绍。

id<MTLCommandBuffer> commandBuffer = 
[[mtkView.device newCommandQueue] commandBuffer];
    MTLRenderPassDescriptor *renderPassDescriptor = mtkView.currentRenderPassDescriptor;
    if (renderPassDescriptor != nil) {
        // 命令编码和资源传输… 请查看1.6节
        [commandBuffer presentDrawable:mtkView.currentDrawable];
    }
    [commandBuffer commit];

首先我们通过MTLDevice来创建一个缓冲区队列,该队列最多只允许64个未完成的命令缓冲区,本章我们通过命令队列的commandBuffer方法创建一个命令缓冲区。

随后从MTKView中检索当前的渲染通道描述符。使用metal渲染时,如果我们使用MTKView来管理渲染目标。MTKView会自动创建并配置一个渲染通道描述符,用于描述当前渲染目标的属性,如颜色附件、深度缓冲等。这个渲染通道描述符被称为当前渲染通道描述符。可以通过MTKView的 currentRenderPassDescriptor 属性获取。注意:它可能在某些情况下会获取失败,因此需要添加判断。

关于命令编码我们放在1.6节中说明,当我们结束命令编码后,首先是调用缓冲区的方法presentDrawable此方法表示将渲染的结果呈现到一个可绘制的对象中,本章节中使用mtkView 的 currentDrawable属性,它表示当前MTKView的可绘制对象,用于接收渲染结果。
注意:
如果MTKView是用于显示的,则currentDrawable会返回一个可以直接呈现到屏幕上的 CAMetalDrawable对象。
如果MTKView用于渲染到纹理,那么currentDrawable可能会返回一个 nil 值,因为不会直接显示,而是用于进一步处理渲染结果。

最后调用缓冲区的commit方法来提交当前的命令缓冲区。一旦调用 commit 方法,命令缓冲区中记录的所有 GPU 命令都将被提交给 GPU 执行。这包括绘制命令、资源管理命令等,GPU 将按照命令缓冲区中的顺序执行这些命令。
在提交后,命令缓冲区会被重置,以便后续的命令记录操作。通常情况下,我们应该在完成所有的命令记录操作后调用 commit 方法,将命令发送给 GPU 执行。

1.6 渲染命令编码器(MTLRenderCommandEncoder)

正如在1.1节中定义着色器方法时说到,着色器方法只是处理由CPU传递过来的数据,而数据则是由渲染命令编码器来负责传递的。

if (renderPassDescriptor != nil) {
    id<MTLRenderCommandEncoder> renderEncoder = 
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
    [renderEncoder setRenderPipelineState:pipelineState];
    static const float vertices[] = {
        0.0, 1.0, 0, 1,
        -1.0, -1.0, 0, 1,
        1.0, -1.0, 0, 1,
    };
    [renderEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0];
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
    [renderEncoder endEncoding];
    [commandBuffer presentDrawable:mtkView.currentDrawable];
}

在1.5节中我们获取了渲染通道描述符后,通过缓冲区来创建一个渲染命令编码器,并用它来记录渲染命令。渲染命令编码器包含了执行渲染操作所需的各种配置和数据。

首先为渲染命令编码器设置渲染管线,在1.4节中我们提到渲染管线状态对象是GPU执行渲染命令时的核心配置,它包含了顶点着色器和片元着色器,因此我们将渲染管线设置到渲染命令编码器,表明该编码器使用我们在1.1节中定义好的着色器方法。

在1.1节中我们提到顶点着色器函数有两个参数:一个表示顶点索引的vid和一个指向顶点数据的vertices数组。顶点索引由metal管线自动生成,而顶点数据则由我们生成并传递。

在代码中我们定义了三角形的3个顶点,三角形将由这3个点所围成,metal在渲染时,顶点坐标会被转换成一个标准化的坐标空间,其中x、y和z坐标的值都在[-1, 1]的范围内。图1-3展示了View坐标和metal坐标的关系。
图1-3 metal裁剪空间

· 在顶点着色器输出顶点坐标后,这些坐标会被转换到裁剪空间,然后通过透视除法转换到归一化设备坐标(Normalized Device Coordinates, NDC)。在透视除法中,裁剪空间的坐标会被它们的齐次 w 分量所除。这个过程会将一个4D的裁剪空间坐标 (x, y, z, w) 转换为一个3D的NDC坐标 (x/w, y/w, z/w)。
本章中我们只是绘制一个2D的三角形因此不用考虑坐标的Z值,关于3D图形的展示会在第三章中详细讲述。
因此我们在代码中设置了三角形的三个点的坐标:

  • 第1个点在空间的正上方,坐标为 (0.0, 1.0,);

  • 第2个点在空间的左下方,坐标为 (-1.0, -1.0,);

  • 第3个点在空间的右下方,坐标为 (1.0, -1.0,);

    定义好3个点后,将存放3个点的数组vertices绑定到命令编码器(renderEncoder)中,并指定它在缓冲区中的位置,在1.1节定义顶点着色器函数时我们将顶点数组的数据位置设置为在缓冲区0的位置,因此此处我们将index设置为0。
    数据设置结束后需要调用命令编码的器的draw方法来指定metal应该如何进行绘制,关于metal绘制时的类型会在第二章中介绍。

    本章使用了最简单的drawPrimitives方法:

  • MTLPrimitiveTypeTriangle表示指定要绘制的图元类型为三角形;

  • vertexStart:0表示从顶点缓冲区的起始位置开始绘制,即从第一个顶点开始。

  • vertexCount:3表示要绘制的顶点数量为 3,即绘制一个三角形。

  • instanceCount:1表示绘制的实例数量为1,表示每个顶点数据将被使用一次。

最后调用命令编码器的endEncoding方法表明结束当前的渲染命令编码操作。

注意:在调用 endEncoding 方法后,你不能再向渲染命令编码器添加新的渲染命令。一旦结束编码操作,你就可以提交渲染命令缓冲区,将记录的渲染命令发送给 GPU 执行。

1.7小结

本章的目标是通过绘制一个简单的三角形,为读者提供Metal基本渲染流程的初步了解。需要注意的是,本章内容并未涵盖所有细节,例如着色器函数参数修饰符的含义、如何在不使用MTKView的情况下进行渲染,以及渲染管线的深入配置等方面。这样的选择是出于以下考虑:由于本章仅覆盖了Metal的部分API,如果一次性介绍过多细节,可能会导致读者在实际应用时记不清楚。因此,这些高级主题将在实际需要时给出详尽的解释。

在您准备进入下一章之前,我们鼓励您根据自己对本章内容的理解,对代码进行封装和模块化处理。您也可以尝试使用CAMetalLayer进行渲染,以此加深对Metal的理解。

在下一章中,我们将展示如何对Metal API进行模块化处理,这将使代码更加易于理解和维护。

第二章 MDLMesh、帧刷新、数据传入GPU

第三章 3D模型和坐标矩阵

第四章 法线和深度

第五章 光源

第六章 纹理

第七章 材质组和glTF

未完待续…

Metal 常见错误处理

	CompileMetalFile XXX, fatal error: 'XXX.h' file not found
	// metal 中 引用其他文件需要其完整的相对路径 
	fileA.h 在 metal 文件的上层 则需要 #include "../fileA.h"

使用 newDefaultLibrary 获取不到 MTLLibrary
目前遇到这种问题是因为在 .a 库中使用Metal 如果shader 代码不多推荐使用 string来编写
如果代码多请使用 LLVM 来生成 .metallib 文件

xcrun -sdk iphoneos metal MyLibrary.metal -o MyLibrary.air
xcrun -sdk iphoneos metallib MyLibrary.air -o MyLibrary.metallib

string 方式

- (void)config {
	NSError *error = NULL;
    id<MTLLibrary> defaultLibrary =[_device newLibraryWithSource:[self metalStrings] options:nil error:&error];
    // ...
}

- (NSString*)metalStrings {
    NSString *str = @"#include <metal_stdlib> \n #include <simd/simd.h> \n using namespace metal; struct LogoData { float4 position [[position]]; }; struct Transform { float4 matrix; }; vertex LogoData loadLogoVertex(constant LogoData *vertices [[buffer(0)]], constant Transform *transform [[buffer(1)]], uint vid [[vertex_id]]) { LogoData out; out.position = vertices[vid].position * transform->matrix; return out; }\n fragment float4 loadLogoFragment(LogoData inData [[stage_in]]) { return float4(0,0.75,1,1); }";
    
    return str;
}

Metal 开发游戏

正在编写 preview1.0 大致是一个逃亡类游戏,后续会上传源码

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Metal编程指南》是一本介绍如何使用Metal框架进行图形引擎开发的编程指南Metal是苹果公司为iOS设备和Mac电脑上的图形处理器(GPU)提供的底层编程接口。这本书将带领读者从基础概念到实际应用,全面深入地了解Metal编程Metal编程的核心思想是利用GPU的强大计算能力进行高性能的并行计算和图形渲染。对于那些想要开发高性能图形应用、游戏引擎或者渲染引擎的开发者来说,这本《Metal编程指南》将是一本宝贵的参考资料。 《Metal编程指南》的内容包括Metal框架的基本概念,如如何创建和配置Metal设备、渲染管道、纹理和缓冲等资源。同时,该书还介绍了Metal Shader Language(MSL),这是一种类似于C语言的语言,开发者可以使用它编写GPU的计算和渲染代码。 此外,该指南还深入讨论了Metal的基础原理和内部机制,包括如何利用并行计算进行高效的图形渲染、光照和阴影计算、粒子系统实现等。此外,还介绍了如何利用Metal进行机器学习、图像处理和数据计算等任务。这些内容能够帮助开发者更好地利用Metal提供的底层接口进行高性能计算和图形渲染。 总之,《Metal编程指南》是一本详尽而全面的关于Metal编程指南,可以帮助开发者理解Metal框架的基本原理和内部机制,掌握Metal编程的技巧和方法。无论是开发图形应用、游戏引擎还是进行高性能计算,这本书都是一本不可多得的参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值