原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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
着色器语言使用Clang
和LLVM
编译器。
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;
将vierportSizePointer
从verctor_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);
七、金字塔
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