BRE Architecture Series Part 4 – Scene format

In this article, we are going to talk about the scene format that we use for BRE.

YAML Format

First of all, I wanted to choose a format that is easy to read, easy to understand and easy to modify by hand, such that you can create an empty file and write a scene from scratch. I discovered the YAML format and it met all the conditions I needed. Doing some research, I also found a library called yaml-cpp that is easy to use and it can serialize and deserialize YAML files, although in my case I only need to deserialize the file because, as you know, BRE scenes are static (once they are loaded, they never change), and then I do not need to write to a YAML file from the application.

Here we have a BRE scene file. As you can see, it is easy to read and understand. That is what I was looking for.

models:
  unreal: resources/models/unreal.obj
  mitsubaSphere: resources/models/mitsubaSphere.obj
  torusKnot: resources/models/torusKnot.obj

textures:
  brick_diffuse: resources/textures/brick/brick.dds
  brick_height: resources/textures/brick/brick_height.dds
  brick_normal: resources/textures/brick/brick_normal.dds
  sky map: resources/textures/cubeMaps/milkmill_cube_map.dds
  cube map diffuse: resources/textures/cubeMaps/milkmill_diffuse_cube_map.dds
  cube map specular: resources/textures/cubeMaps/milkmill_specular_cube_map.dds

material techniques:
  - name: color mapping
  - name: brick
    base color texture: brick_diffuse
    metalness texture: metal0
    roughness texture: roughness1
  - name: brick normal
    base color texture: brick_diffuse
    metalness texture: metal0
    roughness texture: roughness1
    normal texture: brick_normal
  - name: brick height
    base color texture: brick_diffuse
    metalness texture: metal0
    roughness texture: roughness1
    normal texture: brick_normal
    height texture: brick_height

