Skip to content

Commit

Permalink
big changes on chapter 4 and 3
Browse files Browse the repository at this point in the history
  • Loading branch information
vblanco20-1 committed Dec 1, 2023
1 parent 428e0e5 commit adec3e1
Show file tree
Hide file tree
Showing 12 changed files with 1,249 additions and 55 deletions.
16 changes: 15 additions & 1 deletion docs/new_vkguide/chapter_3/building_pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,15 +517,29 @@ void VulkanEngine::draw_geometry(VkCommandBuffer cmd)
vkCmdSetScissor(cmd, 0, 1, &scissor);
//> drawrect
//launch a draw command to draw 3 vertices
vkCmdDraw(cmd, 3, 1, 0, 0);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);
vkCmdPushConstants(cmd,_meshPipelineLayout,VK_SHADER_STAGE_VERTEX_BIT,0, sizeof(VkDeviceAddress), &rectangle.vertexBufferAddress);
GPUDrawPushConstants push_constants;
push_constants.worldMatrix = glm::mat4{1.f};
push_constants.vertexBuffer = rectangle.vertexBufferAddress;
vkCmdPushConstants(cmd,_meshPipelineLayout,VK_SHADER_STAGE_VERTEX_BIT,0, sizeof(GPUDrawPushConstants), &push_constants);
vkCmdBindIndexBuffer(cmd, rectangle.indexBuffer.buffer,0,VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd,6,1,0,0,0);
//< drawrect
push_constants.worldMatrix = glm::mat4{ 1.f };
push_constants.vertexBuffer = testMeshes[2].meshBuffers.vertexBufferAddress;
vkCmdPushConstants(cmd, _meshPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push_constants);
vkCmdBindIndexBuffer(cmd, testMeshes[2].meshBuffers.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, testMeshes[2].surfaces[0].count, 1, testMeshes[2].surfaces[0].startIndex, 0, 0);
vkCmdEndRendering(cmd);
}
Expand Down
176 changes: 176 additions & 0 deletions docs/new_vkguide/chapter_3/loading_meshes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
layout: default
title: Mesh Loading
parent: "New 3. Graphics Pipelines"
nav_order: 12
---

We will do proper scene loading later, but until we go there, we need something better than a rectangle as a mesh. For that, we will begin to load GLTF files but in a very simplified and wrong way, getting only the geometry data and ignoring everything else.

There is a gltf file that came with the starting-point repository, called basic.glb. That one has a cube, a sphere, and a monkey head meshes centered in the origin. Being a file as simple as that one is, its easy to load it correctly without having to setup up real gltf loading.

A GLTF file will contain a list of meshes, each mesh with multiple primitives on it. This separation is for meshes that use multiple materials, and thus need multiple draw calls to draw it. The file also contains a scene-tree of scenenodes, some of them containing meshes. We will only be loading the meshes now, but later we will have the full scene-tree and materials loaded.

Our loading code will all be on the files vk_loader.cpp/h .

Lets start by adding a couple classes for the loaded meshes

```cpp
struct GeoSurface {
uint32_t startIndex;
uint32_t count;
};

struct MeshAsset {
std::string name;

std::vector<GeoSurface> surfaces;
GPUMeshBuffers meshBuffers;
};
```
A given mesh asset will have a name, loaded from the file, and then the mesh buffers. But it will also have an array of GeoSurface that has the sub-meshes of this specific mesh. When rendering each submesh will be its own draw. We will use StartIndex and count for that drawcall as we will be appending all the vertex data of each surface into the same buffer.
Our load function will be this
```cpp
std::optional<std::vector<MeshAsset>> loadGltfMeshes(VulkanEngine* engine, std::string_view filePath);
```

This is the first time see see std::optional being used. This is standard class that wraps a type (the vector of mesh assets here) and allows for it to be errored/null. As file loading can fail for many reasons, it returning null is a good idea. We will be using fastGltf library, which uses all of those new stl features for its loading.

