一步步学Metal图形引擎2-《纹理贴图》

Posted Mr_厚厚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一步步学Metal图形引擎2-《纹理贴图》相关的知识,希望对你有一定的参考价值。

教程 2

纹理贴图

教程源码下载地址: https://github.com/jiangxh1992/MetalTutorialDemos

CSDN完整版专栏: https://blog.csdn.net/cordova/category_9734156.html

一、关键词

  • UV坐标系
  • MTLTexture
  • MTKTextureLoader贴图加载
  • Metal采样对象sampler
  • Filtering纹理滤波
  • Mipmaps
  • Addressing纹理寻址

二、原理

2.1 什么是纹理贴图

纹理texture就像是模型表面的皮肤,在没有光照计算的前提下为表面提供基础的色彩内容,纹理可以体现物体表面的性质,使物体看上去更加真实。上一章我们在片段着色器直接简单返回一个红色,相当于为模型贴了一张纯红色的纹理,只不过没有从外部的贴图资源上采样颜色。

纹理贴图通常是一张二维的图片,纹理贴图将图像离散为一定数量的像素,称为纹素,纹素最终会映射到屏幕上的像素上。

2.2 MTLTexture

MTLTexture是Metal中保存贴图数据的一张纹理缓冲,可以理解为一个framebuffer,可通过配置设置贴图是否可读可写,贴图的尺寸,贴图保存的图像数据格式等。MTLTexture在创建的时候有其decriptor对象:MTLTextureDescriptor。可以设置MTLTextureDescriptor来配置纹理贴图的属性和操作,包括后期高级设置中的storgeMode来控制贴图在内存中的存储方式等。

MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];

textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;

textureDescriptor.width = image.width;
textureDescriptor.height = image.height;

id<MTLTexture> texture = [_device newTextureWithDescriptor:textureDescriptor];

2.3 纹理空间

纹理空间是针对纹理贴图的二维坐标空间,定义在0~1单位坐标系内,和屏幕分辨率宽高比无关。单位化的纹理空间坐标最终通过变换映射到(0,0)~(width,height)的屏幕空间。

纹理坐标又称uv,是在纹理自身的单位纹理空间中定义的,纹理坐标的值在0~1之间,这样确定了模型顶点的纹理坐标后,可以替换不同的纹理,无需更改纹理坐标值。Metal中纹理空间的坐标系如下,左上角为原点(不同于OpenGL纹理坐标空间原点在左下角):


而在上一章我们顶点的坐标空间原点是在中心,我们希望三角形第一个顶点对应纹理贴图的top-center位置,第二个顶点对应bottom-right位置,第三个顶点对应bottom-left位置,因此最终我们三角形三个顶点的uv坐标依次映射为:

  • 顶点:(0,1.0) -------> uv:(0.5,0)
  • 顶点:(1.0,-1.0) ----> uv:(1.0,1.0)
  • 顶点:(-1.0,-1.0) —> uv:(0,1.0)

2.4 Metal采样对象sampler

采样对器是一个配置纹理采样的对象,光栅化阶段光栅器会在顶点之间进行一系列的插值计算,包括纹理的插值计算,采样器则是我们控制纹理采样期间一些插值细节操作的对象。

另外采样对象还可设置特殊情况下纹理采样的寻址方式和纹理滤波模式。

sampler可以直接在着色器函数中创建:

constexpr sampler textureSampler (filter::linear,
                                  address:repeat);

也可以在CPU上代码中创建sampler对象传递给着色器使用:

    MTLSamplerDescriptor *samplerDes = [[MTLSamplerDescriptor alloc] init];
    samplerDes.magFilter = MTLSamplerMinMagFilterLinear;
    samplerDes.minFilter = MTLSamplerMinMagFilterLinear;
    samplerDes.rAddressMode = MTLSamplerAddressModeRepeat;
    samplerDes.sAddressMode = MTLSamplerAddressModeRepeat;
    id<MTLSamplerState> sampler = [_device newSamplerStateWithDescriptor:samplerDes];

