Metal之加载TGA与PNG/JPEG纹理图片

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Metal之加载TGA与PNG/JPEG纹理图片相关的知识,希望对你有一定的参考价值。

TGA纹理

① 效果展示

在这里插入图片描述

② 环境准备
  • 视图控制器类:在 viewDidLoad 函数中创建 MTKView 对象、自定义 render 对象,并设置 view 的代理为 render,其流程请参考:OpenGL之简单渲染一个三角形
  • OC 与 C 的桥接文件:创建 metal 文件与 OC 通用的索引枚举(包括顶点数据索引、纹理索引) 和顶点数据的结构体,如下:
	typedef enum YDWVertexInputIndex {
		// 顶点
	    YDWVertexInputIndexVertices = 0,
	    
		// 视图大小
	    YDWVertexInputIndexViewportSize = 1,
	    
	}YDWVertexInputIndex;
	
	
	// 纹理索引:用于往片元着色器传递纹理数据的索引
	typedef enum YDWTextureIndex {
	    YDWTextureIndexBaseColor = 0
	}YDWTextureIndex;
	
	// 顶点数据结构体:顶点/颜色值
	typedef struct
	{
		// 像素空间的位置
		// 像素中心点(100,100)
		// 顶点坐标
	    vector_float2 position;
	    
		// 2D 纹理
		// 纹理坐标
	    vector_float2 textureCoordinate;
	    
	}YDWVertex;
  • YDWImage 类:该类是将 TGA 转换为位图的工具类。由于平常的开发中很少去加载 TGA 文件,一般都是 PNG/JPG 文件,所以 TGA 文件转换成位图,其本质是将 TGA 文件的数据复制到 BGRA 图像数据指针指向的内存中。
  • 创建 metal 文件
    • 主要是1个结构体+两个函数,结构体中包含顶点坐标 和纹理坐标,作为定点着色器的输出以及片元着色器的输入;
	// 返回值结构体:顶点着色器输出和片元着色器输入(相当于OpenGL ES中的varying修饰的变量,即桥接)
	typedef struct {
		// 顶点坐标
	    float4 clipSpacePosition [[position]];
	    
		// 纹理坐标
	    float2 textureCoordinate;
	    
	}RasterizerData;
    • 顶点着色函数:主要是将顶点坐标归一化处理,并将处理后的顶点坐标和纹理坐标输出,经过 metal 的图元装配和光栅化处理,将顶点数据传入片元着色器,其流程图如下:
      在这里插入图片描述
	vertex RasterizerData
	vertexShader(uint vertexID [[vertex_id]],
	             constant YDWVertex *vertexArray [[buffer(YDWVertexInputIndexVertices)]],
	             constant vector_uint2 *viewportSizePointer [[buffer(YDWVertexInputIndexViewportSize)]])
	{
	        /*
	        处理顶点数据:
	           1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
	           2) 将顶点颜色值传递给返回值
	        */
	       
		// 1、定义out
	    RasterizerData out;
	    
		// 2、初始化输出剪辑空间位置,将w改为2.0,实际运行结果比1.0小一倍
	    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
	    
		// 3、获取当前顶点坐标的xy,因为是2D图形
	    // 索引到我们的数组位置以获得当前顶点
	    // 我们的位置是在像素维度中指定的.
	    float2 pixelSpacePosition = vertexArray[vertexID].position.xy;
	    
	    // 将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型
	    vector_float2 viewportSize = vector_float2(*viewportSizePointer);
	    
		// 4、顶点坐标归一化处理
	    // 每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
	    // 计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
	    // 可以使用一行代码同时分割两个通道。执行除法,然后将结果放入输出位置的x和y通道中
	    out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
	    
	    out.clipSpacePosition.z = 0.0f;
	    out.clipSpacePosition.w = 1.0f;
	    
	    // 把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
		// 纹理坐标桥接
	    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
	    
	    // 完成! 将结构体传递到管道中下一个阶段
	    return out;
	}
    • 片元着色函数:主要是通过采样器获取纹素,相当于 GLSL 中内建函数 texture2D,在 metal 中是通过纹理的 sample 函数获取,主要流程图如下:
      在这里插入图片描述
	fragment float4 fragmentShader(RasterizerData in [[stage_in]],
	                               texture2d<half> colorTexture [[texture(YDWTextureIndexBaseColor)]])
	{
	
		// 当texture2d没有写access时,默认是sampler,
		// 设置采样器:过滤方式
	    constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
	    
		// 读取纹素(即纹理对应像素点的颜色值),相当于GLSL中的内建函数texture2D
		// half4取决于 texture2d<half>中的half ,由于颜色是RGBA,所以是half4
		// GLSL中属性的设置都是通过状态机,
		// 而metal中属性的设置是一个对象的思维,都是纹理采样的属性设置
	    const half4 colorSampler = colorTexture.sample(textureSampler, in.textureCoordinate);
	    
		// 进行灰度/...
		    
		// 将half4 类型转换为 float4类型,如果不想这么麻烦,也可以将texture2d<half>中的half改为float,这样颜色的类型就是float4了
	    return float4(colorSampler);
	}
  • 创建自定义render渲染循环类:苹果建议将渲染循环单独写成一个类,处理 metal 的渲染及委托事件,其中 TGA 文件的加载也是在 render 中处理的,它会将处理好的顶点数据及纹理图片传入着色器,着色器处理完成后会绘制并显示到屏幕上。
