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,提高效率。

具体细节呢有几个不同:

  1. Piccolo在初始化过程中,保持了一些函数指针,用于提高效率。
  2. 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);
    }
}
  1. 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);
}
  1. 与OpenGL不同的是,Vulkan是多线程的渲染API,渲染内部的同步状态需要程序员手动保证,因此产生了类似信号量的概念,用于控制渲染流程同步,保证数据有效性。在Piccolo中,多了一个用于复制texture的信号量。
  2. 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。