I’m building a video game engine using C++ as the coding language and Vulkan for graphics. I am trying to set up a generic renderer using Vulkan that is flexible and will render objects based on a vector that is supplied to it. The renderer will also handle the creation of the window using GLFW and use GLM for all relevant math calls. I am using the ASSIMP library to load 3d models and animations.

The basic class structure is as follows:

1. Core:

- class Engine
- This is the main class that will initialize, run, and shut down other subsystems of the engine (e.g., Window, Renderer, Input, etc.)

2. Window:

- class Window
- This class will handle window creation, resizing, and destruction using GLFW. It should also handle keyboard and mouse input events.

3. Renderer:

- class Renderer
- This class will manage Vulkan objects (e.g., instance, device, swapchain, command buffers) and rendering pipelines.
- class Shader
- To handle shader modules creation and destruction.
- class Texture
- To handle texture loading and destruction.
- class Pipeline
- To encapsulate the description and creation of a Vulkan graphics or compute pipeline.

4. Scene:

- class Scene
- This class will manage the list of objects in the vector, update their transforms, and send them to the Renderer for rendering.
- class GameObject
- To represent individual objects in the scene, store their transformation matrices, and bind the relevant data like vertices and indices.
- class Camera
- To store and manage camera properties such as position, rotation, view, and projection matrices.
- class Mesh
- To handle mesh data (e.g., vertices, indices, etc.) for individual objects.
- class Material
- To store and manage material properties such as colors, shaders, and textures.

5. Math:

- Utilize GLM library to handle vector and matrix math operations.


Here is some of the relevant header and source file code:

Engine.h:

#pragma once

#include "Window.h"
#include "Renderer.h"
#include "Scene.h"

class Engine
{
public:
	Engine();
	~Engine();

	void Run();
	void Shutdown();

private:

	void Initialize();
	void MainLoop();
	void Update(float deltaTime);
	void Render();

	Window window;
	Renderer renderer;
	Scene scene;
};

Engine.cpp:

#include "Engine.h"
#include "Terrain.h"
#include <iostream>

Engine::Engine()
{
	Initialize();
}

Engine::~Engine()
{
	Shutdown();
}

void Engine::Run()
{
	MainLoop();
}

void Engine::Initialize()
{
	// Initialize window, renderer, and scene
	window.Initialize();
	renderer.Initialize(window.GetWindow());
	scene.Initialize();

	VkDescriptorSetLayout descriptorSetLayout = renderer.CreateDescriptorSetLayout();
	VkDescriptorPool descriptorPool = renderer.CreateDescriptorPool(1); // Assuming only one terrain object

	Terrain terrain(0,10,1,renderer.GetDevice(), renderer.GetPhysicalDevice(), renderer.GetCommandPool(), renderer.GetGraphicsQueue());
	terrain.GenerateTerrain(descriptorSetLayout, descriptorPool);

	scene.AddGameObject(terrain.GetTerrainObject());
}

void Engine::MainLoop()
{
	while (!window.ShouldClose())
	{
		window.PollEvents();

		float deltaTime = window.GetDeltaTime();
		Update(deltaTime);
		Render();
	}
}

void Engine::Update(float deltaTime)
{
	scene.Update(deltaTime);
}

void Engine::Render()
{
	renderer.BeginFrame();
	scene.Render(renderer);
	renderer.EndFrame();
}

void Engine::Shutdown()
{
	// Clean up resources in reverse order
	scene.Shutdown();
	renderer.Shutdown();
	window.Shutdown();
}

Scene.h:

#pragma once

#include <vector>
#include "GameObject.h"
#include "Camera.h"
#include "Renderer.h"

class Scene
{
public:
	Scene();
	~Scene();

	void Initialize();
	void Update(float deltaTime);
	void Render(Renderer& renderer);
	void Shutdown();

	void AddGameObject(GameObject* gameObject);
	Camera& GetCamera();

private:
	std::vector<GameObject*> gameObjects;
	Camera camera;
};

Scene.cpp:

#include "Scene.h"

Scene::Scene()
{
}

Scene::~Scene()
{
	Shutdown();
}

void Scene::Initialize()
{
	// Initialize camera and game objects
	camera.SetPosition(glm::vec3(0.0f, 0.0f, -5.0f));

	// Add initial game objects
}