Lets begin by opening the file
```cpp
std::cout << "Loading GLTF: " << filePath << std::endl;

constexpr auto gltfOptions = fastgltf::Options::LoadGLBBuffers | fastgltf::Options::LoadExternalBuffers;

fastgltf::GltfDataBuffer data;
data.loadFromFile(filePath);

auto load = parser.loadBinaryGLTF(&data, filePath.parent_path(), gltfOptions);
if (load) {
gltf = std::move(load.get());
}
else {
fmt::print("Failed to load glTF: {} \n",fastgltf::to_underlying(load.error()));
return {};
}
```

We will only be supporting binary GLTF for this for now. So first we open the file with `loadFromFile` and then we call loadBinaryGLTF to open it. This requires the parent path to find relative paths even if we wont have it yet.

Next we will loop each mesh, copy the vertices and indices into temporary std::vector, and upload them as a mesh to the engine. We will be building an array of `MeshAsset` from this.

```cpp
std::vector<uint32_t> indices;
std::vector<Vertex> vertices;
for (fastgltf::Mesh& mesh : gltf.meshes) {
MeshAsset newmesh;
newmesh.name = mesh.name;

// clear the mesh arrays each mesh, we dont want to merge them by error
indices.clear();
vertices.clear();

for (auto&& p : mesh.primitives) {
GeoSurface newSurface;
newSurface.startIndex = (uint32_t)indices.size();
newSurface.count = (uint32_t)gltf.accessors[p.indicesAccessor.value()].count;

size_t initial_vtx = vertices.size();
{
fastgltf::Accessor& indexaccessor = gltf.accessors[p.indicesAccessor.value()];

fastgltf::iterateAccessor<std::uint32_t>(gltf, indexaccessor, [&](std::uint32_t idx) {
indices.push_back(idx + initial_vtx);
});
}

fastgltf::Accessor& posAccessor =gltf.accessors[p.findAttribute("POSITION")->second];

size_t vidx = initial_vtx;
fastgltf::iterateAccessor<glm::vec3>(gltf, posAccessor,
[&](glm::vec3 v) { vertices[vidx++].position = v;
Vertex newvtx;
newvtx.position = v;
newvtx.normal = {1,0,0};
newvtx.color = glm::vec4{1.f};
newvtx.uv_x = 0;
newvtx.uv_y = 0;
vertices.push_back(newvtx);
});

auto normals = p.findAttribute("NORMAL");
if (normals != p.attributes.end()) {
vidx = initial_vtx;
fastgltf::iterateAccessor<glm::vec3>(gltf, gltf.accessors[(*normals).second],
[&](glm::vec3 v) { vertices[vidx++].normal = v; });
}

auto uv = p.findAttribute("TEXCOORD_0");
if (uv != p.attributes.end()) {
vidx = initial_vtx;
fastgltf::iterateAccessor<glm::vec2>(gltf, gltf.accessors[(*uv).second], [&](glm::vec2 v) {

vertices[vidx].uv_x = v.x;
vertices[vidx].uv_y = v.y;
vidx++;
});
}

auto colors = p.findAttribute("COLOR_0");
if (colors != p.attributes.end()) {
vidx = initial_vtx;
fastgltf::iterateAccessor<glm::vec4>(gltf, gltf.accessors[(*colors).second],
[&](glm::vec4 v) { vertices[vidx++].color = v; });
}

newmesh.surfaces.push_back(newSurface);
}

newmesh.meshBuffers = engine->uploadMesh(indices, vertices);

meshes.emplace_back(std::move(newmesh));
}

return meshes;
```
As we iterate each primitive within a mesh, we use the iterateAccessor functions to access the vertex data we want. We also build the index buffer properly while appending the different primitives into the vertex arrays. At the end, we call uploadMesh to create the final buffers, then we return the mesh list.
The Position array is going to be there always, so we use that to initialize the Vertex structures. For all the other attributes we need to do it checking that the data exists.
Lets draw them.
```
GPUMeshBuffers rectangle;
std::vector<MeshAsset> testMeshes;
```
we begin by adding them to the VulkanEngine class. Lets load them from init_default_data()
```
testMeshes = loadGltfMeshes(this,"..\\..\\assets\\structure.glb").value();
```
In the file provided, index 0 is a cube, index 1 is a sphere, and index 2 is a blender monkeyhead. we will be drawing that last one, draw it right after drawing the rectangle from before
```cpp
vkCmdPushConstants(cmd, _meshPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(VkDeviceAddress), &testMeshes[2].meshBuffers.vertexBufferAddress);
vkCmdBindIndexBuffer(cmd, testMeshes[2].meshBuffers.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, testMeshes[2].surfaces[0].count, 1, testMeshes[2].surfaces[0].startIndex, 0, 0);
```