③ 渲染循环类
  • initWithMetalKitView 函数:
    • 初始化GPU设备:通过传入的view获取获取GPU的使用权限;
    • setupVertex函数:设置顶点相关操作;
    • setupPipeLine函数:设置渲染管道相关操作;
    • setupTexture函数:加载TGA文件;
      在这里插入图片描述
    • setupVertex 函数:主要是出初始化顶点数据,包括顶点坐标和纹理坐标,当顶点坐标的范围不是-1~1时,是位于物体坐标系,需要在 metal 文件的顶点着色器中作归一化处理,并且将顶点数据存储到 MTLBuffer 对象中,GPU 函数流程如下:
      在这里插入图片描述
	// 设置顶点相关操作
	- (void) setupVertex{
	    
		// 1、根据顶点/纹理坐标建立一个MTLBuffer
	    static const YDWVertex quadVertices[] = {
		// 不是-1~1的都是物体坐标系
	        // 像素坐标,纹理坐标
	        { {  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 } },
	    };
	    
		// 2、创建我们的顶点缓冲区,并用我们的Qualsits数组初始化它
	    _vertexBuffer = [_device newBufferWithBytes:quadVertices length:sizeof(quadVertices) options:MTLResourceStorageModeShared];
	    
		// 3、通过将字节长度除以每个顶点的大小来计算顶点的数目
	    _numVertices = sizeof(quadVertices) / sizeof(YDWVertex); 
	}
    • setupPipeLine 函数:主要是渲染管道相关的初始化工作,对应的流程图如下:
      在这里插入图片描述
    • 渲染管道的初始化分为:
      • 加载metal文件;
      • 配置渲染管道;
      • 创建渲染管线对象;
      • 设置 commandQueue 命令对象。
	// 设置渲染管道相关操作
	- (void)setupPipeLine{
	    
		// 1、创建渲染管道
	    // 从项目中加载.metal文件,创建一个library
	    id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
	    // 从库中加载顶点函数
	    id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
	     // 从库中加载片元函数
	    id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
	    
		// 2、配置用于创建渲染管道状态的管道
	    MTLRenderPipelineDescriptor *renderPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
	    // 管道名称
	    renderPipelineDescriptor.label = @"Texturing Pipeline";
	    // 可编程函数,用于处理渲染过程中的各个顶点
	    renderPipelineDescriptor.vertexFunction = vertexFunction;
	    // 可编程函数,用于处理渲染过程总的各个片段/片元
	    renderPipelineDescriptor.fragmentFunction = fragmentFunction;
	    // 设置管道中存储颜色数据的组件格式
	    renderPipelineDescriptor.colorAttachments[0].pixelFormat = ydwMTKView.colorPixelFormat;
	    
		// 3、创建并返回渲染管线对象 & 判断是否创建成功
	    NSError *error;
	    _pipelineState = [_device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor error:&error];
	    if (!_pipelineState) {
	       NSLog(@"Failed to created pipeline state, error %@", error);
	    }
	    
		// 4、使用device创建commandQueue
	    _commandQueue = [_device newCommandQueue];
	}
    • setupTexture 函数:加载TGA图片,如下:
      在这里插入图片描述
    • TGA 图片的加载分为以下步骤:
      • 获取 TGA 文件:主要是 TGA 文件通过转换为 YDWImage 对象,即纹理图片转换为位图:
		// 1、获取TGA文件路径 --- TGA文件解压
	    NSURL *imageFileLocation = [[NSBundle mainBundle] URLForResource:@"circle" withExtension:@"tga"];
	    // 将tag文件->YDWImage对象
	    YDWImage *image = [[YDWImage alloc] initWithTGAFileAtLocation:imageFileLocation];
	    // 判断图片是否转换成功
	    if (!image) {
	        NSLog(@"Failed to create the image from:%@",imageFileLocation.absoluteString);
	        return;
	    }
      • 创建纹理描述对象 & 纹理对象:纹理对象通过创建的纹理描述对象以及解压的位图生成,其中需要设置纹理描述对象的相关属性:
		// 2、创建纹理描述对象 & 设置属性 --- YDWImage --> 纹理(即位图变成纹理对象)
	    MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
	    // 表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
	    // 位图信息
	    textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
	    // 设置纹理的像素尺寸,即纹理的分辨率
	    textureDescriptor.width = image.width;
	    textureDescriptor.height = image.height;
	    
		// 3、创建纹理对象:使用描述符从设备中创建纹理
	    _texture = [_device newTextureWithDescriptor:textureDescriptor];
      • 将纹理图片复制到 texture 对象中:在复制纹理图片之前,首先需要计算图像每行的字节数,以及设置纹理对应的像素区域region,region的定义如下,其中包含两个结构体对象origin和size,其中origin表示开始的顶点坐标(x,y,z),size表示尺寸(宽度、高度、深度):
	typedef struct {
	     MTLOrigin origin; // 开始位置x,y,z
	     MTLSize   size;   // 尺寸width,height,depth
	} MTLRegion;
      • 然后通过replaceRegion: mipmapLevel: withBytes: bytesPerRow:函数将图片复制到纹理对象中:
		// 计算图像每行的字节数
	    NSUInteger bytesPerRow = 4 * image.width;
	    
		// 4、创建MTLRegion结构体
	    /*
	     typedef struct
	     {
	     MTLOrigin origin; //开始位置x,y,z
	     MTLSize   size; //尺寸width,height,depth
	     } MTLRegion;
	     */
	    // MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
	    MTLRegion region = {
	        {0,0,0},
	        {image.width, image.height, 1},
	    };
	    
		// 5、复制图片数据到texture
	    /*
	     将图片复制到纹理0中(即用纹理替换region表示的区域)
	     - (void)replaceRegion:(MTLRegion)region mipmapLevel:(NSUInteger)level withBytes:(const void *)pixelBytes bytesPerRow:(NSUInteger)bytesPerRow;
	     参数1-region:像素区域在纹理中的位置
	     参数2-level:从零开始的值,指定哪个mipmap级别是目标。如果纹理没有mipmap,请使用0。
	     参数3-pixelBytes:指向要复制图片的字节数
	     参数4-bytesPerRow:对于普通或压缩像素格式,源数据行之间的跨度(以字节为单位)。对于压缩像素格式,跨度是从一排块的开头到下一行的开始的字节数。
	     */
	   [_texture replaceRegion:region mipmapLevel:0 withBytes:image.data.bytes bytesPerRow:bytesPerRow];****