void Scene::Update(float deltaTime)
{
	// Update game objects and camera
	for (GameObject* gameObject : gameObjects)
	{
		gameObject->Update(deltaTime);
	}
}

void Scene::Render(Renderer& renderer)
{
	// Render game objects
	for (GameObject* gameObject : gameObjects)
	{
		gameObject->Render(renderer, camera);
	}

	// Submit rendering-related commands to Vulkan queues
}

void Scene::Shutdown()
{
	// Clean up game objects
	for (GameObject* gameObject : gameObjects)
	{
		delete gameObject;
		gameObject = nullptr;
	}
	gameObjects.clear();
	camera.Shutdown();
}

void Scene::AddGameObject(GameObject* gameObject)
{
	gameObjects.push_back(gameObject);
}

Camera& Scene::GetCamera()
{
	return camera;
}

GameObject.h:

#pragma once

#include <glm/glm.hpp>
#include "Mesh.h"
#include "Material.h"
#include "Camera.h"
#include "Renderer.h"

class GameObject
{
public:
	GameObject();
	~GameObject();

	void Initialize(const Mesh& mesh, const Material& material);
	void Update(float deltaTime);
	void Render(Renderer& renderer, const Camera& camera);
	void Shutdown();

	void SetPosition(const glm::vec3& position);
	void SetRotation(const glm::vec3& rotation);
	void SetScale(const glm::vec3& scale);

private:
	glm::mat4 modelMatrix;
	glm::vec3 position;
	glm::vec3 rotation;
	glm::vec3 scale;

	Mesh mesh;
	Material material;

	bool initialized = false;

	void UpdateModelMatrix();
};

GameObject.cpp:

#include "GameObject.h"
#include <glm/gtc/matrix_transform.hpp>

GameObject::GameObject()
	: position(0.0f), rotation(0.0f), scale(1.0f)
{
}

GameObject::~GameObject()
{
    if (initialized)
    {
        Shutdown();
    }
}

void GameObject::Initialize(const Mesh& mesh, const Material& material)
{
	this->mesh = mesh; // copy or share mesh data
	this->material = material; // copy or share material data
    this->initialized = true;
}

void GameObject::Update(float deltaTime)
{
	// Update position, rotation, scale, and other properties
	// Example: Rotate the object around the Y-axis
	rotation.y += deltaTime * glm::radians(90.0f);
	UpdateModelMatrix();
}

void GameObject::Render(Renderer& renderer, const Camera& camera)
{
    // Render this object using the renderer and camera
    VkDevice device = *renderer.GetDevice();

    // Bind mesh vertex and index buffers
    VkBuffer vertexBuffers[] = { mesh.GetVertexBuffer() };
    VkDeviceSize offsets[] = { 0 };
    vkCmdBindVertexBuffers(*renderer.GetCurrentCommandBuffer(), 0, 1, vertexBuffers, offsets);
    vkCmdBindIndexBuffer(*renderer.GetCurrentCommandBuffer(), mesh.GetIndexBuffer(), 0, VK_INDEX_TYPE_UINT32);

    // Update shader uniform buffers with modelMatrix, viewMatrix and projectionMatrix transforms
    struct MVP {
        glm::mat4 model;
        glm::mat4 view;
        glm::mat4 projection;
    } mvp;

    mvp.model = modelMatrix;
    mvp.view = camera.GetViewMatrix();
    mvp.projection = camera.GetProjectionMatrix();

    // Create a new buffer to hold the MVP data temporarily
    VkBuffer mvpBuffer;
    VkDeviceMemory mvpBufferMemory;
    BufferUtils::CreateBuffer(device, *renderer.GetPhysicalDevice(),
        sizeof(MVP), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        mvpBuffer, mvpBufferMemory);

    // Map the MVP data into the buffer and unmap
    void* data = nullptr;
    vkMapMemory(device, mvpBufferMemory, 0, sizeof(MVP), 0, &data);
    memcpy(data, &mvp, sizeof(MVP));
    vkUnmapMemory(device, mvpBufferMemory);

    // TODO: Modify your material, descriptor set, and pipeline to use this new mvpBuffer instead of
    // the default uniform buffer

    // Bind the DescriptorSet associated with the material
    VkDescriptorSet descriptorSet = material.GetDescriptorSet();
    vkCmdBindDescriptorSets(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, material.GetPipelineLayout(), 0, 1, &descriptorSet, 0, nullptr);

    // Call vkCmdDrawIndexed()
    uint32_t numIndices = static_cast<uint32_t>(mesh.GetIndices().size());
    vkCmdDrawIndexed(*renderer.GetCurrentCommandBuffer(), numIndices, 1, 0, 0, 0);

    // Cleanup the temporary buffer
    vkDestroyBuffer(device, mvpBuffer, nullptr);
    vkFreeMemory(device, mvpBufferMemory, nullptr);
}

