iOS图像:Metal 入门

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、初探 Metal
    • 1、Metal 着色语言介绍
    • 2、Metal 着色语言基本语法
    • 3、Metal 着色语言函数修饰符
  • 二、跑马灯
    • 1、替换视图为MTKView
    • 2、使用流程
    • 3、Renderer 渲染器工具类里的初始化方法
    • 4、Renderer 渲染器工具类里设置颜色的方法
    • 5、MTKViewDelegate 委托方法
  • 三、渐变三角形
    • 1、创建 Shaders.metal 文件
    • 2、创建 ShaderTypes.h 文件
    • 3、渲染器工具类里使用到的成员变量
    • 4、初始化MTKView的方法
    • 5、每当视图改变方向或调整大小时调用
    • 6、每当视图需要渲染帧时调用
  • 四、使用MTLBuffer解决顶点数据达到上限问题
    • 1、Renderer 渲染器工具类里的初始化方法
    • 2、加载Metal文件的方法
    • 3、生成顶点数据
    • 4、每当视图需要渲染帧时调用
  • 五、加载TGA文件
    • 1、Renderer 渲染器工具类里的初始化方法
    • 2、设置顶点数据
    • 3、设置渲染管道
    • 4、加载纹理TGA文件
    • 5、创建Shader.metal文件
  • 六、加载PNG图片
    • 1、设置纹理
    • 2、从UIImage中读取Byte数据
  • 七、金字塔
    • 1、创建ShaderTypes.h文件
    • 2、创建Shaders.metal文件
    • 3、准备工作
    • 4、设置MTKView
    • 5、设置管道
    • 6、设置顶点数据
    • 7、设置投影矩阵/模型视图矩阵
    • 8、视图渲染
    • 9、为金字塔添加纹理
  • Demo
  • 参考文献

一、Metal 着色语言

1、Metal 着色语言介绍

Metal着色语言是一个用来编写3D图形渲染逻辑和并行计算核心逻辑的编程语言,编写基于Metal框架的APP需要使用Metal着色语言程序。Metal着色器语言使用ClangLLVM编译器。

Metal与C++

Metal这门语言是基于C++标准设计的,它在C++基础多了一些拓展和限制。


2、Metal 着色语言基本语法

基本数据类型
bool a = true;
char b = 5;
int  d = 15;
size_t c = 1;
向量
bool2 A= [1,2];
float4 pos = float4(1.0,2.0,3.0,4.0);
float x = pos[0];
float y = pos[1];

float4 VB;
for (int i = 0; i < 4 ; I++)
{
    VB[i] = pos[i] * 2.0f;
}
通过向量字母来获取元素
int4 test = int4(0,1,2,3,4);
int a = test.x;
int b = test.y;
int c = test.z;
int d = test.w;

int e = test.r;
int f = test.g;
int g = test.b;
int h = test.a;
通过字母对向量进行赋值
float4 c;
c.xyzw = float4(1.0f,2.0f,3.0f,4.0f);
c.z = 1.0f;
c.xy = float2(3.0f,4.0f);
c.xyz = float3(3.0f,4.0f,5.0f);
不允许超过维度访问向量
float2 pos;
pos.x = 1.0f; //合法
pos.z = 1.0f; //非法
矩阵
float4x4 m;
// 将第二排的值设置为0
m[1] = float4(2.0f);
// 设置第一行/第一列为1.0f
m[0][0] = 1.0f;
// 设置第三行第四列的元素为3.0f
m[2][3] = 3.0f;
float4类型向量的所有可能构造方式
float4(float x);
float4(float x,float y,float z,float w);
float4(float2 a,float2 b);
float4(float2 a,float b,float c);
float4(float a,float2 b,float c);
float4(float a,float b,float2 c);
float4(float3 a,float b);
float4(float a,float3 b);
float4(float4 x);
多个向量构造器的使用
float x = 1.0f,y = 2.0f,z = 3.0f,w = 4.0f;
float4 a = float4(0.0f);
float4 b = float4(x,y,z,w);
float2 c = float2(5.0f,6.0f);
float2 a = float2(x,y);
float2 b = float2(z,w);
float4 x = float4(a.xy,b.xy);
缓存buffer
// 2个修饰符:device(设备空间)constant(设备空间只读)
// 作用:当作函数的参数来传递
device float4 *device_buffer;
struct user_data
{
    float4 a;
    float b;
    int2 c;
};
constant user_data *data;
纹理texture
enum class access {sample,read,write};
texture1d
texture1d_array
texture2d
texture2d_array
texture3d
texturecube
texture2d_ms
纹理在函数中的使用
void foo (texture2d imgA[[texture(0)]],
          texture2d imgB[[texture(1)]],
          texture2d imgC[[texture(2)]])
{
    //...
}

3、Metal 着色语言函数修饰符

函数修饰符号出现在函数返回值声明之前。

vertex修饰顶点着色函数
vertex int TestFouncition(int a,int b)
{
    //...
}
fragment修饰片元着色函数
fragment int TestFouncition(int a,int b)
{
    //...
}
设备地址空间device修饰指针/引用
// 修饰指针变量
device float4 *color;
// 修饰结构体类的指针变量
struct Struct
{
    float a[3];
    int b[2];
};
device Struct *data;
属性修饰符目的
  • 参数表示资源如何定位? 可以理解为端口
  • 在固定管线和可编程管线进行内建变量的传递
  • 将数据沿着渲染管线从顶点函数传递片元函数