drawable objects:
  - model: mitsubaSphere
    material properties: material1
    material technique: brick height
    translation: [0.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material1
    material technique: brick normal
    translation: [50.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material2
    material technique: brick
    translation: [100.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material2
    material technique: color mapping
    translation: [150.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material1
    material technique: brick color normal
    translation: [200.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material1
    material technique: brick color height
    translation: [250.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]

environment:
  - sky box texture: sky map
    diffuse irradiance texture: cube map diffuse
    specular pre convolved environment texture: cube map specular

camera:
  - position: [0.0, 0.0, 0.0]
    look vector: [0.0, 0.0, 1.0]
    up vector: [0.0, 1.0, 0.0]

How do we organize the scene file?

At the time of writing this article, the scene file is divided into different main categories. They are the following

  • models
  • textures
  • material techniques
  • environment
  • camera
  • drawable objects
  • settings

How does BRE load scene files in YAML format?

To be able to read this format, we use yaml-cpp library. Each category has a Loader, for example, the category “drawable objects” has a DrawableObjectLoader, the category “models” has a ModelLoader, etc. The class diagram of Loaders is the following

loaders.jpg

As we can see, the SceneLoader contains all the others, and it makes them to load scene stuff in the correct order. For example, ModelLoader and TextureLoader must be executed first because we need to load models and textures that are going to be referenced by MaterialTechniqueLoader and DrawableObjectLoader. As the SceneLoader source code has several concepts that we did not see yet in these series of articles, I am going to omit it.

Now, we are going to analyze each category and its respective loader.

Models Category and Loader

An example of this category is the following

models:
  unreal: resources/models/unreal.obj
  mitsubaSphere: resources/models/mitsubaSphere.obj
  torusKnot: resources/models/torusKnot.obj
  reference: resource/scenes/other_models.yml

It is mandatory. As you can see, each entry in this category has the form MODEL_NAME: MODEL_PATH. Each model name must be unique, and of course, the model file must be valid. It can also have the reference field pointing to another YAML file (that is optional). It must have the models field and all its models will be taken into account too.

The class that processes this category is ModelLoader and uses the ModelManager for model loading. Its implementation is the following

ModelLoader.h

#pragma once

#include <string>
#include <unordered_map>
#include <wrl.h>

namespace YAML {
class Node;
}

struct ID3D12CommandAllocator;
struct ID3D12GraphicsCommandList;
struct ID3D12Resource;

namespace BRE {
class Model;

///
/// @brief Responsible to load from scene file the models configurations
///
class ModelLoader {
public:
    ModelLoader()
    {}
    ModelLoader(const ModelLoader&) = delete;
    const ModelLoader& operator=(const ModelLoader&) = delete;
    ModelLoader(ModelLoader&&) = delete;
    ModelLoader& operator=(ModelLoader&&) = delete;

    ///
    /// @brief Load models
    /// @param rootNode Scene YAML file root node
    /// @param commandAllocator Command allocator for the command list to load models
    /// @param commandList Command list to load models
    ///
    void LoadModels(const YAML::Node& rootNode,
                    ID3D12CommandAllocator& commandAllocator,
                    ID3D12GraphicsCommandList& commandList) noexcept;

    ///
    /// @brief Get model
    /// @param name Model name
    /// @return Model
    ///
    const Model& GetModel(const std::string& name) const noexcept;

private:
    ///
    /// @brief Load models
    /// @param modelsNode YAML Node representing the "models" field. It must be a map.
    /// @param commandAllocator Command allocator for the command list to load textures
    /// @param commandList Command list to load the textures
    /// @param uploadVertexBuffer Upload buffer to upload the buffer content.
    /// It has to be kept alive after the function call because
    /// the command list has not been executed yet that performs the actual copy.
    /// The caller can Release the uploadVertexBuffer after it knows the copy has been executed.
    /// @param uploadIndexBuffers Upload buffer to upload the buffer content.
    /// It has to be kept alive after the function call because
    /// the command list has not been executed yet that performs the actual copy.
    /// The caller can Release the uploadIndexBuffers after it knows the copy has been executed.
    ///
    void LoadModels(const YAML::Node& modelsNode,
                    ID3D12CommandAllocator& commandAllocator,
                    ID3D12GraphicsCommandList& commandList,
                    std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& uploadVertexBuffers,
                    std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& uploadIndexBuffers) noexcept;

    std::unordered_map<std::string, Model*> mModelByName;
};
}

ModelLoader.cpp

#include "ModelLoader.h"

#include <d3d12.h>
#include <vector>
#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <CommandListExecutor\CommandListExecutor.h>
#include <ModelManager\Model.h>
#include <ModelManager\ModelManager.h>
#include <Utils/DebugUtils.h>

namespace BRE {
void
ModelLoader::LoadModels(const YAML::Node& rootNode,
                        ID3D12CommandAllocator& commandAllocator,
                        ID3D12GraphicsCommandList& commandList) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "models" node. It is a map and its sintax is:
    // models:
    //   modelName1: modelPath1
    //   modelName2: modelPath2
    //   modelName3: modelPath3
    const YAML::Node modelsNode = rootNode["models"];
    BRE_CHECK_MSG(modelsNode.IsDefined(), L"'models' node not found");
    BRE_CHECK_MSG(modelsNode.IsMap(), L"'models' node must be a map");

    std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> uploadVertexBuffers;
    std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> uploadIndexBuffers;
    BRE_CHECK_HR(commandList.Reset(&commandAllocator, nullptr));

    LoadModels(modelsNode,
               commandAllocator,
               commandList,
               uploadVertexBuffers,
               uploadIndexBuffers);

    commandList.Close();

    CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);
}

const Model& ModelLoader::GetModel(const std::string& name) const noexcept
{
    std::unordered_map<std::string, Model*>::const_iterator findIt = mModelByName.find(name);
    const std::wstring errorMsg =
        L"Model name not found: " + StringUtils::AnsiToWideString(name);
    BRE_CHECK_MSG(findIt != mModelByName.end(), errorMsg.c_str());
    BRE_ASSERT(findIt->second != nullptr);

    return *findIt->second;
}

void
ModelLoader::LoadModels(const YAML::Node& modelsNode,
                        ID3D12CommandAllocator& commandAllocator,
                        ID3D12GraphicsCommandList& commandList,
                        std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& uploadVertexBuffers,
                        std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& uploadIndexBuffers) noexcept
{
    BRE_CHECK_MSG(modelsNode.IsMap(), L"'models' node must be a map");

    std::string name;
    std::string path;
    for (YAML::const_iterator it = modelsNode.begin(); it != modelsNode.end(); ++it) {
        name = it->first.as<std::string>();
        path = it->second.as<std::string>();

        // If name is "reference", then path must be a yaml file that specifies "models"
        if (name == "reference") {
            const YAML::Node referenceRootNode = YAML::LoadFile(path);
            const std::wstring errorMsg =
                L"Failed to open yaml file: " + StringUtils::AnsiToWideString(path);
            BRE_CHECK_MSG(referenceRootNode.IsDefined(), errorMsg.c_str());
            const YAML::Node referenceModelsNode = referenceRootNode["models"];
            LoadModels(referenceModelsNode,
                       commandAllocator,
                       commandList,
                       uploadVertexBuffers,
                       uploadIndexBuffers);
        } else {
            const std::wstring errorMsg =
                L"Model name must be unique: " + StringUtils::AnsiToWideString(name);
            BRE_CHECK_MSG(mModelByName.find(name) == mModelByName.end(), errorMsg.c_str());

            uploadVertexBuffers.resize(uploadVertexBuffers.size() + 1);
            uploadIndexBuffers.resize(uploadIndexBuffers.size() + 1);

            Model& model = ModelManager::LoadModel(path.c_str(),
                                                   commandList,
                                                   uploadVertexBuffers.back(),
                                                   uploadIndexBuffers.back());

            mModelByName[name] = &model;
        }
    }
}
}

Textures Category and Loader

An example of this category is the following

textures:
  brick_diffuse: resources/textures/brick/brick.dds
  brick_height: resources/textures/brick/brick_height.dds
  brick_normal: resources/textures/brick/brick_normal.dds
  sky map: resources/textures/cubeMaps/milkmill_cube_map.dds
  cube map diffuse: resources/textures/cubeMaps/milkmill_diffuse_cube_map.dds
  cube map specular: resources/textures/cubeMaps/milkmill_specular_cube_map.dds
  reference: resources/scenes/other_textures.yml

This category is optional, and each entry has the form TEXTURE_NAME: TEXTURE_PATH. Each texture name must be unique, and its file must be valid. It can also have the reference field pointing to another YAML file (that is optional). It must have the textures field and all its textures will be taken into account too.

The class that processes this category is TextureLoader and uses the ResourceManager for textures loading. Its implementation is the following

TextureLoader.h

#pragma once

#include <string>
#include <unordered_map>
#include <wrl.h>

namespace YAML {
class Node;
}

struct ID3D12CommandAllocator;
struct ID3D12GraphicsCommandList;
struct ID3D12Resource;

namespace BRE {
///
/// @brief Responsible to load from scene file the textures
///
class TextureLoader {
public:
    TextureLoader()
    {}
    TextureLoader(const TextureLoader&) = delete;
    const TextureLoader& operator=(const TextureLoader&) = delete;
    TextureLoader(TextureLoader&&) = delete;
    TextureLoader& operator=(TextureLoader&&) = delete;

    ///
    /// @brief Load textures
    /// @param rootNode Scene YAML file root node
    /// @param commandAllocator Command allocator for the command list to load textures
    /// @param commandList Command list to load the textures
    ///
    void LoadTextures(const YAML::Node& rootNode,
                      ID3D12CommandAllocator& commandAllocator,
                      ID3D12GraphicsCommandList& commandList) noexcept;

    ///
    /// @brief Get texture
    /// @param name Texture name
    /// @return Texture
    ///
    ID3D12Resource& GetTexture(const std::string& name) noexcept;

private:
    ///
    /// @brief Load textures
    /// @param texturesNode YAML Node representing the "textures" field. It must be a map.
    /// @param commandAllocator Command allocator for the command list to load textures
    /// @param commandList Command list to load the textures
    /// @param uploadBuffer Upload buffer to upload the texture content.
    /// It has to be kept alive after the function call because
    /// the command list has not been executed yet that performs the actual copy.
    /// The caller can Release the uploadBuffer after it knows the copy has been executed.
    ///
    void LoadTextures(const YAML::Node& texturesNode,
                      ID3D12CommandAllocator& commandAllocator,
                      ID3D12GraphicsCommandList& commandList,
                      std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& uploadBuffers) noexcept;

    std::unordered_map<std::string, ID3D12Resource*> mTextureByName;
};
}

TextureLoader.cpp

#include "TextureLoader.h"

#include <d3d12.h>
#include <vector>
#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <CommandListExecutor\CommandListExecutor.h>
#include <ResourceManager\ResourceManager.h>
#include <Utils/DebugUtils.h>

namespace BRE {
void
TextureLoader::LoadTextures(const YAML::Node& rootNode,
                            ID3D12CommandAllocator& commandAllocator,
                            ID3D12GraphicsCommandList& commandList) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "textures" node. It is a map and its sintax is:
    // textures:
    //   name1: path1
    //   name2: path2
    //   name3: path3
    const YAML::Node texturesNode = rootNode["textures"];

    // 'textures' node can be undefined
    if (texturesNode.IsDefined() == false) {
        return;
    }

    BRE_CHECK_MSG(texturesNode.IsMap(), L"'textures' node must be a map");

    std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> uploadBuffers;
    BRE_CHECK_HR(commandList.Reset(&commandAllocator, nullptr));

    LoadTextures(texturesNode,
                 commandAllocator,
                 commandList,
                 uploadBuffers);

    commandList.Close();
    CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);
}

ID3D12Resource&
TextureLoader::GetTexture(const std::string& name) noexcept
{
    std::unordered_map<std::string, ID3D12Resource*>::iterator findIt = mTextureByName.find(name);
    const std::wstring errorMsg =
        L"Texture name not found: " + StringUtils::AnsiToWideString(name);
    BRE_CHECK_MSG(findIt != mTextureByName.end(), errorMsg.c_str());
    BRE_ASSERT(findIt->second != nullptr);

    return *findIt->second;
}

void
TextureLoader::LoadTextures(const YAML::Node& texturesNode,
                            ID3D12CommandAllocator& commandAllocator,
                            ID3D12GraphicsCommandList& commandList,
                            std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& uploadBuffers) noexcept
{
    BRE_CHECK_MSG(texturesNode.IsMap(), L"'textures' node must be a map");

    std::string name;
    std::string path;
    for (YAML::const_iterator it = texturesNode.begin(); it != texturesNode.end(); ++it) {
        name = it->first.as<std::string>();
        path = it->second.as<std::string>();

        // If name is "reference", then path must be a yaml file that specifies "textures"
        if (name == "reference") {
            const YAML::Node referenceRootNode = YAML::LoadFile(path);
            const std::wstring errorMsg =
                L"Failed to open yaml file: " + StringUtils::AnsiToWideString(path);
            BRE_CHECK_MSG(referenceRootNode.IsDefined(), errorMsg.c_str());
            BRE_CHECK_MSG(referenceRootNode["textures"].IsDefined(),
                           L"Reference file must have 'textures' field");
            const YAML::Node referenceTexturesNode = referenceRootNode["textures"];
            LoadTextures(referenceTexturesNode,
                         commandAllocator,
                         commandList,
                         uploadBuffers);
        } else {
            const std::wstring errorMsg =
                L"Texture name must be unique: " + StringUtils::AnsiToWideString(name);
            BRE_CHECK_MSG(mTextureByName.find(name) == mTextureByName.end(), errorMsg.c_str());

            uploadBuffers.resize(uploadBuffers.size() + 1);

            ID3D12Resource& texture = ResourceManager::LoadTextureFromFile(path.c_str(),
                                                                           commandList,
                                                                           uploadBuffers.back(),
                                                                           nullptr);

            mTextureByName[name] = &texture;
        }
    }
}
}

Material Techniques Category and Loader

An example of this category is the following

material techniques:
  - name: brick
    base color texture: brick_diffuse
    metalness texture: metal0
    roughness texture: roughness1
  - name: brick normal
    base color texture: brick_diffuse
    metalness texture: metal0
    roughness texture: roughness1
    normal texture: brick_normal
  - name: brick height
    base color texture: brick_diffuse
    metalness texture: metal0
    roughness texture: roughness1
    normal texture: brick_normal
    height texture: brick_height
  - reference: resources/scenes/other_material_techniques.yml

This category is mandatory. Each entry must have at least “base color texture”, “metalness texture”, and “roughness texture”, and at most five elements. When it has five elements, the fields are the following

  • name: MATERIAL_TECHNIQUE_NAME
  • base color texture: TEXTURE_NAME
  • metalness texture: TEXTURE_NAME
  • roughness texture: TEXTURE_NAME
  • normal texture: TEXTURE_NAME
  • height texture: TEXTURE_NAME

There are different combinations and each one means a different technique to use.

  • name + base color texture + metalness texture + roughness texture = Texture mapping
  • name + base color texture + metalness texture + roughness texture + normal texture = Normal mapping
  • name + base color texture + metalness texture + roughness texture + normal texture + height texture = Height mapping

It can also have the reference field pointing to another YAML file (that is optional). It must have the material techniques field and all its material techniques will be taken into account too.

The material technique data is stored in a class named MaterialTechnique and its implementation is the following

MaterialTechnique.h

#pragma once

#include <Utils\DebugUtils.h>

struct ID3D12Resource;

namespace BRE {
///
/// @brief Contains material technique data like base color texture, normal texture, height texture, etc.
///
class MaterialTechnique {
public:
    enum TechniqueType {
        COLOR_MAPPING = 0,
        COLOR_NORMAL_MAPPING,
        COLOR_HEIGHT_MAPPING,
        TEXTURE_MAPPING,
        NORMAL_MAPPING,
        HEIGHT_MAPPING,
        NUM_TECHNIQUES,
    };

    MaterialTechnique(ID3D12Resource* baseColorTexture = nullptr,
                      ID3D12Resource* metalnessTexture = nullptr,
                      ID3D12Resource* roughnessTexture = nullptr,
                      ID3D12Resource* normalTexture = nullptr,
                      ID3D12Resource* heightTexture = nullptr)
        : mBaseColorTexture(baseColorTexture)
        , mMetalnessTexture(metalnessTexture)
        , mRoughnessTexture(roughnessTexture)
        , mNormalTexture(normalTexture)
        , mHeightTexture(heightTexture)
    {}

    ///
    /// @brief Get base color texture
    /// @return Base color texture
    ///
    ID3D12Resource& GetBaseColorTexture() const noexcept
    {
        BRE_ASSERT(mBaseColorTexture != nullptr);
        return *mBaseColorTexture;
    }

    ///
    /// @brief Get metalness texture
    /// @return Metalness texture
    ///
    ID3D12Resource& GetMetalnessTexture() const noexcept
    {
        BRE_ASSERT(mMetalnessTexture != nullptr);
        return *mMetalnessTexture;
    }

    ///
    /// @brief Get roughness texture
    /// @return Roughness texture
    ///
    ID3D12Resource& GetRoughnessTexture() const noexcept
    {
        BRE_ASSERT(mRoughnessTexture != nullptr);
        return *mRoughnessTexture;
    }

    ///
    /// @brief Get normal texture
    /// @return Normal texture
    ///
    ID3D12Resource& GetNormalTexture() const noexcept
    {
        BRE_ASSERT(mNormalTexture != nullptr);
        return *mNormalTexture;
    }

    ///
    /// @brief Get height texture
    /// @return Height texture
    ///
    ID3D12Resource& GetHeightTexture() const noexcept
    {
        BRE_ASSERT(mHeightTexture != nullptr);
        return *mHeightTexture;
    }

    ///
    /// @brief Set base color texture
    /// @param texture New base color texture.
    ///
    void SetBaseColorTexture(ID3D12Resource* texture) noexcept
    {
        BRE_ASSERT(texture != nullptr);
        mBaseColorTexture = texture;
    }

    ///
    /// @brief Set metalness texture
    /// @param texture New metalness texture.
    ///
    void SetMetalnessTexture(ID3D12Resource* texture) noexcept
    {
        BRE_ASSERT(texture != nullptr);
        mMetalnessTexture = texture;
    }

    ///
    /// @brief Set roughness texture
    /// @param texture New roughness texture.
    ///
    void SetRoughnessTexture(ID3D12Resource* texture) noexcept
    {
        BRE_ASSERT(texture != nullptr);
        mRoughnessTexture = texture;
    }

    ///
    /// @brief Set normal texture
    /// @param texture New normal texture
    ///
    void SetNormalTexture(ID3D12Resource* texture) noexcept
    {
        BRE_ASSERT(texture != nullptr);
        mNormalTexture = texture;
    }

    ///
    /// @brief Set height texture
    /// @param texture New height texture
    ///
    void SetHeightTexture(ID3D12Resource* texture) noexcept
    {
        BRE_ASSERT(texture != nullptr);
        mHeightTexture = texture;
    }

    ///
    /// @brief Get material technique type
    /// @return Material technique type
    ///
    TechniqueType GetType() const noexcept;

private:
    ID3D12Resource* mBaseColorTexture{ nullptr };
    ID3D12Resource* mMetalnessTexture{ nullptr };
    ID3D12Resource* mRoughnessTexture{ nullptr };
    ID3D12Resource* mNormalTexture{ nullptr };
    ID3D12Resource* mHeightTexture{ nullptr };
};
}

MaterialTechnique.cpp

#include "MaterialTechnique.h"

#include <Utils\DebugUtils.h>

namespace BRE {
MaterialTechnique::TechniqueType
MaterialTechnique::GetType() const noexcept
{
    BRE_CHECK_MSG(mBaseColorTexture != nullptr, L"There is no technique without base color texture");
    BRE_CHECK_MSG(mMetalnessTexture != nullptr, L"There is no technique without metalness texture");
    BRE_CHECK_MSG(mRoughnessTexture != nullptr, L"There is no technique without roughness texture");

    if (mNormalTexture != nullptr) {
        if (mHeightTexture != nullptr) {
            return TechniqueType::HEIGHT_MAPPING;
        } else {
            return TechniqueType::NORMAL_MAPPING;
        }
    } else {
        BRE_CHECK_MSG(mHeightTexture == nullptr, L"There is no technique with base color and height texture but no normal texture");
        return TechniqueType::TEXTURE_MAPPING;
    }

}
}

The class that processes this category is MaterialTechniqueLoader and its implementation is the following

MaterialTechniqueLoader.h

#pragma once

#include <string>
#include <unordered_map>

#include <SceneLoader\MaterialTechnique.h>

namespace YAML {
class Node;
}

namespace BRE {
class TextureLoader;

///
/// @brief Responsible to load from scene file the material techniques
///
class MaterialTechniqueLoader {
public:
    MaterialTechniqueLoader(TextureLoader& textureLoader)
        : mTextureLoader(textureLoader)
    {}

    MaterialTechniqueLoader(const MaterialTechniqueLoader&) = delete;
    const MaterialTechniqueLoader& operator=(const MaterialTechniqueLoader&) = delete;
    MaterialTechniqueLoader(MaterialTechniqueLoader&&) = delete;
    MaterialTechniqueLoader& operator=(MaterialTechniqueLoader&&) = delete;

    ///
    /// @brief Load material techniques
    /// @param rootNode Scene YAML file root node
    ///
    void LoadMaterialTechniques(const YAML::Node& rootNode) noexcept;

    ///
    /// @brief Get material technique
    /// @return Material technique
    ///
    const MaterialTechnique& GetMaterialTechnique(const std::string& name) const noexcept;

    ///
    /// @brief Get default material technique
    ///
    /// This technique is used when 'material technique' is not specified
    /// in a drawable object.
    ///
    /// @return Default MaterialTechnique
    ///
    const MaterialTechnique& GetDefaultMaterialTechnique() const noexcept
    {
        return mDefaultMaterialTechnique;
    }

private:
    ///
    /// @brief Update material technique
    /// @param materialTechniquePropertyName Material technique property name
    /// @param materialTechniqueTextureName Material technique texture name
    /// @param materialTechnique Output material technique
    ///
    void UpdateMaterialTechnique(const std::string& materialTechniquePropertyName,
                                 const std::string& materialTechniqueTextureName,
                                 MaterialTechnique& materialTechnique) const noexcept;

    std::unordered_map<std::string, MaterialTechnique> mMaterialTechniqueByName;

    // This is the default material technique if no 'material technique' is specified
    // for a drawable object
    MaterialTechnique mDefaultMaterialTechnique;

    TextureLoader& mTextureLoader;
};
}

MaterialTechniqueLoader.cpp

#include "MaterialTechniqueLoader.h"

#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <SceneLoader\TextureLoader.h>
#include <SceneLoader\YamlUtils.h>
#include <Utils/DebugUtils.h>

namespace BRE {
void
MaterialTechniqueLoader::LoadMaterialTechniques(const YAML::Node& rootNode) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "material techniques" node. It is a sequence of maps and its sintax is:
    // material techniques:
    //   - name: techniqueName1
    //     base color texture: baseColorTextureName
    //     metalness texture: metalnessTextureName
    //     roughness texture: roughnessTextureName
    //     normal texture: normalTextureName
    //     height texture: heightTextureName
    //   - name: techniqueName2
    //     base color texture: baseColorTextureName
    //     metalness texture: metalnessTextureName
    //     roughness texture: roughnessTextureName
    //     normal texture: normalTextureName
    const YAML::Node materialTechniquesNode = rootNode["material techniques"];

    BRE_CHECK_MSG(materialTechniquesNode.IsDefined(), L"'material techniques' node must be defined");
    BRE_CHECK_MSG(materialTechniquesNode.IsSequence(), L"'material techniques' node must be a map");

    std::string pairFirstValue;
    std::string pairSecondValue;
    std::string materialTechniqueName;
    for (YAML::const_iterator seqIt = materialTechniquesNode.begin(); seqIt != materialTechniquesNode.end(); ++seqIt) {
        const YAML::Node materialMap = *seqIt;
        BRE_ASSERT(materialMap.IsMap());

        // Get material technique name
        YAML::const_iterator mapIt = materialMap.begin();
        BRE_ASSERT(mapIt != materialMap.end());
        pairFirstValue = mapIt->first.as<std::string>();

        BRE_CHECK_MSG(pairFirstValue == std::string("name") || pairFirstValue == std::string("reference"),
                      L"Material techniques 1st parameter must be 'name', or it must be 'reference'");

        // If name is "reference", then path must be a yaml file that specifies "material techniques"
        if (pairFirstValue == "reference") {
            pairSecondValue = mapIt->second.as<std::string>();

            const YAML::Node referenceRootNode = YAML::LoadFile(pairSecondValue);
            const std::wstring errorMsg =
                L"Failed to open yaml file: " + StringUtils::AnsiToWideString(pairSecondValue);
            BRE_CHECK_MSG(referenceRootNode.IsDefined(), errorMsg.c_str());
            BRE_CHECK_MSG(referenceRootNode["material techniques"].IsDefined(),
                          L"Reference file must have 'material techniques' field");
            LoadMaterialTechniques(referenceRootNode);

            continue;
        }

        materialTechniqueName = mapIt->second.as<std::string>();
        const std::wstring errorMsg =
            L"Material technique name must be unique: " + StringUtils::AnsiToWideString(materialTechniqueName);
        BRE_CHECK_MSG(mMaterialTechniqueByName.find(materialTechniqueName) == mMaterialTechniqueByName.end(),
                      errorMsg.c_str());
        ++mapIt;

        // Get material techniques settings (base color texture, normal texture, etc)
        MaterialTechnique materialTechnique;
        while (mapIt != materialMap.end()) {
            pairFirstValue = mapIt->first.as<std::string>();
            pairSecondValue = mapIt->second.as<std::string>();
            UpdateMaterialTechnique(pairFirstValue, pairSecondValue, materialTechnique);
            ++mapIt;
        }

        mMaterialTechniqueByName.insert(std::make_pair(materialTechniqueName, materialTechnique));
    }
}

const MaterialTechnique& MaterialTechniqueLoader::GetMaterialTechnique(const std::string& name) const noexcept
{
    std::unordered_map<std::string, MaterialTechnique>::const_iterator findIt = mMaterialTechniqueByName.find(name);
    const std::wstring errorMsg =
        L"Material technique name not found: " + StringUtils::AnsiToWideString(name);
    BRE_CHECK_MSG(findIt != mMaterialTechniqueByName.end(), errorMsg.c_str());

    return findIt->second;
}

void MaterialTechniqueLoader::UpdateMaterialTechnique(const std::string& materialTechniquePropertyName,
                                                      const std::string& materialTechniqueTextureName,
                                                      MaterialTechnique& materialTechnique) const noexcept
{
    ID3D12Resource& texture = mTextureLoader.GetTexture(materialTechniqueTextureName);
    if (materialTechniquePropertyName == "base color texture") {
        materialTechnique.SetBaseColorTexture(&texture);
    } else if (materialTechniquePropertyName == "metalness texture") {
        materialTechnique.SetMetalnessTexture(&texture);
    } else if (materialTechniquePropertyName == "roughness texture") {
        materialTechnique.SetRoughnessTexture(&texture);
    } else if (materialTechniquePropertyName == "normal texture") {
        materialTechnique.SetNormalTexture(&texture);
    } else if (materialTechniquePropertyName == "height texture") {
        materialTechnique.SetHeightTexture(&texture);
    } else {
        // To avoid warning about 'conditional expression is constant'. This is the same than false
        const std::wstring errorMsg =
            L"Unknown material technique field: " + StringUtils::AnsiToWideString(materialTechniquePropertyName);
        BRE_CHECK_MSG(&materialTechniquePropertyName == nullptr, errorMsg.c_str());
    }
}
}

