Metal 框架之创建纹理及纹理采样

1,269 阅读7分钟

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

概述

Metal 中使用纹理来绘制和处理图像,它是由像素组成的。使用2维数组的纹理来保存图像,每个元素都包含颜色数据。通过纹理映射技术将纹理绘制到几何图元上。 在片段着色器中,使用片段函数对纹理采样来为每个片段生成颜色。 

Metal 中,使用 MTLTexture 对象来表示纹理。 MTLTexture 对象定义了纹理的格式,包括大小和布局、纹理中元素的数量以及这些元素的组织方式。 一旦创建了纹理,它的格式和组织方式就固定不变了,后续只能通过渲染或将数据复制到其中来更改纹理的内容。

Metal 框架没有提供将图像数据从文件加载到生成纹理的功能,它只分配纹理资源,并提供将数据复制到纹理及从纹理复制数据的方法。 因此,只能自己写代码使用其他框架(如 MetalKit、Image I/O、UIKit 或 AppKit)来处理图像文件。在 Metal 中,可以使用 MTKTextureLoader 来加载纹理。 本文将展示如何编写自定义纹理加载器。

加载图像资源

出于一下原因,需要手动创建或更新纹理。

  • 自定义格式的图片数据

  • 需要运行时创建的纹理

  • 从服务器传过来的纹理数据或者需要动态更新的纹理内容

Metal 能够加载的纹理需要是 MTLPixelFormat 类型的数据。 像素格式描述了像素数据在纹理中的布局。 本例使用 MTLPixelFormatBGRA8Unorm 像素格式,每像素占 32 位,按照蓝色、绿色、红色和 alpha 顺序来组织,每个部分占8位:

piexl_formart.png

在填充 Metal 纹理之前,必须将图像数据格式化为纹理的像素格式。 TGA 文件可以提供每像素 32 位格式或每像素 24 位格式的像素数据。 对于每像素 32 位的 TGA 文件,在使用时只需要拷贝像素数据就可以了。 对于每像素 24 位的 BGR 图像,使用时需要做转换处理,复制红色、绿色和蓝色通道并将 alpha 通道设置为 255即可。


// Initialize a source pointer with the source image data that's in BGR form

uint8_t *srcImageData = ((uint8_t*)fileData.bytes +

                         sizeof(TGAHeader) +

                         tgaInfo->IDSize);




// Initialize a destination pointer to which you'll store the converted BGRA

// image data

uint8_t *dstImageData = mutableData.mutableBytes;




// For every row of the image

for(NSUInteger y = 0; y < _height; y++)

{

    // If bit 5 of the descriptor is not set, flip vertically

    // to transform the data to Metal's top-left texture origin

    NSUInteger srcRow = (tgaInfo->topOrigin) ? y : _height - 1 - y;



    // For every column of the current row

    for(NSUInteger x = 0; x < _width; x++)

    {

        // If bit 4 of the descriptor is set, flip horizontally

        // to transform the data to Metal's top-left texture origin

        NSUInteger srcColumn = (tgaInfo->rightOrigin) ? _width - 1 - x : x;



        // Calculate the index for the first byte of the pixel you're

        // converting in both the source and destination images

        NSUInteger srcPixelIndex = srcBytesPerPixel * (srcRow * _width + srcColumn);

        NSUInteger dstPixelIndex = 4 * (y * _width + x);




        // Copy BGR channels from the source to the destination

        // Set the alpha channel of the destination pixel to 255

        dstImageData[dstPixelIndex + 0] = srcImageData[srcPixelIndex + 0];

        dstImageData[dstPixelIndex + 1] = srcImageData[srcPixelIndex + 1];

        dstImageData[dstPixelIndex + 2] = srcImageData[srcPixelIndex + 2];



        if(tgaInfo->bitsPerPixel == 32)

        {

            dstImageData[dstPixelIndex + 3] =  srcImageData[srcPixelIndex + 3];

        }

        else

        {

            dstImageData[dstPixelIndex + 3] = 255;

        }

    }

}

_data = mutableData;

从纹理描述符创建纹理

使用 MTLTextureDescriptor 对象来配置 MTLTexture 对象的纹理尺寸和像素格式等属性。 然后调用 newTextureWithDescriptor: 方法来创建纹理。


MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];



// Indicate that each pixel has a blue, green, red, and alpha channel, where each channel is

// an 8-bit unsigned normalized value (i.e. 0 maps to 0.0 and 255 maps to 1.0)

textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;



// Set the pixel dimensions of the texture

textureDescriptor.width = image.width;

textureDescriptor.height = image.height;



// Create the texture from the device by using the descriptor

id<MTLTexture> texture = [_device newTextureWithDescriptor:textureDescriptor];

使用 Metal 创建一个 MTLTexture 对象并为纹理数据分配内存,此内存并未初始化,因此下一步需要将数据拷贝到纹理中。


MTLRegion region = {

    { 0, 0, 0 },                   // MTLOrigin

    {image.width, image.height, 1} // MTLSize

};

图像数据通常按行来存储,在使用图像时需要告诉 Metal 源图像中行之间的偏移量。 本例中图像数据是按照紧密的格式存储的,因此各行像素之间是紧挨着的。


