本章我們繼續創建兩個資源,用於圖形管線採樣圖像。第一個資源是我們已經見過的,也就是和交換鏈圖像打交道的時候用的,但是第二個則是新的,它和着色器如何從圖像讀取紋素有關。
我們之前就見到過,有了交換鏈圖像和幀緩衝,圖像可以通過圖像視圖訪問而不是直接訪問。也要爲貼圖圖像創建這樣一個圖像視圖。
添加一個類成員textureImageView存儲貼圖圖像的VkImageView,然後創建一個新的方法createTextureImageView,在初始化Vulkan的創建貼圖圖像之後調用。
該方法的代碼基本就和createImageView一樣,就需要修改format和image兩個字段:
VkImageViewCreateInfo viewInfo = {};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = textureImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
我這裏沒有顯式進行viewInfo.components初始化,因爲反正要設置VK_COMPONENT_SWIZZLE_IDENTITY爲0。調用vkCreateImageView創建:
if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) {
throw std::runtime_error("failed to create texture image view!");
}
由於有很多代碼和createImageViews的一樣,所以可以抽象一個新方法createImageView:
VkImageView createImageView(VkImage image, VkFormat format) {
VkImageViewCreateInfo viewInfo = {};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VkImageView imageView;
if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
throw std::runtime_error("failed to create texture image view!");
}
return imageView;
}
createTextureImageView就可以簡化爲:
void createTextureImageView() {
textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM);
}
createImageViews簡化爲:
void createImageViews() {
swapChainImageViews.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImages.size(); i++) {
swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat);
}
}
確保在程序結束的時候銷燬圖像視圖,就在銷燬圖像本身之前:
vkDestroyImageView(device, textureImageView, nullptr);
着色器直接從圖像讀取紋素是可行的,但是當它們用於貼圖的時候很少這麼做。貼圖通常用採樣器訪問,從而在計算最終獲取到的顏色的時候能應用過濾和變換等。
這些過濾器對於應對過採樣很有用。考慮一個映射到幾何體的貼圖,該貼圖的片段比紋素要多。那麼你就簡單地在每個片段中爲貼圖座標應用最近的紋素,那麼你會得到類似第一個圖這樣的效果:
如果你聯合4個最近的像素來進行線性插值,你可以得到一個平滑的效果,類似第二個圖。當然可能你的程序就要第一種那樣的藝術風格,例如我的世界,但是右側的纔是傳統圖形程序所喜歡的。當你從貼圖讀取顏色的時候,採樣器對象能自動應用該過濾。
欠採樣就是相反的問題了,就是紋素比片段多。這會在對高頻圖案採樣的時候導致產生假象,比如象棋貼圖在銳角觀察的時候:
左邊圖像遠處比較模糊,解決辦法是各向異性過濾,這也是採樣器自動應用的。
除了這些過濾器,採樣器還能處理變換問題。當你想要通過尋址模式從圖像外讀取紋素的時候,它能決定要做些什麼。下面這幅圖展示了可能的操作:
現在我們創建一個新的方法createTextureSampler來建立採樣器對象。我們會使用該採樣器從着色器中的貼圖讀取顏色。我們把該方法放在創建貼圖圖像視圖之後。
VkSamplerCreateInfo samplerInfo = {};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = VK_FILTER_LINEAR;
samplerInfo.minFilter = VK_FILTER_LINEAR;
magFilter和minFilter字段指定了如何對將要放大或縮小的紋素進行插值。放大關心的是前面的過採樣問題,縮小則對應於欠採樣。選項有VK_FILTER_NEAREST和VK_FILTER_LINEAR,對應於前面圖像展示的模式。
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
尋址模式可以通過每個軸設置addressMode來指定,可選的值如下:
VK_SAMPLER_ADDRESS_MODE_REPEAT:超過圖像尺寸的時候進行重複;
VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT:和重複類似,但是超過圖像尺寸的時候會翻轉座標進行鏡像操作;
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE:超過圖像尺寸的時候,採用最近的邊的顏色;
VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE:和前面的截斷操作類似,但是會使用最近邊的相反的邊;
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER:當超過圖像尺寸的時候,採樣會返回單色。
用哪種都可以,因爲我們不會在圖像外採樣。但是,重複模式可能是最常用的,因爲它可以用於像地板和牆一樣平鋪貼圖。
samplerInfo.anisotropyEnable = VK_TRUE;
samplerInfo.maxAnisotropy = 16;
這兩個字段指定了是否啓用各向異性過濾。除非考慮性能問題,否則還是要啓用的。maxAnisotropy字段限制了計算最終顏色的紋素樣本個數。較小的值會有更好的性能,但是也會導致效果較差。當前圖形硬件都不會用超過16個樣本,因爲那樣區別就可以忽略了。
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
borderColor字段指定了當使用截斷尋址模式時採樣超過圖像的時候返回什麼顏色。可能會返回黑色、白色或者透明,格式是浮點或整數。你不能指定一個任意顏色。
samplerInfo.unnormalizedCoordinates = VK_FALSE;
unnormalizedCoordinates字段指定了你要用哪個座標系統來對圖像紋素尋址。如果該字段是VK_TRUE,你就能使用[0, texWidth)和[0, texHeight)取鍵的座標。否則,紋素尋址在所有軸上都是[0, 1)區間的。真實世界中的程序基本都是用歸一化座標,因爲這能在相同座標下使用不同的分辨率。
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
如果比較方法啓用的話,紋素會首先和一個值比較,然後將該比較結果用於過濾操作。這主要用於陰影貼圖的百分比靠近過濾。
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.0f;
所有這些字段都應用於Mip貼圖。
採樣器都定義好了,添加一個類成員來保存採樣器對象句柄,然後用vkCreateSampler創建採樣器:
VkSampler textureSampler;
...
if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
throw std::runtime_error("failed to create texture sampler!");
}
注意採樣器不會應用VkImage,採樣器是一個獨立的對象,提供了從貼圖提取顏色的接口。它可以用於任何你想要的圖片,不管是1D、2D或者3D。這和許多舊API不一樣,它們會結合貼圖圖像並過濾到一個單獨的狀態中。
在程序結束時銷燬採樣器:
vkDestroySampler(device, textureSampler, nullptr);
這個就在cleanup方法中的銷燬交換鏈之後。
現在運行程序,發現驗證層提示:
這是因爲各向異性過濾是可選設備特性,我們要更新邏輯設備創建部分的代碼來請求該特性:
VkPhysicalDeviceFeatures deviceFeatures = {};
deviceFeatures.samplerAnisotropy = VK_TRUE;
儘管不支持該特性在現代顯卡上是不太可能發生的,但是我們還是要更新下isDeviceSuitable的代碼來檢查是否可用:
VkPhysicalDeviceFeatures supportedFeatures;
vkGetPhysicalDeviceFeatures(device, &supportedFeatures);
return indices.isComplete() && extensionsSupported && swapChainAdequate
&& supportedFeatures.samplerAnisotropy;
vkGetPhysicalDeviceFeatures重新調整了VkPhysicalDeviceFeatures的意圖來表明支持哪個特性而不是通過設置布爾值來請求。
下一章我們會將圖像和採樣器對象暴露給着色器以將該貼圖繪製到正方形上。