device buffer //设备缓存 [[buffer(index)]]
constant buffer //常量缓存 [[buffer(index)]]
texture Object //纹理对象 [[texture(index)]]
sampler Object //采样器对象 [[sampler(index)]]

二、跑马灯

Metal项目只能真机运行,不能运行在模拟器上。

#import 

1、替换视图为MTKView

在Main.storyboard里进行替换
mtkView = (MTKView *)self.view;
通过代码方式进行替换
self.mtkView = [[MTKView alloc] init];

2、使用流程

- (void)viewDidLoad
{
}
❶ 获取mtkView
@property (nonatomic, strong) MTKView *mtkView;

self.mtkView = (MTKView *)self.view;
❷ 为mtkView设置MTLDevice

一个MTLDevice 对象就代表这一个GPU。通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU对象。

self.mtkView.device = MTLCreateSystemDefaultDevice();
❸ 判断是否设置成功
if (!self.mtkView.device)
{
    NSLog(@"Metal不支持这个设备");
    return;
}
❹ 创建渲染器

分开渲染循环。在我们开发Metal程序时,将渲染循环分为自己创建的类是非常有用的一种方式。使用单独的类,我们可以更好管理初始化Metal以及Metal视图委托。

@property (nonatomic, strong) Renderer *renderer;

self.renderer =[[Renderer alloc] initWithMetalKitView:self.mtkView];
❺ 判断renderer是否创建成功
if (!self.renderer)
{
    NSLog(@"Renderer初始化失败");
    return;
}
❺ 设置MTKView的代理(由renderer来实现MTKView的代理方法)
self.mtkView.delegate = self.renderer;
❻ 为视图设置帧速率,默认每秒60帧
self.mtkView.preferredFramesPerSecond = 60;

3、Renderer 渲染器工具类里的初始化方法

颜色结构体
typedef struct
{
    float red, green, blue, alpha;
} Color;
使用到的成员变量
@implementation Renderer
{
    id _device;
    id _commandQueue;// 命令队列
}
实现初始化方法

使用MTLCommandQueue去创建对象,并且加入到MTLCommandBuffer对象中,确保它们能够按照正确顺序发送到GPU。对于每一帧,一个新的MTLCommandBuffer对象创建并且填满了由GPU执行的命令。

- (id)initWithMetalKitView:(MTKView *)mtkView
{
    self = [super init];
    if(self)
    {
        _device = mtkView.device;
        _commandQueue = [_device newCommandQueue];
    }
    
    return self;
}

4、Renderer 渲染器工具类里设置颜色的方法

- (Color)makeFancyColor
{
    return color;
}
❶ 创建颜色变量
// 用来判断是增加颜色还是减小颜色的变量
static BOOL growing = YES;
// 颜色通道值(0~3),不需要每次修改红绿蓝3个通道,只需要修改1个
static NSUInteger primaryChannel = 0;
// 颜色通道数组 (颜色值)
static float colorChannels[] = {1.0, 0.0, 0.0, 1.0};
// 颜色调整步长
const float DynamicColorRate = 0.015;
❷ 增加颜色
if(growing) 
{
    // 动态颜色通道的索引 (1,2,3,0) ,用来实现通道间切换
    NSUInteger dynamicChannelIndex = (primaryChannel+1)%3;
    
    // 修改对应通道的颜色值,每次只调整0.015
    colorChannels[dynamicChannelIndex] += DynamicColorRate;
    
    // 当颜色通道对应的颜色值 = 1.0
    if(colorChannels[dynamicChannelIndex] >= 1.0)
    {
        // 设置为NO
        growing = NO;
        
        // 将颜色通道修改为新的动态颜色通道的索引
        primaryChannel = dynamicChannelIndex;
    }
}
❸ 减少颜色
// 获取动态颜色通道的索引
NSUInteger dynamicChannelIndex = (primaryChannel+2)%3;

// 将当前颜色的值减去0.015
colorChannels[dynamicChannelIndex] -= DynamicColorRate;

// 当颜色值小于等于0.0
if(colorChannels[dynamicChannelIndex] <= 0.0)
{
    // 又调整为颜色增加
    growing = YES;
}
❹ 修改颜色的RGBA的值
Color color;
color.red   = colorChannels[0];
color.green = colorChannels[1];
color.blue  = colorChannels[2];
color.alpha = colorChannels[3];

5、MTKViewDelegate 委托方法

MTKViewDelegate协议中有2个方法。每当窗口大小变化或者重新布局(设备方向更改)时视图就会调用drawableSizeWillChange方法。每当视图需要渲染时调用drawInMTKView方法。

- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
{
    NSLog(@"当MTKView视图发生大小改变时调用");
}
- (void)drawInMTKView:(nonnull MTKView *)view
{
    NSLog(@"每当视图需要渲染时调用");
}
❶ 获取到颜色值来设置view的clearColor
Color color = [self makeFancyColor];
view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha);
❷ 为每一帧创建一个新的命令缓冲区
id commandBuffer = [_commandQueue commandBuffer];
commandBuffer.label = @"命令缓冲区";
❸ 从视图绘制中获得渲染描述符
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
❹ 判断渲染描述符是否创建成功,未成功则跳过任何渲染
if(renderPassDescriptor != nil)
{
}
❺ 通过渲染描述符创建渲染编码器对象
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
renderEncoder.label = @"渲染编码器";
❻ 结束渲染编码器的工作