Environment Category and Loader

An example of this category is the following

environment:
  - sky box texture: sky map
    diffuse irradiance texture: cube map diffuse
    specular pre-convolved environment texture: cube map specular

It is mandatory and its three fields too. This information is used to create the skybox in the scene and for global illumination purposes. As you can see, these three textures must have been declared in the textures category. Otherwise, they are not going to be found.

The class that processes this category is EnvironmentLoader and uses the ResourceManager to get the textures. Its implementation is the following

EnvironmentLoader.h

#pragma once

#include <Utils\DebugUtils.h>

namespace YAML {
class Node;
}

struct ID3D12Resource;

namespace BRE {
class TextureLoader;

///
/// @brief Responsible to load from scene file the environment configurations
///
class EnvironmentLoader {
public:
    EnvironmentLoader(TextureLoader& textureLoader)
        : mTextureLoader(textureLoader)
    {}
    EnvironmentLoader(const EnvironmentLoader&) = delete;
    const EnvironmentLoader& operator=(const EnvironmentLoader&) = delete;
    EnvironmentLoader(EnvironmentLoader&&) = delete;
    EnvironmentLoader& operator=(EnvironmentLoader&&) = delete;

    ///
    /// @brief Load environment
    /// @param rootNode Scene YAML file root node
    ///
    void LoadEnvironment(const YAML::Node& rootNode) noexcept;

