1.Metal是苹果的图形图像渲染框架,也可以实现普通的GPU高并发计算,流程与OpenGL ES非常类似,且其里面也有封装好的滤镜,可以直接使用无需自己写.metal渲染的代码实现,视情况而定
2.主要思路
1.OC代码段编写Metal渲染所需的相关流程代码
2.OC和metal文件之间的桥接文件提供数据类型和端口索引
3.编写metal的 vertex顶点函数 fragment片元函数
4.在MTKView的代理方法里面渲染每一帧的图片纹理数据
3.具体步骤
1. OC代码段编写Metal渲染所需的相关流程代码
1.创建MTKView,设置代理,这是Metal渲染的目标view,这里的代码都是面向协议的编码,MTKView的 id
2.创建纹理
3.创建顶点
4.创建渲染管道,加载顶点函数和片元函数
2.OC和metal文件之间的桥接文件提供数据类型和端口索引
1.具体的数据结构和索引视情况而定,顶点坐标,纹理坐标,图片纹理,顶点索引,纹理索引,其他需要的参数,比如时间,数组,矩阵,需要的都可以
2.视频的纹理是Y纹理 和 UV纹理两个部分,需要增加YUV到RGB的颜色转换矩阵
3.编写metal的 vertex顶点函数 fragment片元函数
1.编写metal函数,语法看起来有些复杂,其实还好,要注意与桥接对象的数据类型的对应关系,主要有数据结构和端口索引值,类型关键字比如 [[position]] [[buffer]] [[stage_in]] 等等,函数修饰符,参数地址空间修饰符等,其余编码的思路与OpenGL ES基本一致,只是语法有些差异
4.在MTKView的代理方法里面渲染每一帧的图片纹理数据
- MTKView的两个代理方法
// 设置渲染范围
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
self.viewportSize = (vector_int2){size.width, size.height};
}
// MTKViewDelegate
// 每一帧渲染命令的具体实现
- (void)drawInMTKView:(nonnull MTKView *)view
4.代码实现
1.OC类
1.头文件和变量 属性
#import
#import
#import
// 桥接类
#import "YYImageShaderTypes.h"
@interface MetalImageFilterView ()
{
CGRect m_frame;
BOOL isChangeFillMode;
CGSize imageSize;
}
// 渲染范围
@property (nonatomic, assign) vector_int2 viewportSize;
// MTKView Metal渲染的view
@property (nonatomic, strong) MTKView * mtkView;
// 用来渲染的设备(GPU)
@property (nonatomic, strong) id device;
// 渲染管道,管理顶点函数和片元函数
@property (nonatomic, strong) id renderPipelineState;
// 渲染指令队列
@property (nonatomic, strong) id commondQueue;
// 顶点缓存对象
@property (nonatomic, strong) id vertexBuffer;
// 纹理对象
@property (nonatomic, strong) id texture;
// 顶点数量
@property (nonatomic, assign) NSUInteger vertexCount;
//
2.初始化调用
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
m_frame = frame;
// 1.创建 MTKView
[self createMTKView];
// 2.设置顶点 1.0和1.0表示宽高保持默认的拉伸状态,不去动态调整
[self setupVertexsWithWidthScaling:1.0f heightScaling:1.0f];
// 3.设置纹理
[self setupTexture];
// 4.创建渲染管道
[self createPipeLineState];
}
return self;
}
3.创建MTKView
// 创建 MTKView
- (void)createMTKView {
MTKView * mtkView = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, m_frame.size.width, m_frame.size.height)];
mtkView.delegate = self;
// 创建Device
mtkView.device = MTLCreateSystemDefaultDevice();
// 设置device
self.device = mtkView.device;
self.viewportSize = (vector_int2){mtkView.drawableSize.width, mtkView.drawableSize.height};
self.mtkView = mtkView;
[self addSubview:mtkView];
}
4.创建顶点数据结构数组和缓存区
// 2.设置顶点
- (void)setupVertexs {
// 1.顶点纹理数组
// 顶点x,y,z,w 纹理x,y
YYVertex vertexArray[] = {
{{-1.0, -1.0, 0.0, 1.0}, {0.0, 0.0}}, // 左下
{{1.0, -1.0, 0.0, 1.0}, {1.0, 0.0}}, // 右下
{{-1.0, 1.0, 0.0, 1.0}, {0.0, 1.0}}, //左上
{{1.0, 1.0, 0.0, 1.0}, {1.0, 1.0}}, // 右上
};
// 2.生成顶点缓存
// MTLResourceStorageModeShared 属性可共享的,表示可以被顶点或者片元函数或者其他函数使用
self.vertexBuffer = [self.device newBufferWithBytes:vertexArray length:sizeof(vertexArray) options:MTLResourceStorageModeShared];
// 3.获取顶点数量
self.vertexCount = sizeof(vertexArray) / sizeof(YYVertex);
}
5.创建图片纹理
// 3.设置纹理
- (void)setupTexture {
UIImage * image = [UIImage imageNamed:@"linXinRuMetal1.jpg"];
// 1.创建纹理描述符
MTLTextureDescriptor * textureDescriptor = [[MTLTextureDescriptor alloc] init];
// 设置纹理描述符的宽,高,像素存储格式
textureDescriptor.width = image.size.width;
textureDescriptor.height = image.size.height;
imageSize = image.size;
//MTLPixelFormatRGBA8Unorm 表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1)
textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
// 2.创建纹理对象
id texture = [self.device newTextureWithDescriptor:textureDescriptor];
self.texture = texture;
// id -> id
// 3.将图片数据读取到纹理对象内
/*
typedef struct
{
MTLOrigin origin; //开始位置x,y,z
MTLSize size; //尺寸width,height,depth
} MTLRegion;
*/
//MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
//4. 创建MTLRegion 结构体 [纹理上传的范围]
MTLRegion region = {{0, 0, 0}, {image.size.width, image.size.height, 1}};
// 图片的二进制数据 UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
Byte * imageBytes = [self loadImage:image];
// 将图片数据读取到纹理对象内
// region 纹理区域
// 0 mip贴图层次
// imageBytes 图片二进制数据
// image.size.width * 4 每一行字节数
if (imageBytes) {
[self.texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:image.size.width * 4];
}
}
// 图片加载为二进制数据
- (Byte *)loadImage:(UIImage *)image {
CGImageRef spriteImage = image.CGImage;
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
Byte * spriteData = (Byte *)calloc(width * height * 4, sizeof(Byte));
CGContextRef context = CGBitmapContextCreate(spriteData, width, height, 8, width *4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
// 纹理翻转
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), spriteImage);
CFRelease(context);
return spriteData;
}
6.创建渲染管道
// 4.创建渲染管道
// 根据.metal里的函数名,使用MTLLibrary创建顶点函数和片元函数
// 从这里可以看出来,MTLLibrary里面包含所有.metal的文件,所以,不同的.metal里面的函数名不能相同
// id 创建library、MTLRenderPipelineState、MTLCommandQueue
- (void)createPipeLineState {
// 1.从项目中加载.metal文件,创建一个library
id library = [self.device newDefaultLibrary];
// id -> id
// 2.从库中MTLLibrary,加载顶点函数
id vertexFunction = [library newFunctionWithName:@"vertexImageShader"];
// 3.从库中MTLLibrary,加载顶点函数
id fragmentFunction = [library newFunctionWithName:@"fragmentImageShader"];
// 4.创建管道渲染管道描述符
MTLRenderPipelineDescriptor * renderPipeDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 5.设置管道顶点函数和片元函数
renderPipeDescriptor.vertexFunction = vertexFunction;
renderPipeDescriptor.fragmentFunction = fragmentFunction;
// 6.设置管道描述的关联颜色存储方式
renderPipeDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
NSError * error = nil;
// 7.根据渲染管道描述符 创建渲染管道
id renderPipelineState = [self.device newRenderPipelineStateWithDescriptor:renderPipeDescriptor error:&error];
self.renderPipelineState = renderPipelineState;
// id -> id
// 8. 创建渲染指令队列
id commondQueue = [self.device newCommandQueue];
self.commondQueue = commondQueue;
// id -> id
}
7.MTKView代理方法实现,每一帧渲染流程实现
// MTKViewDelegate
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
self.viewportSize = (vector_int2){size.width, size.height};
}
// MTKViewDelegate
- (void)drawInMTKView:(nonnull MTKView *)view {
// 1.为当前渲染的每个渲染传递创建一个新的命令缓冲区
id commandBuffer = [self.commondQueue commandBuffer];
//指定缓存区名称
commandBuffer.label = @"EachCommand";
// 2.获取渲染命令编码器 MTLRenderCommandEncoder的描述符
// currentRenderPassDescriptor描述符包含currentDrawable's的纹理、视图的深度、模板和sample缓冲区和清晰的值。
// MTLRenderPassDescriptor描述一系列attachments的值,类似GL的FrameBuffer;同时也用来创建MTLRenderCommandEncoder
MTLRenderPassDescriptor * renderPassDescriptor = view.currentRenderPassDescriptor;
if (renderPassDescriptor) {
// 设置默认颜色 背景色
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0f);
// 3.根据描述创建x 渲染命令编码器
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
// typedef struct {
// double originX, originY, width, height, znear, zfar;
// } MTLViewport;
// 4.设置绘制区域
[renderEncoder setViewport:(MTLViewport){0, 0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0}];
// 5.设置渲染管道
[renderEncoder setRenderPipelineState:self.renderPipelineState];
// 6.传递顶点缓存
[renderEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:YYImageVertexInputIndexVertexs];
// 7.传递纹理缓存
[renderEncoder setFragmentTexture:self.texture atIndex:YYImageTextureIndexBaseTexture];
// 8.绘制
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:self.vertexCount];
// 9.命令结束
[renderEncoder endEncoding];
// 10.显示
[commandBuffer presentDrawable:view.currentDrawable];
}
// 11. 提交
[commandBuffer commit];
}
8.图片显示模式调整
主要有3种模式,
1.默认大小设置多大就多大
2.图片自适应屏幕不产生拉伸变形
3.全屏且看起来宽高比例正常,其实是略微拉伸了宽度或者高度,只是改变不大,因为如果图片或视频大小不是按照屏幕比例来的就会有形变
- (void)setFillMode:(kMetalImageFilterViewFillModeType)fillMode {
isChangeFillMode = YES;
_fillMode = fillMode;
[self resetVertexWithWidth:imageSize.width height:imageSize.height];
isChangeFillMode = NO;
}
- (void)resetVertexWithWidth:(CGFloat)width height:(CGFloat)height {
dispatch_async(dispatch_get_main_queue(), ^{
CGSize inputImageSize = CGSizeMake(width, height);
CGFloat heightScaling = 1.0, widthScaling = 1.0;
CGSize currentViewSize = self.bounds.size;
CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(inputImageSize, self.bounds);
switch(self.fillMode)
{
case kMetalImageFilterViewFillModeStretch:
{
widthScaling = 1.0;
heightScaling = 1.0;
};
break;
case kMetalImageFilterViewFillModePreserveAspectRatio:
{
widthScaling = insetRect.size.width / currentViewSize.width;
heightScaling = insetRect.size.height / currentViewSize.height;
};
break;
case kMetalImageFilterViewFillModePreserveAspectRatioAndFill:
{
widthScaling = currentViewSize.height / insetRect.size.height;
heightScaling = currentViewSize.width / insetRect.size.width;
};
break;
}
// NSLog(@"widthScaling == %lf", widthScaling);
// NSLog(@"heightScaling == %lf", heightScaling);
[self setupVertexsWithWidthScaling:widthScaling heightScaling:heightScaling];
});
}
2.桥接类
/*
介绍:
头文件包含了 Metal shaders 与C/OBJC 源之间共享的类型和枚举常数
*/
#ifndef YYImageShaderTypes_h
#define YYImageShaderTypes_h
// 这个simd.h文件里有一些桥接的数据类型
#include
// 存储数据的自定义结构,用于桥接OC和Metal代码
// YYVertex结构体类型
typedef struct {
// 顶点坐标 4维向量
vector_float4 position;
// 纹理坐标
vector_float2 textureCoordinate;
} YYVertex;
// 自定义枚举,用于桥接OC和Metal代码
// 顶点的桥接枚举值 YYImageVertexInputIndexVertexs
typedef enum {
YYImageVertexInputIndexVertexs = 0,
} YYImageVertexInputIndex;
// 纹理的桥接枚举值 YYImageTextureIndexBaseTexture
typedef enum {
YYImageTextureIndexBaseTexture = 0,
} YYImageTextureIndex;
#endif /* YYImageShaderTypes_h */
3.Metal 顶点片元代码实现
1.需要注意,函数修饰符,变量修饰符,地址空间修饰符,桥接或者输出的数据结构类型,索引值,内部的逻辑编码与OpenGL ES非常类似,逻辑一致
#include
#import "YYImageShaderTypes.h"
using namespace metal;
// 定义了一个类型为RasterizerData的结构体,里面有一个float4向量和float2向量,其中float4被[[position]]修饰,其表示的变量为顶点
typedef struct {
// float4 4维向量 clipSpacePosition参数名
// position 修饰符的表示顶点 语法是[[position]],这是苹果内置的语法和position关键字不能改变
float4 clipSpacePosition [[position]];
// float2 2维向量 表示纹理
float2 textureCoordinate;
} RasterizerData;
// 顶点函数通过一个自定义的结构体,返回对应的数据,顶点函数的输入参数也可以是自定义结构体
// 顶点函数
// vertex 函数修饰符表示顶点函数,
// RasterizerData返回值类型,
// vertexImageShader函数名
// vertex_id 顶点id修饰符,苹果内置不可变,[[vertex_id]]
// buffer 缓存数据修饰符,苹果内置不可变,YYImageVertexInputIndexVertexs是索引
// [[buffer(YYImageVertexInputIndexVertexs)]]
// constant 变量类型修饰符,表示存储在device区域
vertex RasterizerData vertexImageShader(uint vertexID [[vertex_id]], constant YYVertex * vertexArray [[buffer(YYImageVertexInputIndexVertexs)]]) {
RasterizerData outData;
// 获取YYVertex里面的顶点坐标和纹理坐标
outData.clipSpacePosition = vertexArray[vertexID].position;
outData.textureCoordinate = vertexArray[vertexID].textureCoordinate;
return outData;
}
// 片元函数
// fragment 函数修饰符表示片元函数 float4 返回值类型->颜色RGBA fragmentImageShader 函数名
// RasterizerData 参数类型 input 变量名
// [[stage_in] stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
// texture2d 类型表示纹理 baseTexture 变量名
// [[ texture(index)]] 纹理修饰符
// 可以加索引 [[ texture(0)]]纹理0, [[ texture(1)]]纹理1
// YYImageTextureIndexBaseTexture表示纹理索引
fragment float4 fragmentImageShader(RasterizerData input [[stage_in]], texture2d baseTexture [[ texture (YYImageTextureIndexBaseTexture) ]]) {
// constexpr 修饰符
// sampler 采样器
// textureSampler 采样器变量名
// mag_filter:: linear, min_filter:: linear 设置放大缩小过滤方式
constexpr sampler textureSampler(mag_filter:: linear, min_filter:: linear);
// 得到纹理对应位置的颜色
half4 color = baseTexture.sample(textureSampler, input.textureCoordinate);
// 返回颜色值
return float4(color);
}