我们可以使用渲染编码器来绘制图像,但是这个demo我们仅仅创建编码器就可以了,我们并没有让Metal去执行我们绘制的任务,所以这个时候我们的任务已经完成,即可结束渲染编码器的工作。

[renderEncoder endEncoding];
❼ 添加一个展示命令来显示绘制的屏幕

当编码器结束任务之后,命令缓存区就会接受到2个命令——展示命令和提交命令。因为GPU是不会直接绘制到屏幕上的,因此你不给出展示和提交指令便不会有任何内容渲染到屏幕上。

[commandBuffer presentDrawable:view.currentDrawable];
❽ 完成渲染并将命令缓冲区提交给GPU
[commandBuffer commit];

三、渐变三角形

1、创建 Shaders.metal 文件

❶ 导入框架
#import "ShaderTypes.h"
#include 
using namespace metal;// 使用命名空间 Metal
❷ 顶点着色器输出数据和片段着色器输入数据
typedef struct
{
    // 处理空间的顶点信息 position指的是顶点裁剪后的位置
    float4 clipSpacePosition [[position]];
    float4 color;// float4表示4维向量 颜色

} RasterizerData;
❸ 顶点着色函数(了解即可,语法较诡异)
  • 处理顶点数据:执行坐标系转换,将生成的顶点剪辑空间写入到返回值中,再将顶点颜色值传递给返回值
  • 下一个阶段:当顶点函数执行3次,即三角形的每个顶点执行一次后,再执行管道中的下一个阶段——光栅化
  • RasterizerData:表示返回的数据类型
  • vertexShader:表示函数的名称
  • vertexID:表示顶点ID
  • vertices:表示存储在缓冲区中的顶点数据
  • VertexInputIndexViewportSize:表示视口的大小
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant Vertex *vertices [[buffer(VertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]])
{
    // 定义out
    RasterizerData out;
}

初始化输出剪辑空间位置

out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);

我们的位置是在像素维度中指定的。索引到我们的数组位置以获得当前顶点。

float2 pixelSpacePosition = vertices[vertexID].position.xy;

vierportSizePointerverctor_uint2转换为vector_float2类型

vector_float2 viewportSize = vector_float2(*viewportSizePointer);

每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间)。剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角。计算和写入 XY 值到我们的剪辑空间的位置。为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半。

out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);

把我们输入的颜色直接赋值给输出颜色。这个值将用于构成三角形的顶点的其他颜色值,从而为我们片段着色器中的每个片段生成颜色值。

out.color = vertices[vertexID].color;

完成! 将结构体传递到管道中下一个阶段

return out;
❹ 片元函数
  • fragment:片元函数
  • float4:返回类型为4维向量
  • fragmentShader:函数名称
  • RasterizerData:传入的数据
  • in:表示的是变量名称
  • [[stage_in]]:片元着色函数使用的单个片元输入数据是由顶点着色函数输出,然后经过光栅化生成的。单个片元输入函数数据可以使用[[stage_in]]属性修饰符
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
    // 返回输入的片元的颜色
    return in.color;
}

2、创建 ShaderTypes.h 文件

作用是让文件里面的结构体在OC和Metal之间能够通用(即在国外也能够说中国话)。

缓存区索引值
typedef enum VertexInputIndex
{
    VertexInputIndexVertices = 0,// 顶点
    VertexInputIndexViewportSize = 1,// 视图大小
} VertexInputIndex;
顶点
typedef struct
{
    vector_float2 position;// 像素空间的位置,比如像素中心点(100,100)
    vector_float4 color;// RGBA颜色
} Vertex;

3、渲染器工具类里使用到的成员变量

@implementation TriangleRenderer
{
    id _device;// 用来渲染的设备(又名GPU)
    
    // 渲染管道有顶点着色器和片元着色器,存储在shader.metal文件中
    id _pipelineState;

    // 从命令缓存区获取命令队列
    id _commandQueue;

    // 当前视图大小,在渲染通道时会使用这个视图
    vector_uint2 _viewportSize;
}

4、初始化MTKView的方法

- (id)initWithMetalKitView:(MTKView *)mtkView
{
    self = [super init];
    if(self)
    {
        NSError *error = NULL;
        ......
    }
    return self;
}
❶ 获取GPU设备
_device = mtkView.device;
❷ 从bundle中获取.metal文件,在项目中加载所有的(.metal)着色器文件
id defaultLibrary = [_device newDefaultLibrary];
// 从库中加载顶点函数
id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// 从库中加载片元函数
id fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
❸ 配置用于创建管道状态的管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
// 可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
// 可编程函数,用于处理渲染过程中各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
// 一组存储颜色数据的组件
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
❹ 同步创建并返回渲染管线状态对象
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
// 判断是否返回了管线状态对象
if (!_pipelineState)
{
    // 如果我们没有正确设置管道描述符,则管道状态创建可能失败
    NSLog(@"管道状态创建失败,错误信息为:%@", error);
    return nil;
}
❺ 创建命令队列
_commandQueue = [_device newCommandQueue];

5、每当视图改变方向或调整大小时调用

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
{
    // 保存可绘制的大小,因为当我们绘制时,我们将把这些值传递给顶点着色器
    _viewportSize.x = size.width;
    _viewportSize.y = size.height;
}