    ///
    /// @brief Get sky box texture
    /// @return Sky box texture
    ///
    ID3D12Resource& GetSkyBoxTexture() const noexcept
    {
        BRE_ASSERT(mSkyBoxTexture != nullptr);
        return *mSkyBoxTexture;
    }

    ///
    /// @brief Get diffuse irradiance environment texture
    /// @return Diffuse irradiance environment resource
    ///
    ID3D12Resource& GetDiffuseIrradianceTexture() const noexcept
    {
        BRE_ASSERT(mDiffuseIrradianceTexture != nullptr);
        return *mDiffuseIrradianceTexture;
    }

    ///
    /// @brief Get specular pre convolved environment texture
    /// @return Specular pre convolved environment resource
    ///
    ID3D12Resource& GetSpecularPreConvolvedEnvironmentTexture() const noexcept
    {
        BRE_ASSERT(mSpecularPreConvolvedEnvironmentTexture != nullptr);
        return *mSpecularPreConvolvedEnvironmentTexture;
    }

private:
    ///
    /// @brief Update environment texture
    /// @param environmentName Environment property name
    /// @param environmentTextureName Environment texture name
    ///
    void UpdateEnvironmentTexture(const std::string& environmentPropertyName,
                                  const std::string& environmentTextureName) noexcept;

