GCG Vulkan Task Description Task 6
Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit
GCG Vulkan Task Description
Task 6 (20 Points + 15)
(Document version: 2023.1)
+1 bonus point for choosing the Vulkan route!
Introduction
Real-life objects are usually not colored in a single color but exhibit a surface structure that can be represented using textures. It could be said that textures are images, which are "glued" to the surface of an object. This requires three steps:
1. The texture has to be loaded into GPU memory .
2. The texture should be mapped to objects in some meaningful way (which we are going to use UV coordinates for) .
3. During rendering, color values must be sampled from an appropriate location within the texture for each rendered fragment and used as the surface color with illumination computations .
UV coordinates are a general approach to attach a specific texture location to a vertex, and they can be implemented as additional vertex attributes (just like we have extended the vertex attributes during Task 3 and 5, when adding vertex color and normals) .
Additional Vertex Attributes: UV Coordinates
The objectives of the first three subtasks are to add UV coordinates as additional
vertex attributes to every geometric object .
UV mapping is the task of computing a two-dimensional coordinate (which represents the corresponding location in a texture) for each vertex . This is a rather complex process for arbitrary shapes . Luckily, for primitive shapes there are some easier methods . To ensure a resolution-independent method, UV-coordinates are normalized to the range of 0 to 1, where (0, 0) in Vulkan corresponds to the top-left corner of the texture and (1, 1) corresponds to the bottom-right corner:
Note: You are free to choose how to store the different vertex attributes (positions, normals, and UV coordinates) . One option would be to store them in separate arrays/vectors, so that one array/vector contains all the positions (e.g ., p0, p1, p2, p3, . . . ), another array/vector contains all the normals (e.g ., n0, n1, n2, n3, . . . ), and yet another array/vector contains all the UV coordinates (e.g ., uv0, uv1, uv2, uv3, . . . ). A different option would be to put all vertex attributes into a common struct and store them in an interleaved manner (e.g ., p0, n0, uv0, p1, n1, uv1, p2, n2, uv2, p3, n3, uv3, . . . ) . In both cases, you'll have to provide the correct parameters for the stride members of the VkVertexInputBindingDescription instances . More detailed implementation instructions are described for Subtask 6.4.
Subtask 6.1: Add UV Coordinates to the Box Geometry
Your objective is to add UV coordinates to the box geometry, mapping the entirety of a texture to every face . Therefore, the top left vertex of a face shall be assigned the UV coordinate (0, 0) , and the bottom right vertex of a face shall be assigned the UV coordinate (1, 1) .
Subtask 6.2: Add UV Coordinates to the Cylinder Geometry
Your objective is to add UV coordinates to the cylinder geometry, mapping the entirety of a texture once to the lateral surface, and a circular region of a texture to top and bottom faces .
Starting with the lateral surface (side faces), the texture is wrapped around the
cylinder exactly once in its entirety . Hence, a top vertex always has a V coordinate of 0 ; and each bottom vertex has a V coordinate of 1 . The U coordinate only depends on the angle along the circle . Top and bottom faces of the cylinder are circular
shapes, but the texture is a rectangle (or square in terms of texture coordinates) .
For top and bottom faces, there are many possible ways to map a square to a circle, and any meaningful way is accepted here . The simplest approach is by simply cutting out a circle from the square texture . This is done by using the x and z positions
of a vertex and transforming it to the range [0, 1], where the transformed x corresponds to U, and the transformed z corresponds to V .
Note: The last segment of the lateral surface is distorted . This happens because the
UV coordinates are interpolated between the last and the first segment, jumping from
U=1 to U=0 . Since this problem doesn't have a trivial solution, it is fine to leave it
that way . Fixing this mapping problem can be done as a specialization task (see below) .
Subtask 6.3: Add UV Coordinates to the Sphere Gometry
Your objective is to add UV coordinates to the sphere geometry, mapping the entirety
of a texture once to the sphere's surface .
Spherical UV mapping can be established in a similar way as with the lateral surface
of a cylinder . The texture is wrapped around using horizontal and vertical angles to
compute UV coordinates . Note that the poles are distorted if only one vertex is used
because in such cases only one UV coordinate can be stored, whereas for every adjacent
vertex, a different U coordinate would be necessary . Similar to the cylinder, this
doesn't have to be fixed, but can be done as a specialization task (see below) .
Subtask 6.4: Add UV Coordinates to the Bézier Cylinder
Geometry
Your objective is to add UV coordinates to the Bézier cylinder geometry, mapping the
entirety of a texture once to the lateral surfaces, and a circular region of a texture
to top and bottom faces . Starting with the lateral surface (side faces), we measure
the distance between each sample point of the bezier curve to the previous one and add
it to the V coordinate or each vertex on the cirlce around it . The U coordinate is
calculated the same as you would also do it for the ordinary cylinder .
Subtask 6.5: Pass UV Coordinates As Vertex Attributes
During Subtask 3.4, you used vkCmdBindVertexBuffers to bind vertex position data as vertex attributes and you configured them to be streamed to the vertex shader's input location = 0 (as originally declared in the vertex shader during Subtask 2.1) .
During Subtask 6.6, you have added normals as additonal vertex attributes and bound
them to location = 1 so that they can be read and used in shaders . Now UV coordinates shall be added in the same manner .
Your objectives are:
Add an additional vertex attribute input binding at location = 2 in the vertex shader, which shall receive the UV coordintes that are to be bound as additonal vertex attribute .
Depending on your choice of data layout (separate arrays/vectors or interleaved data, as described above), you might have to add an entry to
V klGraphicsPipelineConfig::vertexInputBuffers (originally set-up during Subtask 2.1, then extended during Subtasks 3.7 and 5.6), describing the format of the
buffer(s) you are going to provide the API for reading vertex attributes from during draw calls . Make sure to read the documentation of
VkVertexInputBindingDescription carefully .
Add an entry to V klGraphicsPipelineConfig::inputAttributeDescriptions
describing where the UV coordintes data can be found! This description goes hand in hand with the data provided to
V klGraphicsPipelineConfig::vertexInputBuffers . Consult the documentation of VkVertexInputAttributeDescription and make sure to have normals streamed to input location = 2 !
Possibly adapt the invocation of vkCmdBindVertexBuffers (as established during Subtask 3.4, and extended during Subtasks 3.7 and 5.6) to match the
configuration provided to V klGraphicsPipelineConfig::vertexInputBuffers , and ensure that the draw call knows where to read the UV coordinates data from!
To test if UV coordinates have been successfully streamed to your vertex shader, you
could pass them on from the vertex shader:
layout (location = 0) out vec2 out_textureCoordinates;
to the fragment shader:
layout (location = 0) in vec2 in_textureCoordinates;
and write them to the fragment shader's output instead of writing color values as
required for the previous subtasks .
You should see the following output, when writing the U coordinate to the R channel, and the V coordinate to the G channel, leaving B at 0 :
Subtask 6.6: Interaction
Your objective is to use the functionality of Subtask 6.5 and implement user input to toggle the standard color view and texture coordinates view at run time! Extend the
key callback implemented during Subtask 1.11 and listen for an additional key [ T ]!
Similar to the previous settings, it should also be possible to set the rendering mode
by reading in a settings file specified in the command line arguments:
std::string init_renderer_filepath = "assets/settings/renderer_standard .ini";
if (cmdline_args .init_renderer) {
init_renderer_filepath = cmdline_args .init_renderer_filepath;
}
INIReader renderer_reader(init_renderer_filepath);
bool draw_texcoords = renderer_reader .GetBoolean("renderer", "texcoords", false);
To communicate this state to the shader, again use the ivec4 in your uniform buffer
created during Subtask 5.6.
When the user holds down key [T]:
Write some value into your newly added ivec4 before the call to
vklCopyDataIntoHostCoherentBuffer !
Evaluate the user input in your fragment shader whether the magic value representing [ T ] is there .
And if so, write texture coordinates to the fragment shader's color output!
For example:
// In main.cpp
ub_data .userInput[1] = draw_texcoords ? 1 : 0;
// In your shader
if (ub_data .userInput[0] == 1) {
out_color = vec4(n, 1);
}
if (ub_data .userInput[1] == 1) {
out_color = vec4(frag_in .textureCoordinates, 0, 1);
}
Subtask 6.7: Load DDS Textures into Images
First of all, textures need to be loaded from files. Along with the provided
framework, several DDS image files have been distributed . The framework provides the function vklLoadDdsImageIntoHostCoherentBuffer to load a DDS texture from file into a host-coherent buffer .
Important: Please always use relative paths when loading files, otherwise the program
won't run on a different computer (which is necessary for the submission talks) .
Hint: Similar to shader file handling, you are expected to place textures in the
directory assets/textures/ relative to the project root (where the CMakeLists .txt is located). A proper path passed to vklLoadDdsImageIntoHostCoherentBuffer would look,
e.g ., like follows: "assets/textures/wood_texture .dds" .
The function vklLoadDdsImageIntoHostCoherentBuffer loads (as its name indicates) the texture data from file into a host-coherent buffer . Host-coherent buffers require
their data to be stored in a certain memory region which allows them to be written
from the host-side directly . While this is very convenient for many use cases, it
often means (depending on the device used) that performance is sacrificed when that
memory is accessed on the device-side (i.e ., from your GPU during rendering) . There
are usually memory regions which can be accessed much faster by the device, and we're going to use such faster memory regions when creating images . The downside of these
faster "device-only" memory regions is that they cannot be accessed directly from the host-side . Therefore, we need an additional step to transfer data from host-coherent memory into device-only memory .
Your objectives are:
Load a DDS texture from file into a host-coherent VkBuffer using the framework function vklLoadDdsImageIntoHostCoherentBuffer !
Create a VkImage that has its associated data stored in device-only memory via the framework function vklCreateDeviceLocalImageWithBackingMemory !
Pass appropriate values for its width , height , and format parameters which you can get via vklGetDdsImageInfo !
Pass appropriate image usage flags to its VkImageUsageFlags parameter which allow the image to be used a) as the destination of a transfer
command, and b) also to be sampled by a shader! You'll find the
appropriate flags in the specification for VkImageUsageFlagBits .
Create a VkCommandPool via vkCreateCommandPool ! Hint: You only need one command pool overall, do not create a new one per texture!
Allocate a VkCommandBuffer from the command pool using
vkAllocateCommandBuffers and record the following instructions into it:
1. Begin recording using vkBeginCommandBuffer !
2. Record an image layout transition on the created VkImage via
vkCmdPipelineBarrier which transitions from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ! Hint: You'll need to use an
instance of VkImageMemoryBarrier with vkCmdPipelineBarrier for that purpose .
3. Copy the contents of the VkBuffer into the VkImage using
vkCmdCopyBufferToImage! Hint: Regarding the destination image, copy to mipmap level 0 , and also to array layer 0 ! There is only one mipmap
level and only one array layer to copy from the buffer into the image .
(Therefore, also the image layout transitions only need to be applied to that subresource range.)
4. Record another image layout transition on the VkImage via
vkCmdPipelineBarrier which transitions from
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL to enable the image to be accessed from shaders!
5. End recording using vkEndCommandBuffer !
Create a VkFence via vkCreateFence and ensure that it is not created in the signaled state, but instead, unsignaled!
Submit the recorded VkCommandBuffer to the queue which you have retrieved
during Subtask 1.7 and also pass the VkFence so that it gets signaled after he commands in the command buffer have completed execution!
Use vkWaitForFences to wait on the fence being signaled before the next usage of the VkImage , ensuring that it contains the texture's data .
Ensure to cleanup the VkFence , the VkCommandBuffer , the VkBuffer , and the VkImage when you no longer need them!
Use the implemented functionality to load two DDS textures: wood_texture .dds and tiles_diffuse .dds !
Optional: Use Synchronization2
You can think about using "VK_KHR_synchronization2" , which has the advantage that the pipeline barriers which are required to perform the image layout transitions are
specified in a clearer and more logical manner .
To enable Synchronization2 in your application, first enable the device extension "VK_KHR_synchronization2" !
After VkDevice creation and if enabling "VK_KHR_synchronization2" was successful, you'll have to get the function pointer for vkCmdPipelineBarrier2KHR , which is the appropriate function to record a pipeline barrier with Synchronization2 and is to be used instead of vkCmdPipelineBarrier . Because it is an extension, the function
pointers are not loaded by default but have to be loaded manually . Proceed as follows:
1. Declare a global variable
PFN_vkCmdPipelineBarrier2KHR g_vkCmdPipelineBarrier2KHR;
2. Get vkCmdPipelineBarrier2KHR 's function pointer as follows:
auto* procAddr = vkGetDeviceProcAddr(vk_device, "vkCmdPipelineBarrier2KHR");
if (procAddr == nullptr) {
// Couldn 't get the function pointer to vkCmdPipelineBarrier2KHR
}
else {
g_vkCmdPipelineBarrier2KHR = reinterpret_cast<PFN_vkCmdPipelineBarrier2KHR>
(procAddr);
}
3. If successful, use the function pointer stored in g_vkCmdPipelineBarrier2KHR just like you would use vkCmdPipelineBarrier2KHR ! I.e ., like follows:
g_vkCmdPipelineBarrier2KHR(my_command_buffer, &my_dependency_info);
Furthermore, ensure to use the enum values from
VkPipelineStageFlagBits2KHR instead of VkPipelineStageFlagBits , and
VkAccessFlagBits2KHR instead of VkAccessFlagBits !
Subtask 6.8: Create an Image View for each Image
Your objective is to create a VkImageView for each one of the VkImage handles created during Subtask 6.6 via vkCreateImageView !
There is no need to change the format nor to change the component mappings . You can leave the baseMipLevel = 0 , the levelCount = 1 , the baseArrayLayer = 0 , and the layerCount = 1 for now; but keep in mind that some of these values will have to be changed according to the requirements of Subtasks 5.10 and 5.13!
Subtask 6.9: Create a Sampler
Your objective is to create a VkSampler via vkCreateSampler which uses linear filtering for minification, magnification, and also for the mipmap lookups! Set minLod = 0 .0f and maxLod = VK_LOD_CLAMP_NONE (in preparation for Subtask 6.11)!
Subtask 6.10: Use the Textures in Shaders
Your objective is to create a descriptor of descriptor type
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER per texture and pass it to shaders . For creating this type of descriptor, you've got to combine a VkImageView handle from Subtask 6.7 with the VkSampler from Subtask 6.8. While you'll require different
VkImageView handles to represent different textures, you can re-use the VkSampler for all the different descriptors .
Furthermore, your objective is to access sampled textures in shaders through a suitable binding location! Use the GLSL function texture together with the UV
coordinates passed during Subtask 6.5 to read color values from the textures! Instead
of using constant color values for the diffuse surface color of an object (as in all
previous tasks so far), now use the color from the texture as an object's diffuse
surface color!
A VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER descriptor can be used in GLSL shaders like follows:
layout (binding = 3) uniform sampler2D diffuse_texture;
// ...
vec2 uv; // TODO: Assign!
vec3 diffuseColor = texture(diffuse_texture, uv) .rgb;
Note: It is sufficient to only extend the Phong-Phong shader!
Hint 1: Do not forget to extend the descriptor pool's size! In particular, make sure to add VkDescriptorPoolSize entries for the newly introduced
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER -type descriptors! Allocate space for at least three of these new descriptors, but still watch out for
VK_ERROR_OUT_OF_POOL_MEMORY errors . Different GPU vendors appear to interpret the sizes differently. While one GPU vendor might size the pool according to
VkDescriptorPoolSize::descriptorCount * VkDescriptorPoolCreateInfo::maxSets , other
vendors might disregard the maxSets parameter and base the total pool size solely on
VkDescriptorPoolSize::descriptorCount . Request an even greater pool size for the specialization tasks!
Hint 2: Do not forget to add the additional descriptors to the descriptor layout when
creating pipelines (see VklGraphicsPipelineConfig::descriptorLayout )!
Hint 3: Do not forget to add additional VkWriteDescriptorSet entries when writing data into the descriptor sets!
2024-01-16