6、每当视图需要渲染帧时调用

- (void)drawInMTKView:(nonnull MTKView *)view
{
}
❶ 顶点数据/颜色数据
static const Vertex triangleVertices[] =
{
    // 2D顶点,RGBA颜色值
    { {  250,  -250 }, { 1, 0, 0, 1 } },
    { { -250,  -250 }, { 0, 1, 0, 1 } },
    { {    0,   250 }, { 0, 0, 1, 1 } },
};
❷ 为当前渲染任务创建一个新的命令缓冲区
id commandBuffer = [_commandQueue commandBuffer];
commandBuffer.label = @"CommandBuffer";// 指定缓存区名称
❸ 一组渲染目标,用作渲染通道生成的像素的输出目标
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
// 判断渲染目标是否为空
if(renderPassDescriptor != nil)
{
}
❹ 创建渲染命令编码器
id renderEncoder =[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
renderEncoder.label = @"RenderEncoder";// 渲染命令编码器名称
❺ 设置可绘制区域

视口指定Metal渲染内容的可绘制区域。视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域。为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。

MTLViewport viewPort =
{
    0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0
};
[renderEncoder setViewport:viewPort];
❻ 设置当前渲染管道状态对象
[renderEncoder setRenderPipelineState:_pipelineState];
❼ 从应用程序OC代码中发送数据给Metal顶点着色器函数
  • 参数一:指向要传递给着色器的内存的指针
  • 参数二:我们想要传递的数据的内存大小
  • 参数三:一个整数索引,它对应于我们的vertexShader函数中的缓冲区属性限定符的索引
[renderEncoder setVertexBytes:triangleVertices
                       length:sizeof(triangleVertices)
                      atIndex:VertexInputIndexVertices];
❽ 视口大小的数据
  • 参数一:发送到顶点着色函数中,视图大小
  • 参数二:视图大小内存空间大小
  • 参数三:对应的索引
[renderEncoder setVertexBytes:&_viewportSize
                       length:sizeof(_viewportSize)
                      atIndex:VertexInputIndexViewportSize];
❾ 画出三角形的3个顶点
  • 参数一:绘制图形组装的基元类型
  • 参数二:从哪个位置数据开始绘制,一般为0
  • 参数三:每个图元的顶点个数,绘制的图型顶点数量
// 在不使用索引列表的情况下绘制图元
// 点 线段 线环 三角形 三角型扇
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                  vertexStart:0
                  vertexCount:3];
❿ 码器生成的命令都已完成则展示和提交
[renderEncoder endEncoding];
// 进行展示
[commandBuffer presentDrawable:view.currentDrawable];
// 在这里完成渲染并将命令缓冲区推送到GPU
[commandBuffer commit];

四、使用MTLBuffer解决顶点数据达到上限问题

之前使用setVertexBytes将顶点数据传递到顶点函数中,但是setVertexBytes有个限制,就是不允许传递超过4096字节的顶点数据,这对于如大型游戏等的渲染来说自然是致命的弱点,所以需要通过Metal提供的缓冲区对象MTLBuffer来进行解决。

下面的范例共有15行25列,2250个顶点。每个顶点需要32个字节,所以共需要72000字节,远远超过了顶点上限。


1、Renderer 渲染器工具类里的初始化方法

// 顶点个数
NSInteger _numVertices;

// 顶点缓存区
id _vertexBuffer;
- (instancetype)initWithMetalKitView:(MTKView *)mtkView
{
    self = [super init];
    if(self)
    {
        // 初始GPU设备
        _device = mtkView.device;
        // 加载Metal文件
        [self loadMetal:mtkView];
    }
    return self;
}

2、加载Metal文件的方法

- (void)loadMetal:(nonnull MTKView *)mtkView
{
}
❶ 设置绘制纹理的像素格式
mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
❷ 从项目中加载所有的.metal着色器文件
id defaultLibrary = [_device newDefaultLibrary];
// 从库中加载顶点函数
id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// 从库中加载片元函数
id fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
❸ 配置用于创建管道状态的管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
// 可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
// 可编程函数,用于处理渲染过程总的各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
// 设置管道中存储颜色数据的组件格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
❹ 同步创建并返回渲染管线对象
NSError *error = NULL;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                         error:&error];
// 判断是否创建成功
if (!_pipelineState)
{
    NSLog(@"创建渲染管线对象失败,错误信息为:%@", error);
}
❺ 获取顶点数据
NSData *vertexData = [LargeDataRenderer generateVertexData];
❻ 创建一个顶点缓冲区,可以由GPU来读取
_vertexBuffer = [_device newBufferWithLength:vertexData.length
                                         options:MTLResourceStorageModeShared];
❼ 复制顶点数据到顶点缓冲区,通过缓存区的内容属性访问指针
// contents:目的地 bytes:源内容 length:长度
memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);
❽ 计算顶点个数 = 顶点数据长度 / 单个顶点大小
_numVertices = vertexData.length / sizeof(Vertex);
❾ 创建命令队列
_commandQueue = [_device newCommandQueue];

3、生成顶点数据