    TextureLoader& mTextureLoader;

    ID3D12Resource* mSkyBoxTexture{ nullptr };
    ID3D12Resource* mDiffuseIrradianceTexture{ nullptr };
    ID3D12Resource* mSpecularPreConvolvedEnvironmentTexture{ nullptr };
};
}

EnvironmentLoader.cpp

#include "EnvironmentLoader.h"

#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <SceneLoader\TextureLoader.h>

namespace BRE {
void
EnvironmentLoader::LoadEnvironment(const YAML::Node& rootNode) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "environment" node. It is a single sequence of maps and its sintax is:
    // environment:
    //   - environment texture: textureName
    //     diffuse irradiance texture: textureName
    //     specular pre convolved environment texture: textureName
    const YAML::Node environmentNode = rootNode["environment"];
    BRE_ASSERT_MSG(environmentNode.IsDefined(), L"'environment' node must be defined");
    BRE_ASSERT_MSG(environmentNode.IsSequence(), L"'environment' node must be a sequence");

    BRE_ASSERT(environmentNode.begin() != environmentNode.end());
    const YAML::Node environmentMap = *environmentNode.begin();
    BRE_ASSERT_MSG(environmentMap.IsMap(), L"'environment' node first sequence element must be a map");

    // Fill the environment textures
    YAML::const_iterator mapIt = environmentMap.begin();
    std::string pairFirstValue;
    std::string pairSecondValue;
    while (mapIt != environmentMap.end()) {
        pairFirstValue = mapIt->first.as<std::string>();
        pairSecondValue = mapIt->second.as<std::string>();
        UpdateEnvironmentTexture(pairFirstValue, pairSecondValue);
        ++mapIt;
    }

    BRE_ASSERT(mSkyBoxTexture != nullptr);
    BRE_ASSERT(mDiffuseIrradianceTexture != nullptr);
    BRE_ASSERT(mSpecularPreConvolvedEnvironmentTexture != nullptr);
}

void EnvironmentLoader::UpdateEnvironmentTexture(const std::string& environmentPropertyName,
                                                 const std::string& environmentTextureName) noexcept
{
    ID3D12Resource& texture = mTextureLoader.GetTexture(environmentTextureName);
    if (environmentPropertyName == "sky box texture") {
        BRE_ASSERT_MSG(mSkyBoxTexture == nullptr, L"Sky box texture must be set once");
        mSkyBoxTexture = &texture;
    } else if (environmentPropertyName == "diffuse irradiance texture") {
        BRE_ASSERT_MSG(mDiffuseIrradianceTexture == nullptr, L"Diffuse irradiance texture must be set once");
        mDiffuseIrradianceTexture = &texture;
    } else if (environmentPropertyName == "specular pre convolved environment texture") {
        BRE_ASSERT_MSG(mSpecularPreConvolvedEnvironmentTexture == nullptr,
                       L"Specular pre convolved enviroment texture must be set once");
        mSpecularPreConvolvedEnvironmentTexture = &texture;
    } else {
        // To avoid warning about 'conditional expression is constant'. This is the same than false
        BRE_ASSERT_MSG(&texture == nullptr, L"Unknown environment field");
    }
}
}

Camera Category and Loader

An example of this category is the following

camera:
  - position: [0.0, 0.0, 0.0]
    look vector: [0.0, 0.0, 1.0]
    up vector: [0.0, 1.0, 0.0]

