目標很單純:把你昨天(Day 12)的骨架,從「清畫面」升級到「真的畫出第一個三角形」。
我們會做 4 件事:(1) 準備 GLSL → SPIR-V、(2) 建 Shader Module、(3) 建 Graphics Pipeline、(4) 在 Render Pass 裡下指令vkCmdDraw
。
特別註明:本篇不使用 Dynamic Rendering,完全走傳統 Render Pass + Framebuffer 流程。
你已經有這些成員與物件(名稱可不同):
VkDevice device
、VkPhysicalDevice phys
、VkQueue graphicsQ/presentQ
VkRenderPass renderPass
(單一 color 附件;finalLayout=VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
)std::vector<VkImageView> swapViews
、std::vector<VkFramebuffer> framebuffers
、VkExtent2D swapExtent
VkCommandPool cmdPool
、std::vector<VkCommandBuffer> cmdBufs
VkSemaphore semImage/semRender
、VkFence inFlight
drawFrame()
主迴圈(Acquire → Record → Submit → Present)若你昨天照做,這些都齊全。沒有的話先回去把「清畫面」跑通。
一開始先用最簡單版本:不用頂點緩衝,直接靠 gl_VertexIndex
在 VS 裡產生三個頂點。顏色在 VS 設,FS 原封不動輸出。
shaders/triangle.vert
#version 450
layout(location = 0) out vec3 vColor;
vec2 pos[3] = vec2[](
vec2( 0.0, -0.6),
vec2( 0.6, 0.6),
vec2(-0.6, 0.6)
);
vec3 col[3] = vec3[](
vec3(1.0, 0.2, 0.2),
vec3(0.2, 1.0, 0.2),
vec3(0.2, 0.4, 1.0)
);
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
vColor = col[gl_VertexIndex];
}
shaders/triangle.frag
#version 450
layout(location = 0) in vec3 vColor;
layout(location = 0) out vec4 outColor;
void main() { outColor = vec4(vColor, 1.0); }
用 glslc 編成 SPIR-V(Vulkan 要吃 SPIR-V):
glslc shaders/triangle.vert -o shaders/triangle.vert.spv
glslc shaders/triangle.frag -o shaders/triangle.frag.spv
小提醒
- 建議把這兩行寫進你的 build scripts(例如 CMake custom command)。
- 之後你也可以用 HLSL → DXC → SPIR-V;流程相同。
讀 .spv
檔到記憶體,呼叫 vkCreateShaderModule
。建立好後可馬上銷毀 Module,因為 Pipeline 會把內容吃進去。
#include <fstream>
static std::vector<char> readFile(const char* path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if(!f) throw std::runtime_error("open file failed");
size_t sz = (size_t)f.tellg();
std::vector<char> buf(sz);
f.seekg(0); f.read(buf.data(), sz);
return buf;
}
VkShaderModule makeShaderModule(const std::vector<char>& code) {
VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
ci.codeSize = code.size();
ci.pCode = reinterpret_cast<const uint32_t*>(code.data());
VkShaderModule m{}; VK_CHECK(vkCreateShaderModule(device, &ci, nullptr, &m));
return m;
}
本例先不使用 Descriptor/Push Constant,所以 Pipeline Layout 是空的:
VkPipelineLayout pipelineLayout{};
void createPipelineLayout() {
VkPipelineLayoutCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
VK_CHECK(vkCreatePipelineLayout(device, &ci, nullptr, &pipelineLayout));
}
這一步把「著色器、輸入拓撲、視口/裁切、光柵化、混色……」打包成 VkPipeline。
注意:必須指定renderPass
和subpass=0
(我們不用 Dynamic Rendering)。
VkPipeline pipeline{};
void createGraphicsPipeline() {
// 4.1 Shader stages
auto vsCode = readFile("shaders/triangle.vert.spv");
auto fsCode = readFile("shaders/triangle.frag.spv");
VkShaderModule vert = makeShaderModule(vsCode);
VkShaderModule frag = makeShaderModule(fsCode);
VkPipelineShaderStageCreateInfo vs{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
vs.stage = VK_SHADER_STAGE_VERTEX_BIT; vs.module = vert; vs.pName = "main";
VkPipelineShaderStageCreateInfo fs{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
fs.stage = VK_SHADER_STAGE_FRAGMENT_BIT; fs.module = frag; fs.pName = "main";
VkPipelineShaderStageCreateInfo stages[] = { vs, fs };
// 4.2 Vertex input(本例不用頂點緩衝,統統空)
VkPipelineVertexInputStateCreateInfo vi{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
// 4.3 組裝拓撲:三角形
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
// 4.4 視口/裁切
VkViewport viewport{};
viewport.x = 0; viewport.y = 0;
viewport.width = (float)swapExtent.width;
viewport.height = (float)swapExtent.height;
viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f;
VkRect2D scissor{{0,0}, swapExtent};
VkPipelineViewportStateCreateInfo vp{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vp.viewportCount = 1; vp.pViewports = &viewport;
vp.scissorCount = 1; vp.pScissors = &scissor;
// 4.5 光柵化(先不剔除背面,避免初學遇到繪製方向的困擾)
VkPipelineRasterizationStateCreateInfo rs{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
rs.polygonMode = VK_POLYGON_MODE_FILL;
rs.cullMode = VK_CULL_MODE_NONE;
rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rs.lineWidth = 1.0f;
// 4.6 取樣(本例不開 MSAA)
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// 4.7 顏色混色(不混,直接覆蓋)
VkPipelineColorBlendAttachmentState att{};
att.colorWriteMask = VK_COLOR_COMPONENT_R_BIT|VK_COLOR_COMPONENT_G_BIT|
VK_COLOR_COMPONENT_B_BIT|VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo bs{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
bs.attachmentCount = 1; bs.pAttachments = &att;
// 4.8 組起來!
VkGraphicsPipelineCreateInfo gp{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gp.stageCount = 2; gp.pStages = stages;
gp.pVertexInputState = &vi;
gp.pInputAssemblyState = &ia;
gp.pViewportState = &vp;
gp.pRasterizationState = &rs;
gp.pMultisampleState = &ms;
gp.pColorBlendState = &bs;
gp.layout = pipelineLayout;
gp.renderPass = renderPass; // ★ 傳統路徑要指定
gp.subpass = 0;
VK_CHECK(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gp, nullptr, &pipeline));
// Shader modules 用完即可釋放
vkDestroyShaderModule(device, vert, nullptr);
vkDestroyShaderModule(device, frag, nullptr);
}
為什麼不要剔除背面?
Vulkan 的「正面方向」與你設定的視口/數學手性有關。初學者常因繪製順序(CW/CCW)不一致,導致三角形被剔除。先把cullMode=NONE
,就不會被絆倒。
把你原本「清畫面」的 recordCmd(imageIndex)
改成下面版本:
(沿用 Render Pass,不做任何 Dynamic Rendering 的函式)
void recordCmd(uint32_t imageIndex) {
VkCommandBuffer cmd = cmdBufs[imageIndex];
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(cmd, &bi));
// 清為深藍色;Render Pass 會自動處理附件 layout 轉換
VkClearValue clear; clear.color = {{0.05f, 0.25f, 0.9f, 1.0f}};
VkRenderPassBeginInfo rbi{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
rbi.renderPass = renderPass;
rbi.framebuffer = framebuffers[imageIndex];
rbi.renderArea = {{0,0}, swapExtent};
rbi.clearValueCount = 1; rbi.pClearValues = &clear;
vkCmdBeginRenderPass(cmd, &rbi, VK_SUBPASS_CONTENTS_INLINE);
// 綁定 graphics pipeline,畫 3 個頂點
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdDraw(cmd, /*vertexCount*/3, /*instanceCount*/1, /*firstVertex*/0, /*firstInstance*/0);
vkCmdEndRenderPass(cmd);
VK_CHECK(vkEndCommandBuffer(cmd));
}
你的 drawFrame()
幀流程可以維持昨天那套(Acquire → Reset/Record → Submit → Present)—
只要把錄製這行換成上面新版的 recordCmd(imageIndex)
即可。
成果:
建立階段(一次性)建議在 swapchain/FB 之後:
createPipelineLayout();
createGraphicsPipeline();
銷毀階段(與其他資源一起):
if (pipeline) vkDestroyPipeline(device, pipeline, nullptr);
if (pipelineLayout) vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
重建 swapchain(視窗大小變了):
swapExtent
改了(viewport/scissor 也跟著改),pipeline 也要重建(最簡單是:先砍掉舊的,再呼叫 createGraphicsPipeline()
取得新的)。畫面全黑
renderPass
與 pipeline 指定的 renderPass
不相容?請確認 pipeline 的 gp.renderPass = renderPass
。vkCmdBindPipeline
就 vkCmdDraw
。clearValueCount
數量與附件數不一致(本例 1 個 color)。Validation Layers 抱怨 subpass/attachment
VkAttachmentDescription
的 format
是否等於你的 swapFormat
。finalLayout
應為 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
(給 Present 用)。三角形不見/被剔除
cullMode = VK_CULL_MODE_NONE
;或調整 frontFace
/頂點順序。視窗改大小後卡著
VK_ERROR_OUT_OF_DATE_KHR
或 VK_SUBOPTIMAL_KHR
時,記得 vkDeviceWaitIdle
,然後重建 swapchain、image views、framebuffers,以及 pipeline。glslc 找不到
sin(time)
做漸變色(把時間透過 push constant 傳進去;需要重建 pipeline layout)。VkBuffer
,練習 vkCmdBindVertexBuffers
。cullMode=BACK
,然後試著調整頂點順序,看什麼時候會被剔掉。畫第一個三角形的最核心步驟:
「GLSL → SPIR-V → Shader Module → Pipeline Layout → Graphics Pipeline(綁定 render pass)→ 在 Render Pass 裡BindPipeline + Draw(3)
」。
這一步做完,你已經跨過 Vulkan 的第一個門檻。接著你能把頂點放進緩衝、加 Depth、加 Descriptor,往真正的場景邁進!