翻译Metal文章:Creating and Sampling Textures

前言

翻译Metal sample code :Creating and Sampling Textures,项目下载地址

概览

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

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

Metal框架没有提供API来直接将图像数据从文件加载到纹理。Metal本身只分配纹理资源,并提供在纹理之间复制数据的方法。金属应用程序依赖于自定义代码或其他框架(如MetalKit、图像I/O、UIKit或AppKit)来处理图像文件。例如,可以使用MTKTextureLoader执行简单的纹理加载。此示例演示如何编写自定义纹理加载程序。

Xcode项目包含在macOS、iOS或tvOS设备上运行示例的方案。默认方案是macOS,它在Mac上运行示例

加载和格式化图像数据

您可以手动创建纹理或更新其内容,这一过程将在接下来的几节中介绍。您这样做可能有多种原因:

  1. 图像数据以自定义格式存储。
  2. 您有需要在运行时生成其内容的纹理。
  3. 您正在从服务器流式传输纹理数据,或者需要动态更新纹理的内容。

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

AAPLImage类不是此示例的焦点,因此不会详细讨论它。该类演示基本的图像加载操作,但不使用或依赖于Metal框架。其唯一目的是方便加载图像数据并将其转换为金属像素格式。如果需要加载自定义格式的图像,可以创建类似的类。

Metal要求使用特定的MTLPixelFormat值格式化所有纹理。像素格式描述了纹理中像素数据的布局。此示例使用MTLPixelFormatBGRA8Unorm像素格式,每像素使用32位,按蓝色、绿色、红色和alpha顺序排列为每个组件8位:
BGRAFormat.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对象并为纹理数据分配内存。创建纹理时,此内存未初始化,因此下一步是将数据复制到纹理中。

将图像数据复制到纹理中

Metal为纹理管理内存,不提供直接访问它的功能。所以你不能得到一个指向内存中纹理数据的指针并自己复制像素。相反,可以对MTLTexture对象调用方法,将数据从可以访问的内存复制到纹理中,反之亦然。

在本示例中,AAPLImage对象为图像数据分配了内存,因此您将告诉纹理对象复制此数据。
使用MTLRegion结构来标识要更新纹理的哪个部分。此示例使用图像数据填充整个纹理;因此创建一个覆盖整个纹理的区域。

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

图像数据通常按行组织,您需要告诉Metal源图像中行之间的偏移量。图像加载代码创建压缩格式的图像数据,因此后续像素行的数据紧跟前一行。将行之间的偏移量计算为行的确切长度(字节)-每个像素的字节数乘以图像宽度。

将纹理映射到几何基本体上

不能单独渲染纹理;必须将其映射到由顶点阶段输出并由光栅化器转换为碎片的几何基本体(在本例中是一对三角形)。每个片段都需要知道纹理的哪个部分应该应用于它。使用纹理坐标定义此映射:将纹理图像上的位置映射到几何曲面上的位置的浮点位置。

对于二维纹理,规格化纹理坐标是x和y方向上从0.0到1.0的值。值(0.0,0.0)指定纹理数据的第一个字节(图像的左上角)处的texel。值(1.0,1.0)指定纹理数据的最后一个字节(图像的右下角)处的texel。


textureCoordinate.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数据结构:

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;

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

out.textureCoordinate = vertexArray[vertexID].textureCoordinate;

从纹理中的位置计算颜色

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

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

使用内置的texture sample()函数对texel数据进行采样。函数的作用是:一个采样器(texture sampler)描述你想要如何采样纹理,另一个纹理坐标(in.textureCoordinate)描述要采样的纹理位置。函数的作用是:从纹理中提取一个或多个像素,并返回从这些像素计算出的颜色。

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

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语言代码中的纹理。

[renderEncoder setFragmentTexture:_texture
                          atIndex:AAPLTextureIndexBaseColor];

总结

看完了Metal如何绘制一个纹理,可以尝试去将一个UIImage使用Metal来绘制。

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