Vjetromjer
Metoda postaviVertice()
Stožac
std::vector verticesStozac = {};
float rStozac = 1;
float hStozac = rStozac * 2;
float nStozac = 15;
Valjak
std::vector verticesValjak = {};
std::vector verticesMaliValjci = {};
//valjak na stozcu
float hPomakValjak = hStozac * 0.7f;
float hValjak = hStozac - hPomakValjak;
float rValjak = 1.0f/2.0f * hValjak;
float nValjak = nStozac;
//valjak mali
float hPomakMaliValjak = hPomakValjak+ hValjak*(1.0f/2.0f);
float hValjakMali = hStozac * 0.6f;
float rValjakMali = rStozac * 0.1f;
float kutIzmedu = 120;
Polukugla
std::vector verticesKugla = {};
//kugle
float rKugla = rStozac * 0.4f;
float mKugla = 10;
Bitni dijelovi koda
Općenito Vertex-i služe za spremanje vrhova koje kasnije šaljemo vertex shaderu.
Kao što i naziv sugerira radi se o strukturi koja ima određeni broj vektora
od kojih svaki predstavlja jedan vrh. U našem slučaju imamo samo dvije vrste vektora:
jedan za poziciju i jedan za boju.
Kako bi koristili ovu strukturu moramo ju vezati (bind), informacije o tome dajemo
kroz opis vezanja u kojem određujemo veličinu koja se učitava sa svakim vertexom,
kod nas je to veličina same strukture (dva vektora), što znači da će se vrhovi
učitavati po toj veličini, jedan vektor pozicije i jedan vektor boje
(broj bajtova između dva člana). Također, zadajemo da li idemo na sljedeći element
nakon svakog vertexa ili nakon svake instance (jedinke). U našem slučaju nemamo instance,
pa je zato zadano da bude nakon svakog vertexa.
Ispod toga imamo polje opisa atributa u kojem zapravo definiramo točku vezanja, lokaciju,
format i offset pojedinih atributa. Preko lokacije vertex shader pristupa određenim vertexima,
za poziciju je lokacija 0, a za boju jest 1. Također su određeni njihovi offseti, udaljenost u
bajtovima od početka strukture, za poziciju to je 0, ali koristimo metodu koja to izračunava,
pa ne moramo razmišljati o tome.
Vertex
struct Vertex {
glm::vec4 pos;
glm::vec3 color;
static VkVertexInputBindingDescription getBindingDescription() {
VkVertexInputBindingDescription bindingDescription{};
bindingDescription.binding = 0;
bindingDescription.stride = sizeof(Vertex);
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
return bindingDescription;
}
static std::array getAttributeDescriptions() {
std::array attributeDescriptions{};
attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32B32A32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);
attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1;
attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[1].offset = offsetof(Vertex, color);
return attributeDescriptions;
}
};
Same vrhove koje izrađujemo preko vektora ovakve strukture (svaki vektor jest jedna instanca strukture)
dodajemo u međuspremnik vertexa i taj međuspremnik onda koristimo tijekom iscrtavanja, tako da ga vežemo
za naredbeni međuspremnik.
U ovom projektu smo koristili dva shadera, jedan vertex shader i jedan fragment shader.
Oba su vrlo jednostavna, ali su dovoljna za potrebe našeg projekta. Kao što vidimo podacima uniform
buffera se pristupa preko njegovog layouta (plana), te točke vezanja. Vrhovima se pristupa preko već
spomenute lokacije, te se u main metodi zapravo određuje gl pozicija koja je ugrađena varijabla
odgovorna za pohranu pozicije vrhova, uz nju imamo i boju fragmenta koju dalje šaljemo fragment shaderu.
shader.vert
#version 450
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
layout(location = 0) in vec4 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * inPosition;
fragColor = inColor;
}
Ovaj shader je odgovoran za samo bojanje fragmenta, ali je sam po sebi vrlo jednostavan: samo postavlja boju i šalje ju sa out. Uz ovo postoje još neki drugi shaderi, ali ovo su jedini koje smo mi koristili.
shader.frag
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
Preko vertexa šaljemo same vrhove, a preko deskriptora šaljemo same transformacije, koji omogućuju shaderima
pristup našim uniform bufferima. Općenito kreacija samih buffera nije pretjerano interesantna, oni se vežu uz
neki skup deskriptora, koji imaju svoj layout, te se sami deskriptori alociraju iz bazena deskriptora i na
posljetku se moraju vezati isto kao i vertex buffer uz sam naredbeni međuspremnik.
Kod ispod je zapravo primjer kako se ažurira memorija uniform buffera za trenutnu sliku koju iscrtavamo,
odnosno ažuriranje samih uniform buffer objekata koje šaljemo shaderu. Ovo nije sav kod već samo primjer
za jedan od uniform baffera koje koristimo. Ovo je za posljednju kuglu, koristimo vrijeme kao brojač,
preko kojeg zapravo postižemo rotaciju, svaka slika ima drugačije vrijeme i samim time drugačiju rotaciju
za svaku sliku u lancu slika.
U model stavljamo sve transformacije koje želimo da se naprave nad objektom, one naravno moraju biti u
određenom poretku, te svaka ima svoju funkciju i parametre koje prima. Rotacija prima model (model za
prvu transformaciju je jedinična matrica), kut rotacije i os rotacije (x, y, z ili bilo koja kombinacija).
Pomak prima model i pomak, koji opet može biti po bilo kojoj osi. Te dvije transformacije su zapravo jedine koje smo koristili.
Preko look at dijela uniform buffer objekta na neki način definiramo kameru. Definiramo iz kojeg smjera gledamo,
na što gledamo i gore vektor (up vector). Dok preko perspective definiramo kut područja pogleda
(field of view), za y smjer, x smjer, te definiramo bliski i udaljeni clipping plane. Na posljetku kopiramo
novi uniform buffer objekt u memoriju uniform buffera za trenutnu sliku.
static auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
float time = std::chrono::duration(currentTime - startTime).count();
UniformBufferObject ubo{};
if (zastava == 7) {
ubo.model = glm::rotate(
glm::rotate(
glm::translate(
glm::rotate(
glm::rotate(
glm::translate(
glm::rotate(glm::mat4(1.0f), time * glm::radians(60.0f), glm::vec3(0.0f, 0.0f, 1.0f))
, glm::vec3(0.0f, 0.0f, hPomakMaliValjak))
, glm::radians(240.0f), glm::vec3(0.0f, 0.0f, 1.0f))
, glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f))
, glm::vec3(-rKugla / 4, 0, hValjakMali + rKugla * 0.9))
, glm::radians(90.0f), glm::vec3(0.0f, 1.0f, 0.0f))
, glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
ubo.view = glm::lookAt(glm::vec3(4.0f, 4.0f, 4.0f), glm::vec3(0.0f, 0.0f, 1.5f), glm::vec3(0.0f, 0.0f, 1.0f));
ubo.proj = glm::perspective(glm::radians(55.0f), swapChainExtent.width / (float)swapChainExtent.height, 0.1f, 10.0f);
ubo.proj[1][1] *= -1;
void* data;
vkMapMemory(device, unifBufMem[currentImage], 0, sizeof(ubo), 0, &data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, unifBufMem[currentImage]);
Kako bi nešto iscrtali sve naredbe koje mislimo koristi prvo moramo staviti u naredbeni međuspremnik. Svaki takav međuspremnik pokrećemo iscrtavanje i preko naredbe iscrtaj, iscrtavamo pojedine objekte. U primjeru koda ispod to je samo jedan objekt, radi smanjenja prostora kojeg zauzima kod. Vidimo da moramo vezati sam grafički cjevovod, vertex buffere i deskriptore. Za iscrtavanje stožca koristimo njegov vertex buffer, skup deskriptora, te u naredbi za crtanje moramo dati veličinu samih vrhova koje šaljemo.
for (size_t i = 0; i < commandBuffers.size(); i++) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("failed to begin recording command buffer!");
}
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];
renderPassInfo.renderArea.offset = { 0, 0 };
renderPassInfo.renderArea.extent = swapChainExtent;
std::array clearValues{};
clearValues[0].color = { {0.0f, 0.0f, 0.0f, 1.0f} };
clearValues[1].depthStencil = { 1.0f, 0 };
renderPassInfo.clearValueCount = static_cast(clearValues.size());
renderPassInfo.pClearValues = clearValues.data();
vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
VkBuffer vertexBuffers[] = { vertexBufferKugla, vertexBufferStozac, vertexBufferValjak, vertexBufferMaliValjki };
VkDeviceSize offsets[] = { 0,0,0,0 };
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, &vertexBuffers[1], &offsets[1]);
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSetsStozac[i], 0, nullptr);
vkCmdDraw(commandBuffers[i], static_cast(verticesStozac.size()), 1, 0, 0);
vkCmdEndRenderPass(commandBuffers[i]);
if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to record command buffer!");
}
}
Vulkan vs WebGL
Vulkan je dosta kompleksniji za implementaciju nego WebGL.
Da bi se pokrenila jednostavna slika npr. trokuta, potrebno
je sva svojstva Vulkan aplikacije specificirati unutar koda.
Prvi korak u kreiranju Vulkan aplikacija jest postavljanje Vulkan API-a
kroz parametar VkInstance koji se kasnije služi za sastavljanje
redova podržanih Vulkan hardware-a, te naknadno i sami odabir jednog ili
više hardware-a putem VkPhysicalDevice parametra. Tu možemo specificirati
svojstva kao naprimjer, veličinu VRAM-a i sl.
Drugi korak jest kreiranje logičkih uređaja (VkDevice) i određivanje
specifikacija fizičkog uređaja ćemo koristiti, npr. multiviewport renderiranje i sl.
Vulkan svoje operacije izvodi asinkrono na način da ih priloži u VkQueue.
Treći korak je kreiranje prozora za prikaz renderiranih slika. Budući da Vulkan nema svoj
način prikaza koristimo biblioteku GLFW. Da bi se slike renderale na prozor, potrebne su
dvije komponente: površina prozora i zamjenski lanac (eng. swap chain). Svrha zamjenskog
lanca jest da prikaže samo kompletne slike na prozoru, na način da provjerava da li je trenutna
slika različita od one koja je već prikazana na prozoru. Svaki put kada se crta ekran, potrebno je
pitati zamjenski lanac da nam proslijedi sliku koja će se rendirati.
Kada imamo sliku u zamjenskom lancu, potrebno ju je zapakirati u VkImageView i
VkFramebuffer. VkImageView referencira specifični dio slike koja će se koristiti,
dok VkFramebuffer referencira poglede slike koji će se koristiti za boju, dubinu i sl.
Također, da bi se slika iscrtala potrebno je kreirati grafički cjevovod (eng. graphics pipeline),
budući da on opisuje konfigurabilna stanja grafičke kartice pomoću VkShaderModule objekta.
Da bi iscrtali trokut na prozor, potrebno je snimiti u VkCommandPool, koji se sastoji od
VkCommandBuffer-a sve potrebne operacije.
Glavna razlika Vulkan API-a i WebGL-a jest da se cijela konfiguracija grafičkog cjevovoda mora
postaviti unaprijed. Što znači da ako se želimo postaviti na drugi shader ili promijeniti redoslijed
vrhova, potrebno je iznova rekreirati grafički cjevovod. Same funkcije za postavljanje točaka su jako slične
u oba programa.