当我们进行基于计算的渲染时,我们已经展示了如何使用图像,但是关于图像,我们仍然需要处理一些事情,特别是如何在图形着色器中使用它们进行渲染和显示。我们将从这里开始,为我们的引擎创建一组默认纹理,然后从文件中加载纹理。
首先,我们需要向 VulkanEngine 类添加函数来处理创建和销毁图像。
将这些函数添加到头文件中的类中。
class VulkanEngine {
AllocatedImage create_image(VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped = false);
AllocatedImage create_image(void* data, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped = false);
void destroy_image(const AllocatedImage& img);
}
现在我们开始在 vk_engine.cpp 上编写函数
AllocatedImage VulkanEngine::create_image(VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped)
{
AllocatedImage newImage;
newImage.imageFormat = format;
newImage.imageExtent = size;
VkImageCreateInfo img_info = vkinit::image_create_info(format, usage, size);
if (mipmapped) {
img_info.mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(size.width, size.height)))) + 1;
}
// always allocate images on dedicated GPU memory
VmaAllocationCreateInfo allocinfo = {};
allocinfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
allocinfo.requiredFlags = VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
// allocate and create the image
VK_CHECK(vmaCreateImage(_allocator, &img_info, &allocinfo, &newImage.image, &newImage.allocation, nullptr));
// if the format is a depth format, we will need to have it use the correct
// aspect flag
VkImageAspectFlags aspectFlag = VK_IMAGE_ASPECT_COLOR_BIT;
if (format == VK_FORMAT_D32_SFLOAT) {
aspectFlag = VK_IMAGE_ASPECT_DEPTH_BIT;
}
// build a image-view for the image
VkImageViewCreateInfo view_info = vkinit::imageview_create_info(format, newImage.image, aspectFlag);
view_info.subresourceRange.levelCount = img_info.mipLevels;
VK_CHECK(vkCreateImageView(_device, &view_info, nullptr, &newImage.imageView));
return newImage;
}
这与我们之前创建绘制图像时所做的相同,只是复制到它自己的函数中。我们首先将大小和格式存储为 AllocatedImage 的一部分,然后我们使用大小、格式和用途创建一个 VkImageCreateInfo,然后我们使用 VMA 分配图像,最后创建图像视图。我们以前没有做的是设置纵横比标志。除非图像是 D32 浮点深度格式,否则我们将其默认为VK_IMAGE_ASPECT_COLOR_BIT
。
要写入图像数据,它的工作方式与我们在上一章中使用缓冲区所做的非常相似。我们需要创建一个临时暂存缓冲区,将我们的像素复制到其中,然后执行一个立即提交,在其中我们调用 VkCmdCopyBufferToImage。让我们也编写该函数。我们将把它作为同一个 create_image 函数的重载版本来做,但是为像素采用 void* 数据参数。我们将在这里硬编码我们的纹理为 RGBA 8 位格式,因为这是大多数图像文件所处的格式。
AllocatedImage VulkanEngine::create_image(void* data, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped)
{
size_t data_size = size.depth * size.width * size.height * 4;
AllocatedBuffer uploadbuffer = create_buffer(data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
memcpy(uploadbuffer.info.pMappedData, data, data_size);
AllocatedImage new_image = create_image(size, format, usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, mipmapped);
immediate_submit([&](VkCommandBuffer cmd) {
vkutil::transition_image(cmd, new_image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
VkBufferImageCopy copyRegion = {};
copyRegion.bufferOffset = 0;
copyRegion.bufferRowLength = 0;
copyRegion.bufferImageHeight = 0;
copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copyRegion.imageSubresource.mipLevel = 0;
copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = 1;
copyRegion.imageExtent = size;
// copy the buffer into the image
vkCmdCopyBufferToImage(cmd, uploadbuffer.buffer, new_image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
©Region);
vkutil::transition_image(cmd, new_image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
});
destroy_buffer(uploadbuffer);
return new_image;
}
我们首先在 CPU_TO_GPU 内存类型上分配一个具有足够像素数据空间的暂存缓冲区。然后我们将像素数据 memcpy 到其中。
之后,我们调用正常的 create_image 函数,但是我们添加了 VK_IMAGE_USAGE_TRANSFER_DST_BIT
和 VK_IMAGE_USAGE_TRANSFER_SRC_BIT
,以便允许将数据复制到其中和从中复制数据。
一旦我们有了图像和暂存缓冲区,我们就会运行一个立即提交,将暂存缓冲区像素数据复制到图像中。
类似于我们对交换链图像所做的那样,我们首先将图像转换为 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
。然后我们创建一个 copyRegion 结构,其中包含复制命令的参数。这将需要图像大小以及目标图像层和 mip 级别。图像层用于具有多层的纹理,最常见的例子之一是立方体贴图纹理,它将有 6 层,每个立方体贴图面一层。我们将在稍后设置反射立方体贴图时执行此操作。
对于 mip 级别,我们将数据复制到 mip 级别 0,这是顶层。图像没有任何更多的 mip 级别。目前,我们只是将 mipmapped 布尔值传递到另一个 create_image 中,但我们没有做任何其他事情。我们将在稍后处理。
对于图像销毁,让我们填写 destroy_image() 函数
void VulkanEngine::destroy_image(const AllocatedImage& img)
{
vkDestroyImageView(_device, img.imageView, nullptr);
vmaDestroyImage(_allocator, img.image, img.allocation);
}
我们首先销毁图像视图,然后使用 VMA 销毁图像本身。这将正确释放图像及其内存。
有了这些函数,我们可以设置一些默认纹理。我们将创建默认白色、默认黑色、默认灰色和棋盘格纹理。这样,当某些东西加载失败时,我们可以使用一些纹理。
让我们将这些测试图像添加到 VulkanEngine 类中,并添加一些我们可以与这些图像和其他图像一起使用的采样器。
AllocatedImage _whiteImage;
AllocatedImage _blackImage;
AllocatedImage _greyImage;
AllocatedImage _errorCheckerboardImage;
VkSampler _defaultSamplerLinear;
VkSampler _defaultSamplerNearest;
让我们去创建它们,作为 init_default_data()
函数的一部分,在我们创建矩形网格的代码之后。
//3 default textures, white, grey, black. 1 pixel each
uint32_t white = glm::packUnorm4x8(glm::vec4(1, 1, 1, 1));
_whiteImage = create_image((void*)&white, VkExtent3D{ 1, 1, 1 }, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT);
uint32_t grey = glm::packUnorm4x8(glm::vec4(0.66f, 0.66f, 0.66f, 1));
_greyImage = create_image((void*)&grey, VkExtent3D{ 1, 1, 1 }, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT);
uint32_t black = glm::packUnorm4x8(glm::vec4(0, 0, 0, 0));
_blackImage = create_image((void*)&black, VkExtent3D{ 1, 1, 1 }, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT);
//checkerboard image
uint32_t magenta = glm::packUnorm4x8(glm::vec4(1, 0, 1, 1));
std::array<uint32_t, 16 *16 > pixels; //for 16x16 checkerboard texture
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
pixels[y*16 + x] = ((x % 2) ^ (y % 2)) ? magenta : black;
}
}
_errorCheckerboardImage = create_image(pixels.data(), VkExtent3D{16, 16, 1}, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT);
VkSamplerCreateInfo sampl = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampl.magFilter = VK_FILTER_NEAREST;
sampl.minFilter = VK_FILTER_NEAREST;
vkCreateSampler(_device, &sampl, nullptr, &_defaultSamplerNearest);
sampl.magFilter = VK_FILTER_LINEAR;
sampl.minFilter = VK_FILTER_LINEAR;
vkCreateSampler(_device, &sampl, nullptr, &_defaultSamplerLinear);
_mainDeletionQueue.push_function([&](){
vkDestroySampler(_device,_defaultSamplerNearest,nullptr);
vkDestroySampler(_device,_defaultSamplerLinear,nullptr);
destroy_image(_whiteImage);
destroy_image(_greyImage);
destroy_image(_blackImage);
destroy_image(_errorCheckerboardImage);
});
对于 3 个默认颜色图像,我们创建以该颜色作为单像素的图像。对于棋盘格,我们编写一个 16x16 的像素颜色数据数组,其中包含一些简单的数学运算,用于黑色/品红色检查图案。
在采样器上,我们将保留所有参数为默认值,除了 min/mag 过滤器,我们将它们设置为 Linear 或 Nearest。Linear 会模糊像素,而 Nearest 会给出像素化的外观。
将图像绑定到着色器
当我们进行基于计算的渲染时,我们使用 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
绑定图像,这是我们用于没有采样逻辑的读/写纹理的类型。这大致相当于绑定缓冲区,只是一个具有不同内存布局的多维缓冲区。但是当我们进行绘制时,我们想使用 GPU 中的固定硬件来访问纹理数据,这需要采样器。我们可以选择使用 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
,它将图像和采样器打包在一起以用于该图像,或者使用 2 个描述符,并将两者分离为 VK_DESCRIPTOR_TYPE_SAMPLER
和 VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
。根据 GPU 供应商的说法,分离的方法可能会更快,因为重复数据较少。但这有点难处理,所以我们现在不会这样做。相反,我们将使用组合描述符来简化我们的着色器。
我们将修改我们之前拥有的矩形绘制,使其变为在矩形中显示图像的绘制。我们需要创建一个新的片段着色器来显示图像。让我们为此创建一个新的片段着色器。我们将其称为 tex_image.frag
//glsl version 4.5
#version 450
//shader input
layout (location = 0) in vec3 inColor;
layout (location = 1) in vec2 inUV;
//output write
layout (location = 0) out vec4 outFragColor;
//texture to access
layout(set =0, binding = 0) uniform sampler2D displayTexture;
void main()
{
outFragColor = texture(displayTexture,inUV);
}
我们对片段着色器有 2 个输入,颜色和 UV。着色器不使用颜色,但我们想继续使用我们之前拥有的相同顶点着色器。
要采样纹理,您可以执行 texture( textureSampler, coordinates )
。还有其他函数用于直接访问给定像素等操作。纹理对象声明为 uniform sampler2D
。
这确实改变了我们的管线布局,所以我们也需要更新它。
让我们将布局添加到 VulkanEngine 类中,因为我们将保留它。
class VulkanEngine {
VkDescriptorSetLayout _singleImageDescriptorLayout;
}
在 init_descriptors() 中,让我们与其他部分一起创建它
{
DescriptorLayoutBuilder builder;
builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
_singleImageDescriptorLayout = builder.build(_device, VK_SHADER_STAGE_FRAGMENT_BIT);
}
一个描述符集,只有一个图像-采样器描述符。我们现在可以使用它更新 init_mesh_pipeline()
函数。我们将修改开始部分,更改片段着色器并将描述符集布局连接到管线布局创建。
void VulkanEngine::init_mesh_pipeline()
{
VkShaderModule triangleFragShader;
if (!vkutil::load_shader_module("../../shaders/tex_image.frag.spv", _device, &triangleFragShader)) {
fmt::print("Error when building the fragment shader \n");
}
else {
fmt::print("Triangle fragment shader succesfully loaded \n");
}
VkShaderModule triangleVertexShader;
if (!vkutil::load_shader_module("../../shaders/colored_triangle_mesh.vert.spv", _device, &triangleVertexShader)) {
fmt::print("Error when building the vertex shader \n");
}
else {
fmt::print("Triangle vertex shader succesfully loaded \n");
}
VkPushConstantRange bufferRange{};
bufferRange.offset = 0;
bufferRange.size = sizeof(GPUDrawPushConstants);
bufferRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
VkPipelineLayoutCreateInfo pipeline_layout_info = vkinit::pipeline_layout_create_info();
pipeline_layout_info.pPushConstantRanges = &bufferRange;
pipeline_layout_info.pushConstantRangeCount = 1;
pipeline_layout_info.pSetLayouts = &_singleImageDescriptorLayout;
pipeline_layout_info.setLayoutCount = 1;
VK_CHECK(vkCreatePipelineLayout(_device, &pipeline_layout_info, nullptr, &_meshPipelineLayout));
}
现在,在我们的绘制函数中,我们可以动态创建绑定此管线时所需的描述符集,并使用它来显示我们要绘制的纹理。
这进入 draw_geometry()
函数,更改猴子网格的绘制
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);
//bind a texture
VkDescriptorSet imageSet = get_current_frame()._frameDescriptors.allocate(_device, _singleImageDescriptorLayout);
{
DescriptorWriter writer;
writer.write_image(0, _errorCheckerboardImage.imageView, _defaultSamplerNearest, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(_device, imageSet);
}
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipelineLayout, 0, 1, &imageSet, 0, nullptr);
glm::mat4 view = glm::translate(glm::vec3{ 0,0,-5 });
// camera projection
glm::mat4 projection = glm::perspective(glm::radians(70.f), (float)_drawExtent.width / (float)_drawExtent.height, 10000.f, 0.1f);
// invert the Y direction on projection matrix so that we are more similar
// to opengl and gltf axis
projection[1][1] *= -1;
GPUDrawPushConstants push_constants;
push_constants.worldMatrix = projection * view;
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);
我们从帧描述符集分配器分配一个新的描述符集,使用着色器使用的 _singleImageDescriptorLayout。
然后我们使用描述符写入器在绑定 0 上写入单个图像描述符,这将是 _errorCheckerboardImage。我们给它最近邻采样器,这样它就不会在像素之间混合。然后我们使用写入器更新描述符集,并绑定该集。然后我们继续绘制。
结果应该是猴头现在有一个品红色图案。
下一步: 引擎架构