This category is not mandatory. If we omit it, then the default values will be position = [0.0, 0.0, 0.0], look vector = [0.0, 0.0, 1.0], and up vector = [0.0, 1.0, 0.0].

Other camera settings like field of view, aspect ratio, near and far plane Z coordinates, etc are in ApplicationSettings library.

The class that processes this category is CameraLoader and its implementation is the following

CameraLoader.h

#pragma once

#include <string>

#include <Camera\Camera.h>

namespace YAML {
class Node;
}

namespace BRE {

///
/// @brief Responsible to load from scene file the camera configuration
///
class CameraLoader {
public:
    CameraLoader() = default;
    CameraLoader(const CameraLoader&) = delete;
    const CameraLoader& operator=(const CameraLoader&) = delete;
    CameraLoader(CameraLoader&&) = delete;
    CameraLoader& operator=(CameraLoader&&) = delete;

    ///
    /// @brief Load camera settings
    /// @param rootNode Scene YAML file root node
    ///
    void LoadCamera(const YAML::Node& rootNode) noexcept;

    ///
    /// @brief Get camera
    /// @return Camera
    ///
    const Camera& GetCamera() const noexcept
    {
        return mCamera;
    }

private:
    Camera mCamera;
};
}

CameraLoader.cpp

#include "CameraLoader.h"

#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <SceneLoader\YamlUtils.h>
#include <Utils/DebugUtils.h>

using namespace DirectX;

namespace BRE {
void
CameraLoader::LoadCamera(const YAML::Node& rootNode) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "camera" node. It is a single sequence with a map and its sintax is:
    // camera:
    //   - position: [0.0, 0.0, 0.0]
    //     look vector: [0.0, 0.0, 1.0]
    //     up vector: [0.0, 1.0, 0.0]
    //     vertical field of view: scalar
    //     near plane z: scalar
    //     far plane z: scalar
    const YAML::Node cameraNode = rootNode["camera"];
    if (cameraNode.IsDefined() == false) {
        return;
    }

    BRE_ASSERT_MSG(cameraNode.IsSequence(), L"'camera' node must be a sequence");

    BRE_ASSERT_MSG(cameraNode.begin() != cameraNode.end(), L"'camera' node is empty");
    const YAML::Node cameraMap = *cameraNode.begin();
    BRE_ASSERT_MSG(cameraMap.IsMap(), L"'camera' node first sequence is not a map");

    // Get data to set camera
    float position[3]{ 0.0f, 0.0f, 0.0f };
    float lookVector[3]{ 0.0f, 0.0f, 1.0f };
    float upVector[3]{ 0.0f, 1.0f, 0.0f };
    std::string propertyName;
    YAML::const_iterator mapIt = cameraMap.begin();
    while (mapIt != cameraMap.end()) {
        propertyName = mapIt->first.as<std::string>();

        if (propertyName == "position") {
            YamlUtils::GetSequence(mapIt->second, position, 3U);
        } else if (propertyName == "look vector") {
            YamlUtils::GetSequence(mapIt->second, lookVector, 3U);
        } else if (propertyName == "up vector") {
            YamlUtils::GetSequence(mapIt->second, upVector, 3U);
        } else {
            // To avoid warning about 'conditional expression is constant'. This is the same than false
            BRE_ASSERT_MSG(&propertyName == nullptr, L"Unknown camera field");
        }

        ++mapIt;
    }

    mCamera.SetPosition(XMFLOAT3(position[0U], position[1U], position[2U]));
    mCamera.SetLookAndUpVectors(XMFLOAT3(lookVector[0U], lookVector[1U], lookVector[2U]),
                                XMFLOAT3(upVector[0U], upVector[1U], upVector[2U]));
}
}

Drawable Objects Category and Loader

An example of this category is the following

