Hari ini saya ingin menyajikan terjemahan dari bab baru di bagian dasar-dasar pipeline Graphics yang disebut Fungsi tetap.
Kandungan
Tahapan pipeline yang tidak dapat diprogram
- Masukan simpul
- Perakit masukan
- Area pandang dan gunting
- Rasterizer
- Multisampling
- Tes kedalaman dan tes stensil
- Pencampuran warna
- Keadaan dinamis
- Tata Letak Pipa
- Kesimpulan
API grafik awal menggunakan status default untuk sebagian besar tahapan pipeline grafik. Di Vulkan, semua status harus dijelaskan secara eksplisit, dimulai dengan ukuran viewport dan diakhiri dengan fungsi pencampuran warna. Dalam bab ini, kami akan menyiapkan tahapan pipeline yang tidak dapat diprogram.
Masukan simpul
Struktur VkPipelineVertexInputStateCreateInfo mendeskripsikan format data vertex yang diteruskan ke vertex shader. Ada dua jenis deskripsi:
- Deskripsi atribut: tipe data yang diteruskan ke vertex shader, mengikat ke buffer data dan offset di dalamnya
- Binding: jarak antara item data dan bagaimana data dan geometri keluaran terikat (per-instance atau vertex binding) (lihat Contoh geometri )
Karena kami melakukan hardcode data vertex di vertex shader, kami akan menunjukkan bahwa tidak ada data untuk dimuat. Untuk melakukan ini, mari isi strukturnya
VkPipelineVertexInputStateCreateInfo
. Kita akan kembali ke pertanyaan ini nanti di bab tentang buffer vertex.
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional
Anggota
pVertexBindingDescriptions
dan
pVertexAttributeDescriptions
tunjuk ke larik struktur yang mendeskripsikan data di atas untuk memuat atribut simpul. Tambahkan struktur ini ke fungsi
createGraphicsPipeline
tepat setelahnya
shaderStages
.
Perakit masukan
Struktur VkPipelineInputAssemblyStateCreateInfo menjelaskan 2 hal: geometri apa yang terbentuk dari simpul dan apakah memulai ulang geometri diperbolehkan untuk geometri seperti strip garis dan strip segitiga. Geometri ditunjukkan di lapangan
topology
dan dapat memiliki nilai berikut:
VK_PRIMITIVE_TOPOLOGY_POINT_LIST
: geometri digambar sebagai titik-titik terpisah, setiap simpul merupakan titik yang terpisahVK_PRIMITIVE_TOPOLOGY_LINE_LIST
: geometri digambar sebagai satu set segmen garis, setiap pasang simpul membentuk garis terpisahVK_PRIMITIVE_TOPOLOGY_LINE_STRIP
: geometri digambar sebagai polyline kontinu, setiap simpul berikutnya menambahkan satu segmen ke polylineVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
: geometri digambar sebagai satu set segitiga, dengan setiap 3 simpul membentuk segitiga independenVK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
: ,
Biasanya, simpul dimuat secara berurutan dalam urutan yang Anda letakkan di buffer simpul. Namun, dengan buffer indeks, Anda dapat mengubah urutan pemuatan. Ini memungkinkan pengoptimalan seperti menggunakan kembali simpul. Jika Anda
primitiveRestartEnable
menentukan nilai di lapangan
VK_TRUE
, Anda dapat menyela garis dan segitiga dengan topologi
VK_PRIMITIVE_TOPOLOGY_LINE_STRIP
dan
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
dan mulai menggambar primitif baru menggunakan indeks khusus
0xFFFF
atau
0xFFFFFFFF
.
Dalam tutorial, kita akan menggambar segitiga individu, jadi kita akan menggunakan struktur berikut:
VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; inputAssembly.primitiveRestartEnable = VK_FALSE;
Area pandang dan gunting
Area pandang mendeskripsikan area framebuffer yang menjadi tujuan perenderan output. Hampir selalu koordinat dari
(0, 0)
hingga ditetapkan untuk area pandang
(width, height)
.
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float) swapChainExtent.width;
viewport.height = (float) swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
Perlu diketahui bahwa ukuran rantai pertukaran dan gambar mungkin berbeda dari nilai
WIDTH
dan
HEIGHT
jendelanya. Nanti, gambar dari swap chain akan digunakan sebagai framebuffers, jadi kita harus menggunakan ukurannya persis.
minDepth
dan
maxDepth
menentukan kisaran nilai kedalaman untuk framebuffer. Nilai ini harus dalam kisaran
[0,0f, 1,0f]
, dan
minDepth
mungkin lebih
maxDepth
. Gunakan nilai default -
0.0f
dan
1.0f
jika Anda tidak akan melakukan sesuatu yang tidak biasa.
Jika viewport menentukan bagaimana gambar akan direntangkan di framebuffer, maka gunting menentukan piksel mana yang akan disimpan. Semua piksel di luar kotak gunting akan dibuang selama rasterisasi. Persegi panjang kliping digunakan untuk memotong gambar, bukan mengubahnya. Perbedaannya ditunjukkan pada gambar di bawah ini. Harap dicatat bahwa persegi panjang kliping di sebelah kiri hanyalah salah satu dari banyak kemungkinan opsi untuk mendapatkan gambar seperti itu, selama ukurannya lebih besar dari ukuran viewport.
Dalam tutorial ini, kami ingin merender gambar ke seluruh framebuffer, jadi kami akan menentukan bahwa persegi panjang gunting sepenuhnya tumpang tindih dengan viewport:
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
Sekarang kita perlu menggabungkan informasi tentang viewport dan gunting menggunakan struktur VkPipelineViewportStateCreateInfo . Pada beberapa kartu video, beberapa area pandang dan persegi panjang kliping dapat digunakan secara bersamaan, sehingga informasi tentangnya ditransmisikan sebagai larik. Untuk menggunakan beberapa area pandang sekaligus, Anda perlu mengaktifkan opsi GPU yang sesuai.
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
Rasterizer
Rasterizer mengubah geometri dari shader vertex menjadi beberapa fragmen. Uji kedalaman , pemusnahan wajah , uji gunting juga dilakukan di sini, dan metode pengisian poligon dengan fragmen dikonfigurasikan: mengisi seluruh poligon, atau hanya tepi poligon (rendering bingkai gambar). Semua ini dikonfigurasi dalam struktur VkPipelineRasterizationStateCreateInfo .
VkPipelineRasterizationStateCreateInfo rasterizer{}; rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizer.depthClampEnable = VK_FALSE;
Jika bidang
depthClampEnable
ditetapkan
VK_TRUE
, fragmen yang berada di luar bidang dekat dan jauh, tidak terputus, dan mendorongnya. Ini bisa berguna, misalnya, saat membuat peta bayangan. Untuk menggunakan parameter ini, Anda harus mengaktifkan opsi GPU yang sesuai.
rasterizer.rasterizerDiscardEnable = VK_FALSE;
Jika
rasterizerDiscardEnable
disetel
VK_TRUE
, tahap rasterisasi dinonaktifkan dan tidak ada keluaran yang diteruskan ke framebuffer.
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
polygonMode
menentukan bagaimana potongan dihasilkan. Mode berikut tersedia:
VK_POLYGON_MODE_FILL
: poligon terisi penuh dengan fragmenVK_POLYGON_MODE_LINE
: tepi poligon diubah menjadi garisVK_POLYGON_MODE_POINT
: simpul poligon digambar sebagai titik
Untuk menggunakan mode ini, kecuali
VK_POLYGON_MODE_FILL
, Anda perlu mengaktifkan opsi GPU yang sesuai.
rasterizer.lineWidth = 1.0f;
Bidang
lineWidth
mengatur ketebalan segmen. Lebar potongan maksimum yang didukung bergantung pada perangkat keras Anda, dan potongan yang lebih tebal
1,0f
memerlukan opsi GPU untuk diaktifkan
wideLines
.
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
Parameter tersebut
cullMode
menentukan jenis pemusnahan wajah. Anda dapat menonaktifkan pemangkasan seluruhnya, atau mengaktifkan pemangkasan untuk bagian depan dan / atau non-depan. Variabel
frontFace
menentukan urutan simpul dilintasi (searah jarum jam atau berlawanan arah jarum jam) untuk menentukan permukaan depan.
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional
Rasterizer dapat mengubah nilai kedalaman dengan menambahkan nilai konstan atau mengimbangi kedalaman bergantung pada kemiringan fragmen. Ini biasanya digunakan saat membuat peta bayangan. Kami tidak membutuhkan ini, jadi kami akan
depthBiasEnable
menginstalnya untuk
VK_FALSE
.
Multisampling
Struktur VkPipelineMultisampleStateCreateInfo mengonfigurasi multisampling - salah satu metode anti-aliasing . Ini bekerja terutama di bagian tepi, menggabungkan warna dari poligon berbeda yang di-raster menjadi piksel yang sama. Ini memungkinkan Anda menyingkirkan artefak yang paling terlihat. Keuntungan utama multisampling adalah bahwa dalam banyak kasus shader fragmen dijalankan hanya sekali per piksel, yang jauh lebih baik, misalnya, daripada merender pada resolusi yang lebih tinggi dan kemudian melakukan pengecilan. Untuk menggunakan multisampling, Anda harus mengaktifkan opsi GPU yang sesuai.
VkPipelineMultisampleStateCreateInfo multisampling{};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // Optional
multisampling.pSampleMask = nullptr; // Optional
multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional
Sampai kami memasukkannya, kami akan kembali ke salah satu artikel berikut.
Tes kedalaman dan tes stensil
Saat menggunakan buffer kedalaman dan / atau buffer stensil, Anda perlu mengonfigurasinya menggunakan VkPipelineDepthStencilStateCreateInfo . Kami belum membutuhkan ini, jadi kami akan meneruskannya
nullptr
alih-alih penunjuk ke struktur ini. Kami akan kembali ke ini di bab tentang buffer kedalaman.
Pencampuran warna
Warna yang dikembalikan oleh shader fragmen perlu digabungkan dengan warna yang sudah ada di framebuffer. Proses ini disebut pencampuran warna, dan ada dua cara untuk melakukannya:
- Campurkan nilai lama dan baru untuk mendapatkan warna keluaran
- Gabungkan nilai lama dan baru menggunakan operasi bitwise
Dua jenis struktur digunakan untuk mengonfigurasi pencampuran warna: struktur VkPipelineColorBlendAttachmentState berisi setelan untuk setiap framebuffer yang terhubung, struktur VkPipelineColorBlendStateCreateInfo berisi setelan pencampuran warna global. Dalam kasus kami, hanya satu framebuffer yang digunakan:
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional
Strukturnya
VkPipelineColorBlendAttachmentState
memungkinkan Anda menyesuaikan pencampuran warna dengan cara pertama. Pseudocode berikut adalah demonstrasi terbaik dari semua operasi yang dilakukan:
if (blendEnable) {
finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);
finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);
} else {
finalColor = newColor;
}
finalColor = finalColor & colorWriteMask;
Jika
blendEnable
disetel
VK_FALSE
, warna dari shader fragmen diteruskan tanpa perubahan. Jika disetel
VK_TRUE
, dua operasi pencampuran digunakan untuk menghitung warna baru. Warna akhir disaring menggunakan
colorWriteMask
untuk menentukan saluran mana dari gambar keluaran yang sedang ditulis.
Pencampuran warna yang paling umum adalah pencampuran alfa, di mana warna baru dicampur dengan warna lama berdasarkan transparansi.
finalColor
dihitung sebagai berikut:
finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;
Ini dapat dikonfigurasi menggunakan opsi berikut:
colorBlendAttachment.blendEnable = VK_TRUE; colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
Semua operasi yang mungkin dapat ditemukan di VkBlendFactor dan VkBlendOp mantri dalam spesifikasi.
Struktur kedua mengacu pada larik struktur untuk semua framebuffers dan memungkinkan penetapan konstanta pencampuran yang dapat digunakan sebagai faktor pencampur dalam perhitungan di atas.
VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f; // Optional
colorBlending.blendConstants[1] = 0.0f; // Optional
colorBlending.blendConstants[2] = 0.0f; // Optional
colorBlending.blendConstants[3] = 0.0f; // Optional
Jika Anda ingin menggunakan metode pencampuran kedua (operasi bitwise), atur
VK_TRUE
ke
logicOpEnable
. Kemudian Anda dapat menentukan operasi bitwise di lapangan
logicOp
. Perhatikan bahwa metode pertama secara otomatis menjadi tidak tersedia, seolah-olah masing-masing yang terhubung ke framebuffer
blendEnable
telah ditemukan
VK_FALSE
! Perhatikan bahwa
colorWriteMask
ini juga digunakan untuk operasi bitwise untuk menentukan konten saluran mana yang akan diubah. Anda dapat mematikan kedua mode, seperti yang kami lakukan, dalam hal ini warna fragmen akan ditulis ke framebuffer tanpa perubahan.
Keadaan dinamis
Beberapa status pipeline grafis dapat diubah tanpa membuat ulang pipeline, seperti ukuran viewport, lebar potongan, dan konstanta pencampuran. Untuk melakukannya, isi struktur VkPipelineDynamicStateCreateInfo :
VkDynamicState dynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_LINE_WIDTH
};
VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;
Akibatnya, nilai pengaturan ini tidak diperhitungkan pada tahap pembuatan pipeline, dan Anda perlu menentukannya tepat pada saat rendering. Kami akan kembali ke ini di bab berikutnya. Anda dapat menggunakan
nullptr
sebagai pengganti penunjuk ke struktur ini jika Anda tidak ingin menggunakan status dinamis.
Tata Letak Pipa
Di shader, Anda dapat menggunakan
uniform
-variabel - variabel global yang dapat diubah secara dinamis untuk mengubah perilaku shader tanpa harus membuatnya kembali. Mereka biasanya digunakan untuk meneruskan matriks transformasi ke shader vertex atau untuk membuat sampler tekstur di shader fragmen.
Seragam ini harus ditentukan saat membuat pipeline menggunakan objek VkPipelineLayout . Meskipun kami tidak akan menggunakan variabel ini untuk saat ini, kami masih perlu membuat tata letak pipeline kosong.
Mari buat anggota kelas untuk menampung objek, karena nanti kita akan merujuknya dari fungsi lain:
VkPipelineLayout pipelineLayout;
Kemudian mari kita buat sebuah objek dalam sebuah fungsi
createGraphicsPipeline
:
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // Optional
pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional
if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
throw std::runtime_error("failed to create pipeline layout!");
}
Struktur tersebut juga menetapkan konstanta push, yang merupakan cara lain untuk meneruskan variabel dinamis ke shader. Kami akan mengenal mereka nanti. Kami akan menggunakan pipeline selama seluruh siklus hidup program, jadi kami harus menghancurkannya di bagian paling akhir:
void cleanup() {
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
...
}
Kesimpulan
Hanya itu yang perlu diketahui tentang status yang tidak dapat diprogram! Butuh banyak pekerjaan untuk menyiapkannya dari awal, tetapi sekarang Anda tahu hampir semua yang terjadi di pipeline grafis!
Untuk membuat pipeline grafis, tetap membuat objek terakhir - render pass.