引擎架构之 Vulkan Renderer 封装
基本思路
封装的基本思路是分层渲染,每一层保留自己的 uniform、descriptor set、pipeline、render pass、framebuffer,这通过继承 RendererBase 类实现,然后在渲染的循环里,在每一帧循环的最外层构建 command buffer,然后遍历不同层的 renderer,提交命令,代码如下:
VK_CHECK(vkBeginCommandBuffer(commandBuffer, &bi));
for (auto& r : renderers)
r->fillCommandBuffer(commandBuffer, imageIndex;
VK_CHECK(vkEndCommandBuffer(commandBuffer));
构建好命令后,提交到渲染队列,并等待呈现。
模块剖析
RendererBase
RendererBase 作为所以 layer 的父类,负责保存 Vulkan 渲染需要的状态,以及根据这些状态拼接对应的命令,见代码:
class RendererBase
{
public:
explicit RendererBase(const VulkanRenderDevice& vkDev, VulkanImage depthTexture)
: device_(vkDev.device)
, framebufferWidth_(vkDev.framebufferWidth)
, framebufferHeight_(vkDev.framebufferHeight)
, depthTexture_(depthTexture)
{}
virtual ~RendererBase();
virtual void fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) = 0;
inline VulkanImage getDepthTexture() const { return depthTexture_; }
protected:
void beginRenderPass(VkCommandBuffer commandBuffer, size_t currentImage);
bool createUniformBuffers(VulkanRenderDevice& vkDev, size_t uniformDataSize);
VkDevice device_ = nullptr;
uint32_t framebufferWidth_ = 0;
uint32_t framebufferHeight_ = 0;
// Depth buffer
VulkanImage depthTexture_;
// Descriptor set (layout + pool + sets) -> uses uniform buffers, textures, framebuffers
VkDescriptorSetLayout descriptorSetLayout_ = nullptr;
VkDescriptorPool descriptorPool_ = nullptr;
std::vector<VkDescriptorSet> descriptorSets_;
// Framebuffers (one for each command buffer)
std::vector<VkFramebuffer> swapchainFramebuffers_;
// 4. Pipeline & render pass (using DescriptorSets & pipeline state options)
VkRenderPass renderPass_ = nullptr;
VkPipelineLayout pipelineLayout_ = nullptr;
VkPipeline graphicsPipeline_ = nullptr;
// 5. Uniform buffer
std::vector<VkBuffer> uniformBuffers_;
std::vector<VkDeviceMemory> uniformBuffersMemory_;
};
先看方法:
- fillCommandBuffer,填充绘制命令;
- getDepthTexture,获取深度缓冲,为了在各个 layer 中共用;
- beginRenderPass,设置 vkCmdBeginRenderPass、vkCmdBindPipeline、vkCmdBindDescriptorSet;
- createUniformBuffers,更新 uniform buffer;
再看属性:
- device_,VkDevice;
- framebufferWidth、framebufferHeight,framebuffer 大小;
- depthTexture_,深度缓冲;
- swapchainFramebuffers_,framebuffer;
- descriptorSetLayout、descriptorPool、descriptorSets_,保存 uniform 变量;
- uniformBuffers、uniformBuffersMemory,uniform 缓冲;
- renderPass、pipelineLayout、graphicsPipeline_,render pass 和 pipeline;
方法实现:
createUniformBuffers
bool RendererBase::createUniformBuffers(VulkanRenderDevice& vkDev, size_t uniformDataSize) { uniformBuffers_.resize(vkDev.swapchainImages.size()); uniformBuffersMemory_.resize(vkDev.swapchainImages.size()); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { if (!createUniformBuffer(vkDev, uniformBuffers_[i], uniformBuffersMemory_[i], uniformDataSize)) { printf("Cannot create uniform buffer\n"); fflush(stdout); return false; } } return true; }
根据 swapchainImages 大小,调用子类的 createUniformBuffer,填充 uniformBuffers_;
beginRenderPass
void RendererBase::beginRenderPass(VkCommandBuffer commandBuffer, size_t currentImage) { const VkRect2D screenRect = { .offset = { 0, 0 }, .extent = {.width = framebufferWidth_, .height = framebufferHeight_ } }; const VkRenderPassBeginInfo renderPassInfo = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .pNext = nullptr, .renderPass = renderPass_, .framebuffer = swapchainFramebuffers_[currentImage], .renderArea = screenRect }; vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_); vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, 0, 1, &descriptorSets_[currentImage], 0, nullptr); }
开始 renderPass,绑定 pipeline 和 descriptorSets;
VulkanClear
构造函数
VulkanClear::VulkanClear(VulkanRenderDevice& vkDev, VulkanImage depthTexture): RendererBase(vkDev, depthTexture) , shouldClearDepth(depthTexture.image != VK_NULL_HANDLE) { if (!createColorAndDepthRenderPass( vkDev, shouldClearDepth, &renderPass_, RenderPassCreateInfo{ .clearColor_ = true, .clearDepth_ = true, .flags_ = eRenderPassBit_First })) { printf("VulkanClear: failed to create render pass\n"); exit(EXIT_FAILURE); } createColorAndDepthFramebuffers(vkDev, renderPass_, depthTexture.imageView, swapchainFramebuffers_); }
- 创建 renderPass;
- 创建 swapchainFramebuffers_,注意:FrameBuffer 相当于 DescriptorSet,本身只是 SwapChainImage 的集合,不持有 image,所以可以每个 Layer 都创建一个,但是底层持有的 SwapChainImage 是同一份;
fillCommandBuffer
void VulkanClear::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t swapFramebuffer) { EASY_FUNCTION(); const VkClearValue clearValues[2] = { VkClearValue { .color = { 1.0f, 1.0f, 1.0f, 1.0f } }, VkClearValue { .depthStencil = { 1.0f, 0 } } }; const VkRect2D screenRect = { .offset = { 0, 0 }, .extent = {.width = framebufferWidth_, .height = framebufferHeight_ } }; const VkRenderPassBeginInfo renderPassInfo = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = renderPass_, .framebuffer = swapchainFramebuffers_[swapFramebuffer], .renderArea = screenRect, .clearValueCount = static_cast<uint32_t>(shouldClearDepth ? 2 : 1), .pClearValues = &clearValues[0] }; vkCmdBeginRenderPass( commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE ); vkCmdEndRenderPass( commandBuffer ); }
注意,这里只是清除 Framebuffer 的颜色和深度缓冲,不需要创建 Pipeline;
VulkanFinish
VulkanFinish::VulkanFinish(VulkanRenderDevice& vkDev, VulkanImage depthTexture): RendererBase(vkDev, depthTexture)
{
if (!createColorAndDepthRenderPass(
vkDev, (depthTexture.image != VK_NULL_HANDLE), &renderPass_, RenderPassCreateInfo{ .clearColor_ = false, .clearDepth_ = false, .flags_ = eRenderPassBit_Last }))
{
printf("VulkanFinish: failed to create render pass\n");
exit(EXIT_FAILURE);
}
createColorAndDepthFramebuffers(vkDev, renderPass_, depthTexture.imageView, swapchainFramebuffers_);
}
void VulkanFinish::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage)
{
EASY_FUNCTION();
const VkRect2D screenRect = {
.offset = { 0, 0 },
.extent = {.width = framebufferWidth_, .height = framebufferHeight_ }
};
const VkRenderPassBeginInfo renderPassInfo = {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = renderPass_,
.framebuffer = swapchainFramebuffers_[currentImage],
.renderArea = screenRect
};
vkCmdBeginRenderPass( commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE );
vkCmdEndRenderPass( commandBuffer );
}
VulkanFinish 类似 VulkanClear,并不进行真正的渲染,只是将 FrameBuffer 中 Image 的 Layout 更新为是和呈现的布局,同时因为是最后的 Pass,所以不用清除缓存;
ModelRenderer
上述两个 Renderer 只是热身,这次来看渲染模型的 Renderer 如何组织。
类声明
class ModelRenderer: public RendererBase { public: ModelRenderer(VulkanRenderDevice& vkDev, const char* modelFile, const char* textureFile, uint32_t uniformDataSize); ModelRenderer(VulkanRenderDevice& vkDev, bool useDepth, VkBuffer storageBuffer, VkDeviceMemory storageBufferMemory, uint32_t vertexBufferSize, uint32_t indexBufferSize, VulkanImage texture, VkSampler textureSampler, const std::vector<const char*>& shaderFiles, uint32_t uniformDataSize, bool useGeneralTextureLayout = true, VulkanImage externalDepth = { .image = VK_NULL_HANDLE }, bool deleteMeshData = true); virtual ~ModelRenderer(); virtual void fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) override; void updateUniformBuffer(VulkanRenderDevice& vkDev, uint32_t currentImage, const void* data, const size_t dataSize); // HACK to allow sharing textures between multiple ModelRenderers void freeTextureSampler() { textureSampler_ = VK_NULL_HANDLE; } private: bool useGeneralTextureLayout_ = false; bool isExternalDepth_ = false; bool deleteMeshData_ = true; size_t vertexBufferSize_; size_t indexBufferSize_; // 6. Storage Buffer with index and vertex data VkBuffer storageBuffer_; VkDeviceMemory storageBufferMemory_; VkSampler textureSampler_; VulkanImage texture_; bool createDescriptorSet(VulkanRenderDevice& vkDev, uint32_t uniformDataSize); };
相对于父类,新增了以下方法和属性:
- updateUniformBuffer,更新 uniform 变量;
- vertexBufferSize、indexBufferSize、storageBuffer、storageBufferMemory,存储顶点;
- textureSampler、texture,模型纹理;
构造方法
ModelRenderer::ModelRenderer(VulkanRenderDevice& vkDev, const char* modelFile, const char* textureFile, uint32_t uniformDataSize): RendererBase(vkDev, VulkanImage()) { // Resource loading part if (!createTexturedVertexBuffer(vkDev, modelFile, &storageBuffer_, &storageBufferMemory_, &vertexBufferSize_, &indexBufferSize_)) { printf("ModelRenderer: createTexturedVertexBuffer() failed\n"); exit(EXIT_FAILURE); } createTextureImage(vkDev, textureFile, texture_.image, texture_.imageMemory); createImageView(vkDev.device, texture_.image, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, &texture_.imageView); createTextureSampler(vkDev.device, &textureSampler_); if (!createDepthResources(vkDev, vkDev.framebufferWidth, vkDev.framebufferHeight, depthTexture_) || !createColorAndDepthRenderPass(vkDev, true, &renderPass_, RenderPassCreateInfo()) || !createUniformBuffers(vkDev, uniformDataSize) || !createColorAndDepthFramebuffers(vkDev, renderPass_, depthTexture_.imageView, swapchainFramebuffers_) || !createDescriptorPool(vkDev, 1, 2, 1, &descriptorPool_) || !createDescriptorSet(vkDev, uniformDataSize) || !createPipelineLayout(vkDev.device, descriptorSetLayout_, &pipelineLayout_) || !createGraphicsPipeline(vkDev, renderPass_, pipelineLayout_, {"data/shaders/chapter03/VK02.vert", "data/shaders/chapter03/VK02.frag", "data/shaders/chapter03/VK02.geom" }, &graphicsPipeline_)) { printf("ModelRenderer: failed to create pipeline\n"); exit(EXIT_FAILURE); } }
- 加载顶点数组的 storage buffer;
- 加载纹理 texture;
- 创建 uniform buffer、descriptor set、descriptor set layout、pipeline、render pass、framebuffer、depth image;
createDescriptorSet
bool ModelRenderer::createDescriptorSet(VulkanRenderDevice& vkDev, uint32_t uniformDataSize) { const std::array<VkDescriptorSetLayoutBinding, 4> bindings = { descriptorSetLayoutBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT) }; const VkDescriptorSetLayoutCreateInfo layoutInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data() }; VK_CHECK(vkCreateDescriptorSetLayout(vkDev.device, &layoutInfo, nullptr, &descriptorSetLayout_)); std::vector<VkDescriptorSetLayout> layouts(vkDev.swapchainImages.size(), descriptorSetLayout_); const VkDescriptorSetAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, .descriptorPool = descriptorPool_, .descriptorSetCount = static_cast<uint32_t>(vkDev.swapchainImages.size()), .pSetLayouts = layouts.data() }; descriptorSets_.resize(vkDev.swapchainImages.size()); VK_CHECK(vkAllocateDescriptorSets(vkDev.device, &allocInfo, descriptorSets_.data())); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { VkDescriptorSet ds = descriptorSets_[i]; const VkDescriptorBufferInfo bufferInfo = { uniformBuffers_[i], 0, uniformDataSize }; const VkDescriptorBufferInfo bufferInfo2 = { storageBuffer_, 0, vertexBufferSize_ }; const VkDescriptorBufferInfo bufferInfo3 = { storageBuffer_, vertexBufferSize_, indexBufferSize_ }; const VkDescriptorImageInfo imageInfo = { textureSampler_, texture_.imageView, useGeneralTextureLayout_ ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; const std::array<VkWriteDescriptorSet, 4> descriptorWrites = { bufferWriteDescriptorSet(ds, &bufferInfo, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo2, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo3, 2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), imageWriteDescriptorSet( ds, &imageInfo, 3) }; vkUpdateDescriptorSets(vkDev.device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } return true; }
descriptorSet,对于每个 layer,是最不一样的部分,由各个 layer 自己负责单独实现,步骤如下:
- 确定 bindings,主要包括对应 buffer 的类型,被使用的 shader 类型;
- 根据 bindings,创建 descriptorSetLayout;
- 根据 descriptorSetLayout,创建 descriptorSet 数组;
- 通过 vkUpdateDescriptorSets,填充 buffer 到 descriptorSet 中;
updateUniformBuffer
void ModelRenderer::updateUniformBuffer(VulkanRenderDevice& vkDev, uint32_t currentImage, const void* data, const size_t dataSize) { uploadBufferData(vkDev, uniformBuffersMemory_[currentImage], 0, data, dataSize); }
更新 uniform 变量;
fillCommandBuffer
void ModelRenderer::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) { EASY_FUNCTION(); beginRenderPass(commandBuffer, currentImage); vkCmdDraw(commandBuffer, static_cast<uint32_t>(indexBufferSize_ / (sizeof(unsigned int))), 1, 0, 0); vkCmdEndRenderPass(commandBuffer); }
发起 DrawCall;
VulkanCanvas
对于调试来说,line 和 plane 是很常用的手段,但是并不能在 vulkan 中很方便的画出,本模块是实现即时模式下的画线和画平面操作;
类定义
class VulkanCanvas: public RendererBase { public: explicit VulkanCanvas(VulkanRenderDevice& vkDev, VulkanImage depth); virtual ~VulkanCanvas(); virtual void fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) override; void clear(); void line(const vec3& p1, const vec3& p2, const vec4& c); void plane3d(const vec3& orig, const vec3& v1, const vec3& v2, int n1, int n2, float s1, float s2, const vec4& color, const vec4& outlineColor); void updateBuffer(VulkanRenderDevice& vkDev, size_t currentImage); void updateUniformBuffer(VulkanRenderDevice& vkDev, const glm::mat4& modelViewProj , float time, uint32_t currentImage); private: struct VertexData { vec3 position; vec4 color; }; struct UniformBuffer { glm::mat4 mvp; float time; }; bool createDescriptorSet(VulkanRenderDevice& vkDev); std::vector<VertexData> lines_; // 7. Storage Buffer with index and vertex data std::vector<VkBuffer> storageBuffer_; std::vector<VkDeviceMemory> storageBufferMemory_; static constexpr unsigned kMaxLinesCount = 65536; static constexpr unsigned kMaxLinesDataSize = kMaxLinesCount * sizeof(VulkanCanvas::VertexData) * 2; };
这里主要的不同点在于新增了 line、plane3d 等,和几何绘制相关的方法,以及 lines_,保存顶点的属性;
构造函数
VulkanCanvas::VulkanCanvas(VulkanRenderDevice& vkDev, VulkanImage depth): RendererBase(vkDev, depth) { const size_t imgCount = vkDev.swapchainImages.size(); storageBuffer_.resize(imgCount); storageBufferMemory_.resize(imgCount); for(size_t i = 0 ; i < imgCount ; i++) { if (!createBuffer(vkDev.device, vkDev.physicalDevice, kMaxLinesDataSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, storageBuffer_[i], storageBufferMemory_[i])) { printf("VaulkanCanvas: createBuffer() failed\n"); exit(EXIT_FAILURE); } } if (!createColorAndDepthRenderPass(vkDev, (depth.image != VK_NULL_HANDLE), &renderPass_, RenderPassCreateInfo()) || !createUniformBuffers(vkDev, sizeof(UniformBuffer)) || !createColorAndDepthFramebuffers(vkDev, renderPass_, depth.imageView, swapchainFramebuffers_) || !createDescriptorPool(vkDev, 1, 1, 0, &descriptorPool_) || !createDescriptorSet(vkDev) || !createPipelineLayout(vkDev.device, descriptorSetLayout_, &pipelineLayout_) || !createGraphicsPipeline(vkDev, renderPass_, pipelineLayout_, { "data/shaders/chapter04/Lines.vert", "data/shaders/chapter04/Lines.frag" }, &graphicsPipeline_, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, (depth.image != VK_NULL_HANDLE), true )) { printf("VulkanCanvas: failed to create pipeline\n"); exit(EXIT_FAILURE); } }
注意:
- 由于顶点数据是动态变化的,所以这里需要每一个 swapChainImage 都对应一个 storageBuffer,并且为了更新数据方便,而且数据量不大的情况下,直接采用 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 标志,创建 buffer;
- createGraphicsPipeline 方法中的 图元类型选择 VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
createDescriptorSet
bool VulkanCanvas::createDescriptorSet(VulkanRenderDevice& vkDev) { const std::array<VkDescriptorSetLayoutBinding, 2> bindings = { descriptorSetLayoutBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) }; const VkDescriptorSetLayoutCreateInfo layoutInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data() }; VK_CHECK(vkCreateDescriptorSetLayout(vkDev.device, &layoutInfo, nullptr, &descriptorSetLayout_)); std::vector<VkDescriptorSetLayout> layouts(vkDev.swapchainImages.size(), descriptorSetLayout_); const VkDescriptorSetAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, .descriptorPool = descriptorPool_, .descriptorSetCount = static_cast<uint32_t>(vkDev.swapchainImages.size()), .pSetLayouts = layouts.data() }; descriptorSets_.resize(vkDev.swapchainImages.size()); VK_CHECK(vkAllocateDescriptorSets(vkDev.device, &allocInfo, descriptorSets_.data())); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { VkDescriptorSet ds = descriptorSets_[i]; const VkDescriptorBufferInfo bufferInfo = { uniformBuffers_[i], 0, sizeof(UniformBuffer) }; const VkDescriptorBufferInfo bufferInfo2 = { storageBuffer_[i], 0, kMaxLinesDataSize }; const std::array<VkWriteDescriptorSet, 2> descriptorWrites = { bufferWriteDescriptorSet(ds, &bufferInfo, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo2, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) }; vkUpdateDescriptorSets(vkDev.device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } return true; }
创建 DescriptorSet,除了注意每个 swapChainImage 有自己的 storageBuffer 之外,没有什么特别的点;
fillCommandBuffer
void VulkanCanvas::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) { EASY_FUNCTION(); if (lines_.empty()) return; beginRenderPass(commandBuffer, currentImage); vkCmdDraw( commandBuffer, static_cast<uint32_t>(lines_.size()), 1, 0, 0 ); vkCmdEndRenderPass( commandBuffer ); }
填充绘制命令
updateBuffer
void VulkanCanvas::updateBuffer(VulkanRenderDevice& vkDev, size_t currentImage) { if (lines_.empty()) return; const VkDeviceSize bufferSize = lines_.size() * sizeof(VertexData); uploadBufferData(vkDev, storageBufferMemory_[currentImage], 0, lines_.data(), bufferSize); }
更新顶点缓冲;
updateUniformBuffer
void VulkanCanvas::updateUniformBuffer(VulkanRenderDevice& vkDev, const glm::mat4& modelViewProj, float time, uint32_t currentImage) {
const UniformBuffer ubo = { .mvp = modelViewProj, .time = time }; uploadBufferData(vkDev, uniformBuffersMemory_[currentImage], 0, &ubo, sizeof(ubo));
}
更新 Uniform 缓冲;
绘制线、面,以及清空
void VulkanCanvas::clear() { lines_.clear(); } void VulkanCanvas::line(const vec3& p1, const vec3& p2, const vec4& c) { lines_.push_back( { .position = p1, .color = c } ); lines_.push_back( { .position = p2, .color = c } ); } void VulkanCanvas::plane3d(const vec3& o, const vec3& v1, const vec3& v2, int n1, int n2, float s1, float s2, const vec4& color, const vec4& outlineColor) { line(o - s1 / 2.0f * v1 - s2 / 2.0f * v2, o - s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor); line(o + s1 / 2.0f * v1 - s2 / 2.0f * v2, o + s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor); line(o - s1 / 2.0f * v1 + s2 / 2.0f * v2, o + s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor); line(o - s1 / 2.0f * v1 - s2 / 2.0f * v2, o + s1 / 2.0f * v1 - s2 / 2.0f * v2, outlineColor); for (int i = 1; i < n1; i++) { float t = ((float)i - (float)n1 / 2.0f) * s1 / (float)n1; const vec3 o1 = o + t * v1; line(o1 - s2 / 2.0f * v2, o1 + s2 / 2.0f * v2, color); } for (int i = 1; i < n2; i++) { const float t = ((float)i - (float)n2 / 2.0f) * s2 / (float)n2; const vec3 o2 = o + t * v2; line(o2 - s1 / 2.0f * v1, o2 + s1 / 2.0f * v1, color); } }
这里分开说:
- clear 清空函数,清空数据,这里的实现逻辑并不支持在每一帧里动态修改、清除数据;
- line 划线函数,设置线的起点和终点;
- plane3d 绘制平面,o 是中心点,一般是 (0,y,0) 形式,表示沿着 y 轴的位置,然后 v1 和 v2 是扩展的两个方向,一般用 (1,0,0) 和 (0,0,1),表示沿着 x 轴和 z 轴扩展,其实就是以 y 轴为中心,扩展一个和 y 轴垂直的平面,s 表示扩展的倍数,n 表示细分成多少网格;
VulkanImGui
类定义
class ImGuiRenderer: public RendererBase { public: explicit ImGuiRenderer(VulkanRenderDevice& vkDev); explicit ImGuiRenderer(VulkanRenderDevice& vkDev, const std::vector<VulkanTexture>& textures); virtual ~ImGuiRenderer(); virtual void fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) override; void updateBuffers(VulkanRenderDevice& vkDev, uint32_t currentImage, const ImDrawData* imguiDrawData); private: const ImDrawData* drawData = nullptr; bool createDescriptorSet(VulkanRenderDevice& vkDev); /* Descriptor set with multiple textures (for offscreen buffer display etc.) */ bool createMultiDescriptorSet(VulkanRenderDevice& vkDev); std::vector<VulkanTexture> extTextures_; // storage buffer with index and vertex data VkDeviceSize bufferSize_; std::vector<VkBuffer> storageBuffer_; std::vector<VkDeviceMemory> storageBufferMemory_; VkSampler fontSampler_; VulkanImage font_; };
注意 updateBuffers 函数的 ImDrawData 参数,里面保留了绘制 ImGUI 所需要的全部数据,并将其保存在
const ImDrawData* drawData = nullptr;
属性中;createDescriptorSet
constexpr uint32_t ImGuiVtxBufferSize = 512 * 1024 * sizeof(ImDrawVert); constexpr uint32_t ImGuiIdxBufferSize = 512 * 1024 * sizeof(uint32_t); bool ImGuiRenderer::createDescriptorSet(VulkanRenderDevice& vkDev) { const std::array<VkDescriptorSetLayoutBinding, 4> bindings = { descriptorSetLayoutBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT) }; const VkDescriptorSetLayoutCreateInfo layoutInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data() }; VK_CHECK(vkCreateDescriptorSetLayout(vkDev.device, &layoutInfo, nullptr, &descriptorSetLayout_)); std::vector<VkDescriptorSetLayout> layouts(vkDev.swapchainImages.size(), descriptorSetLayout_); const VkDescriptorSetAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, .descriptorPool = descriptorPool_, .descriptorSetCount = static_cast<uint32_t>(vkDev.swapchainImages.size()), .pSetLayouts = layouts.data() }; descriptorSets_.resize(vkDev.swapchainImages.size()); VK_CHECK(vkAllocateDescriptorSets(vkDev.device, &allocInfo, descriptorSets_.data())); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { VkDescriptorSet ds = descriptorSets_[i]; const VkDescriptorBufferInfo bufferInfo = { uniformBuffers_[i], 0, sizeof(mat4) }; const VkDescriptorBufferInfo bufferInfo2 = { storageBuffer_[i], 0, ImGuiVtxBufferSize }; const VkDescriptorBufferInfo bufferInfo3 = { storageBuffer_[i], ImGuiVtxBufferSize, ImGuiIdxBufferSize }; const VkDescriptorImageInfo imageInfo = { fontSampler_, font_.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; const std::array<VkWriteDescriptorSet, 4> descriptorWrites = { bufferWriteDescriptorSet(ds, &bufferInfo, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo2, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo3, 2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), imageWriteDescriptorSet( ds, &imageInfo, 3) }; vkUpdateDescriptorSets(vkDev.device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } return true; }
和之前的函数类似,需要注意的是,这里一开始设定了 ImGuiVtxBufferSize 和 ImGuiIdxBufferSize,预先分配了顶点缓冲和索引缓冲的大小;
构造函数
ImGuiRenderer::ImGuiRenderer(VulkanRenderDevice& vkDev): RendererBase(vkDev, VulkanImage()) { // Resource loading ImGuiIO& io = ImGui::GetIO(); createFontTexture(io, "data/OpenSans-Light.ttf", vkDev, font_.image, font_.imageMemory); createImageView(vkDev.device, font_.image, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, &font_.imageView); createTextureSampler(vkDev.device, &fontSampler_); // Buffer allocation const size_t imgCount = vkDev.swapchainImages.size(); storageBuffer_.resize(imgCount); storageBufferMemory_.resize(imgCount); bufferSize_ = ImGuiVtxBufferSize + ImGuiIdxBufferSize; for(size_t i = 0 ; i < imgCount ; i++) { if (!createBuffer(vkDev.device, vkDev.physicalDevice, bufferSize_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, storageBuffer_[i], storageBufferMemory_[i])) { printf("ImGuiRenderer: createBuffer() failed\n"); exit(EXIT_FAILURE); } } // Pipeline creation if (!createColorAndDepthRenderPass(vkDev, false, &renderPass_, RenderPassCreateInfo()) || !createColorAndDepthFramebuffers(vkDev, renderPass_, VK_NULL_HANDLE, swapchainFramebuffers_) || !createUniformBuffers(vkDev, sizeof(mat4)) || !createDescriptorPool(vkDev, 1, 2, 1, &descriptorPool_) || !createDescriptorSet(vkDev) || !createPipelineLayout(vkDev.device, descriptorSetLayout_, &pipelineLayout_) || !createGraphicsPipeline(vkDev, renderPass_, pipelineLayout_, { "data/shaders/chapter04/imgui.vert", "data/shaders/chapter04/imgui.frag" }, &graphicsPipeline_, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, true, true, true)) { printf("ImGuiRenderer: pipeline creation failed\n"); exit(EXIT_FAILURE); } }
主要分为三部分:
- font_,加载字体纹理;
- storageBuffer_,分配几何数据缓存;
- graphicsPipeline_,创建绘图管线;
createFontTexture
bool createFontTexture(ImGuiIO& io, const char* fontFile, VulkanRenderDevice& vkDev, VkImage& textureImage, VkDeviceMemory& textureImageMemory) { // Build texture atlas ImFontConfig cfg = ImFontConfig(); cfg.FontDataOwnedByAtlas = false; cfg.RasterizerMultiply = 1.5f; cfg.SizePixels = 768.0f / 32.0f; cfg.PixelSnapH = true; cfg.OversampleH = 4; cfg.OversampleV = 4; ImFont* Font = io.Fonts->AddFontFromFileTTF(fontFile, cfg.SizePixels, &cfg); unsigned char* pixels = nullptr; int texWidth = 1, texHeight = 1; io.Fonts->GetTexDataAsRGBA32(&pixels, &texWidth, &texHeight); if (!pixels || !createTextureImageFromData(vkDev, textureImage, textureImageMemory, pixels, texWidth, texHeight, VK_FORMAT_R8G8B8A8_UNORM)) { printf("Failed to load texture\n"); fflush(stdout); return false; } io.Fonts->TexID = (ImTextureID)0; io.FontDefault = Font; io.DisplayFramebufferScale = ImVec2(1, 1); return true; }
添加字体时,使用到了 createFontTexture 函数,这个函数主要是给 io 的 Fonts->TexID 和 FontDefault 赋值,Fonts->TexID 赋值默认 0 即可,FontDefault 赋值需要通过 io.Fonts->AddFontFromFileTTF 加载 ttf 字体文件,同时我们通过 io.Fonts->GetTexDataAsRGBA32 获取 BitMap,并构造纹理对象;
fillCommandBuffer
void ImGuiRenderer::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) { EASY_FUNCTION(); beginRenderPass(commandBuffer, currentImage); ImVec2 clipOff = drawData->DisplayPos; // (0,0) unless using multi-viewports ImVec2 clipScale = drawData->FramebufferScale; // (1,1) unless using retina display which are often (2,2) int vtxOffset = 0; int idxOffset = 0; for (int n = 0; n < drawData->CmdListsCount; n++) { const ImDrawList* cmdList = drawData->CmdLists[n]; for (int cmd = 0; cmd < cmdList->CmdBuffer.Size ; cmd++) { const ImDrawCmd* pcmd = &cmdList->CmdBuffer[cmd]; addImGuiItem(framebufferWidth_, framebufferHeight_, commandBuffer, pcmd, clipOff, clipScale, idxOffset, vtxOffset, extTextures_, pipelineLayout_); } idxOffset += cmdList->IdxBuffer.Size; vtxOffset += cmdList->VtxBuffer.Size; } vkCmdEndRenderPass(commandBuffer); }
这里通过 addImGuiItem 函数解析每一条 ImCMD
addImGuiItem
void addImGuiItem(uint32_t width, uint32_t height, VkCommandBuffer commandBuffer, const ImDrawCmd* pcmd, ImVec2 clipOff, ImVec2 clipScale, int idxOffset, int vtxOffset, const std::vector<VulkanTexture>& textures, VkPipelineLayout pipelineLayout) { if (pcmd->UserCallback) return; // Project scissor/clipping rectangles into framebuffer space ImVec4 clipRect; clipRect.x = (pcmd->ClipRect.x - clipOff.x) * clipScale.x; clipRect.y = (pcmd->ClipRect.y - clipOff.y) * clipScale.y; clipRect.z = (pcmd->ClipRect.z - clipOff.x) * clipScale.x; clipRect.w = (pcmd->ClipRect.w - clipOff.y) * clipScale.y; if (clipRect.x < width && clipRect.y < height && clipRect.z >= 0.0f && clipRect.w >= 0.0f) { if (clipRect.x < 0.0f) clipRect.x = 0.0f; if (clipRect.y < 0.0f) clipRect.y = 0.0f; // Apply scissor/clipping rectangle const VkRect2D scissor = { .offset = { .x = (int32_t)(clipRect.x), .y = (int32_t)(clipRect.y) }, .extent = { .width = (uint32_t)(clipRect.z - clipRect.x), .height = (uint32_t)(clipRect.w - clipRect.y) } }; vkCmdSetScissor(commandBuffer, 0, 1, &scissor); // this is added in the Chapter 6: Using descriptor indexing in Vulkan to render an ImGui UI if (textures.size() > 0) { uint32_t texture = (uint32_t)(intptr_t)pcmd->TextureId; vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(uint32_t), (const void*)&texture); } vkCmdDraw(commandBuffer, pcmd->ElemCount, 1, pcmd->IdxOffset + idxOffset, pcmd->VtxOffset + vtxOffset); } }
这里注意两点:
- 使用 vkCmdSetScissor 设定裁剪窗口,这里在调用 createGraphicsPipeline 函数时,设置了 dynamicScissorState 参数为 true;
注意 vkCmdDraw 最后两个参数分别表示 firstVerticle 和 firstInstance,乍一看非常迷惑,其实理解了 ImGUI 的每条命令的数据结构后就不难理解了,附上 imgui.vert 的代码:
#version 460 layout(location = 0) out vec2 uv; layout(location = 1) out vec4 color; struct ImDrawVert{ float x, y, u, v; uint color; }; layout(binding = 0) uniform UniformBuffer { mat4 inMtx; } ubo; layout(binding = 1) readonly buffer SBO { ImDrawVert data[]; } sbo; layout(binding = 2) readonly buffer IBO { uint data[]; } ibo; void main() { uint idx = ibo.data[gl_VertexIndex] + gl_BaseInstance; ImDrawVert v = sbo.data[idx]; uv = vec2(v.u, v.v); color = unpackUnorm4x8(v.color); gl_Position = ubo.inMtx * vec4(v.x, v.y, 0.0, 1.0); }
- pcmd->ElemCount,设定了 gl_VertexIndex 的结束范围是 pcmd->ElemCount;
- pcmd->IdxOffset + idxOffset,设定了 gl_VertexIndex 的开始范围是 pcmd->IdxOffset + idxOffset,所以目前 gl_VertexIndex 的范围是[pcmd->IdxOffset + idxOffset, pcmd->ElemCount];
- pcmd->VtxOffset + vtxOffset,设定了 gl_BaseInstance 的值;
updateBuffers
void ImGuiRenderer::updateBuffers(VulkanRenderDevice& vkDev, uint32_t currentImage, const ImDrawData* imguiDrawData) { drawData = imguiDrawData; const float L = drawData->DisplayPos.x; const float R = drawData->DisplayPos.x + drawData->DisplaySize.x; const float T = drawData->DisplayPos.y; const float B = drawData->DisplayPos.y + drawData->DisplaySize.y; const mat4 inMtx = glm::ortho(L, R, T, B); uploadBufferData(vkDev, uniformBuffersMemory_[currentImage], 0, glm::value_ptr(inMtx), sizeof(mat4)); void* data = nullptr; vkMapMemory(vkDev.device, storageBufferMemory_[currentImage], 0, bufferSize_, 0, &data); ImDrawVert* vtx = (ImDrawVert*)data; for (int n = 0; n < drawData->CmdListsCount; n++) { const ImDrawList* cmdList = drawData->CmdLists[n]; memcpy(vtx, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.Size * sizeof(ImDrawVert)); vtx += cmdList->VtxBuffer.Size; } uint32_t* idx = (uint32_t*)((uint8_t*)data + ImGuiVtxBufferSize); for (int n = 0; n < drawData->CmdListsCount; n++) { const ImDrawList* cmdList = drawData->CmdLists[n]; const uint16_t* src = (const uint16_t*)cmdList->IdxBuffer.Data; for (int j = 0; j < cmdList->IdxBuffer.Size; j++) *idx++ = (uint32_t)*src++; } vkUnmapMemory(vkDev.device, storageBufferMemory_[currentImage]); }
注意:
- imguiDrawData 的数据是在外面设定好了,然后传进来,该函数只是起到一个更新几何数据的作用;
- 遍历 drawData,填充几何数据缓冲;
VulkanCube
通过 Cube,绘制场景背景
类声明
class CubeRenderer: public RendererBase { public: CubeRenderer(VulkanRenderDevice& vkDev, VulkanImage inDepthTexture, const char* textureFile); virtual ~CubeRenderer(); virtual void fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) override; void updateUniformBuffer(VulkanRenderDevice& vkDev, uint32_t currentImage, const mat4& m); private: VkSampler textureSampler; VulkanImage texture; bool createDescriptorSet(VulkanRenderDevice& vkDev); };
主要新增一个 texture,用来保存 cubemap;
构造函数
CubeRenderer::CubeRenderer(VulkanRenderDevice& vkDev, VulkanImage inDepthTexture, const char* textureFile): RendererBase(vkDev, inDepthTexture) { // Resource loading createCubeTextureImage(vkDev, textureFile, texture.image, texture.imageMemory); createImageView(vkDev.device, texture.image, VK_FORMAT_R32G32B32A32_SFLOAT, VK_IMAGE_ASPECT_COLOR_BIT, &texture.imageView, VK_IMAGE_VIEW_TYPE_CUBE, 6); createTextureSampler(vkDev.device, &textureSampler); // Pipeline initialization if (!createColorAndDepthRenderPass(vkDev, true, &renderPass_, RenderPassCreateInfo()) || !createUniformBuffers(vkDev, sizeof(mat4)) || !createColorAndDepthFramebuffers(vkDev, renderPass_, depthTexture_.imageView, swapchainFramebuffers_) || !createDescriptorPool(vkDev, 1, 0, 1, &descriptorPool_) || !createDescriptorSet(vkDev) || !createPipelineLayout(vkDev.device, descriptorSetLayout_, &pipelineLayout_) || !createGraphicsPipeline(vkDev, renderPass_, pipelineLayout_, { "data/shaders/chapter04/VKCube.vert", "data/shaders/chapter04/VKCube.frag" }, &graphicsPipeline_)) { printf("CubeRenderer: failed to create pipeline\n"); exit(EXIT_FAILURE); } }
通过 createCubeTextureImage 函数创建一个 cubemap 的纹理对象;
shared/UtilsVulkan.cpp
bool createCubeTextureImage(VulkanRenderDevice& vkDev, const char* filename, VkImage& textureImage, VkDeviceMemory& textureImageMemory, uint32_t* width, uint32_t* height) { int w, h, comp; const float* img = stbi_loadf(filename, &w, &h, &comp, 3); std::vector<float> img32(w * h * 4); float24to32(w, h, img, img32.data()); if (!img) { printf("Failed to load [%s] texture\n", filename); fflush(stdout); return false; } stbi_image_free((void*)img); Bitmap in(w, h, 4, eBitmapFormat_Float, img32.data()); Bitmap out = convertEquirectangularMapToVerticalCross(in); Bitmap cube = convertVerticalCrossToCubeMapFaces(out); if (width && height) { *width = w; *height = h; } return createTextureImageFromData(vkDev, textureImage, textureImageMemory, cube.data_.data(), cube.w_, cube.h_, VK_FORMAT_R32G32B32A32_SFLOAT, 6, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT); } bool createImageView(VkDevice device, VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, VkImageView* imageView, VkImageViewType viewType, uint32_t layerCount, uint32_t mipLevels) { const VkImageViewCreateInfo viewInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .pNext = nullptr, .flags = 0, .image = image, .viewType = viewType, .format = format, .subresourceRange = { .aspectMask = aspectFlags, .baseMipLevel = 0, .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = layerCount } }; return (vkCreateImageView(device, &viewInfo, nullptr, imageView) == VK_SUCCESS); }
注意,这里生成 cubemap texture 的过程和普通的 texture 的过程没什么区别,主要是通过 layerCount 设置为 6,在生成 image 和 imageView 是都需要设置 6 个面(+x、-x、+y、-y、+z、-z);
其他方法
bool CubeRenderer::createDescriptorSet(VulkanRenderDevice& vkDev) { const std::array<VkDescriptorSetLayoutBinding, 2> bindings = { descriptorSetLayoutBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), descriptorSetLayoutBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT) }; const VkDescriptorSetLayoutCreateInfo layoutInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data() }; VK_CHECK(vkCreateDescriptorSetLayout(vkDev.device, &layoutInfo, nullptr, &descriptorSetLayout_)); std::vector<VkDescriptorSetLayout> layouts(vkDev.swapchainImages.size(), descriptorSetLayout_); const VkDescriptorSetAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, .descriptorPool = descriptorPool_, .descriptorSetCount = static_cast<uint32_t>(vkDev.swapchainImages.size()), .pSetLayouts = layouts.data() }; descriptorSets_.resize(vkDev.swapchainImages.size()); VK_CHECK(vkAllocateDescriptorSets(vkDev.device, &allocInfo, descriptorSets_.data())); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { VkDescriptorSet ds = descriptorSets_[i]; const VkDescriptorBufferInfo bufferInfo = { uniformBuffers_[i], 0, sizeof(mat4) }; const VkDescriptorImageInfo imageInfo = { textureSampler, texture.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; const std::array<VkWriteDescriptorSet, 2> descriptorWrites = { bufferWriteDescriptorSet(ds, &bufferInfo, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), imageWriteDescriptorSet( ds, &imageInfo, 1) }; vkUpdateDescriptorSets(vkDev.device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } return true; } void CubeRenderer::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) { EASY_FUNCTION(); beginRenderPass(commandBuffer, currentImage); vkCmdDraw(commandBuffer, 36, 1, 0, 0); vkCmdEndRenderPass(commandBuffer); } void CubeRenderer::updateUniformBuffer(VulkanRenderDevice& vkDev, uint32_t currentImage, const mat4& m) { uploadBufferData(vkDev, uniformBuffersMemory_[currentImage], 0, glm::value_ptr(m), sizeof(mat4)); }
其余部分和其他 Renderer 基本一致,注意我们会将 mvp 矩阵,传递给 cubemap,以便用来采样;
shader
// VKCude.vert #version 460 core layout (location=0) out vec3 dir; layout(binding = 0) uniform UniformBuffer { mat4 mvp; } ubo; const vec3 pos[8] = vec3[8]( vec3(-1.0,-1.0, 1.0), vec3( 1.0,-1.0, 1.0), vec3( 1.0, 1.0, 1.0), vec3(-1.0, 1.0, 1.0), vec3(-1.0,-1.0,-1.0), vec3( 1.0,-1.0,-1.0), vec3( 1.0, 1.0,-1.0), vec3(-1.0, 1.0,-1.0) ); const int indices[36] = int[36]( // front 0, 1, 2, 2, 3, 0, // right 1, 5, 6, 6, 2, 1, // back 7, 6, 5, 5, 4, 7, // left 4, 0, 3, 3, 7, 4, // bottom 4, 5, 1, 1, 0, 4, // top 3, 2, 6, 6, 7, 3 ); void main() { float cubeSize = 10.0; int idx = indices[gl_VertexIndex]; gl_Position = ubo.mvp * vec4(cubeSize * pos[idx], 1.0); dir = pos[idx].xyz; } //VKCube.frag #version 460 core layout (location=0) in vec3 dir; layout (location=0) out vec4 outColor; layout (binding=1) uniform samplerCube texture1; void main() { outColor = texture(texture1, dir); };
通过 Shader 可以看出来,其实就是将摄像机放到一个大立方体里面,然后从里往外看;
VulkanMultiMeshRenderer
Vulkan 间接绘制的 Renderer,对于多个 Mesh,可以调用一次 DrawCall,从而降低性能瓶颈
类声明
class MultiMeshRenderer: public RendererBase { public: virtual void fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) override; MultiMeshRenderer( VulkanRenderDevice& vkDev, const char* meshFile, const char* drawDataFile, const char* materialFile, const char* vtxShaderFile, const char* fragShaderFile); void updateIndirectBuffers(VulkanRenderDevice& vkDev, size_t currentImage, bool* visibility = nullptr); void updateGeometryBuffers(VulkanRenderDevice& vkDev, uint32_t vertexCount, uint32_t indexCount, const void* vertices, const void* indices); void updateMaterialBuffer(VulkanRenderDevice& vkDev, uint32_t materialSize, const void* materialData); void updateUniformBuffer(VulkanRenderDevice& vkDev, size_t currentImage, const mat4& m); void updateDrawDataBuffer(VulkanRenderDevice& vkDev, size_t currentImage, uint32_t drawDataSize, const void* drawData); void updateCountBuffer(VulkanRenderDevice& vkDev, size_t currentImage, uint32_t itemCount); virtual ~MultiMeshRenderer(); uint32_t vertexBufferSize_; uint32_t indexBufferSize_; private: VulkanRenderDevice& vkDev; uint32_t maxVertexBufferSize_; uint32_t maxIndexBufferSize_; uint32_t maxShapes_; uint32_t maxDrawDataSize_; uint32_t maxMaterialSize_; // 6. Storage Buffer with index and vertex data VkBuffer storageBuffer_; VkDeviceMemory storageBufferMemory_; VkBuffer materialBuffer_; VkDeviceMemory materialBufferMemory_; std::vector<VkBuffer> indirectBuffers_; std::vector<VkDeviceMemory> indirectBuffersMemory_; std::vector<VkBuffer> drawDataBuffers_; std::vector<VkDeviceMemory> drawDataBuffersMemory_; // Buffer for draw count std::vector<VkBuffer> countBuffers_; std::vector<VkDeviceMemory> countBuffersMemory_; /* DrawData loaded from file. Converted to indirectBuffers[] and uploaded to drawDataBuffers[] */ std::vector<DrawData> shapes; MeshData meshData_; bool createDescriptorSet(VulkanRenderDevice& vkDev); void loadDrawData(const char* drawDataFile); };
首先,看下私有属性部分:
- maxXXX_ 等,一些固定的最大限制值;
- storageBuffer、storageBufferMemory,用来存储 indexData 和 vertexData;
- materialBuffer、materialBufferMemory,用来存储材质数据;
- indirectBuffers、indirectBuffersMemory,每帧的间接绘制命令;
- drawDataBuffers、drawDataBuffersMemory,每帧变更的 mesh 数据;
- countBuffers、countBuffersMemory,每帧的 DrawCount;
- std::vector
shapes,从 drawData 文件读取的,每个 mesh 对应的信息; - MeshData meshData_,从 mesh 文件读取的,合并后的几何数据;
createDescriptorSet
bool MultiMeshRenderer::createDescriptorSet(VulkanRenderDevice& vkDev) { const std::array<VkDescriptorSetLayoutBinding, 5> bindings = { descriptorSetLayoutBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), /* vertices [part of this.storageBuffer] */ descriptorSetLayoutBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), /* indices [part of this.storageBuffer] */ descriptorSetLayoutBinding(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), /* draw data [this.drawDataBuffer] */ descriptorSetLayoutBinding(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT), /* material data [this.materialBuffer] */ descriptorSetLayoutBinding(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT) }; const VkDescriptorSetLayoutCreateInfo layoutInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data() }; VK_CHECK(vkCreateDescriptorSetLayout(vkDev.device, &layoutInfo, nullptr, &descriptorSetLayout_)); std::vector<VkDescriptorSetLayout> layouts(vkDev.swapchainImages.size(), descriptorSetLayout_); const VkDescriptorSetAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, .descriptorPool = descriptorPool_, .descriptorSetCount = static_cast<uint32_t>(vkDev.swapchainImages.size()), .pSetLayouts = layouts.data() }; descriptorSets_.resize(vkDev.swapchainImages.size()); VK_CHECK(vkAllocateDescriptorSets(vkDev.device, &allocInfo, descriptorSets_.data())); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { VkDescriptorSet ds = descriptorSets_[i]; const VkDescriptorBufferInfo bufferInfo = { uniformBuffers_[i], 0, sizeof(mat4) }; const VkDescriptorBufferInfo bufferInfo2 = { storageBuffer_, 0, maxVertexBufferSize_ }; const VkDescriptorBufferInfo bufferInfo3 = { storageBuffer_, maxVertexBufferSize_, maxIndexBufferSize_ }; const VkDescriptorBufferInfo bufferInfo4 = { drawDataBuffers_[i], 0, maxDrawDataSize_ }; const VkDescriptorBufferInfo bufferInfo5 = { materialBuffer_, 0, maxMaterialSize_ }; const std::array<VkWriteDescriptorSet, 5> descriptorWrites = { bufferWriteDescriptorSet(ds, &bufferInfo, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo2, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo3, 2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo4, 3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER), bufferWriteDescriptorSet(ds, &bufferInfo5, 4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) }; vkUpdateDescriptorSets(vkDev.device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } return true; } //VK01.vert #version 460 layout(location = 0) out vec3 uvw; struct ImDrawVert { float x, y, z; float u, v; float nx, ny, nz; }; struct DrawData { uint mesh; uint material; uint lod; uint indexOffset; uint vertexOffset; uint transformIndex; }; struct MaterialData { uint tex2D; }; layout(binding = 0) uniform UniformBuffer { mat4 inMtx; } ubo; layout(binding = 1) readonly buffer SBO { ImDrawVert data[]; } sbo; layout(binding = 2) readonly buffer IBO { uint data[]; } ibo; layout(binding = 3) readonly buffer DrawBO { DrawData data[]; } drawDataBuffer; void main() { DrawData dd = drawDataBuffer.data[gl_BaseInstance]; uint refIdx = dd.indexOffset + gl_VertexIndex; ImDrawVert v = sbo.data[ibo.data[refIdx] + dd.vertexOffset]; uvw = normalize(vec3(v.x, v.y, v.z)); // mat4 xfrm(1.0); // = transpose(drawDataBuffer.data[gl_BaseInstance].xfrm); gl_Position = ubo.inMtx /* xfrm*/ * vec4(v.x, v.y, v.z, 1.0); }
这里和之前的创建 descriptorSet 的过程类似,可以看出,主要将 uniform、顶点、索引和偏移信息传递进来;
updateXXXBuffer
void MultiMeshRenderer::updateUniformBuffer(VulkanRenderDevice& vkDev, size_t currentImage, const mat4& m) { uploadBufferData(vkDev, uniformBuffersMemory_[currentImage], 0, glm::value_ptr(m), sizeof(mat4)); } void MultiMeshRenderer::updateGeometryBuffers(VulkanRenderDevice& vkDev, uint32_t vertexCount, uint32_t indexCount, const void* vertices, const void* indices) { uploadBufferData(vkDev, storageBufferMemory_, 0, vertices, vertexCount); uploadBufferData(vkDev, storageBufferMemory_, maxVertexBufferSize_, indices, indexCount); } void MultiMeshRenderer::updateDrawDataBuffer(VulkanRenderDevice& vkDev, size_t currentImage, uint32_t drawDataSize, const void* drawData) { uploadBufferData(vkDev, drawDataBuffersMemory_[currentImage], 0, drawData, drawDataSize); } void MultiMeshRenderer::updateMaterialBuffer(VulkanRenderDevice& vkDev, uint32_t materialSize, const void* materialData) { } void MultiMeshRenderer::updateCountBuffer(VulkanRenderDevice& vkDev, size_t currentImage, uint32_t itemCount) { uploadBufferData(vkDev, countBuffersMemory_[currentImage], 0, &itemCount, sizeof(uint32_t)); }
上述 buffer,都是 HOST 可见的,直接通过 upload 方法更新;
updateIndirectBuffers
void MultiMeshRenderer::updateIndirectBuffers(VulkanRenderDevice& vkDev, size_t currentImage, bool* visibility) { VkDrawIndirectCommand* data = nullptr; vkMapMemory(vkDev.device, indirectBuffersMemory_[currentImage], 0, 2 * sizeof(VkDrawIndirectCommand), 0, (void **)&data); for (uint32_t i = 0 ; i < maxShapes_ ; i++) { const uint32_t j = shapes[i].meshIndex; const uint32_t lod = shapes[i].LOD; data[i] = { .vertexCount = meshData_.meshes_[j].getLODIndicesCount(lod), .instanceCount = visibility ? (visibility[i] ? 1u : 0u) : 1u, .firstVertex = 0, .firstInstance = i }; } vkUnmapMemory(vkDev.device, indirectBuffersMemory_[currentImage]); }
更新间接绘制命令参数的 indirectBuffersMemory_;
fillCommandBuffer
void MultiMeshRenderer::fillCommandBuffer(VkCommandBuffer commandBuffer, size_t currentImage) { beginRenderPass(commandBuffer, currentImage); /* For CountKHR (Vulkan 1.1) we may use indirect rendering with GPU-based object counter */ /// vkCmdDrawIndirectCountKHR(commandBuffer, indirectBuffers_[currentImage], 0, countBuffers_[currentImage], 0, maxShapes_, sizeof(VkDrawIndirectCommand)); /* For Vulkan 1.0 vkCmdDrawIndirect is enough */ vkCmdDrawIndirect(commandBuffer, indirectBuffers_[currentImage], 0, maxShapes_, sizeof(VkDrawIndirectCommand)); vkCmdEndRenderPass(commandBuffer); }
调用 vkCmdDrawIndirect,触发间接绘制;
构造函数
MultiMeshRenderer::MultiMeshRenderer( VulkanRenderDevice& vkDev, const char* meshFile, const char* drawDataFile, const char* materialFile, const char* vertShaderFile, const char* fragShaderFile) : vkDev(vkDev), RendererBase(vkDev, VulkanImage()) { if (!createColorAndDepthRenderPass(vkDev, false, &renderPass_, RenderPassCreateInfo())) { printf("Failed to create render pass\n"); exit(EXIT_FAILURE); } framebufferWidth_ = vkDev.framebufferWidth; framebufferHeight_ = vkDev.framebufferHeight; createDepthResources(vkDev, framebufferWidth_, framebufferHeight_, depthTexture_); loadDrawData(drawDataFile); MeshFileHeader header = loadMeshData(meshFile, meshData_); const uint32_t indirectDataSize = maxShapes_ * sizeof(VkDrawIndirectCommand); maxDrawDataSize_ = maxShapes_ * sizeof(DrawData); maxMaterialSize_ = 1024; countBuffers_.resize(vkDev.swapchainImages.size()); countBuffersMemory_.resize(vkDev.swapchainImages.size()); drawDataBuffers_.resize(vkDev.swapchainImages.size()); drawDataBuffersMemory_.resize(vkDev.swapchainImages.size()); indirectBuffers_.resize(vkDev.swapchainImages.size()); indirectBuffersMemory_.resize(vkDev.swapchainImages.size()); if (!createBuffer(vkDev.device, vkDev.physicalDevice, maxMaterialSize_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, materialBuffer_, materialBufferMemory_)) { printf("Cannot create material buffer\n"); fflush(stdout); exit(EXIT_FAILURE); } maxVertexBufferSize_ = header.vertexDataSize; maxIndexBufferSize_ = header.indexDataSize; VkPhysicalDeviceProperties devProps; vkGetPhysicalDeviceProperties(vkDev.physicalDevice, &devProps); const uint32_t offsetAlignment = static_cast<uint32_t>(devProps.limits.minStorageBufferOffsetAlignment); if ((maxVertexBufferSize_ & (offsetAlignment - 1)) != 0) { int floats = (offsetAlignment - (maxVertexBufferSize_ & (offsetAlignment - 1))) / sizeof(float); for (int ii = 0; ii < floats; ii++) meshData_.vertexData_.push_back(0); maxVertexBufferSize_ = (maxVertexBufferSize_ + offsetAlignment) & ~(offsetAlignment - 1); } if (!createBuffer(vkDev.device, vkDev.physicalDevice, maxVertexBufferSize_ + maxIndexBufferSize_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, storageBuffer_, storageBufferMemory_)) { printf("Cannot create vertex/index buffer\n"); fflush(stdout); exit(EXIT_FAILURE); } updateGeometryBuffers(vkDev, header.vertexDataSize, header.indexDataSize, meshData_.vertexData_.data(), meshData_.indexData_.data()); for (size_t i = 0; i < vkDev.swapchainImages.size(); i++) { if (!createBuffer(vkDev.device, vkDev.physicalDevice, indirectDataSize, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, // | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, /* for debugging we make it host-visible */ indirectBuffers_[i], indirectBuffersMemory_[i])) { printf("Cannot create indirect buffer\n"); fflush(stdout); exit(EXIT_FAILURE); } updateIndirectBuffers(vkDev, i); if (!createBuffer(vkDev.device, vkDev.physicalDevice, maxDrawDataSize_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, /* for debugging we make it host-visible */ drawDataBuffers_[i], drawDataBuffersMemory_[i])) { printf("Cannot create draw data buffer\n"); fflush(stdout); exit(EXIT_FAILURE); } updateDrawDataBuffer(vkDev, i, maxDrawDataSize_, shapes.data()); if (!createBuffer(vkDev.device, vkDev.physicalDevice, sizeof(uint32_t), VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, /* for debugging we make it host-visible */ countBuffers_[i], countBuffersMemory_[i])) { printf("Cannot create count buffer\n"); fflush(stdout); exit(EXIT_FAILURE); } updateCountBuffer(vkDev, i, maxShapes_); } if (!createUniformBuffers(vkDev, sizeof(mat4)) || !createColorAndDepthFramebuffers(vkDev, renderPass_, VK_NULL_HANDLE, swapchainFramebuffers_) || !createDescriptorPool(vkDev, 1, 4, 0, &descriptorPool_) || !createDescriptorSet(vkDev) || !createPipelineLayout(vkDev.device, descriptorSetLayout_, &pipelineLayout_) || !createGraphicsPipeline(vkDev, renderPass_, pipelineLayout_, { vertShaderFile, fragShaderFile }, &graphicsPipeline_)) { printf("Failed to create pipeline\n"); fflush(stdout); exit(EXIT_FAILURE); } }
一步步分析,构造器做了什么:
- createColorAndDepthRenderPass,构造renderPass;
- createDepthResources,创建深度缓冲;
loadDrawData(drawDataFile),加载 drawData 文件到 shapes 变量:
void MultiMeshRenderer::loadDrawData(const char* drawDataFile) { FILE* f = fopen(drawDataFile, "rb"); if (!f) { printf("Unable to open draw data file. Run MeshConvert first\n"); exit(255); } fseek(f, 0, SEEK_END); size_t fsize = ftell(f); fseek(f, 0, SEEK_SET); maxShapes_ = static_cast<uint32_t>(fsize / sizeof(DrawData)); printf("Reading draw data items: %d\n", (int)maxShapes_); fflush(stdout); shapes.resize(maxShapes_); if (fread(shapes.data(), sizeof(DrawData), maxShapes_, f) != maxShapes_) { printf("Unable to read draw data\n"); exit(255); } fclose(f); }
MeshFileHeader header = loadMeshData(meshFile, meshData),加载 mesh 文件到 meshData 变量;
MeshFileHeader loadMeshData(const char* meshFile, MeshData& out) { MeshFileHeader header; FILE* f = fopen(meshFile, "rb"); assert(f); // Did you forget to run "Ch5_Tool05_MeshConvert"? if (!f) { printf("Cannot open %s. Did you forget to run \"Ch5_Tool05_MeshConvert\"?\n", meshFile); exit(EXIT_FAILURE); } if (fread(&header, 1, sizeof(header), f) != sizeof(header)) { printf("Unable to read mesh file header\n"); exit(EXIT_FAILURE); } out.meshes_.resize(header.meshCount); if (fread(out.meshes_.data(), sizeof(Mesh), header.meshCount, f) != header.meshCount) { printf("Could not read mesh descriptors\n"); exit(EXIT_FAILURE); } out.boxes_.resize(header.meshCount); if (fread(out.boxes_.data(), sizeof(BoundingBox), header.meshCount, f) != header.meshCount) { printf("Could not read bounding boxes\n"); exit(255); } out.indexData_.resize(header.indexDataSize / sizeof(uint32_t)); out.vertexData_.resize(header.vertexDataSize / sizeof(float)); if ((fread(out.indexData_.data(), 1, header.indexDataSize, f) != header.indexDataSize) || (fread(out.vertexData_.data(), 1, header.vertexDataSize, f) != header.vertexDataSize)) { printf("Unable to read index/vertex data\n"); exit(255); } fclose(f); return header; }
创建 storageBuffer,存储顶点和索引数据,注意这里要对使用的内存进行对齐,下面解释一下为什么要对齐,以及如何对齐:
- 为什么要对齐,简而言之,是为了让 CPU/GPU 的读取更有效率,参考C/C++内存对齐详解;
如何进行对齐,首先我们要想对齐在数学上的本质是什么?其实就是模运算,比如以 4 字节对齐,那么
1 -> 4 2 -> 4 3 -> 4 4 -> 4 5 -> 8 6 -> 8 7 -> 8 8 -> 8
对齐,其实就是把模运算里面空缺的部分补齐,我们再来看下具体代码:
const uint32_t offsetAlignment = static_cast<uint32_t>(devProps.limits.minStorageBufferOffsetAlignment); if ((maxVertexBufferSize_ & (offsetAlignment - 1)) != 0) { int floats = (offsetAlignment - (maxVertexBufferSize_ & (offsetAlignment - 1))) / sizeof(float); for (int ii = 0; ii < floats; ii++) meshData_.vertexData_.push_back(0); maxVertexBufferSize_ = (maxVertexBufferSize_ + offsetAlignment) & ~(offsetAlignment - 1); }
maxVertexBufferSize_ & (offsetAlignment - 1)
,本质上等于以下模运算:maxVertexBufferSize_ % offsetAlignment
,也就是获取按照 offsetAlignment 求模后的余数, 对于(maxVertexBufferSize_ + offsetAlignment) & ~(offsetAlignment - 1)
,其实是先把当前的数字扩大一个 offsetAlignment 的大小,然后再把求模后多余的部分截断,这里的原理可以参考这篇文章:C++内存对齐-位运算公式;
- 创建并更新 drawData、count 和 indirect Buffer;
其余方法和一般 renderer 类似;
注意,这里为了使用方便,把 mesh、drawData、count、indirect 为了方便,把 buffer 声明成 HOST 可见的,其实对于 CPU 之后不会再读写更改的数据,可以通过 stagingBuffer 直接复制到 GPU 中,可以提升性能,其中和 compute shader 结合后,尤其可以在 GPU 端自己完成 Cull 和 LOD 的计算等,可以减轻 CPU 端的性能负担;