+ (NSData *)generateVertexData
{
    ...
    return vertexData;
}
❶ 正方形 = 上三角形 + 下三角形
const Vertex quadVertices[] =
{
    // Pixel像素位置, RGBA颜色
    { { -20,   20 },    { 1, 0, 0, 1 } },
    { {  20,   20 },    { 1, 0, 0, 1 } },
    { { -20,  -20 },    { 1, 0, 0, 1 } },
    
    { {  20,  -20 },    { 0, 0, 1, 1 } },
    { { -20,  -20 },    { 0, 0, 1, 1 } },
    { {  20,   20 },    { 0, 0, 1, 1 } },
};
❷ 使用到的常量
// 行/列 数量
const NSUInteger NUM_COLUMNS = 25;
const NSUInteger NUM_ROWS = 15;
// 顶点个数
const NSUInteger NUM_VERTICES_PER_QUAD = sizeof(quadVertices) / sizeof(Vertex);
// 四边形间距
const float QUAD_SPACING = 50.0;
// 数据大小 = 单个四边形大小 * 行 * 列
NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;
❸ 开辟空间
NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
// 当前四边形
Vertex * currentQuad = vertexData.mutableBytes;
❹ 获取顶点坐标(循环计算)
for(NSUInteger row = 0; row < NUM_ROWS; row++)// 行
{
    for(NSUInteger column = 0; column < NUM_COLUMNS; column++)// 列
    {
        ....
    }
}
❺ 计算左上角X和Y位置

坐标系基于2D笛卡尔坐标系中心点为(0,0),所以会出现负数位置

vector_float2 upperLeftPosition;// 左上角的位置
upperLeftPosition.x = ((-((float)NUM_COLUMNS) / 2.0) + column) * QUAD_SPACING + QUAD_SPACING/2.0;
upperLeftPosition.y = ((-((float)NUM_ROWS) / 2.0) + row) * QUAD_SPACING + QUAD_SPACING/2.0;
❻ 将quadVertices数据复制到currentQuad
memcpy(currentQuad, &quadVertices, sizeof(quadVertices));
❼ 遍历currentQuad中的数据,修改vertexInQuad中的position
// 获取到小正方形(2个三角形)的6个顶点位置
for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++)
{
    currentQuad[vertexInQuad].position += upperLeftPosition;
}
❽ 更新索引
currentQuad += 6;

4、每当视图需要渲染帧时调用

- (void)drawInMTKView:(nonnull MTKView *)view
{
}
将_vertexBuffer设置到顶点缓存区中
[renderEncoder setVertexBuffer:_vertexBuffer
                                offset:0
                               atIndex:VertexInputIndexVertices];
开始绘图
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                          vertexStart:0
                          vertexCount:_numVertices];

五、加载TGA文件

// 存储在Metal buffer的顶点数据
id _vertices;

// Metal 纹理对象
id _texture;

MTKView *_mtkView;

1、Renderer 渲染器工具类里的初始化方法

- (instancetype)initWithMetalKitView:(MTKView *)mtkView
{
    self = [super init];
    if(self)
    {
        // 1.获取GPU设备
        _device = mtkView.device;
        _mtkView = mtkView;
        
        // 2.设置顶点
        [self setupVertex];
        
        // 3.设置渲染管道
        [self setupPipeLine];
        
        // 4.加载纹理TGA文件
        [self setupTexture];
        
    }
    return self;
}

2、设置顶点数据

- (void)setupVertex
{
}
❶ 根据顶点/纹理坐标建立一个MTLBuffer
static const Vertex quadVertices[] =
{
    // 像素坐标 纹理坐标
    { {  250,  -250 },  { 1.f, 0.f } },
    { { -250,  -250 },  { 0.f, 0.f } },
    { { -250,   250 },  { 0.f, 1.f } },
    
    { {  250,  -250 },  { 1.f, 0.f } },
    { { -250,   250 },  { 0.f, 1.f } },
    { {  250,   250 },  { 1.f, 1.f } },
};
❷ 创建顶点缓冲区,并用quadVertices数组初始化它
_vertices = [_device newBufferWithBytes:quadVertices
                                     length:sizeof(quadVertices)
                                    options:MTLResourceStorageModeShared];
❸ 通过将字节长度除以每个顶点的大小来计算顶点的数目
_numVertices = sizeof(quadVertices) / sizeof(Vertex);

3、设置渲染管道

- (void)setupPipeLine
{
}
❶ 创建渲染通道
// 从项目中加载.metal文件,创建一个library
iddefalutLibrary = [_device newDefaultLibrary];
// 从库中加载顶点函数
idvertexFunction = [defalutLibrary newFunctionWithName:@"vertexShader"];
// 从库中加载片元函数
id fragmentFunction = [defalutLibrary newFunctionWithName:@"fragmentShader"];
❷ 配置用于创建管道状态的管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 管道名称
pipelineStateDescriptor.label = @"Texturing Pipeline";
// 可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
// 可编程函数,用于处理渲染过程总的各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
// 设置管道中存储颜色数据的组件格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = _mtkView.colorPixelFormat;
❸ 同步创建并返回渲染管线对象
NSError *error = NULL;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
// 判断是否创建成功
if (!_pipelineState)
{
    NSLog(@"创建渲染管线对象失败,错误信息为:%@", error);
}
❹ 使用_device创建commandQueue
_commandQueue = [_device newCommandQueue];

4、加载纹理TGA文件

- (void)setupTexture
{
}
❶ 获取tag的路径
NSURL *imageFileLocation = [[NSBundle mainBundle]  URLForResource:@"Image"withExtension:@"tga"];
❷ 将tag文件转化为Image对象

Image工具类作为图像数据容器,我们无需学习。

