Metal 创建和采样纹理

您可以使用纹理在Metal中绘制和处理图像。纹理是纹理元素的结构化集合,通常称为纹理元素或像素。这些纹理元素的确切配置取决于纹理的类型。此示例使用一个由2D元素数组构成的纹理来保存图像,每个元素都包含颜色数据。纹理通过称为纹理映射的过程绘制到几何图元上。片段函数通过采样纹理为每个片段生成颜色。

纹理由MTLTexture对象管理。一个MTLTexture对象定义了纹理的格式,包括元素的大小和布局,纹理中元素的数量,以及这些元素的组织方式。一旦创建,纹理的格式和组织永远不会改变。但是,可以通过渲染纹理或将数据复制到纹理中来更改纹理的内容。

Metal框架没有提供一个应用编程接口来直接将图像数据从文件加载到纹理中。Metal本身只分配纹理资源,并提供从纹理复制数据的方法。Metal应用依赖于定制代码或其他框架,如Metal工具包、图像输入/输出、UIKit或应用工具包来处理图像文件。例如,您可以使用MTKTextureLoader来执行简单的纹理加载。此示例显示了如何编写自定义纹理加载器。

加载和格式化图像数据

在示例中,AAPLImage类从TGA文件中加载和解析图像数据。该类将TGA文件中的像素数据转换为Metal能够理解的像素格式。该示例使用图像的元数据创建新的Metal纹理,并将像素数据复制到纹理中。

Metal要求所有纹理都用特定的像素格式值进行格式化。像素格式描述了纹理中像素数据的布局。本示例使用的是MTLPixelFormatBGRA8Unorm无序像素格式,即每个像素使用32位,按蓝、绿、红和阿尔法顺序排列为每个组件8位:


image

在填充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对象并为纹理数据分配内存。创建纹理时,该内存未初始化,因此下一步是将数据复制到纹理中。

将图像数据复制到纹理中

Metal管理纹理的内存,并且不提供直接访问它的权限。因此,您将无法获得指向内存中纹理数据的指针并自行复制像素。相反,您可以在MTLTexture对象上调用方法以从内存中复制数据,然后可以将其访问到纹理中,反之亦然。

在此示例中,AAPLImage对象为图像数据分配了内存,因此您将告诉纹理对象复制该数据。

使用MTLRegion结构来标识要更新纹理的哪一部分。该示例使用图像数据填充整个纹理。因此,创建一个覆盖整个纹理的区域。

MTLRegion region = {
    { 0, 0, 0 },                   // MTLOrigin
    {image.width, image.height, 1} // MTLSize
};

图像数据通常按行组织,并且您需要告诉Metal在源图像中行之间的偏移量。图像加载代码以紧密打包的格式创建图像数据,因此后续像素行的数据紧随前一行。将行之间的偏移量计算为一行的确切长度(以字节为单位),即每像素的字节数乘以图像宽度。

NSUInteger bytesPerRow = 4 * image.width;

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

[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)指定纹理数据最后一个字节的纹理元素(图像的右下角)。


image

向顶点格式添加一个字段来保存纹理坐标:

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数据结构添加纹理坐标值:

typedef struct
{
    // 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;

} RasterizerData;

在顶点着色器中,通过将纹理坐标写入textureCoordinate字段,将纹理坐标传递给光栅化器阶段。光栅化器阶段在四边形的三角形片段上插入这些坐标。

out.textureCoordinate = vertexArray[vertexID].textureCoordinate;

从纹理中的位置计算颜色

您可以对纹理进行采样,从纹理中的某个位置计算颜色。为了采样纹理数据,片段函数需要纹理坐标和对要采样的纹理的引用。除了从光栅化器阶段传入的参数之外,还应传入一个带有texture2d类型和[[texture(index)]属性限定符的colorTexture参数。此参数是对要采样的MTLTexture对象的引用。

fragment float4
samplingShader(RasterizerData in [[stage_in]],
               texture2d<half> colorTexture [[ texture(AAPLTextureIndexBaseColor) ]])

使用内置的纹理采样函数来采集纹理像素数据,该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);

编码绘图参数

编码和提交绘图命令的过程与使用渲染管道渲染图元中显示的过程相同,因此下面没有显示完整的代码。此示例的不同之处在于片段着色器有一个附加参数。当您编码命令的参数时,设置片段函数的纹理参数。此示例使用AAPLtextureIndex基底颜色索引来识别目标C和Metal着色语言代码中的纹理。

[renderEncoder setFragmentTexture:_texture
                          atIndex:AAPLTextureIndexBaseColor];
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268