void GameObject::Shutdown()
{
	// Clean up resources, if necessary
	// (depending on how Mesh and Material resources are managed)
    this->initialized = false;
}

void GameObject::SetPosition(const glm::vec3& position)
{
	this->position = position;
	UpdateModelMatrix();
}

void GameObject::SetRotation(const glm::vec3& rotation)
{
	this->rotation = rotation;
	UpdateModelMatrix();
}

void GameObject::SetScale(const glm::vec3& scale)
{
	this->scale = scale;
	UpdateModelMatrix();
}

void GameObject::UpdateModelMatrix()
{
	modelMatrix = glm::mat4(1.0f);
	modelMatrix = glm::translate(modelMatrix, position);
	modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
	modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
	modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
	modelMatrix = glm::scale(modelMatrix, scale);
}

Terrain.h:

#pragma once

#include "Simplex.h"
#include "Mesh.h"
#include "Material.h"
#include "GameObject.h"

class Terrain {
public:
    Terrain(int seed, int worldSize, float scale, VkDevice* device, VkPhysicalDevice* physicalDevice, VkCommandPool* commandPool, VkQueue* graphicsQueue);
    ~Terrain();

    void GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool);
    GameObject* GetTerrainObject();

private:
    void GenerateHeightMap();
    void ConvertHeightMapToMeshData();

    int seed;
    int worldSize;
    float scale;

    VkDevice* device;
    VkPhysicalDevice* physicalDevice;
    VkCommandPool* commandPool;
    VkQueue* graphicsQueue;

    SimplexNoise noise;
    std::vector<std::vector<float>> heightMap;
    std::vector<Vertex> vertices;
    std::vector<uint32_t> indices;

    Mesh terrainMesh;
    Material terrainMaterial;
    GameObject terrainObject;
};

Terrain.cpp:

#include "Terrain.h"

Terrain::Terrain(int seed, int worldSize, float scale, VkDevice* device, VkPhysicalDevice* physicalDevice, VkCommandPool* commandPool, VkQueue* graphicsQueue)
    : seed(seed), worldSize(worldSize), scale(scale), noise(seed), device(device), physicalDevice(physicalDevice), commandPool(commandPool), graphicsQueue(graphicsQueue) {
}

Terrain::~Terrain() {
    // Cleanup any resources associated with the Terrain class
    //terrainMesh.Cleanup();
    //terrainMaterial.Cleanup();
}

void Terrain::GenerateHeightMap() {
    heightMap = std::vector<std::vector<float>>(worldSize, std::vector<float>(worldSize, 0.0f));

    for (int x = 0; x < worldSize; ++x) {
        for (int z = 0; z < worldSize; ++z) {
            float noiseValue = noise.noise(x * scale, z * scale);
            heightMap[x][z] = noiseValue;
        }
    }
}

void Terrain::ConvertHeightMapToMeshData() {
    // … Convert height map to vertices and indices (code from the previous example)
}

void Terrain::GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    GenerateHeightMap();
    ConvertHeightMapToMeshData();
    terrainMesh.Initialize(vertices, indices, *device, *physicalDevice, *commandPool, *graphicsQueue);

    // Initialize terrainMaterial with the loaded shaders and texture
    // Assume descriptorSetLayout and descriptorPool are created before this call
    terrainMaterial.Initialize("C:/shaders/vert.spv", "C:/shaders/frag.spv", "C:/textures/texture.jpg", *device, descriptorSetLayout, descriptorPool, *physicalDevice, *commandPool, *graphicsQueue);

    terrainObject.Initialize(terrainMesh, terrainMaterial);
}

GameObject* Terrain::GetTerrainObject() {
    return &terrainObject;
}

Material.h:

#pragma once

#include <vulkan/vulkan.h>
#include "Texture.h"
#include "Shader.h"
#include <memory> // Don’t forget to include <memory>

class Material
{
public:
	Material();
	~Material();

	void Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, VkDevice device, VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Cleanup();