④ MTKViewDelegate 代理方法
  • delegate的代理方法有两种,这里主要是说明绘制drawInMTKView代理方法,其流程图如下:

在这里插入图片描述

  • 向着色器中传递的数据有三种:
    • 顶点数据:包含顶点坐标和纹理坐标,由于顶点数据是存储在缓存区中的,所以需要通过setVertexBuffer函数传递到顶点着色函数中;
    • 视图大小数据:需要通过setVertexBytes函数传递到顶点着色器中;
    • 纹理图片:将纹理对象加载到GPU中,需要通过setFragmentTexture函数传递到片元着色函数,通过采样器读物纹素,加载TGA图片。
  • 传递数据对应的代码如下:
	/*
	 需要传递的数据有以下三种:
	 1)顶点数据、纹理坐标,
	 2)viewportSize视图大小
	 3)纹理图片
	 */
	// 将数据加载到MTLBuffer (即metal文件中的顶点着色函数)--> 顶点函数
	[commandEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:YDWVertexInputIndexVertices];
	//将数据加载到GPU(即metal文件中的顶点着色函数) --> 视图大小
	[commandEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:YDWVertexInputIndexViewportSize];
	    
	//将纹理对象传递到片元着色器(即metal中的片元着色函数) -- 纹理图片
	[commandEncoder setFragmentTexture:_texture atIndex:YDWTextureIndexBaseColor];
