您可以使用纹理在Metal中绘制和处理图像。纹理是纹理元素的结构化集合,通常称为纹理元素或像素。这些纹理元素的确切配置取决于纹理的类型。此示例使用一个由2D元素数组构成的纹理来保存图像,每个元素都包含颜色数据。纹理通过称为纹理映射的过程绘制到几何图元上。片段函数通过采样纹理为每个片段生成颜色。
纹理由MTLTexture对象管理。一个MTLTexture对象定义了纹理的格式,包括元素的大小和布局,纹理中元素的数量,以及这些元素的组织方式。一旦创建,纹理的格式和组织永远不会改变。但是,可以通过渲染纹理或将数据复制到纹理中来更改纹理的内容。
Metal框架没有提供一个应用编程接口来直接将图像数据从文件加载到纹理中。Metal本身只分配纹理资源,并提供从纹理复制数据的方法。Metal应用依赖于定制代码或其他框架,如Metal工具包、图像输入/输出、UIKit或应用工具包来处理图像文件。例如,您可以使用MTKTextureLoader来执行简单的纹理加载。此示例显示了如何编写自定义纹理加载器。
加载和格式化图像数据
在示例中,AAPLImage类从TGA文件中加载和解析图像数据。该类将TGA文件中的像素数据转换为Metal能够理解的像素格式。该示例使用图像的元数据创建新的Metal纹理,并将像素数据复制到纹理中。
Metal要求所有纹理都用特定的像素格式值进行格式化。像素格式描述了纹理中像素数据的布局。本示例使用的是MTLPixelFormatBGRA8Unorm无序像素格式,即每个像素使用32位,按蓝、绿、红和阿尔法顺序排列为每个组件8位:
在填充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)指定纹理数据最后一个字节的纹理元素(图像的右下角)。
向顶点格式添加一个字段来保存纹理坐标:
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];