1. 程式人生 > >Vulkan Tutorial 11 Shader modules

Vulkan Tutorial 11 Shader modules

格式 點數據 函數類 常量數組 一個 沒有 image sizeof 光柵

操作系統:Windows8.1

顯卡:Nivida GTX965M

開發工具:Visual Studio 2017


與之前的圖像API不同,Vulkan中的著色器代碼必須以二進制字節碼的格式使用,而不是像GLSLHLSL這樣具有比較好的可讀性的語法。此字節格式成為SPIR-V,它可以與Vulkan和OpenCL一同使用。這是一種可以編寫圖形和計算著色器的格式,但我們重點介紹本教程中Vulkan圖形流水線使用的著色器。

使用二進制字節碼格式的優點之一是 使得GPU廠商編寫將著色器代碼轉換為本地代碼的編譯器復雜度減少了很多。經驗表明使用可讀性比較強的語法,比如GLSL一些GPU廠商相當靈活地理解這個標準。這導致一種情況會發生,比如編寫好,並在一個廠商的GPU運行的不錯的著色器程序,可能在其他的GPU廠商的GPU驅動程序運行異常,可能是語法的問題,或者更糟的是不同GPU廠商編寫的編譯器差異,導致著色器運行錯誤。如果直接使用編譯好的二進制字節碼格式,可以避免這種情況。

但是,並不意味著我們要手寫字節碼。Khronos發布了與廠商無關的編譯器,它將GLSL編譯成SPIR-V。該編譯器用於驗證著色器代碼是否符合標準,並生成與Vulkan功能運行的SPRIR-V二進制文件。除此之外還可以將此編譯器作為庫在運行時編譯生成SPRI-V,但在本教程中不會這樣操作。編譯器glslangValidator.exe包含在LunarG SDK中,因此不需要下載任何額外的內容。

GLSL是具有C風格語法的著色語言。在程序中需要定義編寫main函數作為入口。GLSL不會使用輸入參數和返回值作為輸出,而是使用全局變量來處理輸入和輸出。該語言包括很多功能簡化圖形編程,比如內置的基於向量和矩陣的叉積操作函數,矩陣和矢量乘法操作函數。矢量類型為vec

,數字表示元素的數量。例如3D位置存儲在vec3中。可以通過諸如.x之類的成員訪問單個組件,也可以通過多個組件創建一個新的向量。比如,表達式vec3(1.0, 2.0, 3.0).xy截取前兩個元素,並賦予新的vec2中。向量的構造函數也可以采用矢量對象和標量值的組合。比如vec3可以用vec3(vec2(1.0, 2.0), 3.0)構造。

如前面提到的一樣,我們需要編寫一個vertex shader和一個fragment shader繪制三角形在屏幕。下面兩個小節會探討與之相關的GLSL代碼,並展示如何生成兩個SPIR-V二進制文件,最後加載到程序中。

Vertex shader


頂點著色器處理每一個頂點數據。它的屬性,如世界坐標,顏色,法線和紋理UV坐標作為輸入。輸出的是最終的clip coordinates 裁剪坐標和需要傳遞到片元著色器的屬性,包括顏色和紋理UV坐標。這些值會在光柵化階段進行內插值,以產生平滑的過度。

裁剪坐標是將世界坐標等轉換,映射、投影到緩沖區範圍為[-1, 1]的[-1, 1]坐標系統的步驟,如下所示:

技術分享

如果之前的計算機圖形比較熟悉的話,對這部分會比較熟悉。如果你之前使用過OpenGL,你會註意到Y坐標軸是反轉的,Z坐標軸的範圍與Direct3D是一致的範圍,從0到1.

對於第一個三角形,我們不會做任何轉換操作,我們直接在裁剪坐標中指定三個頂點的位置,創建如下圖形:

技術分享

通常情況下頂點坐標數據是存儲在一個頂點緩沖區中,但是在Vulkan中創建一個頂點緩沖區並填充數據的過程並不是直接的。所以我們後置這些步驟,直到我們滿意的看到一個三角形出現在屏幕上。同時我們需要做一些非正統的事情:將坐標直接包含在頂點著色器的內部。代碼如下所示:

#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
    vec4 gl_Position;
};

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}

main函數的執行應用於每個頂點,內置的gl_VertexIndex變量包含了當前頂點的索引信息。通常是頂點緩沖區的索引,但是在這裏我們硬編碼到頂點數據的集合中。每個頂點的位置從常量數組中訪問,並與zw分量組合使用,以產生裁剪坐標中的有效位置信息。內置的gl_Position變量作為輸出。最後Vulkan中使用shader,需要確保GL_ARG_separate_shader_objects擴展開啟。