drawable objects:
  - model: mitsubaSphere
    material properties: material1
    material technique: brick height
    translation: [0.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material1
    material technique: brick normal
    translation: [50.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material2
    material technique: brick
    translation: [100.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material2
    material technique: color mapping
    translation: [150.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material1
    material technique: brick color normal
    translation: [200.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - model: mitsubaSphere
    material properties: material1
    material technique: brick color height
    translation: [250.0, 0.0, 50.0]
    scale: [0.2, 0.2, 0.2]
  - reference: resources/scenes/other_drawable_objects.yml

This category is mandatory and all its fields too. The first field must be the model name, then the other fields can appear in the order you want. As you can see, many of these fields reference data from other categories, whereby this data must exist. Translation, rotation, and scale fields are optional. If they are not present, then its default values will be translation = [0.0, 0.0, 0.0], rotation = [0.0, 0.0, 0.0], and scale = [1.0, 1.0, 1.0]. It can also have the reference field pointing to another YAML file (that is optional). It must have the drawable objects field and all its models will be taken into account too.

The drawable object data is stored in a class named DrawableObject and its implementation is the following

DrawableObject.h

#pragma once

#include <DirectXMath.h>

#include <MathUtils\MathUtils.h>
#include <Utils\DebugUtils.h>

namespace BRE {
class MaterialProperties;
class MaterialTechnique;
class Model;

///
/// @brief Represents the data needed to draw an object
///
class DrawableObject {
public:
    DrawableObject(const Model& model,
                   const MaterialProperties& materialProperties,
                   const MaterialTechnique& materialTechnique,
                   const DirectX::XMFLOAT4X4& worldMatrix)
        : mModel(&model)
        , mMaterialProperties(&materialProperties)
        , mMaterialTechnique(&materialTechnique)
        , mWorldMatrix(worldMatrix)
    {}

    ///
    /// @brief Get model
    /// @return Model
    ///
    const Model& GetModel() const noexcept
    {
        BRE_ASSERT(mModel != nullptr);
        return *mModel;
    }

    ///
    /// @brief Get material properties
    /// @return Material properties
    ///
    const MaterialProperties& GetMaterialProperties() const noexcept
    {
        BRE_ASSERT(mMaterialProperties != nullptr);
        return *mMaterialProperties;
    }

    ///
    /// @brief Get material technique
    /// @return Material technique
    ///
    const MaterialTechnique& GetMaterialTechnique() const noexcept
    {
        BRE_ASSERT(mMaterialTechnique != nullptr);
        return *mMaterialTechnique;
    }

    ///
    /// @brief Get world matrix
    /// @return World matrix
    ///
    const DirectX::XMFLOAT4X4& GetWorldMatrix() const noexcept
    {
        return mWorldMatrix;
    }

private:
    const Model* mModel{ nullptr };
    const MaterialProperties* mMaterialProperties{ nullptr };
    const MaterialTechnique* mMaterialTechnique{ nullptr };
    DirectX::XMFLOAT4X4 mWorldMatrix{ MathUtils::GetIdentity4x4Matrix() };
};
}

The class that processes this category is DrawableObjectLoader and its implementation is the following

DrawableObjectLoader.h

#pragma once

#include <string>
#include <unordered_map>
#include <vector>

#include <SceneLoader\DrawableObject.h>
#include <SceneLoader\MaterialTechnique.h>

namespace YAML {
class Node;
}

namespace BRE {
class MaterialPropertiesLoader;
class MaterialTechniqueLoader;
class ModelLoader;

///
/// @brief Responsible to load from scene file the objects configurations
///
class DrawableObjectLoader {
public:
    using DrawableObjectsByModelName = std::unordered_map<std::string, std::vector<DrawableObject>>;

    DrawableObjectLoader(const MaterialPropertiesLoader& materialPropertiesLoader,
                         const MaterialTechniqueLoader& materialTechniqueLoader,
                         const ModelLoader& modelLoader)
        : mMaterialPropertiesLoader(materialPropertiesLoader)
        , mMaterialTechniqueLoader(materialTechniqueLoader)
        , mModelLoader(modelLoader)
    {}

    DrawableObjectLoader(const DrawableObjectLoader&) = delete;
    const DrawableObjectLoader& operator=(const DrawableObjectLoader&) = delete;
    DrawableObjectLoader(DrawableObjectLoader&&) = delete;
    DrawableObjectLoader& operator=(DrawableObjectLoader&&) = delete;

    ///
    /// @brief Load drawable objects
    /// @param rootNode Scene YAML file root node
    ///
    void LoadDrawableObjects(const YAML::Node& rootNode) noexcept;

    ///
    /// @brief Get drawable objects by model name by technique
    /// @return Drawable object by model name
    ///
    const DrawableObjectsByModelName& GetDrawableObjectsByModelNameByTechniqueType(
        const MaterialTechnique::TechniqueType techniqueType) const noexcept
    {
        return mDrawableObjectsByModelName[techniqueType];
    }

private:
    DrawableObjectsByModelName mDrawableObjectsByModelName[MaterialTechnique::NUM_TECHNIQUES];

    const MaterialPropertiesLoader& mMaterialPropertiesLoader;
    const MaterialTechniqueLoader& mMaterialTechniqueLoader;
    const ModelLoader& mModelLoader;
};
}

DrawableObjectLoader.cpp

#include "DrawableObjectLoader.h"

#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <SceneLoader\MaterialPropertiesLoader.h>
#include <SceneLoader\MaterialTechniqueLoader.h>
#include <SceneLoader\ModelLoader.h>
#include <SceneLoader\YamlUtils.h>
#include <Utils/DebugUtils.h>

using namespace DirectX;

namespace BRE {
void
DrawableObjectLoader::LoadDrawableObjects(const YAML::Node& rootNode) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "drawable objects" node. It is a sequence of maps and its sintax is:
    // drawable objects:
    //   - model: modelName
    //     material properties: materialPropertiesName
    //     material technique: drawableObjectName
    //     translation: [10.0, 0.0, 12.0]
    //     rotation: [3.14, 0.0, 0.0]
    //     scale: [1, 1, 3]
    //   - model: modelName
    //     material properties: materialPropertiesName
    //     material technique: drawableObjectName
    //     scale: [1, 3, 3]
    const YAML::Node drawableObjectsNode = rootNode["drawable objects"];
    BRE_CHECK_MSG(drawableObjectsNode.IsDefined(), L"'drawable objects' node must be defined");
    BRE_CHECK_MSG(drawableObjectsNode.IsSequence(), L"'drawable objects' node must be a sequence");

    // We need model name to fill mDrawableObjectsByModelName
    std::string modelName;
    std::string pairFirstValue;
    std::string pairSecondValue;
    for (YAML::const_iterator seqIt = drawableObjectsNode.begin(); seqIt != drawableObjectsNode.end(); ++seqIt) {
        const YAML::Node drawableObjectMap = *seqIt;
        BRE_CHECK_MSG(drawableObjectMap.IsMap(), L"Each drawable object must be a map");

        // Get data to build drawable object
        const Model* model = nullptr;
        const MaterialProperties* materialProperties = nullptr;
        const MaterialTechnique* materialTechnique = nullptr;
        float translation[3]{ 0.0f, 0.0f, 0.0f };
        float rotation[3]{ 0.0f, 0.0f, 0.0f };
        float scale[3]{ 1.0f, 1.0f, 1.0f };
        YAML::const_iterator mapIt = drawableObjectMap.begin();
        while (mapIt != drawableObjectMap.end()) {
            pairFirstValue = mapIt->first.as<std::string>();

            if (pairFirstValue == "model") {
                BRE_CHECK_MSG(model == nullptr, L"Drawable object model must be set once");
                pairSecondValue = mapIt->second.as<std::string>();
                modelName = pairSecondValue;
                model = &mModelLoader.GetModel(pairSecondValue);
            } else if (pairFirstValue == "material properties") {
                BRE_CHECK_MSG(materialProperties == nullptr, L"Drawable object material properties must be set once");
                pairSecondValue = mapIt->second.as<std::string>();
                materialProperties = &mMaterialPropertiesLoader.GetMaterialProperties(pairSecondValue);
            } else if (pairFirstValue == "material technique") {
                BRE_CHECK_MSG(materialTechnique == nullptr, L"Drawable object material technique must be set once");
                pairSecondValue = mapIt->second.as<std::string>();
                materialTechnique = &mMaterialTechniqueLoader.GetMaterialTechnique(pairSecondValue);
            } else if (pairFirstValue == "translation") {
                YamlUtils::GetSequence(mapIt->second, translation, 3U);
            } else if (pairFirstValue == "rotation") {
                YamlUtils::GetSequence(mapIt->second, rotation, 3U);
            } else if (pairFirstValue == "scale") {
                YamlUtils::GetSequence(mapIt->second, scale, 3U);
            } else if (pairFirstValue == "reference") {
                // If the first field is "reference", then the second field must be a yaml file
                // that specifies "drawable objects"
                const YAML::Node referenceRootNode = YAML::LoadFile(pairSecondValue);
                BRE_CHECK_MSG(referenceRootNode.IsDefined(), L"Failed to open yaml file");
                BRE_CHECK_MSG(referenceRootNode["drawable objects"].IsDefined(),
                              L"Reference file must have 'drawable objects' field");
                LoadDrawableObjects(referenceRootNode);
            } else {
                // To avoid warning about 'conditional expression is constant'. This is the same than false
                const std::wstring errorMsg =
                    L"Unknown drawable object field: " + StringUtils::AnsiToWideString(pairFirstValue);
                BRE_CHECK_MSG(&scale == nullptr, errorMsg.c_str());
            }

            ++mapIt;
        }

        BRE_CHECK_MSG(model != nullptr, L"'model' field was not present in current drawable object");

        // If "material technique" field is not present, then it defaults to "color mapping" technique
        if (materialTechnique == nullptr) {
            materialTechnique = &mMaterialTechniqueLoader.GetDefaultMaterialTechnique();
        }

        // If "material properties" field is not present, then we get the default material properties
        if (materialProperties == nullptr) {
            materialProperties = &mMaterialPropertiesLoader.GetDefaultMaterialProperties();
        }

        // Build worldMatrix
        XMFLOAT4X4 worldMatrix;
        MathUtils::ComputeMatrix(worldMatrix,
                                 translation[0],
                                 translation[1],
                                 translation[2],
                                 scale[0],
                                 scale[1],
                                 scale[2],
                                 rotation[0],
                                 rotation[1],
                                 rotation[2]);

        DrawableObject drawableObject(*model,
                                      *materialProperties,
                                      *materialTechnique,
                                      worldMatrix);

        DrawableObjectsByModelName& drawableObjectsByModelName = mDrawableObjectsByModelName[materialTechnique->GetType()];
        drawableObjectsByModelName[modelName].emplace_back(drawableObject);
    }
}
}

Settings Loader

An example of this category is the following

settings:
  - screen width: 1920
    screen height: 1080
    near plane z: 1.0
    far plane z: 5000.0
    ambient occlusion radius: 50.0f
    height mapping height scale: 40.0f

This category is optional. Basically, it updates the settings in ApplicationSettings, AmbientOcclusionSettings and GeometrySettings. At the time of writing this article, the settings are the following:

  • screen width
  • screen width
  • CPU processors
  • near plane z
  • far plane z
  • vertical field of view
  • fullscreen
  • ambient occlusion sample kernel size
  • ambient occlusion noise texture dimension
  • ambient occlusion radius
  • ambient occlusion power
  • height mapping min tessellation distance
  • height mapping max tessellation distance
  • height mapping min tessellation factor
  • height mapping max tessellation factor
  • height mapping height scale

This loader is not used by the SceneLoader but it is created and executed at the beginning of the application. We need to do this because it modifies important settings like screen width and height that must be set before even the main window is created.

SettingsLoader.h

#pragma once

namespace YAML {
class Node;
}

namespace BRE {

///
/// @brief Responsible to load from scene file the settings
///
class SettingsLoader {
public:
    SettingsLoader() = default;
    SettingsLoader(const SettingsLoader&) = delete;
    const SettingsLoader& operator=(const SettingsLoader&) = delete;
    SettingsLoader(SettingsLoader&&) = delete;
    SettingsLoader& operator=(SettingsLoader&&) = delete;

    ///
    /// @brief Load settings
    /// @param rootNode Scene YAML file root node
    ///
    void LoadSettings(const YAML::Node& rootNode) noexcept;
};
}

SettingsLoader.cpp

#include "SettingsLoader.h"

#pragma warning( push )
#pragma warning( disable : 4127)
#include <yaml-cpp/yaml.h>
#pragma warning( pop ) 

#include <AmbientOcclusionPass\AmbientOcclusionSettings.h>
#include <ApplicationSettings\ApplicationSettings.h>
#include <GeometryPass\GeometrySettings.h>
#include <SceneLoader\YamlUtils.h>
#include <Utils/DebugUtils.h>

namespace BRE {
void
SettingsLoader::LoadSettings(const YAML::Node& rootNode) noexcept
{
    BRE_ASSERT(rootNode.IsDefined());

    // Get the "settings" node. It is a single sequence with a map and its sintax is:
    // camera:
    //   - screen width: N
    //     screen height: M
    //     CPU processors: K
    const YAML::Node settingsNode = rootNode["settings"];
    if (settingsNode.IsDefined() == false) {
        return;
    }

    BRE_CHECK_MSG(settingsNode.IsSequence(), L"'settings' node must be a sequence");

    BRE_CHECK_MSG(settingsNode.begin() != settingsNode.end(), L"'settings' node is empty");
    const YAML::Node settingsMap = *settingsNode.begin();
    BRE_CHECK_MSG(settingsMap.IsMap(), L"'settings' node first sequence is not a map");

    std::string propertyName;
    YAML::const_iterator mapIt = settingsMap.begin();
    while (mapIt != settingsMap.end()) {
        propertyName = mapIt->first.as<std::string>();

        if (propertyName == "screen height") {
            YamlUtils::GetScalar(mapIt->second,
                                 ApplicationSettings::sWindowHeight);
            ApplicationSettings::sScissorRect.bottom =
                static_cast<LONG>(ApplicationSettings::sWindowHeight);
            ApplicationSettings::sScreenViewport.Height =
                static_cast<float>(ApplicationSettings::sWindowHeight);
        } else if (propertyName == "screen width") {
            YamlUtils::GetScalar(mapIt->second,
                                 ApplicationSettings::sWindowWidth);
            ApplicationSettings::sScissorRect.right =
                static_cast<LONG>(ApplicationSettings::sWindowWidth);
            ApplicationSettings::sScreenViewport.Width =
                static_cast<float>(ApplicationSettings::sWindowWidth);
        } else if (propertyName == "CPU processors") {
            YamlUtils::GetScalar(mapIt->second,
                                 ApplicationSettings::sCpuProcessorCount);
        } else if (propertyName == "near plane z") {
            YamlUtils::GetScalar(mapIt->second,
                                 ApplicationSettings::sNearPlaneZ);
        } else if (propertyName == "far plane z") {
            YamlUtils::GetScalar(mapIt->second,
                                 ApplicationSettings::sFarPlaneZ);
        } else if (propertyName == "vertical field of view") {
            YamlUtils::GetScalar(mapIt->second,
                                 ApplicationSettings::sVerticalFieldOfView);
        } else if (propertyName == "fullscreen") {
            std::uint32_t isFullscreen;
            YamlUtils::GetScalar(mapIt->second,
                                 isFullscreen);
            ApplicationSettings::sIsFullscreenWindow = isFullscreen > 0U;
        } else if (propertyName == "ambient occlusion sample kernel size") {
            YamlUtils::GetScalar(mapIt->second,
                                 AmbientOcclusionSettings::sSampleKernelSize);
        } else if (propertyName == "ambient occlusion noise texture dimension") {
            YamlUtils::GetScalar(mapIt->second,
                                 AmbientOcclusionSettings::sNoiseTextureDimension);
        } else if (propertyName == "ambient occlusion radius") {
            YamlUtils::GetScalar(mapIt->second,
                                 AmbientOcclusionSettings::sOcclusionRadius);
        } else if (propertyName == "ambient occlusion power") {
            YamlUtils::GetScalar(mapIt->second,
                                 AmbientOcclusionSettings::sSsaoPower);
        } else if (propertyName == "height mapping min tessellation distance") {
            YamlUtils::GetScalar(mapIt->second,
                                 GeometrySettings::sMinTessellationDistance);
        } else if (propertyName == "height mapping max tessellation distance") {
            YamlUtils::GetScalar(mapIt->second,
                                 GeometrySettings::sMaxTessellationDistance);
        } else if (propertyName == "height mapping min tessellation factor") {
            YamlUtils::GetScalar(mapIt->second,
                                 GeometrySettings::sMinTessellationFactor);
        } else if (propertyName == "height mapping max tessellation factor") {
            YamlUtils::GetScalar(mapIt->second,
                                 GeometrySettings::sMaxTessellationFactor);
        } else if (propertyName == "height mapping height scale") {
            YamlUtils::GetScalar(mapIt->second,
                                 GeometrySettings::sHeightScale);
        } else {
            // To avoid warning about 'conditional expression is constant'. This is the same than false
            const std::wstring errorMsg =
                L"Unknown settings field: " + StringUtils::AnsiToWideString(propertyName);
            BRE_CHECK_MSG(&propertyName == nullptr, errorMsg.c_str());
        }

        ++mapIt;
    }
}
}

Future Work

  • Support technique/passes deactivation. For example, enable/disable ambient occlusion, enable/disable skybox, etc
  • Add support for area lights addition. This will be possible once we support area lights in BRE
Advertisements

One thought on “BRE Architecture Series Part 4 – Scene format

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s