音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总
使用Metal实现图片的加载,重点在于纹理的处理和metal文件的书写
1、简单介绍
案例地址: 图片加载
效果:
主要学习内容:
- metal的语法回顾
- 多顶点数据的传递
- 图片转纹理数据的过程
- 纹理数据的传递
2、metal语法回顾
为了回顾metal语法,我这里会把metal中涉及的语法都分析一下。
具体的metal语法,可以查看我之前写的博客十四、Metal - Metal Shader language (着色语言规范)总结
传递数据结构体,顶点着色器输出和片元着色器输入
结构体包含有顶点数据和纹理数据,分别作用于顶点着色器和片元着色器。
typedef struct
{
//顶点数据
//[[position]]是内建的变量修饰符,表示顶点数据
float4 clipSpacePosition [[position]];
//纹理数据
float2 textureCoordinate;
} RasterizerData;
说明:
- float4表示四维向量,向量数据为float类型,float表示为二维向量。
- [[position]]是内建的变量修饰符,表示顶点数据
顶点着色器
代码:
//顶点着色函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant CCVertex *vertexArray [[buffer(CCVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])
{
/*
处理顶点数据:
1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
2) 将顶点颜色值传递给返回值
*/
//定义out
RasterizerData out;
//初始化输出剪辑空间位置
out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
// 索引到我们的数组位置以获得当前顶点
// 我们的位置是在像素维度中指定的.
float2 pixelSpacePosition = vertexArray[vertexID].position.xy;
//将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
//每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
//计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
out.clipSpacePosition.z = 0.0f;
out.clipSpacePosition.w = 1.0f;
//把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
//完成! 将结构体传递到管道中下一个阶段:
return out;
}
说明:
- vertex表示函数类型,这个函数是一个顶点函数(在metal中称呼着色器为函数)
- RasterizerData返回值,就是上面定义的结构体,会将结果返回,之后会传到片元着色器中。
- vertexShader是函数名,可以任意修改,当然要写成有意义的。
- uint vertexID [[vertex_id]]是一个参数,unit表示无符号int型,vertextId是参数名,[[vertex_id]]是内建的变量修饰符,表示顶点ID,用来获取当前的顶点。每个顶点函数都要加上这个参数。
- constant CCVertex *vertexArray [[buffer(CCVertexInputIndexVertices)]]:顶点数组变量。
- constant是地址空间修饰符,表示vertexArray是存储在显存中,且是个常量,只读不可写
- CCVertex是参数类型,是一个结构体
- [[buffer(CCVertexInputIndexVertices)]]是属性标识符,说明这个变量是从外界传入的数据,CCVertexInputIndexVertices就是传入的通道标识,在外部进行传入时要写上对应的通道标识。因此这个变量是constant,所以需要写成buffer。
- constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]]。视口大小变量
- vector_uint2其实就是uint2,也就是无符号的int型,而且是二维的向量。
- [[buffer(CCVertexInputIndexViewportSize)]]说明这个参数也是外界传入的,CCVertexInputIndexViewportSize是传入的通道标识。因为这个变量是constant,所以需要写成buffer。
- float2 pixelSpacePosition = vertexArray[vertexID].position.xy;获取到当前顶点
- vertexArray[vertexID]:通过vertextID获取顶点数组中的当前顶点
- .position.xy得到当前顶点的坐标。
- out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
- 得到当前顶点的纹理信息。
片元着色器
代码:
// 片元函数
//[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.
//一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.
//被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.
fragment float4 fragmentShader(RasterizerData in [[stage_in]],
texture2d colorTexture [[texture(CCTextureIndexBaseColor)]])
{
constexpr sampler textureSampler(mag_filter::linear,
min_filter::linear);
const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
return float4(colorSampler);
//返回输入的片元颜色
//return in.color;
}
说明:
- fragment是函数修饰符,表示这个函数是片元函数
- RasterizerData in [[stage_in]]表示从顶点着色器传入的变量
- RasterizerData是参数类型
- in是参数变量
- [[stage_in]]是内建变量修饰符,表示这个变量是从顶点着色器传递进入的。
- texture2d
colorTexture [[texture(CCTextureIndexBaseColor)]]是纹理参数 - texture2d
表示纹理类型,而且写入颜色时的数据类型是half,没有写access,默认是sample - [[texture(CCTextureIndexBaseColor)]]是属性修饰符,texture表示这个变量是纹理,CCTextureIndexBaseColor是传入的通道ID,直接从CPU传递过来的数据,而不是通过顶点着色器
- texture2d
- constexpr sampler textureSampler(mag_filter::linear,min_filter::linear);是设置采样器类型
- constexpr是设置采样器类型的修饰符,必须写
- sampler表示是个采样器
- textureSampler是采样器名称
- (mag_filter::linear,min_filter::linear)是对采样器进行设置,这里设置了最大过滤是邻近过滤,最小过滤方式也是邻近过滤
- const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);获取采样的色值
- const表示常量,half4是四维向量
- colorTexture.sample是对纹理进行采样,传入的参数有采样器和纹理坐标。
3、大量顶点数据的传递
前文我们通过setVertexBytes的API传递顶点数据,但是如果顶点数据特别多,就无法使用了,上限是4000多个字节。此时就应该使用setVertexBuffer的API传递数据。
过程:
- 使用MTLBuffer来存储顶点数据
- 将内存中的顶点数据拷贝到MTLBuffer
- 设置的MTLBuffer数据到顶点着色器中
API:
拷贝数据到MTLBuffer
//顶点缓存区
id _vertexBuffer;
//5.获取顶点数据
NSData *vertexData = [CCRenderer generateVertexData];
//创建一个vertex buffer,可以由GPU来读取
_vertexBuffer = [_device newBufferWithLength:vertexData.length options:MTLResourceStorageModeShared];
//复制vertex data 到vertex buffer 通过缓存区的"content"内容属性访问指针
/*
memcpy(void *dst, const void *src, size_t n);
dst:目的地
src:源内容
n: 长度
*/
memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);
//计算顶点个数 = 顶点数据长度 / 单个顶点大小
_numVertices = vertexData.length / sizeof(CCVertex);
传递数据到顶点着色器
//5.我们调用-[MTLRenderCommandEncoder setVertexBuffer:offset:atIndex:] 为了从我们的OC代码找发送数据预加载的MTLBuffer 到我们的Metal 顶点着色函数中
/* 这个调用有3个参数
1) buffer - 包含需要传递数据的缓冲对象
2) offset - 它们从缓冲器的开头字节偏移,指示“顶点指针”指向什么。在这种情况下,我们通过0,所以数据一开始就被传递下来.偏移量
3) index - 一个整数索引,对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。注意,此参数与 -[MTLRenderCommandEncoder setVertexBytes:length:atIndex:] “索引”参数相同。
*/
//将_vertexBuffer 设置到顶点缓存区中
[renderEncoder setVertexBuffer:_vertexBuffer
offset:0
atIndex:CCVertexInputIndexVertices];
4、纹理数据的加载
过程:
- 使用MTLTexture纹理对象来存储纹理数据
- 将tag/png/jpg图片数据解析到纹理对象中(重点)
- 将纹理数据传递到顶点着色器(就是一个API)
- 在metal中纹理数据的处理
注:metal对纹理数据的处理在上文已经说明了,这里就不再解读了
4.1 解析纹理
通过图片得到纹理的过程
- 获取图片对象
- 设置纹理描述符,纹理的像素尺寸
- 通过纹理描述符创建纹理
- 通过图片对象获取图片数据
- 设置纹理的图片数据。
代码:
// Metal 纹理对象
id _texture;
//1.获取图片
UIImage *image = [UIImage imageNamed:@"cat.jpg"];
//2.纹理描述符
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
//表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
//设置纹理的像素尺寸
textureDescriptor.width = image.size.width;
textureDescriptor.height = image.size.height;
//3.使用描述符从设备中创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
/*
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}};
//5.获取图片数据
Byte *imageBytes = [self loadImage:image];
//6.UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
if (imageBytes) {
[_texture replaceRegion:region
mipmapLevel:0
withBytes:imageBytes
bytesPerRow:4 * image.size.width];
free(imageBytes);
imageBytes = NULL;
}
通过图片获取图片数据
详细过程之前的博客已经有详细的解读
请查看八、OpenGL ES - GLSL的使用的4.3节加载纹理
- (Byte *)loadImage:(UIImage *)image {
// 1.获取图片的CGImageRef
CGImageRef spriteImage = image.CGImage;
// 2.读取图片的大小
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
//3.计算图片大小.rgba共4个byte
Byte * spriteData = (Byte *) calloc(width * height * 4, sizeof(Byte));
//4.创建画布
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
//5.在CGContextRef上绘图
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
//6.图片翻转过来
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);
//7.释放spriteContext
CGContextRelease(spriteContext);
return spriteData;
}
4.2 将纹理数据传递到顶点着色器
//7.设置纹理对象
[renderEncoder setFragmentTexture:_texture atIndex:CCTextureIndexBaseColor];
可以看到就是调用setFragmentTexture的API,参数传递纹理对象即可。