前言
翻译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上运行示例
加载和格式化图像数据
您可以手动创建纹理或更新其内容,这一过程将在接下来的几节中介绍。您这样做可能有多种原因:
- 图像数据以自定义格式存储。
- 您有需要在运行时生成其内容的纹理。
- 您正在从服务器流式传输纹理数据,或者需要动态更新纹理的内容。
在示例中,AAPLImage类从TGA文件加载和解析图像数据。该类将TGA文件中的像素数据转换为Metal可以理解的像素格式。示例使用图像的元数据创建新的Metal纹理,并将像素数据复制到纹理中。
Metal要求使用特定的MTLPixelFormat值格式化所有纹理。像素格式描述了纹理中像素数据的布局。此示例使用MTLPixelFormatBGRA8Unorm像素格式,每像素使用32位,按蓝色、绿色、红色和alpha顺序排列为每个组件8位: BGRAFormat.pngAAPLImage类不是此示例的焦点,因此不会详细讨论它。该类演示基本的图像加载操作,但不使用或依赖于Metal框架。其唯一目的是方便加载图像数据并将其转换为金属像素格式。如果需要加载自定义格式的图像,可以创建类似的类。
在填充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来绘制。
网友评论