2.5 纹理滤波Filtering

纹理贴图将图像离散为一定规模的纹素,但是绘制的时候,纹素的规模可能比屏幕分辨率大也可能比屏幕分辨率小,所以这些情况下都需要定义相应的模式来为屏幕像素选择合适的纹素颜色。纹理贴图在绘制到屏幕上的时候有两种过滤情况:magnification和minification,含义如下:

  • magnification:当屏幕上的分辨率比纹理贴图的分辨率大,绘制的时候纹理实际尺寸比其原尺寸要大的情况;
  • minification:当屏幕上的分辨率比纹理贴图的分辨率小,绘制的时候纹理实际尺寸比其原尺寸要小的情况;

根据屏幕绘制分辨率需求来伸缩纹理贴图,改变纹理贴图数据规模的过程就叫纹理过滤。

Metal中提供了两种纹理过滤的模式:linear 和 nearest。

  • linear:线性模式,这种模式下纹理过滤过程为:选择离纹理坐标最近的四个纹素,用按照距离加权平均的结果来填充像素颜色。这种模式下效果会更加自然平滑,但是效率会降低;
  • nearest:这种模式下的纹理过滤会选择离纹理坐标最近的那个纹素来填充像素颜色。优点是速度快效率高,但是放大会有块状现象,颜色不平滑。

2.6 纹理寻址

纹理寻址解决的是纹理采样超出纹理边界时如何采样的问题。寻址模式有:repeat、mirrored_repeat、clamp_to_edge、clamp_to_zero、clamp_to_border。

  • repeat:repeat寻址模式下超出纹理空间后,回到纹理坐标0重复采样。寻址坐标重复模式为:[0~1][0~1][0~1]…
  • mirrored_repeat:镜像重复寻址,也是超出后重复寻址采样,只是重复模式为:[01][10][0~1]…
  • clamp_to_edge:默认的寻址模式,指的是超出边界重复纹理边缘的像素;
  • clamp_to_zero:指的是超出边界使用黑色或者透明颜色填充;
  • clamp_to_border:指的是超出边界使用纹理边框颜色填充.

*2.7 Mipmaping

Mip贴图,是预处理过滤的多个规模精度不同的子贴图,以应对不同精度的需求。靠近物体的时候使用精度高的贴图增加细节,远离物体的时候替换精度低的贴图,提高性能。

如下图所示,原贴图被依次缩小精度的到不同精度的子贴图,每张子贴图是之前贴图精度的一半。

Mip贴图通常是美术使用工具来制作的,但Metal中提供了方法来自动为一张原始贴图制作MipMap,在创建MTLTexture的时候可以配置MTLTextureDescriptor来开启Mipmap:

MTLTextureDescriptor *texDes = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width: image.width height: image.height mipmapped:YES];
texDes.mipmapLevelCount = 3;

其中mipmapLevelCount指的是生成Mip贴图的数量,若mipmapped设置为NO则默认mipmapLevelCount为1,表示不生成MipMap。

2.8 MTKTextureLoader贴图加载

MTKTextureLoader是MetalKit框架中的一个将普通图片加载解码为MTLTexture的对象,将普通的JPG、PNG、TIFF等格式的图片数据加载到MTLTexture对象中供在Metal中使用。

    MTKTextureLoader *loader = [[MTKTextureLoader alloc] initWithDevice: device];
    
    id<MTLTexture> texture = [loader newTextureWithContentsOfURL:url options:nil error:nil];
    
    if(!texture)
    
        NSLog(@"Failed to create the texture from %@", url.absoluteString);
        return nil;
    

三、源码分析

3.1 Render.m

// 顶点buffer
    static const Vertex vert[] = 
        0,1.0,     0.5,0,
        1.0,-1.0,  1.0,1.0,
        -1.0,-1.0, 0,1.0
    ;