⑤ 整体流程

在这里插入图片描述

PNG/JPEG纹理

  • 与加载 TGA 图片相比,在原代码基础上,需要将 setupTexture 函数修改为 setupTexturePNG 函数;
① setupTexturePNG函数
  • 该函数的功能是加载PNG/JPG图片,其加载的流程如图所示:

在这里插入图片描述

② loadImage 函数
  • 除了获取图片以及loadImage函数,其他步骤与加载TGA图片时,步骤是一致的,将PNG/JPG图片加载成位图的,通过CGContextRef对象将图片进行重绘,解压成位图数据的,图片解压为位图的流程如图所示:

在这里插入图片描述

③ 实现如下
// 加载纹理TGA文件
- (void)setupTexturePNG{
    
	// 1、获取图片
    UIImage *image = [UIImage imageNamed:@"mouse.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];
    
    
	// 4、创建MTLRegion对象
     // MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
    /*
    typedef struct
    {
    MTLOrigin origin; //开始位置x,y,z
    MTLSize   size; //尺寸width,height,depth (即宽、高、深度)
    } MTLRegion;
    */
    MTLRegion region = {
        {0, 0, 0},
        {image.size.width, image.size.height, 1},
    };
    
	// 5、获取纹理图片:通过context重绘获取纹理图片
    Byte *imageBytes = [self loadImage:image];
    
	// 6、UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
    if (imageBytes) {
        // 将纹理图片复制到texture
        [_texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:4 * image.size.width];
        // 释放
        free(imageBytes);
        imageBytes = NULL;
    }
}

// 从UIImage 中读取Byte 数据返回 -- png/jpg 都是通过context重绘 解压成位图
 - (Byte *)loadImage:(UIImage *)image{
	// 1、将UIImage转换为CGImageRef
    CGImageRef spriteImage = image.CGImage;
    
	// 2、读取图片的大小
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
	// 3、计算图片字节数
    Byte *spriteData = (Byte *)calloc(width * height * 4, sizeof(Byte));
    
	// 4、创建context
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
	// 5、在context上绘图
    CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
    
	// 6、图片翻转过来
    CGRect rect = CGRectMake(0, 0, width, height);
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    CGContextDrawImage(spriteContext, rect, spriteImage);
    
	// 7、释放context
    CGContextRelease(spriteContext);
    return spriteData;
}
④ 总结

根据TGA图片加载以及PNG/JPG图片加载过程的分析,如下:

  • 将纹理图片解压成位图;
  • 创建 MTLTextureDescriptor 对象,即纹理描述对象,并设置 pixelFormat 像素信息、width以及height尺寸信息;
  • 使用纹理描述对象创建MTLTexture对象,即纹理对象;
  • 创建MTLRegion对象,用于标识纹理的像素区域;
  • 通过texture的replaceRegion:mipmapLevel:withBytes:bytesPerRow:函数将纹理图片解压的位图数据复制到纹理对象中;
  • 绘制回调函数drawInMTKView中,通过MTLrenderCommandEncoder对象的setFragmentTexture:atIndex:函数,将纹理传递到片元着色函数。

以上是关于Metal之加载TGA与PNG/JPEG纹理图片的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL 加载DDS文件(压缩纹理)

OpenGL 七 - OpenGL 纹理基础与案例演示

threejs纹理

游戏内图片-文件格式与纹理格式

在 OpenGL c++ 项目中使用资源中的 .tga (targa) 文件

一步步学Metal图形引擎2-《纹理贴图》