	void LoadTexture(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void LoadShaders(const std::string& vertFilename, const std::string& fragFilename, VkDevice device);

	VkDescriptorSet GetDescriptorSet() const;
	VkPipelineLayout GetPipelineLayout() const;

private:
	VkDevice device;
	std::shared_ptr <Shader> vertexShader;
	std::shared_ptr <Shader> fragmentShader;
	std::shared_ptr<Texture> texture;

	VkDescriptorSet descriptorSet;
	VkPipelineLayout pipelineLayout;
};

Material.cpp:

#include "Material.h"

Material::Material()
	: device(VK_NULL_HANDLE), descriptorSet(VK_NULL_HANDLE), pipelineLayout(VK_NULL_HANDLE)
{
}

Material::~Material()
{
	Cleanup();
}

void Material::Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, VkDevice device, VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->device = device;

	// Load shaders and texture
	LoadTexture(texturePath, device, physicalDevice, commandPool, graphicsQueue);
	LoadShaders(vertShaderPath, fragShaderPath, device);

	// Create descriptor set, pipeline layout, and other required objects for the material
	// …
}

void Material::Cleanup()
{
	// Clean up resources, if necessary
	// (depending on how Shader and Texture resources are managed)
}

VkDescriptorSet Material::GetDescriptorSet() const
{
	return descriptorSet;
}

VkPipelineLayout Material::GetPipelineLayout() const
{
	return pipelineLayout;
}

void Material::LoadTexture(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	texture = std::shared_ptr<Texture>(new Texture{}, Texture::Cleanup); // Create a new Texture using shared_ptr
	texture->LoadFromFile(filename, device, physicalDevice, commandPool, graphicsQueue);
}

void Material::LoadShaders(const std::string& vertFilename, const std::string& fragFilename, VkDevice device)
{
	vertexShader = std::shared_ptr<Shader>(new Shader{}, Shader::Cleanup);
	fragmentShader = std::shared_ptr<Shader>(new Shader{}, Shader::Cleanup);
	vertexShader->LoadFromFile(vertFilename, device, VK_SHADER_STAGE_VERTEX_BIT);
	fragmentShader->LoadFromFile(fragFilename, device, VK_SHADER_STAGE_FRAGMENT_BIT);
}

Texture.h:

#pragma once

#include <vulkan/vulkan.h>

#include "stb_image.h" // Include the stb_image header
#include "BufferUtils.h"
#include <string>

class Texture
{
public:
	Texture();
	~Texture();

	void LoadFromFile(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);

	VkImageView GetImageView() const;
	VkSampler GetSampler() const;
	static void Cleanup(Texture* texture);

private:
	VkDevice device;
	VkImage image;
	VkDeviceMemory imageMemory;
	VkImageView imageView;
	VkSampler sampler;
	VkPhysicalDevice physicalDevice;
	VkCommandPool commandPool;
	VkQueue graphicsQueue;
	bool initialized = false;

	void CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties);
	void TransitionImageLayout(VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels);
	void CreateImageView(VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels);
	void CreateSampler(uint32_t mipLevels);
	void CopyBufferToImage(VkBuffer buffer, uint32_t width, uint32_t height);
	

	// Additional helper functions for texture loading…
};

Mesh.h:

#pragma once

#include <vector>
#include <vulkan/vulkan.h>
#include <glm/glm.hpp>
#include "BufferUtils.h"

struct Vertex
{
	glm::vec3 position;
	glm::vec3 color;
};

class Mesh
{
public:
	Mesh();
	~Mesh();

	void Initialize(std::vector<Vertex> vertices, std::vector<uint32_t> indices, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Cleanup();

	const std::vector<Vertex>& GetVertices() const;
	const std::vector<uint32_t>& GetIndices() const;
	VkBuffer GetVertexBuffer() const;
	VkBuffer GetIndexBuffer() const;

	void SetVertices(const std::vector<Vertex>& vertices);
	void SetIndices(const std::vector<uint32_t>& indices);

private:
	VkDevice device;
	std::vector<Vertex> vertices;
	std::vector<uint32_t> indices;

	VkBuffer vertexBuffer;
	VkDeviceMemory vertexBufferMemory;
	VkBuffer indexBuffer;
	VkDeviceMemory indexBufferMemory;
};


The Material class is currently missing the code necessary to create the descriptor set and pipeline layout. Can you help to fill in the code associated with these components?