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進行一次採點,然後把採點得到的值作爲這個片段的返回值。
運行程序,應該在屏幕上得到一張笑臉
在這裏插入圖片描述

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