Vulkan Integration

In this section, we’ll integrate our PBR implementation with the rest of the Vulkan rendering pipeline. We’ll update our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. The techniques we develop here will be applied in the Loading_Models chapter when we load and render glTF models.

To keep the flow concrete and avoid repeating earlier theory, use this quick roadmap:

1) Extend the renderer with PBR pipeline objects and a material push-constant block 2) Create the PBR pipeline (layout, shaders, blending, formats) alongside the main pipeline 3) Record draws: bind PBR pipeline, bind geometry, and push per-material constants per mesh 4) Clean up via RAII (no special teardown required)

We won’t re-explain PBR theory or push-constant fundamentals here. See push constants, and Introduction (and PBR Rendering) for PBR concepts.

The PBR pass slots into the graphics pipeline as shown below:

Rendering pipeline flowchart showing where the PBR pass fits

Updating the Renderer Class

First, let’s update our renderer class to include the new members we need for our PBR implementation:

class Renderer {
public:
    // ... existing members ...

    // PBR pipeline
    vk::raii::PipelineLayout pbrPipelineLayout;
    vk::raii::Pipeline pbrPipeline;

    // Push constant block for PBR material properties
    struct PushConstantBlock {
        glm::vec4 baseColorFactor;
        float metallicFactor;
        float roughnessFactor;
        int baseColorTextureSet;
        int physicalDescriptorTextureSet;
        int normalTextureSet;
        int occlusionTextureSet;
        int emissiveTextureSet;
        float alphaMask;
        float alphaMaskCutoff;
    };

    // ... existing methods ...

    // New methods
    bool createPBRPipeline();
    void pushMaterialProperties(vk::CommandBuffer commandBuffer, const Model* model, uint32_t materialIndex);
};

We’ve added members for the PBR pipeline and a struct for PBR material properties. We’ve also added methods for creating the PBR pipeline and pushing material properties to the shader.

Updating the Initialization

Next, we need to update the initialization process to create our PBR pipeline:

bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) {
    // ... existing initialization code ...

    // Create graphics pipeline
    if (!createGraphicsPipeline()) {
        return false;
    }

    // Create PBR pipeline
    if (!createPBRPipeline()) {
        std::cerr << "Failed to create PBR pipeline" << std::endl;
        return false;
    }

    // ... rest of initialization code ...

    initialized = true;
    return true;
}

Updating the Cleanup

We also need to update the cleanup process to destroy our PBR pipeline:

void Renderer::Cleanup() {
    // ... existing cleanup code ...

    // With vk::raii, pipeline and pipeline layout objects are automatically destroyed
    // when they go out of scope, so we don't need explicit destruction calls

    // ... rest of cleanup code ...
}

Updating the Rendering Process

Finally, we need to update the rendering process to use our PBR pipeline and push material properties:

void Renderer::recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t imageIndex) {
    // ... existing command buffer recording code ...

    // Bind the PBR pipeline
    commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrPipeline);

    // For each model in the scene
    for (const auto& model : models) {
        // Bind vertex and index buffers
        vk::Buffer vertexBuffers[] = {model->vertexBuffer};
        vk::DeviceSize offsets[] = {0};
        commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets);
        commandBuffer.bindIndexBuffer(model->indexBuffer, 0, vk::IndexType::eUint32);

        // For each mesh in the model
        for (const auto& mesh : model->meshes) {
            // Push material properties
            pushMaterialProperties(commandBuffer, model, mesh.materialIndex);

            // Bind descriptor sets
            commandBuffer.bindDescriptorSets(
                vk::PipelineBindPoint::eGraphics,
                *pbrPipelineLayout,
                0,
                1,
                &descriptorSets[imageIndex],
                0,
                nullptr
            );

            // Draw
            commandBuffer.drawIndexed(mesh.indexCount, 1, mesh.firstIndex, 0, 0);
        }
    }

    // ... rest of command buffer recording code ...
}

PBR Shader Reference

This chapter reuses the exact PBR shader defined in the previous section to avoid duplication and drift. Please refer to Implementing the PBR Shader for the full pbr.slang source and detailed explanations. Here we focus strictly on Vulkan integration: pipeline layout, descriptor bindings, push constants, and draw submission.

Compiling the Shader

After creating the shader file, we need to compile it using slangc. This is typically done as part of the build process, but we can also do it manually:

slangc shaders/pbr.slang -target spirv -profile spirv_1_4 -o shaders/pbr.spv

Testing the Implementation with glTF Models

To test our implementation, we can use glTF models, which already have PBR materials defined that are compatible with our implementation. In the Loading_Models chapter, we’ll learn how to load these models, but for now, let’s assume we have a way to load them.

Here’s an example of how to set up a test scene with glTF models:

void Renderer::renderTestScene() {
    // Set up camera
    glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
    glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
    glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);

    // Set up lights
    // Light 1: White light from above
    glm::vec4 lightPos1 = glm::vec4(0.0f, 5.0f, 5.0f, 1.0f);
    glm::vec4 lightColor1 = glm::vec4(300.0f, 300.0f, 300.0f, 1.0f);

    // Light 2: Blue light from the left
    glm::vec4 lightPos2 = glm::vec4(-5.0f, 0.0f, 0.0f, 1.0f);
    glm::vec4 lightColor2 = glm::vec4(0.0f, 0.0f, 300.0f, 1.0f);

    // Load glTF models
    Model* damagedHelmet = modelLoader.loadModel("models/DamagedHelmet/DamagedHelmet.gltf");
    Model* flightHelmet = modelLoader.loadModel("models/FlightHelmet/FlightHelmet.gltf");

    // The models already have PBR materials defined in the glTF file
    // We can render them directly with our PBR pipeline

    // Render the models with different transformations
    renderModel(damagedHelmet, glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.5f));
    renderModel(flightHelmet, glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.5f));

    // We can also experiment with modifying the material properties
    // For example, to make the damaged helmet more metallic:
    if (damagedHelmet->materials.size() > 0) {
        // Store the original value to restore later
        float originalMetallic = damagedHelmet->materials[0].metallicFactor;

        // Modify the material
        damagedHelmet->materials[0].metallicFactor = 1.0f;

        // Render with modified material
        renderModel(damagedHelmet, glm::vec3(-2.0f, 0.0f, 0.0f), glm::vec3(0.5f));

        // Restore original value
        damagedHelmet->materials[0].metallicFactor = originalMetallic;
    }
}

Conclusion

In this section, we’ve integrated our PBR implementation with the rest of the Vulkan rendering pipeline. We’ve updated our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. We’ve created a PBR shader based on the concepts we’ve learned and shown how to test the implementation with glTF models.

This approach provides a solid foundation for rendering physically accurate materials, which we’ll apply in the Loading_Models chapter when we load and render glTF models. It also gives us the flexibility to modify and extend the material properties as needed for our specific rendering requirements.

In the next section, we’ll explore how to add high-quality shadows using Vulkan Ray Query.