Image *image = [[Image alloc] initWithTGAFileAtLocation:imageFileLocation];
if(!image)// 判断图片是否转换成功
{
    NSLog(@"创建图片失败,tga文件路径为: %@",imageFileLocation.absoluteString);
}
❸ 创建纹理描述对象
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc]init];
// 表示每个像素有蓝色、绿色、红色和alpha通道。其中每个通道都是8位无符号归一化的值(即0映射成0,255映射成1)
textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
// 设置纹理的像素尺寸
textureDescriptor.width = image.width;
textureDescriptor.height = image.height;
❹ 使用描述符从设备中创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
❺ 创建MTLRegion结构体

MLRegion结构用于标识纹理的特定区域。本demo使用的图像数据填充整个纹理,因此覆盖整个纹理的像素区域等于纹理的尺寸。

MTLRegion region =
{
    {0,0,0},// 开始位置 x,y,z
    {image.width,image.height,1}// 尺寸 width,height,depth
};
❻ 复制图片数据到纹理
[_texture replaceRegion:region mipmapLevel:0 withBytes:image.data.bytes bytesPerRow:bytesPerRow];

5、创建Shader.metal文件

❶ 修改RasterizerData结构体
// 2维纹理坐标
float2 textureCoordinate;
❷ 修改顶点着色函数
// 计算和写入 XYZW 值到我们的剪辑空间的位置
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
out.clipSpacePosition.z = 0.0f;
out.clipSpacePosition.w = 1.0f;

// 把输入的纹理坐标直接赋值给输出纹理坐标
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
❸ 修改片元函数
  • texture2d:类型为纹理
  • 精度
  • colorTexture:名称
  • texture(TextureIndexBaseColor):第一个纹理
fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d colorTexture [[texture(TextureIndexBaseColor)]])
{
    // 设置纹理的属性。放大和缩小的过滤方式为线性(非邻近过滤)
    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    // 获取对应坐标下的纹理颜色值
    const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
    
    // 输出颜色值
    return float4(colorSampler);
}
❹ 在ShaderTypes.h文件里添加纹理索引
typedef enum TextureIndex
{
    TextureIndexBaseColor = 0
} TextureIndex;
typedef struct
{
    vector_float2 textureCoordinate;// 2D纹理
} Vertex;
❺ 在drawInMTKView方法里设置纹理对象
[renderEncoder setFragmentTexture:_texture atIndex:TextureIndexBaseColor];

六、加载PNG图片

1、设置纹理

- (void)setupTexture
{
}
❶ 获取图片
UIImage *image = [UIImage imageNamed:@"luckcoffee.jpg"];
❷ 创建纹理描述符
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
textureDescriptor.width = image.size.width;
textureDescriptor.height = image.size.height;
❸ 使用描述符从设备中创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
❹ 创建MTLRegion结构体
MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}};
❺ 获取图片数据
Byte *imageBytes = [self loadImage:image];
❻ UIImage的数据需要转成二进制才能上传
if (imageBytes)
{
    [_texture replaceRegion:region
                    mipmapLevel:0
                      withBytes:imageBytes
                    bytesPerRow:4 * image.size.width];
    free(imageBytes);
    imageBytes = NULL;
}

2、从UIImage中读取Byte数据

- (Byte *)loadImage:(UIImage *)image
{
    return spriteData;
}
❶ 获取图片的CGImageRef
CGImageRef spriteImage = image.CGImage;
❷ 读取图片的大小
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage); 
❸ 计算图片大小 rgba共4个byte
Byte * spriteData = (Byte *) calloc(width * height * 4, sizeof(Byte));
❹ 创建画布
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
❺ 在CGContextRef上绘图
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
❻ 图片翻转过来
CGRect rect = CGRectMake(0, 0, width, height);
CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(spriteContext, 0, rect.size.height);
CGContextScaleCTM(spriteContext, 1.0, -1.0);
CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
CGContextDrawImage(spriteContext, rect, spriteImage);
❼ 释放spriteContext
CGContextRelease(spriteContext);

七、金字塔

绕Z轴旋转
添加纹理后的图形

1、创建ShaderTypes.h文件

顶点数据结构
typedef struct
{
    vector_float4 position;          //顶点 xyzw
    vector_float3 color;             //颜色 rgb
    vector_float2 textureCoordinate; //纹理坐标 xy
} Vertex;
矩阵结构体
typedef struct
{
    matrix_float4x4 projectionMatrix; //投影矩阵
    matrix_float4x4 modelViewMatrix;  //模型视图矩阵
} Matrix;
输入索引
typedef enum VertexInputIndex
{
    VertexInputIndexVertices     = 0, //顶点坐标索引
    VertexInputIndexMatrix       = 1, //矩阵索引
} VertexInputIndex;
片元着色器索引
typedef enum FragmentInputIndex
{
    FragmentInputIndexTexture     = 0,//片元输入纹理索引
} FragmentInputIndex;

2、创建Shaders.metal文件