You will see that the monkey head is completely white due to the vertex color. Lets modify the loading code a little bit to force color to be the vertex normal. this way we can display something better for now until we have fancier shaders.

```cpp
for (Vertex& vtx : vertices) {
vtx.color = glm::vec4(vtx.normal, 1.f);
}

newmesh.meshBuffers = engine->uploadMesh(indices, vertices);
```
Now we have the monkey head and its visible, but its also upside down. we need to pass object matrix to the shader
47 changes: 33 additions & 14 deletions docs/new_vkguide/chapter_3/mesh_buffers.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,10 @@ With this we can create our mesh structure and setup the vertex buffer.
## mesh buffers on gpu

vk_types.h
<!-- codegen from tag vbuf_types on file E:\ProgrammingProjects\vulkan-guide-2\shared/vk_types.h -->
```cpp
//our vertex format. Very basic unoptimized vertex with position, normal, color, and UV
struct Vertex {

glm::vec3 position;
float uv_x;
glm::vec3 normal;
Expand All @@ -126,12 +127,20 @@ struct GPUMeshBuffers {
AllocatedBuffer vertexBuffer;
VkDeviceAddress vertexBufferAddress;
};

// push constants for our mesh object draws
struct GPUDrawPushConstants {
glm::mat4 worldMatrix;
VkDeviceAddress vertexBuffer;
};
```
We need a vertex format, so lets use this one. when creating a vertex format its very important to compact the data as much as possible, but for the current stage of the tutorial it wont matter. We will optimize this vertex format later. The reason the uv parameters are interleaved is due to alignement limitations on GPUs. We want this structure to match the shader version so interleaving it like this improves it.
We store our mesh data into a GPUMeshBuffers struct, which will contain the allocated buffer for both indices and vertices, plus the buffer device adress for the vertices.
We will create a struct for the push-constants we want to draw the mesh, it will contain the transform matrix for the object, and the device adress for the mesh buffer.
Now we need a function to create those buffers and fill them on the gpu.
<!-- codegen from tag mesh_create_1 on file E:\ProgrammingProjects\vulkan-guide-2\chapter-3/vk_engine.cpp -->
Expand Down Expand Up @@ -204,6 +213,7 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{
//push constants block
layout( push_constant ) uniform constants
{
mat4 render_matrix;
VertexBuffer vertexBuffer;
} PushConstants;

Expand All @@ -213,7 +223,7 @@ void main()
Vertex v = PushConstants.vertexBuffer.vertices[gl_VertexIndex];

//output data
gl_Position = vec4(v.position, 1.0f);
gl_Position = PushConstants.render_matrix * vec4(v.position, 1.0f);
outColor = v.color.xyz;
}
```
Expand All @@ -224,10 +234,10 @@ Then we have the vertex struct, which is the exact same one as the one we have o
After that, we declare the VertexBuffer, which is a readonly buffer that has an array (unsized) of Vertex structures. by having the `buffer_reference` in the layout, that tells the shader that this object is used from buffer adress. `std430` is the alignement rules for the structure.
We have our push_constant block which holds a single instance of our VertexBuffer. Because the vertex buffer is declared as buffer_reference, this is a uint64 handle.
We have our push_constant block which holds a single instance of our VertexBuffer, and a matrix. Because the vertex buffer is declared as buffer_reference, this is a uint64 handle, while the matrix is a normal matrix (no references).
From our main(), we index the vertex array using `gl_VertexIndex`, same as we did with the hardcoded array. We dont have -> like in cpp when accessing pointers, in glsl buffer address is accessed as a reference so it uses `.` to access it
With the vertex grabbed, we just output the color and position we want.
With the vertex grabbed, we just output the color and position we want, multiplying the position with the render matrix.
Lets create the pipeline now. We will create a new pipeline function, separate from `init_triangle_pipeline()` but almost the same. Add this to vulkanEngine class
Expand Down Expand Up @@ -264,7 +274,7 @@ Its going to be mostly a copypaste of `init_triangle_pipeline()`

VkPushConstantRange bufferRange{};
bufferRange.offset = 0;
bufferRange.size = sizeof(VkDeviceAddress);
bufferRange.size = sizeof(GPUDrawPushConstants);
bufferRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

VkPipelineLayoutCreateInfo pipeline_layout_info = vkinit::pipeline_layout_create_info();
Expand All @@ -276,7 +286,7 @@ Its going to be mostly a copypaste of `init_triangle_pipeline()`
```
We change the vertex shader to load `colored_triangle_mesh.vert.spv`, and we modify the pipeline layout to give it a single int64 worth of data on the vertex shader stage.
We change the vertex shader to load `colored_triangle_mesh.vert.spv`, and we modify the pipeline layout to give it the push constants struct we defined above.
For the rest of the function, we do the same as in the triangle pipeline function, but changing the pipeline layout and the pipeline name to be the new ones.
Expand Down Expand Up @@ -362,6 +372,8 @@ void VulkanEngine::init_default_data() {
rect_indices[5] = 3;

rectangle = uploadMesh(rect_indices,rect_vertices);

testMeshes = loadGltfMeshes(this,"..\\..\\assets\\basicmesh.glb").value();
}
```

Expand All @@ -373,25 +385,32 @@ We can now execute the draw. We will add the new draw command `on draw_geometry(
//launch a draw command to draw 3 vertices
vkCmdDraw(cmd, 3, 1, 0, 0);

vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);
//launch a draw command to draw 3 vertices
vkCmdDraw(cmd, 3, 1, 0, 0);

vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);

GPUDrawPushConstants push_constants;
push_constants.worldMatrix = glm::mat4{1.f};
push_constants.vertexBuffer = rectangle.vertexBufferAddress;

vkCmdPushConstants(cmd,_meshPipelineLayout,VK_SHADER_STAGE_VERTEX_BIT,0, sizeof(VkDeviceAddress), &rectangle.vertexBufferAddress);
vkCmdBindIndexBuffer(cmd, rectangle.indexBuffer.buffer,0,VK_INDEX_TYPE_UINT32);
vkCmdPushConstants(cmd,_meshPipelineLayout,VK_SHADER_STAGE_VERTEX_BIT,0, sizeof(GPUDrawPushConstants), &push_constants);
vkCmdBindIndexBuffer(cmd, rectangle.indexBuffer.buffer,0,VK_INDEX_TYPE_UINT32);

vkCmdDrawIndexed(cmd,6,1,0,0,0);
vkCmdDrawIndexed(cmd,6,1,0,0,0);

vkCmdEndRendering(cmd);
```
We bind another pipeline, this time the rectangle mesh one.
Then, we use push-constants to upload the vertexBufferAdress to the gpu.
Then, we use push-constants to upload the vertexBufferAdress to the gpu. For the matrix, we will be defaulting it for now until we implement mesh transformations.
We then need to do a cmdBindIndexBuffer to bind the index buffer for graphics. Sadly there is no way of using device adress here, and you need to give it the VkBuffer and offsets.
We then need to do a cmdBindIndexBuffer to bind the index buffer for graphics. Sadly there is no way of using device adress here, and you need to give it the VkBuffer and offsets.
Last, we use `vkCmdDrawIndexed` to draw 2 triangles (6 indices). This is the same as the vkCmdDraw, but it uses the currently bound index buffer to draw meshes.
Thats all, we now have a generic way of rendering any mesh. If you want, you can try to change or add triangles, or have multiple `GPUMeshBuffers` for multiple meshes. We will go further in there in the next chapter.
Thats all, we now have a generic way of rendering any mesh.
Last thing in this chapter will be setting up blending and transparency.
Next we will load mesh files from a GLTF in the most basic way so we can play around with fancier things than a rectangle.
Loading

0 comments on commit adec3e1

Please sign in to comment.