Fragment shader


由頂點著色器的位置數據形成的三角形用片段著色器填充屏幕上的區域中。片段著色器針對一個或者多個framebuffer幀緩沖區的每個片元產生具體的顏色和深度信息。一個簡單的片段著色器為完成的三角形輸出紅色信息的代碼如下:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1.0, 0.0, 0.0, 1.0);
}

fragment sahder中的main函數與vertex shader中的main函數類似,會為每一個片元調用處理。顏色的信息在GLSL中是4個元素矢量,包括R,G,B和Alpha通道,值域收斂在[0, 1]範圍內。不像頂點著色器的gl_Position,它沒有內置的變量為當前片元輸出顏色信息。在這裏必須為framebuffer定義輸出變量,layout(location = 0)修飾符明確framebuffer的索引。紅色信息寫進outColor變量中,該變量鏈接第一個framebuffer中,索引為0

Per-vertex colors


一個純紅色的三角形看起來並不是很酷炫,為什麽不試著酷炫一些呢?

技術分享

我們針對兩個類型的著色器嘗試做一些改變,完成上圖的效果。首先,我們需要為每個頂點設置差異化的顏色。頂點著色器應該包含一個顏色數組,就像位置信息的數組一樣:

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

現在我們需要把每個頂點的顏色傳遞到片段著色器中,從而輸出經過插值後的顏色信息到framebuffer中。為頂點著色器添增加輸出顏色支持,在main函數中定義如下:

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

下一步,我們需要將片段著色器的輸入匹配頂點著色器的輸出:

layout(location = 0) in vec3 fragColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

輸入的變量不一定要同名,它們將通過location索引指令鏈接在一起。main函數中修改將要輸出的顏色alpha值。就像之前討論的一樣,fragColor將會為三個頂點所屬的片元自動進行內插值,形成平滑的顏色過度。

Compiling the shaders


在項目根目錄下創建一個子目錄,名shaders用於存儲頂點著色器文件shader.vert和片段著色器文件shader.frag。GLSL著色器官方沒有約定的擴展名,但是這兩個擴展名是比較普遍通用的。

shader.vert內容如下:

#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
    vec4 gl_Position;
};

layout(location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

shader.frag文件內容如下:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

現在我們嘗試使用glslangValidator程序編譯SPIR-V二進制碼。

Windows

創建一個compile.bat批處理文件,內容如下:

C:/VulkanSDK/1.0.17.0/Bin32/glslangValidator.exe -V shader.vert
C:/VulkanSDK/1.0.17.0/Bin32/glslangValidator.exe -V shader.frag
pause

glslangValidator.exe的path路徑替換為你的VulkanSDK安裝路徑,然後雙擊該文件運行。

End of platform-specific instructions

這兩個命令使用-V標誌調用編譯器,該標誌告訴它將GLSL源文件編譯為SPIR-V字節碼。運行編譯腳本時,會看到創建了兩個SPIR-V二進制文件:vert.spvfrag.spv。這些名稱從著色器中派生而來,但是可以重命名為任何名字。在編譯著色器時,可能收到關於某些功能缺失的警告信息,在這裏放心的忽略它們。

如果著色器包含語法錯誤,那麽編譯器會按照您的預期告訴具體的行號和問題。嘗試省略一個分號,然後重新運行編譯腳本。還可以嘗試運行編譯器,而無需任何參數來查看它支持哪些類型的標誌。例如,它可以將字節碼輸出為可讀的格式,以便準確了解著色器正在執行的操作以及在此階段應用的任何優化。

Loading a shader


現在我們有一種產生SPIR-V著色器的方法,是時候加載它們到我們的程序中,以便在適當的時候插入到圖形管線中。首先我們編寫一個輔助函數用以加載二進制數據文件。

#include <fstream>

...

static std::vector<char> readFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open()) {
        throw std::runtime_error("failed to open file!");
    }
}

readFile函數將會從文件中讀取所有的二進制數據,並用std::vector字節集合管理。我們使用兩個標誌用以打開文件:

  • ate:在文件末尾開始讀取
  • binary:以二進制格式去讀文件(避免字符格式的轉義)

從文件末尾開始讀取的優點是我們可以使用讀取位置來確定文件的大小並分配緩沖區:

size_t fileSize = (size_t) file.tellg();
std::vector<char> buffer(fileSize);

之後我們可以追溯到文件的開頭,同時讀取所有的字節:

file.seekg(0);
file.read(buffer.data(), fileSize);

最後關閉文件,返回字節數據:

file.close();

return buffer;

我們調用函數createGraphicsPipeline加載兩個著色器的二進制碼:

void createGraphicsPipeline() {
    auto vertShaderCode = readFile("shaders/vert.spv");
    auto fragShaderCode = readFile("shaders/frag.spv");
}

確保著色器正確加載,並打印緩沖區的大小是否與文件實際大小一致。

Creating shader modules


在將代碼傳遞給渲染管線之前,我們必須將其封裝到VkShaderModule對象中。讓我們創建一個輔助函數createShaderModule實現該邏輯。

VkShaderModule createShaderModule(const std::vector<char>& code) {

}

該函數需要字節碼的緩沖區作為參數,並通過緩沖區創建VkShaderModule

創建shader module是比較簡單的,我們僅僅需要指定二進制碼緩沖區的指針和它的具體長度。這些信息被填充在VkShaderModuleCreateInfo結構體中。需要留意的是字節碼的大小是以字節指定的,但是字節碼指針是一個uint32_t指針,而不是一個char指針。因此我們需要做臨時拷貝操作,將二進制碼拷貝到一個uint32_t內存對齊的容器中:

VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();

std::vector<uint32_t> codeAligned(code.size() / sizeof(uint32_t) + 1);
memcpy(codeAligned.data(), code.data(), code.size());
createInfo.pCode = codeAligned.data();

調用vkCreateShaderMoudle創建VkShaderModule:

VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
    throw std::runtime_error("failed to create shader module!");
}

參數與之前創建對象功能類似:邏輯設備,創建對象信息結構體的指針,自定義分配器和保存結果的句柄變量。在shader module創建完畢後,可以對二進制碼的緩沖區進行立即的釋放。最後不要忘記返回創建好的shader module。

return shaderModule;

shader module對象僅僅在渲染管線處理過程中需要,所以我們會在createGraphicsPipeline函數中定義本地變量保存它們,而不是定義類成員變量持有它們的句柄:

VkShaderModule vertShaderModule;
VkShaderModule fragShaderModule;

調用加載shader module的輔助函數:

vertShaderModule = createShaderModule(vertShaderCode);
fragShaderModule = createShaderModule(fragShaderCode);

在圖形管線創建完成且createGraphicsPipeline函數返回的時候,它們應該被清理掉,所以在該函數後刪除它們:

 ...
    vkDestroyShaderModule(device, fragShaderModule, nullptr);
    vkDestroyShaderModule(device, vertShaderModule, nullptr);
}

Shader stage creation


VkShaderModule對象只是字節碼緩沖區的一個包裝容器。著色器並沒有彼此鏈接,甚至沒有給出目的。通過VkPipelineShaderStageCreateInfo結構將著色器模塊分配到管線中的頂點或者片段著色器階段。VkPipelineShaderStageCreateInfo結構體是實際管線創建過程的一部分。

我們首先在createGraphicsPipeline函數中填寫頂點著色器結構體。

VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;

除了強制的sType成員外,第一個需要告知Vulkan將在哪個流水線階段使用著色器。在上一個章節的每個可編程階段都有一個對應的枚舉值。

vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";

接下來的兩個成員指定包含代碼的著色器模塊和調用的主函數。這意味著可以將多個片段著色器組合到單個著色器模塊中,並使用不同的入口點來區分它們的行為。在這種情況下,我們堅持使用標準main函數作為入口。

還有一個可選成員,pSpecializationInfo,在這裏我們不會使用它,但是值得討論一下。它允許為著色器指定常量值。使用單個著色器模塊,通過為其中使用不同的常量值,可以在流水線創建時對行為進行配置。這比在渲染時使用變量配置著色器更有效率,因為編譯器可以進行優化,例如消除if值判斷的語句。如果沒有這樣的常量,可以將成員設置為nullptr,我們的struct結構體初始化自動進行。

修改結構體滿足片段著色器的需要:

VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";

完成兩個結構體的創建,並通過數組保存,這部分引用將會在實際的管線創建開始。

VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};

到此為止,就是所以關於可編程管線階段的邏輯。在下一章節我們會看一下固定管線各個階段。

Vulkan Tutorial 11 Shader modules