结构体
typedef struct
{
    // 处理空间的顶点信息。position是默认属性修饰符,用来指定顶点
    float4 clipSpacePosition [[position]];
    // 颜色
    float3 pixelColor;
    // 纹理坐标
    float2 textureCoordinate;
} RasterizerData;
顶点函数
vertex RasterizerData
vertexShader(uint vertexID [[ vertex_id ]],
             constant Vertex *vertexArray [[ buffer(VertexInputIndexVertices) ]],
             constant Matrix *matrix [[ buffer(VertexInputIndexMatrix) ]])
{
    // 定义输出
    RasterizerData out;
    // 计算裁剪空间坐标 = 投影矩阵 * 模型视图 矩阵 * 顶点
    out.clipSpacePosition = matrix->projectionMatrix * matrix->modelViewMatrix * vertexArray[vertexID].position;
    // 纹理坐标
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    // 像素颜色值
    out.pixelColor = vertexArray[vertexID].color;
    
    return out;
}
片元函数
fragment float4
samplingShader(RasterizerData input [[stage_in]],
               texture2d textureColor [[ texture(FragmentInputIndexTexture) ]])
{
    // 颜色值 从三维变量RGB -> 四维变量RGBA
    half4 colorTex = half4(input.pixelColor.x, input.pixelColor.y, input.pixelColor.z, 1);
    
    // 返回颜色
    return float4(colorTex);
}

3、准备工作

使用到的私有属性
@property (nonatomic, strong) MTKView *mtkView;// 渲染视图
@property (nonatomic, assign) vector_uint2 viewportSize;// 视口
@property (nonatomic, strong) id pipelineState;// 渲染管道
@property (nonatomic, strong) id commandQueue;// 命令队列
@property (nonatomic, strong) id texture;// 纹理
@property (nonatomic, strong) id vertices;// 顶点缓存区
@property (nonatomic, strong) id indexs;// 索引缓存区
@property (nonatomic, assign) NSUInteger indexCount;// 索引个数
整体流程
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self createSubviews];
    
    // 1.设置MTKView
    [self setupMtkView];
    // 2.设置管道
    [self setupPipeline];
    // 3.设置顶点数据
    [self setupVertex];
}

4、设置MTKView

- (void)setupMtkView
{
}
❶ 获取MTKView
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
❷ 获取代表默认的GPU单个对象
self.mtkView.device = MTLCreateSystemDefaultDevice();
❸ 将mtkView添加到self.view上
[self.view insertSubview:self.mtkView atIndex:0];
❹ 设置代理 表示由viewController实现代理方法
self.mtkView.delegate = self;
❺ 获取视口大小
self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};

5、设置管道

- (void)setupPipeline
{
}
❶ 在项目中加载所有的着色器文件
id defaultLibrary = [self.mtkView.device newDefaultLibrary];
// 从库中加载顶点函数
id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// 从库中加载片元函数
id fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"];
❷ 创建渲染管道描述符
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
// 可编程函数,用于处理渲染过程总的各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
// 设置管道中存储颜色数据的组件格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
❸ 设置渲染管道
self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:NULL];
❹ 创建命令队列
self.commandQueue = [self.mtkView.device newCommandQueue];

6、设置顶点数据

❶ 金字塔的顶点坐标、顶点颜色、纹理坐标数据
static const Vertex quadVertices[] =
{  // 顶点坐标                          顶点颜色                    纹理坐标
    {{-0.5f, 0.5f, 0.0f, 1.0f},      {0.0f, 0.0f, 0.5f},       {0.0f, 1.0f}},//左上
    {{0.5f, 0.5f, 0.0f, 1.0f},       {0.0f, 0.5f, 0.0f},       {1.0f, 1.0f}},//右上
    {{-0.5f, -0.5f, 0.0f, 1.0f},     {0.5f, 0.0f, 1.0f},       {0.0f, 0.0f}},//左下
    {{0.5f, -0.5f, 0.0f, 1.0f},      {0.0f, 0.0f, 0.5f},       {1.0f, 0.0f}},//右下
    {{0.0f, 0.0f, 1.0f, 1.0f},       {1.0f, 1.0f, 1.0f},       {0.5f, 0.5f}},//顶点
};
❷ 创建顶点数组缓存区
self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
                                                     length:sizeof(quadVertices)
                                            options:MTLResourceStorageModeShared];
❸ 索引数组
static int indices[] =
{
    0, 3, 2,
    0, 1, 3,
    0, 2, 4,
    0, 4, 1,
    2, 3, 4,
    1, 4, 3,
};
❹ 创建索引数组缓存区
self.indexs = [self.mtkView.device newBufferWithBytes:indices
                                                   length:sizeof(indices)
                                            options:MTLResourceStorageModeShared];
❺ 计算索引个数
self.indexCount = sizeof(indices) / sizeof(int);

7、设置投影矩阵/模型视图矩阵

#import 

- (void)setupMatrixWithEncoder:(id)renderEncoder
{
    CGSize size = self.view.bounds.size;
    float aspect = fabs(size.width / size.height);// 纵横比
    static float x = 0.0, y = 0.0, z = M_PI;// x=0,y=0,z=180
    ......
}
❶ 创建投影矩阵

因为是3D视图所以需要使用到投影矩阵。

GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(90.0), aspect, 0.1f, 10.f);
❷ 创建模型视图矩阵

因为视图要进行旋转操作,所以需要使用到模型视图矩阵。

