Metal学习:纹理和采样

纹理基本概念

在上一篇文章中,我们知道如何用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,有四种方式可以设置

CLAMP_TO_EDGE

在这种方式下,就用边缘像素的值来作为采样的值返回
在这里插入图片描述

CLAMP_TO_ZERO

采样返回0或者1
在这里插入图片描述

REPEAT

纹理不断重复
在这里插入图片描述

MIRRORED_REPEAT

纹理不断重复,但是这次是以镜像的方式重复
在这里插入图片描述

用Metal画一张笑脸

首选我们基于上一篇的建立一个新的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纹理

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程序中创建采样器

如下的代码是从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文件

这次我们的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进行一次采点,然后把采点得到的值作为这个片段的返回值。
运行程序,应该在屏幕上得到一张笑脸
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章