首先我们在三角形的顶点缓冲中加入了每个顶点的uv坐标值,同时顶点的结构体也做了更新:

typedef struct

    vector_float2 pos;
    vector_float2 uv;
 Vertex;

顶点结构体要和顶点缓冲中的数据格式对应。

// 加载贴图
    NSError *error;
    MTKTextureLoader* textureLoader = [[MTKTextureLoader alloc] initWithDevice:_device];
    NSDictionary *textureLoaderOptions =
    @
      MTKTextureLoaderOptionTextureUsage       : @(MTLTextureUsageShaderRead),
      MTKTextureLoaderOptionTextureStorageMode : @(MTLStorageModePrivate)
      ;
    mtltexture01 = [textureLoader newTextureWithName:@"texture01"
                                         scaleFactor:1.0
                                              bundle:nil
                                             options:textureLoaderOptions
                                               error:&error];
    if(!mtltexture01 || error)
    
        NSLog(@"Error creating texture %@", error.localizedDescription);
    

这里使用device设备上下文创建了一个MTKTextureLoader,设置了纹理贴图的一些参数。MTLTextureUsageShaderRead表示我们这张贴图是只读的,不可写入,符合我们只是采样读取贴图数据的需求,明确配置只读是为了让Metal内部做一些相关的优化,只有在必要的时候才设置可读可写;MTLStorageModePrivate表示我们张贴图只有GPU可以访问,CPU不可访问,这种模式下Metal可以进一步做一些优化,提高性能。

        [renderEncoder setFragmentTexture:mtltexture01 atIndex:0]; // 设置纹理贴图

这里我们在调用draw call开始绘制之前将我们的纹理贴图传递到片段着色器中,使用setFragmentTexture方法我们可以将贴图资源传入片段着色器的textrure buffer中,从而可以在片段着色器中访问,注意index要和片段着色器参数的语义绑定相对应。

至此我们向GPU传递了新的顶点纹理坐标和纹理贴图数据,接下来GPU即可根据uv坐标采样纹理贴图。

3.2 着色器Shaders.metal

typedef struct

    float4 position [[position]];
    float2 texCoord;
 ColorInOut;

vertex ColorInOut vertexShader(constant Vertex *vertexArr [[buffer(0)]],
                               uint vid [[vertex_id]])

    ColorInOut out;

    float4 position = vector_float4(vertexArr[vid].pos, 0 , 1.0);
    out.position = position;
    out.texCoord = vertexArr[vid].uv;

    return out;


fragment half4 fragmentShader(ColorInOut in [[stage_in]],
                               texture2d<half> mtltexture01 [[texture(0)]])

    // 纹理采样对象
    constexpr sampler textureSampler (mag_filter::linear,
    min_filter::linear);
    
    // 采样贴图
    const half4 color = mtltexture01.sample(textureSampler, in.texCoord);
    
    return color;

顶点着色器中我们从顶点buffer中读取了纹理坐标并传递给下个阶段。片段着色器中利用语义绑定语法[[texture(0)]]我们访问传进来的纹理贴图,并定义了sampler采样对象,然后根据光栅器插值后得到的当前像素的uv坐标在纹理贴图上采样颜色值,并将最终的颜色值返回。

四、运行效果

按照期望我们将纹理贴图贴到了我们的三角形上,根据mark文字标记可以证明纹理坐标的设置是正确的。

以上是关于一步步学Metal图形引擎2-《纹理贴图》的主要内容,如果未能解决你的问题,请参考以下文章

一步步学Metal图形引擎7-《镜面反射》

一步步学Metal图形引擎7-《镜面反射》

一步步学Metal图形引擎3-《MTLVertexDescriptor》

一步步学Metal图形引擎3-《MTLVertexDescriptor》

一步步学Metal图形引擎5-《Uniform Buffer》

一步步学Metal图形引擎5-《Uniform Buffer》