GLKMatrix4 modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0.0f, 0.0f, -2.0f);
❸ 判断X/Y/Z的开关状态,修改旋转的角度
if (self.rotationX.on)
{
    x += self.slider.value;
}
if (self.rotationY.on)
{
    y += self.slider.value;
}
if (self.rotationZ.on)
{
    z += self.slider.value;
}
❹ 将模型视图矩阵围绕(x,y,z)轴渲染相应的角度
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, x, 1, 0, 0);
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, y, 0, 1, 0);
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, z, 0, 0, 1);
❺ 将GLKit Matrix 转化为 MetalKit Matrix
matrix_float4x4 pm = [self getMetalMatrixFromGLKMatrix:projectionMatrix];
matrix_float4x4 mm = [self getMetalMatrixFromGLKMatrix:modelViewMatrix];

没有发现metalKit或者simd相关的接口可以快捷创建矩阵的,于是只能从GLKit里面借力。

- (matrix_float4x4)getMetalMatrixFromGLKMatrix:(GLKMatrix4)matrix
{
    matrix_float4x4 ret = (matrix_float4x4)
    {
        simd_make_float4(matrix.m00, matrix.m01, matrix.m02, matrix.m03),
        simd_make_float4(matrix.m10, matrix.m11, matrix.m12, matrix.m13),
        simd_make_float4(matrix.m20, matrix.m21, matrix.m22, matrix.m23),
        simd_make_float4(matrix.m30, matrix.m31, matrix.m32, matrix.m33),
    };
    return ret;
}
❻ 将投影矩阵和模型视图矩阵加载到矩阵结构体
Matrix matrix = {pm,mm};
❼ 将矩阵结构体里的数据通过渲染编码器传递到顶点/片元函数中使用
[renderEncoder setVertexBytes:&matrix
                           length:sizeof(matrix)
                          atIndex:VertexInputIndexMatrix];

8、视图渲染

- (void)drawInMTKView:(MTKView *)view
{
    .......
    if(renderPassDescriptor != nil)
    {
        ......
        // 结束编码
        [renderEncoder endEncoding];
        
        // 展示视图
        [commandBuffer presentDrawable:view.currentDrawable];
    }
    // 完成渲染并将命令缓冲区推送到GPU
    [commandBuffer commit];
}
❶ 为当前渲染的每个渲染传递创建一个新的命令缓存区
id commandBuffer = [self.commandQueue commandBuffer];
❷ 获取视图的渲染描述符
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

// 判断是否获取成功
if(renderPassDescriptor != nil)
{
    ...
}
❸ 通过渲染描述符修改背景颜色
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.6, 0.2, 0.5, 1.0f);
❹ 设置颜色附着点加载方式为写入指定附件中的每个像素
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
❺ 根据渲染描述信息创建渲染编码器
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
❻ 设置视口
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }];
❼ 设置渲染管道
[renderEncoder setRenderPipelineState:self.pipelineState];
❽ 设置投影矩阵/渲染矩阵
[self setupMatrixWithEncoder:renderEncoder];
❾ 将顶点数据传递到Metal文件的顶点函数
[renderEncoder setVertexBuffer:self.vertices
                                offset:0
                               atIndex:VertexInputIndexVertices];
❿ 设置正背面剔除
// 设置逆时钟三角形为正面,其为默认值所以可省略此步骤
[renderEncoder setFrontFacingWinding:MTLWindingCounterClockwise];
// 设置为背面剔除
[renderEncoder setCullMode:MTLCullModeBack];
开始绘制(索引绘图)
[renderEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
                                  indexCount:self.indexCount
                                   indexType:MTLIndexTypeUInt32
                                 indexBuffer:self.indexs
                           indexBufferOffset:0];

9、为金字塔添加纹理

- (void)setupTexture
{
}
❶ 获取图片
UIImage *image = [UIImage imageNamed:@"luckcoffee.jpg"];
❷ 创建纹理描述符
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
// 表示每个像素有蓝色、绿色、红色和alpha通道
textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
// 设置纹理的像素尺寸
textureDescriptor.width = image.size.width;
textureDescriptor.height = image.size.height;
❸ 使用描述符从设备中创建纹理
_texture = [self.mtkView.device newTextureWithDescriptor:textureDescriptor];
❹ 创建MTLRegion结构体用来设置纹理填充的范围
MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}};
❺ 获取图片数据
Byte *imageBytes = [self loadImage:image];
❻ UIImage的数据需要转成二进制才能上传
if (imageBytes)
{
    [_texture replaceRegion:region
                mipmapLevel:0
                  withBytes:imageBytes
                bytesPerRow:4 * image.size.width];
    free(imageBytes);
    imageBytes = NULL;
}
❼ 修改Shaders.metal文件里的片元函数

Sampler是采样器,决定如何对一个纹理进行采样操作。在Metal程序里初始化的采样器必须使用constexpr修饰符声明。 采样器对指针和引用是不支持的,会导致编译错误。

fragment float4
samplingShader(RasterizerData input [[stage_in]],
               texture2d textureColor [[ texture(FragmentInputIndexTexture) ]])
{
    // 颜色值 从三维变量RGB -> 四维变量RGBA
    // half4 colorTex = half4(input.pixelColor.x, input.pixelColor.y, input.pixelColor.z, 1);
 
    constexpr sampler textureSampler (mag_filter::linear ,min_filter::linear);
    half4 colorTex = textureColor.sample(textureSampler, input.textureCoordinate);
    
    // 返回颜色
    return float4(colorTex);
}
❽ 在drawInMTKView方法中新增给片元着色器传递纹理的代码
[renderEncoder setFragmentTexture:self.texture atIndex:FragmentInputIndexTexture];

Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

你可能感兴趣的:(iOS图像:Metal 入门)