Loading 3D Models - Vulkan Game Engine Tutorial 17

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
welcome back in this video we are going to move past the simple 3d cube and extend the model class to be able to read and model data using the wavefront object file format up until this point we've hard coded our model data directly into our code this is time consuming inflexible and prone to errors it would be much better if we could specify all model data in a separate file that could be loaded in at runtime the wavefront object file format is an open geometry definition file supported by most 3d modeling programs a v indicates a vertex position v n indicates a vertex normal and v t a texture coordinate with the subsequent values being the x y and z component values so for example the wavefront object file for the cube we've been rendering in the previous tutorials may look something like this the first eight values specify the eight vertex positions one for each corner of the cube then we have 14 texture coordinate values these indicate how a 2d image would get mapped to cover the surface geometry of the model and finally six normal values one for each face of the cube i will explain in more detail in the upcoming tutorials the purpose and use of vertex normals and texture coordinates the final portion of this file format that we care about lists how to construct each individual face which for our rendering pipeline will all be triangles each face of the model will be listed on a separate line that starts with the letter f and will consist of three face elements one for each vertex of the triangle a face element is made up of index values the first value is an index into the list of vertex positions so for the first triangle we want to use the fifth vertex position value then following the forward slash the next value is an index into the texture coordinate list so the first texture coordinate value and then as you can probably guess that leaves the last value of the base element as the index into the list of normal vectors therefore the first vertex of our triangle is given by one negative one point eight seven five point five and zero negative 1 and 0. to construct the rest of the triangle we do the same for the other two face elements so the explanation i have provided here is a simplification of the full file format specification i've ignored materials as well as some other details for more information check out the wikipedia article on the topic rather than write our own code to read in this file format we will make use of tiny object loader a single file wavefront object loader written in c plus so let's start by adding tiny object loader to the project follow the link in the description below to navigate to the git repo for tiny object loader you can download the entire master branch but really all we need is this single header file in my development folder i have a directory called libs and i will create a new folder called tiny object loader then back on github i'll open the raw version of the file and save it to the tiny object loader directory i just created when saving the file make sure you don't accidentally include an extra file extension such as txt next we need to include tiny object loader to be built with the rest of our project how you do this will differ depending on how you build your project i am using a makefile so i first need to add the path to the directory i just created into my dot m file so tiny object path is equal to users brendan dev libs tiny object loader then in my make file i will add the tiny object path to be included in with the c flags and that's it one other thing you might want to do is if you are using vs code is add this to the include path of your ccpp properties file this way the code completion will work correctly within the vs code editor setup for visual studio on windows is pretty similar create a folder in your library's directory called tiny object loader and then save in it the tinyobjectloader.h file then in visual studio navigate to project project properties and in the c c plus settings underneath in the general heading we can add the path to tiny object loader to additional include directories so click edit and add a new entry inputting the folder's path now let's quickly test that we've included tiny object loader correctly before progressing with the rest of the tutorial open the model implementation file and near the top first define tiny object loader underscore implementation you only want to have this defined in a single cpp file within the entire project the define signals to the preprocessor that this file in the project will contain the implementation for tiny object loader then include tinyobjectloader.h in angle brackets and you can build and run your code if things are still working that means tiny object loader has been successfully added finally i've provided some model files to get started with follow the link to my google drive open the models folder and click download all unzip the folder and copy it to your project directory okay let's get to coding open your model header and extend the vertex struct to also support normals and texture coordinates add a field of type glm vect3 called normal followed by a glm vect2 called uv uvs is just another commonly used shorthand for two-dimensional texture coordinates then inside the builder struct i'm going to declare a void load models function that takes a const std string reference called file name as the argument following that let's create a helper function to create a unique pointer model object so first include the memory header then inside the model class declare a static function that returns an std unique pointer of lve model type called create model from file and this will take an lve device reference called device and a const std string reference called file path let's copy this function declaration and head on over to the model implementation file i will paste the function in add my classnamescope and remove the static keyword then inside i'll initialize a variable of type builder called builder next call the builder.loadmodel function passing through the filepath argument and then return std make unique of type lve model with arguments device and builder i'm going to include iostream and then add a print statement that logs builder.vertices.size this way for now we will be able to see how many vertices a loaded model contains next we need to implement the load model function so at the bottom of my file but still within the namespace i will add void lve model builder load model with the argument const std string reference file path then declare a local variable with type tiny object colon colon a trip t called a trip next an std vector would type tiny object shape t called shapes then another vector but with type material t called materials and finally declare two std strings called warn and error all of these values will be set by tiny object loader and will store the results of reading a wavefront object file so in an if statement we will have not tiny object load object with the arguments reference to a trib reference to shapes reference to materials reference to warn and error and finally file path dot c string which provides a car pointer representation of the string variable if we fail to load the object we'll throw an std runtime error with warn plus error as the output so following this load object function call succeeding these local variables we declared should now be initialized with the data from the dot obj file the trip variable stores the position color normal and texture coordinate data the shapes variable contains the index values for each face element and the materials we will ignore for now and leave to a future tutorial okay so we've got our data first let's call vertices.clear and indices.clear to start from a fresh builder state then add four const auto reference shape and shapes followed by a second for loop with const autoreference index in shape.mesh dot indices this loops through each face element in the model getting the index values inside the for loops declare and initialize a vertex local variable let's start by setting the vertex's position field to do so we should first check if index.vertex index is greater than or equal to zero the vertex index is the first value of the face element and says what position value to use index values are optional and a negative value indicates that no index was provided if a vertex index is provided then we can set the vertex dot position equal to curly brackets attrib.vertices at an index of three times index dot vertex index plus zero each vertex has three values that are tightly packed into the trib dot vertices array so to read the corresponding position we need to multiply by three and then add zero for the initial component then duplicate this line twice updating plus zero to plus one and plus zero to plus two for the positions y and z components respectively now reading the normals and texture coordinates is all very similar so i'll duplicate this entire if block and paste it just below change each occurrence from vertex index to normal index vertex.position to vertex dot normal and attrib.vertices to attrib.normals and repeat this one more time for the texture coordinates change each occurrence of normal index to chord index a trib dot normals to a trip dot text chords and vertex dot normal to vertex dot uv the only other notable difference is that the uvs are two components rather than three so remove the last component and change to using two times index dot text chord index and then we can add this vertex to the builder's list of vertices with vertices.pushback vertex and that would be pretty much it except we haven't handled colors yet and that is because the wavefront object file format doesn't actually support them however there does exist an unofficial extension that is supported by tiny object loader some applications support vertex colors by putting red green and blue values after the x y and z vertex position components i've included a colored cube object file to demonstrate this feature so inside the if block for vertex index we can extend it to add color support first i'm going to calculate the last color component's index value and store it in auto color index is equal to three times index dot vertex index plus two then check if color index is less than a trib dot colors dot size we use the last index because color attributes are optional and this is a convenient way to check that a color has been provided and the index is in bounds if this is true then we can initialize the vertex.color field this is pretty much what we had before except each component is equal to a trip dot colors rather than a trip dot vertices and rather than plus zero one and two we instead have color index minus two minus one and minus zero finally add an else statement setting the vertex color to some default value if no color has been provided okay now all that's left to do is load in a model and render it open the app implementation file and we can get rid of the create cube model function then in load game objects change create cube model to lve model create model from file passing in the device and models coloredcube.obj note that this is a relative path if you run into an error try using an absolute file path instead for example this is what i use on my windows machine build and run your code and we get a rainbow colored cube that's because each vertex only specifies one color and then the fragment color is interpolated across the face of each triangle just like in tutorial seven if we wanted a model to look like the previous six colored cube we had we would need to duplicate each vertex position twice so that we could specify a different color per face now let's try a more complex model update the file path to load the smooth vase object file if you build and run this appears kind of small so i'm going to change the transform.scale to equal glm back 3 with a multiplier of 3 and i will also rename the cube local variable to be game object as that's a bit more applicable now and there we go we have a nice white vase with a vertex count of 30 888 however even with so many vertices you can't really see anything because the entire object is just painted white to get something with visible detail we need to apply lighting which will do so in next week's tutorial so we've successfully loaded in a model using just vertices but we didn't make use of the index buffer that we added just back in the previous tutorial index buffers are a great optimization for reducing the memory required to store complex 3d models so we definitely want to be able to use them when loading objects from a file but one thing you've probably noticed is that each face element definition can use a different index value for the position texture coordinate and normal but in vulcan we can't do this we can only have a single index buffer and we need all the values that make up each vertex to be grouped together so creating an index buffer isn't quite as straightforward as just reading in the face elements index values the purpose of an index buffer is to avoid having any duplicate vertices within the vertex buffer as each face element is processed we need to come up with a way to detect if we've encountered this vertex value before if the vertex is new we add it to the vertex buffer and add its position in the list to the index buffer if the vertex already exists then we can discard it and add the index of the existing vertex into the index buffer once every face element has been visited we will be left with a list of unique vertices and a list of index values pointing into the vertex list indicating how to construct each triangle that makes up our object this method i have just explained is the exact method provided by vulcantutorials.com so if you found my explanation hard to follow check out the loading models tutorial linked in the description below to quickly be able to check if a vertex is new or has been seen before we will use an std unordered map which requires we provide a hash function that can take all the data in our vertex struct and map it to a single fixed sized value the unordered map can then use this hash value as the key to look up the vertex's position in the list of vertices so first let's create a new file called lve utils.hpp i've included this function in a paste bin link below this is a function that can combine an arbitrary number of hash values together hash functions are kind of a complex topic and i assume most of you guys are already familiar with how unordered maps work so i'm not going to delve into the details of why this will work well it's based on a stack overflow answer in the link provided note that it makes use of fold expressions which is a c plus plus 17 feature okay now in the model header we need to overload the equality operator for the vertex struct so add bool operator equal sign equal sign const vertex reference called other mark the function const and then we simply check that all the struct fields are equal so position equals other dot position color equals other dot color normal equals other dot normal and uv equals other dot uv then in the model implementation scroll to the top and include lveutils.hpp in addition to the equality operator we need to implement a hash function for the vertex struct which will make use of the hash combined function we just wrote but to hash the individual glm vet components we need to include the glm hash functionality this functionality is technically experimental and subject to change so we first need to define glm enable experimental but in practice the glm library is very stable so i don't think this is something to really worry about then include glm gtx hpp finally we also need to include unordered map i am going to inject a hash function for the vertex struct into the std namespace so just following the include statements but outside the lve namespace i will add namespace std followed by template angle brackets next we declare a struct called hash with type lve lve model vertex inside we need to overload the function call operator so we have return type size t then operator brackets and as the operator function calls argument we have an lve lve model vertex const reference called vertex and also mark the function as const initialize a variable of type size t called seed equal to zero this will store the final hash value then call lve combine with the arguments seed vertex dot position vertex dot color vertex dot normal and vertex dot uv and then finally return seed with this we can now take an instance of the vertex struct and hash it to a single value of type size t which can then be used by an unordered map as the key okay scroll down and in the load model function let's initialize an empty std unordered map using vertex as the key type and a unin 32t as the value type and i'll call it unique vertices this map will keep track of the vertices which have already been added to the builder.vertices vector and store the position at which the vertex was originally added then scroll down to the end of the function but still inside the for loops and replace vertices.pushback with if uniquevertices.count of vertex is equal to zero then unique vertices at vertex is equal to static cast uint32 type of vertices.size then also call vertices.pushback vertex so what this does is if the vertex is new we add it to the unique vertices map its position within the builder's vertices vector is given by the vertices vector's current size finally after the if block call indices.pushback unique vertices at vertex with this we add the position of the vertex to the builder's indices vector and that's it we can build run and see our vase object once more the big difference is that the vertex count has been reduced from 30 888 to just 5545 by using an index buffer we're saving a lot of gpu memory as a final step let's clean up i o stream and remove the print statement before you go i would like to mention that we have a discord it's a great place to share code get feedback and meet like-minded people who are also crazy enough to try and make their own game engine so if that is something you would like to join links in the description below thank you for watching and keep on coding cheers you
Info
Channel: Brendan Galea
Views: 4,187
Rating: undefined out of 5
Keywords: Tinyobjloader, 3D models, wavefront, obj file, 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: jdiPVfIHmEA
Channel Id: undefined
Length: 22min 47sec (1367 seconds)
Published: Sun Aug 01 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.