NSUInteger bytesPerRow = 4 * image.width;

调用纹理上的 replaceRegion:mipmapLevel:withBytes:bytesPerRow: 方法将像素数据从图像对象复制到纹理中。


[texture replaceRegion:region

            mipmapLevel:0

              withBytes:image.data.bytes

            bytesPerRow:bytesPerRow];

将纹理映射到几何图元

纹理不能被直接渲染,必须将其映射到几何图元上才可以。这些几何图元由顶点阶段输出并由光栅化器转换为片段的几何图元(在本例中为一对三角形)。 每个片段都需要知道纹理的哪一部分作用到它上面,可以使用纹理坐标定义此映射:将纹理图像上的位置映射到几何表面上的位置。

对于 2D 纹理,采用归一化之后的纹理坐标, 在 x 轴和 y 轴方向上都是从 0.0 到 1.0。 (0.0, 0.0) 指定纹理数据的第一个字节(图像的左上角)处的纹素。  (1.0, 1.0) 指定纹理数据最后一个字节(图像的右下角)的纹素。

texture_coordinate.png

定义一种数据结构来保存顶点数据与纹理坐标:


typedef struct

{

    // Positions in pixel space. A value of 100 indicates 100 pixels from the origin/center.

    vector_float2 position;


    // 2D texture coordinate

    vector_float2 textureCoordinate;

} AAPLVertex;

在顶点数据中,将四边形的四个角映射到纹理的四个角上:


static const AAPLVertex quadVertices[] =

{

    // Pixel positions, Texture coordinates

    { {  250-250 },  { 1.f, 1.f } },

    { { -250-250 },  { 0.f, 1.f } },

    { { -250,   250 },  { 0.f, 0.f } },



    { {  250-250 },  { 1.f, 1.f } },

    { { -250,   250 },  { 0.f, 0.f } },

    { {  250,   250 },  { 1.f, 0.f } },

};

定义 RasterizerData 数据结构来存储纹理坐标 textureCoordinate 值,该值后续将被传进片段着色器中:


struct RasterizerData

{

    // The [[position]] attribute qualifier of this member indicates this value is

    // the clip space position of the vertex when this structure is returned from

    // the vertex shader

    float4 position [[position]];



    // Since this member does not have a special attribute qualifier, the rasterizer

    // will interpolate its value with values of other vertices making up the triangle

    // and pass that interpolated value to the fragment shader for each fragment in

    // that triangle.

    float2 textureCoordinate;


};

在顶点着色器中,需要将纹理坐标写入 textureCoordinate 字段中,才能将纹理坐标传递给光栅化阶段。 光栅化阶段会在四边形的三角形片段中插入一些坐标点来产生屏幕上的像素。


out.textureCoordinate = vertexArray[vertexID].textureCoordinate;

计算顶点颜色

通过纹理采样来计算某个位置点的颜色。具体颜色的计算依赖于片段函数,它需要根据纹理坐标以及纹理来采样纹理数据得到颜色值。 除了从光栅化阶段传入的参数之外,还传入一个 texture2d 类型的 colorTexture 参数,该参数指向 MTLTexture 对象。


fragment float4

samplingShader(RasterizerData in [[stage_in]],

               texture2d<half> colorTexture [[ texture(AAPLTextureIndexBaseColor) ]])

使用内置的纹理 sample() 函数对纹素数据进行采样。 sample() 函数接受两个参数:一个采样器 (textureSampler) 描述您想要如何对纹理进行采样,以及描述纹理中要采样的位置的纹理坐标 (in.textureCoordinate)。 sample() 函数从纹理中获取一个或多个像素,并返回根据这些像素计算出的颜色。

当渲染到的区域与纹理的大小不同时,采样器可以使用不同的算法来准确计算 sample() 函数应该返回的纹素颜色。 设置mag_filter模式指定区域大于纹理大小时采样器如何计算返回颜色,设置min_filter模式指定区域小于纹理大小时采样器如何计算返回颜色。 为两个过滤器设置线性模式会使采样器平均给定纹理坐标周围像素的颜色,从而产生更平滑的输出图像。

constexpr sampler textureSampler (mag_filter::linear,

                                  min_filter::linear);



// Sample the texture to obtain a color

const half4 colorSample = colorTexture.sample(textureSampler, in.textureCoordinate)

设置绘图参数

编码和提交绘图命令的过程与使用渲染管道渲染基元中所示的过程相同。当对命令的参数进行编码时,需要设置片段函数的纹理参数,本例使用 AAPLTextureIndexBaseColor 索引来标识 Objective-C 和 Metal Shading Language 代码中的纹理。


[renderEncoder setFragmentTexture:_texture

                          atIndex:AAPLTextureIndexBaseColor];

总结

本文介绍了 Metal 中使用纹理来绘制和处理图像的步骤。一个纹理先要通过纹理映射技术将纹理映射到几何图元上后才可以被渲染出来。

首先是加载图像数据,转换成 MTLTexture 对象的纹理,然后进行纹理映射、设置滤波方式,最后采样计算颜色值,最后编码和提交绘图命令。