Vulkan API的函数都带有一个小写的vk,枚举和结构体名带有一个Vk前缀,枚举值带有一个VK_前缀。Vulkan API对结构体非常依赖,大量函数的参数由结构体提供。
几乎所有Vulkan都会返回一个VkResult来表示调用的执行情况,它的值要么是VK_SUCCESS,要么是一个错误代码。Vulkan规范文档描述了这些函数返回的错误代码的意义。
根据Tutorial中描述,我们可以更好的理解Vulkan的运行与使用,所以推荐先去学习一下Vulkan的教程再继续学习Piccolo的渲染系统。
1. 渲染系统初始化
与渲染相关的系统不止一个,在渲染过程中会遇到多个系统的交互。首先是渲染相关系统的初始化,包括Window System初始化,Particle Manager初始化,Render System初始化,Debug Draw Manager初始化以及Editor UI初始化几个部分,虽然之前的文章已经分析过初始化流程,但是对渲染部分并没有深入展开,本节将会深入学习一下渲染系统的初始化部分。
1.1. Window System初始化与Particle Manager初始化
这两个部分已经有详细介绍,且内容很简单,就不再重复介绍了。
1.2. Render System初始化
Render System初始化时用到了RenderSystemInitInfo
这个结构体,其主要目的是传递Window System的实例,因为在初始化Vulkan相关部分时,需要用到Window System内的glfw。Render System初始化流程如下:
- 初始化Vulkan RHI
- Global渲染资源的加载
- Level渲染资源加载
- 上传渲染资源
- 设置Render Camera
- 设置Render Scene
- 初始化Render Pipeline
1.2.1. Vulkan RHI初始化
Piccolo引擎使用的是Vulkan作为底层渲染API,根据Vulkan Tutorial里面描述的,Vulkan的使用流程如下:
- 创建一个VkInstance
- 选择一个支持Vulkan的图形设备(VkPhysicsDevice)
- 为绘制和显示操作创建VkDevice和VkQueue
- 创建一个窗口,窗口表面和交换链
- 将交换链图像包装进VkImageView
- 创建一个渲染层指定渲染目标和使用方式
- 为渲染层创建帧缓冲
- 配置图形管线
- 为每一个交换链图像分配指令缓冲
- 从交换链获取图像进行绘制操作,提交图像对应的指令缓冲,返回图像到交换链
教程中初始化代码如下:
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createDescriptorSetLayout();
createGraphicsPipeline();
createCommandPool();
createColorResources();
createDepthResources();
createFramebuffers();
createTextureImage();
createTextureImageView();
createTextureSampler();
loadModel();
createVertexBuffer();
createIndexBuffer();
createUniformBuffers();
createDescriptorPool();
createDescriptorSets();
createCommandBuffers();
createSyncObjects();
}
根据上方描述,我们看看Vulkan RHI的初始化代码:
void VulkanRHI::initialize(RHIInitInfo init_info)
{
m_window = init_info.window_system->getWindow();
std::array<int, 2> window_size = init_info.window_system->getWindowSize();
m_viewport = {0.0f, 0.0f, (float)window_size[0], (float)window_size[1], 0.0f, 1.0f};
m_scissor = {{0, 0}, {(uint32_t)window_size[0], (uint32_t)window_size[1]}};
// Setup platform dependent info
...
createInstance();
initializeDebugMessenger();
createWindowSurface();
initializePhysicalDevice();
createLogicalDevice();
createCommandPool();
createCommandBuffers();
createDescriptorPool();
createSyncPrimitives();
createSwapchain();
createSwapchainImageViews();
createFramebufferImageAndView();
createAssetAllocator();
}
可以看出,两者的流程基本一致,但是Piccolo引擎多了一个createAssetAllocator
的步骤,用于加载AMD的vma,提高效率。
具体细节呢有几个不同:
- Piccolo在初始化过程中,保持了一些函数指针,用于提高效率。
- Piccolo的createCommandPool函数不太一样,创建了多个command pool,数量与帧缓冲的数量一致:
void VulkanRHI::createCommandPool()
{
// default graphics command pool
{
m_rhi_command_pool = new VulkanCommandPool();
VkCommandPool vk_command_pool;
VkCommandPoolCreateInfo command_pool_create_info {};
command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
command_pool_create_info.pNext = NULL;
command_pool_create_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
command_pool_create_info.queueFamilyIndex = m_queue_indices.graphics_family.value();
if (vkCreateCommandPool(m_device, &command_pool_create_info, nullptr, &vk_command_pool) != VK_SUCCESS)
{
LOG_ERROR("vk create command pool");
}
((VulkanCommandPool*)m_rhi_command_pool)->setResource(vk_command_pool);
}
// other command pools
{
VkCommandPoolCreateInfo command_pool_create_info;
command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
command_pool_create_info.pNext = NULL;
command_pool_create_info.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
command_pool_create_info.queueFamilyIndex = m_queue_indices.graphics_family.value();
for (uint32_t i = 0; i < k_max_frames_in_flight; ++i)
{
if (vkCreateCommandPool(m_device, &command_pool_create_info, NULL, &m_command_pools[i]) != VK_SUCCESS)
{
LOG_ERROR("vk create command pool");
}
}
}
}
而教程中只创建了一个:
void createCommandPool() {
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create graphics command pool!");
}
}
与command pool相对应的command buffer,Piccolo中也是创建了多个:
void VulkanRHI::createCommandBuffers()
{
VkCommandBufferAllocateInfo command_buffer_allocate_info {};
command_buffer_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
command_buffer_allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
command_buffer_allocate_info.commandBufferCount = 1U;
for (uint32_t i = 0; i < k_max_frames_in_flight; ++i)
{
command_buffer_allocate_info.commandPool = m_command_pools[i];
VkCommandBuffer vk_command_buffer;
if (vkAllocateCommandBuffers(m_device, &command_buffer_allocate_info, &vk_command_buffer) != VK_SUCCESS)
{
LOG_ERROR("vk allocate command buffers");
}
m_vk_command_buffers[i] = vk_command_buffer;
m_command_buffers[i] = new VulkanCommandBuffer();
((VulkanCommandBuffer*)m_command_buffers[i])->setResource(vk_command_buffer);
}
}
- Descriptor Pool用于用来在着色器中访问缓冲和图像数据的一种方式。由于教程中的Shader很简单,所以用到的Descriptor Pool也相对简单,但是Piccolo中存在多个不同的Shader,因此创建的Descriptor Pool就相对复杂:
教程中的Descriptor Pool创建:
void createDescriptorPool() {
std::array<VkDescriptorPoolSize, 2> poolSizes{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
poolInfo.pPoolSizes = poolSizes.data();
poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor pool!");
}
}
Piccolo中的Descriptor Pool创建:
void VulkanRHI::createDescriptorPool()
{
// Since DescriptorSet should be treated as asset in Vulkan, DescriptorPool
// should be big enough, and thus we can sub-allocate DescriptorSet from
// DescriptorPool merely as we sub-allocate Buffer/Image from DeviceMemory.
VkDescriptorPoolSize pool_sizes[7];
pool_sizes[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
pool_sizes[0].descriptorCount = 3 + 2 + 2 + 2 + 1 + 1 + 3 + 3;
pool_sizes[1].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
pool_sizes[1].descriptorCount = 1 + 1 + 1 * m_max_vertex_blending_mesh_count;
pool_sizes[2].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
pool_sizes[2].descriptorCount = 1 * m_max_material_count;
pool_sizes[3].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_sizes[3].descriptorCount = 3 + 5 * m_max_material_count + 1 + 1; // ImGui_ImplVulkan_CreateDeviceObjects
pool_sizes[4].type = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
pool_sizes[4].descriptorCount = 4 + 1 + 1 + 2;
pool_sizes[5].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
pool_sizes[5].descriptorCount = 3;
pool_sizes[6].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
pool_sizes[6].descriptorCount = 1;
VkDescriptorPoolCreateInfo pool_info {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.poolSizeCount = sizeof(pool_sizes) / sizeof(pool_sizes[0]);
pool_info.pPoolSizes = pool_sizes;
pool_info.maxSets = 1 + 1 + 1 + m_max_material_count + m_max_vertex_blending_mesh_count + 1 +
1; // +skybox + axis descriptor set
pool_info.flags = 0U;
if (vkCreateDescriptorPool(m_device, &pool_info, nullptr, &m_vk_descriptor_pool) != VK_SUCCESS)
{
LOG_ERROR("create descriptor pool");
}
m_descriptor_pool = new VulkanDescriptorPool();
((VulkanDescriptorPool*)m_descriptor_pool)->setResource(m_vk_descriptor_pool);
}
- 与OpenGL不同的是,Vulkan是多线程的渲染API,渲染内部的同步状态需要程序员手动保证,因此产生了类似信号量的概念,用于控制渲染流程同步,保证数据有效性。在Piccolo中,多了一个用于复制texture的信号量。
- Piccolo中将Vulkan Pipeline相关的创建剥离了出来,在其他地方进行初始化。
1.2.2. 上传渲染资源
第二个步骤是加载与上传一些全局贴图资源。关于环境贴图等资源的理论,可以去学习一下GAMES202课程,有关于这些的理论分析与实际应用。
void RenderResource::uploadGlobalRenderResource(std::shared_ptr<RHI> rhi, LevelResourceDesc level_resource_desc)
{
// create and map global storage buffer
createAndMapStorageBuffer(rhi);
// sky box irradiance
SkyBoxIrradianceMap skybox_irradiance_map = level_resource_desc.m_ibl_resource_desc.m_skybox_irradiance_map;
std::shared_ptr<TextureData> irradiace_pos_x_map = loadTextureHDR(skybox_irradiance_map.m_positive_x_map);
std::shared_ptr<TextureData> irradiace_neg_x_map = loadTextureHDR(skybox_irradiance_map.m_negative_x_map);
std::shared_ptr<TextureData> irradiace_pos_y_map = loadTextureHDR(skybox_irradiance_map.m_positive_y_map);
std::shared_ptr<TextureData> irradiace_neg_y_map = loadTextureHDR(skybox_irradiance_map.m_negative_y_map);
std::shared_ptr<TextureData> irradiace_pos_z_map = loadTextureHDR(skybox_irradiance_map.m_positive_z_map);
std::shared_ptr<TextureData> irradiace_neg_z_map = loadTextureHDR(skybox_irradiance_map.m_negative_z_map);
// sky box specular
SkyBoxSpecularMap skybox_specular_map = level_resource_desc.m_ibl_resource_desc.m_skybox_specular_map;
std::shared_ptr<TextureData> specular_pos_x_map = loadTextureHDR(skybox_specular_map.m_positive_x_map);
std::shared_ptr<TextureData> specular_neg_x_map = loadTextureHDR(skybox_specular_map.m_negative_x_map);
std::shared_ptr<TextureData> specular_pos_y_map = loadTextureHDR(skybox_specular_map.m_positive_y_map);
std::shared_ptr<TextureData> specular_neg_y_map = loadTextureHDR(skybox_specular_map.m_negative_y_map);
std::shared_ptr<TextureData> specular_pos_z_map = loadTextureHDR(skybox_specular_map.m_positive_z_map);
std::shared_ptr<TextureData> specular_neg_z_map = loadTextureHDR(skybox_specular_map.m_negative_z_map);
// brdf
std::shared_ptr<TextureData> brdf_map = loadTextureHDR(level_resource_desc.m_ibl_resource_desc.m_brdf_map);
// create IBL samplers
createIBLSamplers(rhi);
// create IBL textures, take care of the texture order
std::array<std::shared_ptr<TextureData>, 6> irradiance_maps = {irradiace_pos_x_map,
irradiace_neg_x_map,
irradiace_pos_z_map,
irradiace_neg_z_map,
irradiace_pos_y_map,
irradiace_neg_y_map};
std::array<std::shared_ptr<TextureData>, 6> specular_maps = {specular_pos_x_map,
specular_neg_x_map,
specular_pos_z_map,
specular_neg_z_map,
specular_pos_y_map,
specular_neg_y_map};
createIBLTextures(rhi, irradiance_maps, specular_maps);
// create brdf lut texture
rhi->createGlobalImage(
m_global_render_resource._ibl_resource._brdfLUT_texture_image,
m_global_render_resource._ibl_resource._brdfLUT_texture_image_view,
m_global_render_resource._ibl_resource._brdfLUT_texture_image_allocation,
brdf_map->m_width,
brdf_map->m_height,
brdf_map->m_pixels,
brdf_map->m_format);
// color grading
std::shared_ptr<TextureData> color_grading_map =
loadTexture(level_resource_desc.m_color_grading_resource_desc.m_color_grading_map);
// create color grading texture
rhi->createGlobalImage(
m_global_render_resource._color_grading_resource._color_grading_LUT_texture_image,
m_global_render_resource._color_grading_resource._color_grading_LUT_texture_image_view,
m_global_render_resource._color_grading_resource._color_grading_LUT_texture_image_allocation,
color_grading_map->m_width,
color_grading_map->m_height,
color_grading_map->m_pixels,
color_grading_map->m_format);
}
1.2.3. 设置Render Camera
Render Camera就是普通的透视摄像机,具体可以参考这篇文章。
1.2.4. 设置Render Scene
Render Scene初始化包括设置环境光照和环境光颜色,以及设置环境中对象的Visibility。
// setup render scene
m_render_scene = std::make_shared<RenderScene>();
m_render_scene->m_ambient_light = {global_rendering_res.m_ambient_light.toVector3()};
m_render_scene->m_directional_light.m_direction =
global_rendering_res.m_directional_light.m_direction.normalisedCopy();
m_render_scene->m_directional_light.m_color = global_rendering_res.m_directional_light.m_color.toVector3();
m_render_scene->setVisibleNodesReference();
void RenderScene::setVisibleNodesReference()
{
RenderPass::m_visiable_nodes.p_directional_light_visible_mesh_nodes = &m_directional_light_visible_mesh_nodes;
RenderPass::m_visiable_nodes.p_point_lights_visible_mesh_nodes = &m_point_lights_visible_mesh_nodes;
RenderPass::m_visiable_nodes.p_main_camera_visible_mesh_nodes = &m_main_camera_visible_mesh_nodes;
RenderPass::m_visiable_nodes.p_axis_node = &m_axis_node;
}
具体的场景加载之类的操作,是在运行时完成的。
1.2.5. 初始化Render Pipeline
对于Render Pipeline的初始化,从下面的代码可以看出,就是对渲染所需的每个Render Pass进行初始化。在Vulkan中,Render Pass的初始化必须在渲染前完成,创建的Render Pass在运行时基本没有什么可以动态更改的部分,因此为了效率,所有Pass提前创建完是比较好的选择。
void RenderPipeline::initialize(RenderPipelineInitInfo init_info)
{
m_point_light_shadow_pass = std::make_shared<PointLightShadowPass>();
m_directional_light_pass = std::make_shared<DirectionalLightShadowPass>();
m_main_camera_pass = std::make_shared<MainCameraPass>();
m_tone_mapping_pass = std::make_shared<ToneMappingPass>();
m_color_grading_pass = std::make_shared<ColorGradingPass>();
m_ui_pass = std::make_shared<UIPass>();
m_combine_ui_pass = std::make_shared<CombineUIPass>();
m_pick_pass = std::make_shared<PickPass>();
m_fxaa_pass = std::make_shared<FXAAPass>();
m_particle_pass = std::make_shared<ParticlePass>();
RenderPassCommonInfo pass_common_info;
pass_common_info.rhi = m_rhi;
pass_common_info.render_resource = init_info.render_resource;
m_point_light_shadow_pass->setCommonInfo(pass_common_info);
m_directional_light_pass->setCommonInfo(pass_common_info);
m_main_camera_pass->setCommonInfo(pass_common_info);
m_tone_mapping_pass->setCommonInfo(pass_common_info);
m_color_grading_pass->setCommonInfo(pass_common_info);
m_ui_pass->setCommonInfo(pass_common_info);
m_combine_ui_pass->setCommonInfo(pass_common_info);
m_pick_pass->setCommonInfo(pass_common_info);
m_fxaa_pass->setCommonInfo(pass_common_info);
m_particle_pass->setCommonInfo(pass_common_info);
m_point_light_shadow_pass->initialize(nullptr);
m_directional_light_pass->initialize(nullptr);
std::shared_ptr<MainCameraPass> main_camera_pass = std::static_pointer_cast<MainCameraPass>(m_main_camera_pass);
std::shared_ptr<RenderPass> _main_camera_pass = std::static_pointer_cast<RenderPass>(m_main_camera_pass);
std::shared_ptr<ParticlePass> particle_pass = std::static_pointer_cast<ParticlePass>(m_particle_pass);
ParticlePassInitInfo particle_init_info{};
particle_init_info.m_particle_manager = g_runtime_global_context.m_particle_manager;
m_particle_pass->initialize(&particle_init_info);
main_camera_pass->m_point_light_shadow_color_image_view =
std::static_pointer_cast<RenderPass>(m_point_light_shadow_pass)->getFramebufferImageViews()[0];
main_camera_pass->m_directional_light_shadow_color_image_view =
std::static_pointer_cast<RenderPass>(m_directional_light_pass)->m_framebuffer.attachments[0].view;
MainCameraPassInitInfo main_camera_init_info;
main_camera_init_info.enble_fxaa = init_info.enable_fxaa;
main_camera_pass->setParticlePass(particle_pass);
m_main_camera_pass->initialize(&main_camera_init_info);
std::static_pointer_cast<ParticlePass>(m_particle_pass)->setupParticlePass();
std::vector<RHIDescriptorSetLayout*> descriptor_layouts = _main_camera_pass->getDescriptorSetLayouts();
std::static_pointer_cast<PointLightShadowPass>(m_point_light_shadow_pass)
->setPerMeshLayout(descriptor_layouts[MainCameraPass::LayoutType::_per_mesh]);
std::static_pointer_cast<DirectionalLightShadowPass>(m_directional_light_pass)
->setPerMeshLayout(descriptor_layouts[MainCameraPass::LayoutType::_per_mesh]);
m_point_light_shadow_pass->postInitialize();
m_directional_light_pass->postInitialize();
ToneMappingPassInitInfo tone_mapping_init_info;
tone_mapping_init_info.render_pass = _main_camera_pass->getRenderPass();
tone_mapping_init_info.input_attachment =
_main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd];
m_tone_mapping_pass->initialize(&tone_mapping_init_info);
ColorGradingPassInitInfo color_grading_init_info;
color_grading_init_info.render_pass = _main_camera_pass->getRenderPass();
color_grading_init_info.input_attachment =
_main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_even];
m_color_grading_pass->initialize(&color_grading_init_info);
UIPassInitInfo ui_init_info;
ui_init_info.render_pass = _main_camera_pass->getRenderPass();
m_ui_pass->initialize(&ui_init_info);
CombineUIPassInitInfo combine_ui_init_info;
combine_ui_init_info.render_pass = _main_camera_pass->getRenderPass();
combine_ui_init_info.scene_input_attachment =
_main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd];
combine_ui_init_info.ui_input_attachment =
_main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_even];
m_combine_ui_pass->initialize(&combine_ui_init_info);
PickPassInitInfo pick_init_info;
pick_init_info.per_mesh_layout = descriptor_layouts[MainCameraPass::LayoutType::_per_mesh];
m_pick_pass->initialize(&pick_init_info);
FXAAPassInitInfo fxaa_init_info;
fxaa_init_info.render_pass = _main_camera_pass->getRenderPass();
fxaa_init_info.input_attachment =
_main_camera_pass->getFramebufferImageViews()[_main_camera_pass_post_process_buffer_odd];
m_fxaa_pass->initialize(&fxaa_init_info);
}
除了Point Light Shadow Passs、Directional Light Pass、Pick Pass和Particle Pass,其他所有Pass均是Main Camera Pass的子Pass。