Vertex Buffers - Vulkan Game Engine Tutorial 06

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in previous tutorials we've hard-coded vertex data directly into our vertex shader however this is clearly not a feasible solution for drawing anything remotely complicated 3d models for modern games typically have thousands of triangles if not more so in this tutorial we will cover how vertex buffers store data and can be bound to graphics pipelines a vertex buffer is really just the chunk of memory that we pass into our vertex shader we can put whatever data we would like into this memory as long as we tell our graphics pipeline how it is structured the first way we structure this data is by grouping it into attributes a vertex attribute is just an input variable that is specified on a per-vertex basis so for example we could create a vertex attribute for our two-dimensional position if this was our only vertex attribute then our vertex buffer would look like this where every two numbers are grouped together to make up the x and y position of our vector input variable now let's say we want to specify an rgb color for each vertex we could add a second vertex attribute this time of f3 if we add this to our vertex buffer then our buffer would look like this the first two values in the buffer are grouped together to make up the position attribute for the first vertex the next three values are the red green and blue values these will be grouped together to form the color attribute the next five values are used for the second vertex and so on the other way this data can be structured is by grouping attributes into separate bindings for example if we add a third attribute we could interleave all three attributes into a single binding or have one binding per attribute really any combination we would like for each binding we need to provide a vertex binding description at pipeline creation in the description we need to specify the binding index the input rate and the stride we can mostly ignore input rate for now this will always be input rate vertex with the alternative being input rate instance for when working with instanced data the stride specifies the interval in bytes from one entry to the next so you can imagine a pointer at the start of a vertex buffer the stride indicates how many bytes it needs to advance each time to get to the start of the next vertices data for each attribute we must provide a vertex attribute description attribute descriptions need four pieces of information the binding index for the vertex buffer the attribute is located in the location value for the input in the vertex shader the offset of the attribute in bytes from the vertex start and a vk format value describing the type of data for example our color attribute could be in our first buffer at binding index zero and the location we specified in the vertex shader is location one the offset would be eight because the position has four bytes for the x component plus another four bytes for the y component therefore the color attribute values start at an offset of 8 bytes from the start of the vertex data and finally the format specifies the data type of the attribute so the color attribute is a vect3 which corresponds to vk format r32 g32 b32 s float these are some commonly used data types and formats note that even though the format implies a color like structure we use the same format even if we are representing non-color data for example the vect2 position attribute would still use a vk format r32g32s float even though it's not a color so far i've been using binding and buffer interchangeably which might imply that for each binding we have a separate buffer but typically you will want to minimize the amount of memory allocations so often for something like vertex data even if we have multiple bindings we will only allocate one large vertex buffer and have each binding mapped to a different region of the same buffer so is it better to use a single interleaved binding or one binding per tribute or something in between i won't delve into this topic too much right now as performance differences tend to be modest anyway and there is no one right answer typically a single interleaved binding would be the recommended way to go and for simpler pipelines and basic algorithms this is usually true however depending on your overall rendering pipeline the algorithms being used and even your specific hardware using separate buffers may yield better performance so for now don't worry about it just keep this in the back of your mind that in the future if you're working on some specific rendering implementation and need to squeeze every millisecond of performance out that restructuring your vertex data may be beneficial okay let's get to coding the first thing we're going to do is create a new header file for our model class so i'm going to call my file lve underscore model.hpp i'll use pragma once to create a header guard and then add the lve namespace i've been using then make a new class lve model within the namespace so the purpose of this class is to be able to take vertex data created by or right in a file on the cpu and then allocate the memory and copy the data over to our device gpu so it can be rendered efficiently so like with most of the other classes we've created so far we need a device reference to work with it so create a private lve device reference variable called lve device and at the top of the file add an include for lve device next add three more private variables vk buffer vertex buffer vk device memory vertex buffer memory and a un32 type vertex count moving on for the public portion of the model class i'm going to go to my first app header and just copy these four lines paste them in and replace the class name with lve model we must delete the copy constructors because the model class manages the vulkan buffer and memory objects now take notice that for buffer objects in vulcan the buffer and its assigned memory are two separate objects rather than memory being automatically assigned for the buffer this puts us the programmer in control of memory management now add a public function void bind that takes a vk command buffer as the argument then duplicate this line and rename the second function to draw now jumping over to the vertex shader first completely remove the hard-coded positions variable we're going to replace this with layout bracket location equals zero closing bracket in back to position so here we've specified our first vertex attribute like before the data type is still a vec2 but note that now we have this in keyword that signifies that this variable takes its value from a vertex buffer the layout location sets the storage of where this variable value will come from this is how we connect the attribute description to the variable we mean to reference in the shader and now replace positions at gl vertex index with just position we don't need to use gl vertex index anymore because the position attribute will automatically be set with the values from the vertex buffer so remember that the vertex shader runs once for each vertex we provide and each time this position variable will contain a different x and y component from the vertex buffer and that's it for the vertex shader back in the model header we are going to create a representation of our vertex attributes that's easy to work with first let's include the vector library glm you should have already downloaded this and connected it to your project way back in tutorial 1 when we also set up glfw for windowing just above the include for glm add the line define glm force radians this makes sure that no matter what system you're on the glm functions will expect angles to be specified in radians not degrees back in university in a computer graphics course i must have wasted two hours debugging an assignment because i was thinking in degrees when i should have been working in radians don't make the same mistake additionally at a define glm force depth 0 to 1. this will affect some glm functions we will eventually use this tells glm to expect our depth buffer values to range from 0 to 1 opposed to negative 1 to 1 which is if i'm remembering correctly what opengl uses so now just underneath the public annotation in the model class create a struct vertex so for now all we have is a glm vect2 position also add a static function std vector angle bracket vk vertex input binding description closing bracket get binding descriptions function and then also include vector at the top of the file copy this line and paste it to add a second function but this time it will be get attribute descriptions and instead return vk vertex input attribute description type oh and these should be descriptions with an s then add a private function void create vertex buffers that takes a const standard vector reference of type vertex vertices argument now finally for our header update the model class constructor to take a lve device reference device and const standard vector vertices reference as arguments okay now let's add our implementation create a new file lve underscore model.cpp and then include the model header and add your namespace copy your constructor signature and the destructor and paste them in add your class name scope and then initialize the device member variable lve device curly brace device for the constructor body just call create vertex buffers and pass through the vertices argument next add the class name scope to the destructor and for the destructor body we have vk destroy buffer lve device dot device vertex buffer and null pointer for our allocation callback then vk free memory lve device dot device vertex buffer memory and null pointer okay let's quickly talk about these allocation callbacks that we've routinely passed null pointers to so we've seen that vulkan lets us manage our buffers and memory separately and the main reason for this is that allocating memory takes time and there's a hard limit to the total number of active allocations that varies by gpu typically only in the thousands though so if we continue on as we have been as soon as you want to create a scene of high complexity with many different types of models this model class will quickly run into those max allocation limits so the recommended solution is to allocate bigger chunks of memory and assign parts of them to particular resources so just for the short term to demonstrate and learn the basics of memory allocation we will continue to do so manually but i think eventually we will integrate a well-established allocator library like vma if you would like to read about what might go into creating a memory allocator yourself i've provided a link to a blog post by kyle holliday where he does just that now moving on let's implement the create vertex buffers function grab the function signature add your class name scope and then in the function body the first thing we'll do is set the vertex count member variable to static cast uint32 type vertices.size now let's add an assertion that the vertex count is at least three so that we know our model class consists of at least one triangle so first include c assert and then add a cert vertex count is greater than or equal to three with the label vertex count must be at least 3. next create a local variable of type vk device size called buffer size equal to sizeof vertices index 0 times the vertex count so the size of operator returns the number of bytes required per vertex and then multiplying by the vertex count gives us the total number of bytes required for our vertex buffer to store all the vertices of the model next we're going to call the create buffer function this is a helper function i wrote that is in the lve device class let's go over this now so the create buffer function takes the buffer's size usage and properties as arguments and then returns a buffer and its associated memory by initializing the buffer and buffer memory references this code here should seem somewhat familiar we have a create infostruct where we set some members such as the size and how we plan to use this buffer and then call our vkcreate function we then query the buffer's memory requirements so that we can allocate memory of the proper size that also has the required properties that we specified as an argument finally if that was successful we bind the buffer to the memory we just allocated eventually we will have to rewrite this function once we integrate a memory allocator okay so back in our create vertex buffers function we have our buffer size so now call lve device dot create buffer the first argument is the buffer size then next we have vk buffer usage vertex buffer bit this just tells our device that we want to create a buffer that is going to be used to hold vertex input data following that we need to specify our memory properties with vk memory property host visible bit and vk memory property host coherent bit so make sure to use the bitwise or operator here which combines the bit flags together the host visible bit memory property tells vulcan that we want the allocated memory to be accessible from our host aka the cpu this is necessary for our host to be able to write to the device memory the host coherent memory property keeps the host and device memory regions consistent with each other if this property is absent then we are required to call vk flashed mapped memory ranges in order to propagate changes from host to device memory then the final two arguments are the vertex buffer and vertex buffer memory member variables now finally declare a local variable void pointer data then call vkmap memory lvedevice.device vertex buffer memory 0 as the offset buffer size then 0 for not providing any vk memory mapped flags and finally a pointer to data this function creates a region of host memory mapped to device memory and sets data to point to the beginning of the mapped memory range then next we need to use the memcopy function so first include c-string then call mem copy data vertices.data static cast size t buffer size and finally call vk unmapped memory lve device.device vertex buffer memory so mem copy takes the vertices data and copies it into the host mapped memory region now because we have this host coherent bit the host memory will automatically be flushed to update the device memory if this bit is absent we'd be required to call vk flush mapped memory ranges in order for the changes to propagate okay now the draw function implementation is very simple copy and paste the function signature and add the class name scope then inside the body just call vk command draw command buffer vertex count and then one for instance zero for first vertex index and zero for first in first instance index and that's it the bind function is pretty similar copy and paste the function signature and add the class name scope make a local variable vk buffer buffers array equal to curly braces vertex buffer and then vk device size offsets array equal to curly braces 0 then just call vk command bind vertex buffers command buffer first binding at 0 binding count of 1 then buffers and offsets so this function will record to our command buffer to bind one vertex buffer starting at binding zero with an offset of zero into the buffer and when we eventually might want to add multiple bindings we can easily do so by adding additional elements to these arrays next let's implement the get binding descriptions function for its scope we have lve model scope then vertex scope get binding descriptions make a vector vk vertex input binding description local variable binding descriptions of size 1. then binding descriptions index 0 dot binding equals zero then dot stride equals size of vertex and dot input rate equals vk vertex input rate vertex then return the binding description alternatively if you prefer you can just use a brace construction but i've done it this way to make the code more readable so this binding description corresponds to our single vertex buffer it will occupy the first binding at index 0 the stride advances by size of vertex bytes per vertex now finally for get attributes descriptions copy the function signature and add your model and vertex scopes don't forget to remove the static keyword so similarly create a local variable with type vector of vk vertex input attribute descriptions called attribute descriptions then attributes index zero dot binding equals zero next dot location equals zero this corresponds to the location specified in the vertex shader next dot format equals vk format r32 g32 s float this specifies the data type that we have a two components that are each 32-bit sign floats finally dot offset equals zero and return attribute descriptions next we need to provide our pipeline with these descriptions so it actually knows how to read our vertex buffer data first we need to include our model class now currently in our create graphics pipeline function our vertex input info struct was pretty much empty start by adding two local variables auto binding descriptions is equal to lve model vertex get binding descriptions and auto attribute descriptions is equal to lve model vertex get attribute descriptions then update the attribute description count to equal static cast uin32 type attributes.size and binding description count to equal to staticast uint32 type bindingdescriptions.size then set p vertex attribute descriptions to attributes descriptions.data and p vertex binding descriptions to bindingdescriptions.data now all that's left to do is create an instance of our model and draw it so open your first header include lve model and then create a lve model unique pointer lve model then add a private load models function now in the first app implementation file implement the function void first app load models inside its body create a vector of type lve model vertex and we will use this to initialize the vertex data positions so i'm going to use the same positions values we had before of 0 negative point five point five point five and negative point five point five make sure to get your braces correct here we have three levels the outermost initializes the vector then we initialize each model vertex and then the innermost braces initialize the glm vect2 position member next initialize the model with lve model equals std make unique lve model with lve device as the first argument and vertices as the second then in your first app constructor call the load models function as the first line then in the create command buffers function remove the command draw function call and add lve model arrow bind command buffers at index i and lve model arrow draw command buffers at index i and that's it our command buffer in each render pass will bind our graphics pipeline then binder model which contains the vertex data and then record a command buffer to draw every vertex contained by the model now build and run and okay i've done something wrong it's an easy fix i swap the names of the bind and draw functions also make sure that your vertex shader has been recompiled if you haven't set it to automatically do so if i fix that and now build and run our familiar triangle appears but what's different this time is that the vertex positions are no longer hard coded into the vertex shader finally as a completely optional exercise now that we can provide any position data we want see if you can create something like the sierpinski triangle that's displayed in the tutorials thumbnail you will probably want to create a recursive function that generates a vector list of vertices every three vertices forms the next triangle then initialize your model with that vertex list i've included a link to my solution in the video's description and that's it for today we can now pass in a non-fixed amount of position data in the next video we will add an additional color attribute and show how to pass data from the vertex shader to the fragment shader thanks for watching and see you next time
Info
Channel: Brendan Galea
Views: 9,538
Rating: undefined out of 5
Keywords: Vertex buffers, interleaved vertex buffer, vbo, separate buffer, interleave vs separate, vulkan buffer, Vulkan, vulkan api, vulkan tutorial, 3d game engine, coding tutorial, vulkan coding, vulkan graphics, 3d graphics, gpu programming, vulkan programming, vulkan game engine, vulkan game engine tutorial, vulkan engine tutorial, learn vulkan, how to code vulkan, Vulkan beginner, graphics beginner, vulkan noob, vulkan from scratch, vulcan, vulcan api, vulcan tutorial
Id: mnKp501RXDc
Channel Id: undefined
Length: 25min 14sec (1514 seconds)
Published: Thu Feb 04 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.