在上一篇文章中,我们知道如何用metal在屏幕上画一个三角形,并且也了解了如何给顶点传递颜色来改变三角形的颜色。但是在计算机图形中仅仅靠程序指定颜色是远远不够的,如果想要图像看起来逼真生动,那么就需要使用纹理。本文介绍如何在metal中使用纹理,包括从一副图片构造纹理,然后从纹理采样,并贴到之前的三角形上给。 在GPU中的纹理,可以理解为GPU中的一个内存,这个内存可以由CPU去更新数据。例如可以在CPU读取图像的数据到内存,然后更新GPU中的纹理。而采样是一个动作,它控制GPU在画图时如何从纹理读取数据。
在metal中,纹理的原点坐标在左上角,这和openGL是不同的(OpenGL的纹理原点坐标在左下角),如下图所示: 当把图像的数据上传到纹理时,需要保持图像和纹理的坐标一致。一般来说纹理是有大小的,比如上传一个800X600的图像,所创建的纹理大小也应该是800X600。但是在metal使用这个纹理的时候,一般是使用的一个归一化的坐标,横坐标和纵坐标都是从0到1,我们把这个归一化的坐标叫做纹理坐标。也就是左上角的坐标为(0, 0),右下角的坐标为(1, 1)。采样的时候是通过纹理坐标来获取纹理对应的颜色。
纹理是有大小的,它是由一定数量的像素点组成的,但是当在画图的时候,有可能要贴图的物体大小和纹理的大小不一致,这时候就需要告诉GPU如何将纹理的像素映射到纹理坐标,这一个过程就叫过滤,metal提供两种过滤的方式nearest和linear。nearest是简单的找一个离纹理坐标最近的像素点的值,这种方式速度非常快,但是在放大的时候,会产生块状现象。linear是找到纹理坐标周围4个像素点,然后给根据像素点离纹理坐标的距离产生一个权重,最终进行相加得到一个数值。linear可以产生比nearest更好的效果,但是效率上来说低一点。 可以知道分别有放大和缩小两种情况,当纹理小于被画的物体时,是放大(magnification),当大于被画的物体时,是缩小(minification),可以对这两种情况设置不同的filter。
通常来说纹理的坐标应该设置为0到1之间的数值,但是当超出0到1的范围,也是可以的。这时候就需要设置纹理的环绕方式,Metal把这个行为称之为Addressing,有四种方式可以设置
在这种方式下,就用边缘像素的值来作为采样的值返回
采样返回0或者1
纹理不断重复
纹理不断重复,但是这次是以镜像的方式重复
首选我们基于上一篇的建立一个新的iOS项目,这个项目的目的是把一张笑脸用metal画在屏幕上。 以下是在初始化我们需要做的一些工作
使用纹理第一步就是我们得把图片读到内存里面来,这里我们使用stb_image.h来加载图像,可以从这里下载这个头文件。只需要在你的工程中包含这个头文件,就可以使用它提供的函数来加载各种图片。
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h"我们先从这里下载一张笑脸,然后把它添加到工程中,通过下面的代码就可以把这个笑脸读到我们的内存中来了。
NSString *path = [[NSBundle mainBundle] pathForResource:@"awesomeface" ofType:@"png"]; const char *image_path = [path UTF8String]; int width, height, nrChannels; unsigned char *data = stbi_load(image_path, &width, &height, &nrChannels, 0);metal的纹理使用MetalTexture来表示,它是通过一个MTLTextureDescriptor的描述符从MTLDevice里面创建,一下的代码就是创建一个Texture,并将上面读到内存中的图像上传到纹理。
MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO]; texture = [mtlDevice newTextureWithDescriptor:textureDescriptor]; MTLRegion region = MTLRegionMake2D(0, 0, width, height); [texture replaceRegion:region mipmapLevel:NO withBytes:data bytesPerRow:4*width];在metal中,从纹理中进行采样需要指定一个采样器,这个采样器包含了上文中提到的纹理过滤方式和纹理环绕方式。采样器可以在shader程序中创建,也可以在应用代码中创建。
如下的代码是从shader程序中创建采样器
constexpr sampler s(coord::normalized, address::repeat, filter:linear);constexpr是C++11引入的新的关键字,它表示这个对象是在编译时而不是在执行时创建,也就是说它是一个静态的变量,在运行时只有一份实例。coord表示的是纹理坐标,取值可以是normalized或者pixel。address表示的是纹理环绕方式,取值可以是clamp_to_zero, clamp_to_edge, repeat, mirriored_repeat。filter表示纹理过滤方式,取值可以是nearest或者linear。
如下代码是从应用代码中创建采样器
MTLSamplerDescriptor *samplerDescriptor = [MTLSamplerDescriptor new]; samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; samplerDescriptor.sAddressMode = MTLSamplerAddressModeRepeat; samplerDescriptor.tAddressMode = MTLSamplerAddressModeRepeat; sampler = [mtlDevice newSamplerStateWithDescriptor:samplerDescriptor];不同于上文中绘制一个三角形,这次我们需要绘制一个四边形,来放置我们的笑脸。四边形其实就是两个三角形组成的,并且设置每个顶点对应的纹理坐标。
static float vertices[] = { -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, -1.0, 0.0, 1.0, -1.0, 1.0, 0.0, 1.0, 1.0, -1.0, 0.0, 1.0, -1.0, -1.0, 0.0, 1.0 }; vertexBuffer = [mtlDevice newBufferWithBytes:vertices length:sizeof(vertices) options:MTLResourceOptionCPUCacheModeDefault]; static float coord[] = { 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, }; texCoordBuffer = [mtlDevice newBufferWithBytes:coord length:sizeof(coord) options:MTLResourceOptionCPUCacheModeDefault];渲染过程,我们要做的是把上面生成的纹理坐标和纹理设置到fragment的程序中,让fragment能访问到这个纹理。同时和前文中画三角形不同,这次我们要画四方形,也就是两个三角形。新增加的代码如下所示:
[renderEncoder setVertexBuffer:texCoordBuffer offset:0 atIndex:1]; [renderEncoder setFragmentTexture:texture atIndex:0]; [renderEncoder setFragmentSamplerState:sampler atIndex:0]; [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];这次我们的metal文件增加了很多东西,如下所示。
struct VertexOut { float4 position [[position]]; float2 texCoord; }; vertex VertexOut vertexShader(device float4* vertices [[buffer(0)]], constant float2* uv [[buffer(1)]], uint vid [[vertex_id]]) { VertexOut vert; vert.position = vertices[vid]; vert.texCoord = uv[vid]; return vert; } fragment float4 fragmentShader(VertexOut vert [[stage_in]], texture2d<float> texture [[texture(0)]], sampler sam [[sampler(0)]]) { float4 samplerColor = texture.sample(sam, vert.texCoord); return samplerColor; }VertexOut这个数据结构是vertex和fragment直接进行传递的数据,position是顶点的位置,texCoord是纹理的坐标,都是从CPU传递过来的。访问方式分别是buffer(0)和buffer(1),这就是在每一帧的渲染函数中我们设置的
[renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0]; [renderEncoder setVertexBuffer:texCoordBuffer offset:0 atIndex:1];相应的在fragment中,texture和sampler也是从CPU设置过来的,通过如下代码设置,然后在fragment中就可以通过texutre(0),sampler(0)分别进行访问
[renderEncoder setFragmentTexture:texture atIndex:0]; [renderEncoder setFragmentSamplerState:sampler atIndex:0];texture.sample(sam, vert.texCoord)是进行采样操作,具体就是按照sampler设置的纹理取样方法在texCoord的坐标上对texture进行一次采点,然后把采点得到的值作为这个片段的返回值。 运行程序,应该在屏幕上得到一张笑脸