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.

I have run into a problem. Here is some code for context:

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;
};

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();
	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);

	Mesh* GetMesh();
	Material* GetMaterial();

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()
{
    mesh = new Mesh{};
    material = new Material{};
    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();
    material->UpdateBufferBinding(descriptorSet, mvpBuffer, device, sizeof(MVP));
    renderer.CreateGraphicsPipeline(mesh, material);
    vkCmdBindPipeline(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.GetPipeline().get()->GetPipeline());
    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)
    delete mesh;
    delete material;
    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);
}

Mesh* GameObject::GetMesh()
{
    return mesh;
}
Material* GameObject::GetMaterial()
{
    return material;
}

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);

	std::vector<VkVertexInputBindingDescription> GetVertexInputBindingDescriptions() const;
	std::vector<VkVertexInputAttributeDescription> GetVertexInputAttributeDescriptions() const;

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

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

Mesh.cpp:

#include "Mesh.h"

Mesh::Mesh()
	: device(VK_NULL_HANDLE), vertexBuffer(VK_NULL_HANDLE), vertexBufferMemory(VK_NULL_HANDLE), indexBuffer(VK_NULL_HANDLE), indexBufferMemory(VK_NULL_HANDLE)
{
}

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

void Mesh::Initialize(std::vector<Vertex> vertices, std::vector<uint32_t> indices, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->vertices = vertices;
	this->indices = indices;
	this->device = device;

	Initialize(device, physicalDevice, commandPool, graphicsQueue);
	// Create vertex buffer and index buffer
	// (assuming you have helper functions CreateBuffer and CopyBuffer)
	// …
}

void Mesh::Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->device = device;

	// Create vertex buffer and index buffer
	// (assuming you have helper functions CreateBuffer and CopyBuffer)
	// …

	// Declare and initialize stagingBuffer and bufferSize here

	VkBuffer stagingBuffer;
	VkDeviceMemory stagingBufferMemory;
	VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingBuffer, stagingBufferMemory);

	void* data;
	vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
	memcpy(data, vertices.data(), (size_t)bufferSize);
	vkUnmapMemory(device, stagingBufferMemory);

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		vertexBuffer, vertexBufferMemory);

	BufferUtils::CopyBuffer(device, commandPool, graphicsQueue, stagingBuffer, vertexBuffer, bufferSize);

	vkDestroyBuffer(device, stagingBuffer, nullptr);
	vkFreeMemory(device, stagingBufferMemory, nullptr);

	bufferSize = sizeof(indices[0]) * indices.size();
	VkBuffer stagingIndexBuffer;
	VkDeviceMemory stagingIndexBufferMemory;

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingIndexBuffer, stagingIndexBufferMemory);

	vkMapMemory(device, stagingIndexBufferMemory, 0, bufferSize, 0, &data);
	memcpy(data, indices.data(), (size_t)bufferSize);
	vkUnmapMemory(device, stagingIndexBufferMemory);

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		indexBuffer, indexBufferMemory);

	BufferUtils::CopyBuffer(device, commandPool, graphicsQueue, stagingIndexBuffer, indexBuffer, bufferSize);

	vkDestroyBuffer(device, stagingIndexBuffer, nullptr);
	vkFreeMemory(device, stagingIndexBufferMemory, nullptr);
}

void Mesh::Cleanup()
{
	if (device != VK_NULL_HANDLE)
	{
		if (vertexBuffer != VK_NULL_HANDLE)
		{
			vkDestroyBuffer(device, vertexBuffer, nullptr);
			vkFreeMemory(device, vertexBufferMemory, nullptr);
			vertexBuffer = VK_NULL_HANDLE;
			vertexBufferMemory = VK_NULL_HANDLE;
		}

		if (indexBuffer != VK_NULL_HANDLE)
		{
			vkDestroyBuffer(device, indexBuffer, nullptr);
			vkFreeMemory(device, indexBufferMemory, nullptr);
			indexBuffer = VK_NULL_HANDLE;
			indexBufferMemory = VK_NULL_HANDLE;
		}
	}
}

const std::vector<Vertex>& Mesh::GetVertices() const
{
	return vertices;
}

const std::vector<uint32_t>& Mesh::GetIndices() const
{
	return indices;
}

VkBuffer Mesh::GetVertexBuffer() const
{
	return vertexBuffer;
}

VkBuffer Mesh::GetIndexBuffer() const
{
	return indexBuffer;
}
void Mesh::SetVertices(const std::vector<Vertex>& vertices)
{
	this->vertices = vertices;
}

void Mesh::SetIndices(const std::vector<uint32_t>& indices)
{
	this->indices = indices;
}

std::vector<VkVertexInputBindingDescription> Mesh::GetVertexInputBindingDescriptions() const
{
	std::vector<VkVertexInputBindingDescription> bindingDescriptions(1);

	bindingDescriptions[0].binding = 0;
	bindingDescriptions[0].stride = sizeof(Vertex);
	bindingDescriptions[0].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

	return bindingDescriptions;
}

std::vector<VkVertexInputAttributeDescription> Mesh::GetVertexInputAttributeDescriptions() const
{
	std::vector<VkVertexInputAttributeDescription> attributeDescriptions(2);

	// Position attribute
	attributeDescriptions[0].binding = 0;
	attributeDescriptions[0].location = 0;
	attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
	attributeDescriptions[0].offset = offsetof(Vertex, position);

	// Color attribute
	attributeDescriptions[1].binding = 0;
	attributeDescriptions[1].location = 1;
	attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
	attributeDescriptions[1].offset = offsetof(Vertex, color);

	return attributeDescriptions;
}

Material.h:

#pragma once

#include <vulkan/vulkan.h>
#include "Texture.h"
#include "Shader.h"
#include <stdexcept>
#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);

    void UpdateBufferBinding(VkDescriptorSet descriptorSet, VkBuffer newBuffer, VkDevice device, VkDeviceSize devicesize);
    VkDescriptorSet GetDescriptorSet() const;
    VkPipelineLayout GetPipelineLayout() const;

    std::shared_ptr <Shader> GetvertexShader();
    std::shared_ptr <Shader> GetfragmentShader();

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

    void CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool);
    void CreatePipelineLayout(VkDescriptorSetLayout descriptorSetLayout);
    

    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 and pipeline layout
    CreateDescriptorSet(descriptorSetLayout, descriptorPool);
    CreatePipelineLayout(descriptorSetLayout);
}

void Material::CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    VkDescriptorSetAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
    allocInfo.descriptorPool = descriptorPool;
    allocInfo.descriptorSetCount = 1;
    allocInfo.pSetLayouts = &descriptorSetLayout;

    if (vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet) != VK_SUCCESS) {
        throw std::runtime_error("Failed to allocate descriptor sets!");
    }

    VkDescriptorImageInfo imageInfo{};
    imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageInfo.imageView = texture->GetImageView();
    imageInfo.sampler = texture->GetSampler();

    VkWriteDescriptorSet descriptorWrite{};
    descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrite.dstSet = descriptorSet;
    descriptorWrite.dstBinding = 0;
    descriptorWrite.dstArrayElement = 0;
    descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorWrite.descriptorCount = 1;
    descriptorWrite.pImageInfo = &imageInfo;

    vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}

void Material::CreatePipelineLayout(VkDescriptorSetLayout descriptorSetLayout)
{
    VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipelineLayoutInfo.setLayoutCount = 1;
    pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

    if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
        throw std::runtime_error("Failed to create pipeline layout!");
    }
}



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;
}

std::shared_ptr <Shader> Material::GetvertexShader()
{
    return vertexShader;
}

std::shared_ptr <Shader> Material::GetfragmentShader()
{
    return fragmentShader;
}

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);
}

void Material::UpdateBufferBinding(VkDescriptorSet descriptorSet, VkBuffer newBuffer, VkDevice device, VkDeviceSize devicesize)
{
    VkDescriptorBufferInfo bufferInfo{};
    bufferInfo.buffer = newBuffer;
    bufferInfo.offset = 0;
    bufferInfo.range = devicesize;

    VkWriteDescriptorSet descriptorWrite{};
    descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrite.dstSet = descriptorSet;
    descriptorWrite.dstBinding = 0;
    descriptorWrite.dstArrayElement = 0;
    descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    descriptorWrite.descriptorCount = 1;
    descriptorWrite.pBufferInfo = &bufferInfo;

    vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}

Pipeline.h:

#pragma once

#include <vulkan/vulkan.h>
#include <vector>
#include <array>
#include <stdexcept>
#include "Shader.h"

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

	void CreateGraphicsPipeline(const std::vector<VkVertexInputBindingDescription>& vertexBindingDescriptions,
		const std::vector<VkVertexInputAttributeDescription>& vertexAttributeDescriptions,
		VkExtent2D swapchainExtent,
		const std::vector<Shader*>& shaders,
		VkRenderPass renderPass,
		VkPipelineLayout pipelineLayout,
		VkDevice device);

	void Cleanup();

	VkPipeline GetPipeline() const;

	bool IsInitialized() const;

private:
	VkDevice device;
	VkPipeline pipeline;
	bool initialized;

	void CreateShaderStages(const std::vector<Shader*>& shaders, std::vector<VkPipelineShaderStageCreateInfo>& shaderStages);
};

Pipeline.cpp:

#include "Pipeline.h"

Pipeline::Pipeline()
	: device(VK_NULL_HANDLE), pipeline(VK_NULL_HANDLE), initialized(false)
{
}

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

void Pipeline::CreateGraphicsPipeline(const std::vector<VkVertexInputBindingDescription>& vertexBindingDescriptions,
	const std::vector<VkVertexInputAttributeDescription>& vertexAttributeDescriptions,
	VkExtent2D swapchainExtent,
	const std::vector<Shader*>& shaders,
	VkRenderPass renderPass,
	VkPipelineLayout pipelineLayout,
	VkDevice device)
{
	this->device = device;

	VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
	vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
	vertexInputInfo.vertexBindingDescriptionCount = static_cast<uint32_t>(vertexBindingDescriptions.size());
	vertexInputInfo.pVertexBindingDescriptions = vertexBindingDescriptions.data();
	vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexAttributeDescriptions.size());
	vertexInputInfo.pVertexAttributeDescriptions = vertexAttributeDescriptions.data();

	VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
	inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
	inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
	inputAssembly.primitiveRestartEnable = VK_FALSE;

	VkViewport viewport{};
	viewport.x = 0.0f;
	viewport.y = 0.0f;
	viewport.width = static_cast<float>(swapchainExtent.width);
	viewport.height = static_cast<float>(swapchainExtent.height);
	viewport.minDepth = 0.0f;
	viewport.maxDepth = 1.0f;

	VkRect2D scissor{};
	scissor.offset = { 0, 0 };
	scissor.extent = swapchainExtent;

	VkPipelineViewportStateCreateInfo viewportState{};
	viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
	viewportState.viewportCount = 1;
	viewportState.pViewports = &viewport;
	viewportState.scissorCount = 1;
	viewportState.pScissors = &scissor;

	VkPipelineRasterizationStateCreateInfo rasterizer{};
	rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
	rasterizer.depthClampEnable = VK_FALSE;
	rasterizer.rasterizerDiscardEnable = VK_FALSE;
	rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
	rasterizer.lineWidth = 1.0f;
	rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
	rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
	rasterizer.depthBiasEnable = VK_FALSE;

	VkPipelineMultisampleStateCreateInfo multisampling{};
	multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
	multisampling.sampleShadingEnable = VK_FALSE;
	multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

	VkPipelineColorBlendAttachmentState colorBlendAttachment{};
	colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
	colorBlendAttachment.blendEnable = VK_FALSE;

	VkPipelineColorBlendStateCreateInfo colorBlending{};
	colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
	colorBlending.logicOpEnable = VK_FALSE;
	colorBlending.logicOp = VK_LOGIC_OP_COPY;
	colorBlending.attachmentCount = 1;
	colorBlending.pAttachments = &colorBlendAttachment;
	colorBlending.blendConstants[0] = 0.0f;
	colorBlending.blendConstants[1] = 0.0f;
	colorBlending.blendConstants[2] = 0.0f;
	colorBlending.blendConstants[3] = 0.0f;

	std::vector<VkPipelineShaderStageCreateInfo> shaderStages;
	CreateShaderStages(shaders, shaderStages);

	VkGraphicsPipelineCreateInfo pipelineCreateInfo{};
	pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
	pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
	pipelineCreateInfo.pStages = shaderStages.data();
	pipelineCreateInfo.pVertexInputState = &vertexInputInfo;
	pipelineCreateInfo.pInputAssemblyState = &inputAssembly;
	pipelineCreateInfo.pViewportState = &viewportState;
	pipelineCreateInfo.pRasterizationState = &rasterizer;
	pipelineCreateInfo.pMultisampleState = &multisampling;
	pipelineCreateInfo.pColorBlendState = &colorBlending;
	pipelineCreateInfo.layout = pipelineLayout;
	pipelineCreateInfo.renderPass = renderPass;
	pipelineCreateInfo.subpass = 0;

	if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &pipeline) != VK_SUCCESS)
	{
		throw std::runtime_error("Failed to create graphics pipeline.");
	}

	initialized = true;
}

bool Pipeline::IsInitialized() const
{
	return initialized;
}

void Pipeline::Cleanup()
{
	if (pipeline != VK_NULL_HANDLE && initialized)
	{
		//vkDestroyPipeline(device, pipeline, nullptr);
		pipeline = VK_NULL_HANDLE;
		initialized = false;
	}
}

VkPipeline Pipeline::GetPipeline() const
{
	return pipeline;
}

void Pipeline::CreateShaderStages(const std::vector<Shader*>& shaders, std::vector<VkPipelineShaderStageCreateInfo>& shaderStages)
{
	for (auto shader : shaders)
	{
		shaderStages.push_back(shader->GetPipelineShaderStageCreateInfo());
	}
}

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) {
    terrainMesh = new Mesh{};
    terrainMaterial = new Material{};
}

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

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()
{
    vertices.clear();
    indices.clear();

    // Generate vertices
    for (int x = 0; x < worldSize; ++x)
    {
        for (int z = 0; z < worldSize; ++z)
        {
            Vertex vertex;
            vertex.position = glm::vec3(static_cast<float>(x), heightMap[x][z], static_cast<float>(z));

            // Set color based on height, feel free to customize this as desired
            float color = heightMap[x][z] * 0.5f + 0.5f; // Normalize the height value between 0 and 1
            vertex.color = glm::vec3(color, color, color);

            vertices.push_back(vertex);
        }
    }

    // Generate indices for rendering using triangle strips
    for (int x = 0; x < worldSize - 1; ++x)
    {
        for (int z = 0; z < worldSize - 1; ++z)
        {
            int topLeft = x * worldSize + z;
            int topRight = topLeft + 1;
            int bottomLeft = (x + 1) * worldSize + z;
            int bottomRight = bottomLeft + 1;

            // First triangle
            indices.push_back(topLeft);
            indices.push_back(bottomRight);
            indices.push_back(bottomLeft);

            // Second triangle
            indices.push_back(topLeft);
            indices.push_back(topRight);
            indices.push_back(bottomRight);
        }
    }
}

void Terrain::GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    GenerateHeightMap();
    ConvertHeightMapToMeshData();
    terrainObject = new GameObject();
    terrainObject->Initialize();
    terrainMesh = terrainObject->GetMesh();
    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 = terrainObject->GetMaterial();
    terrainMaterial->Initialize("C:/shaders/vert_depth2.spv", "C:/shaders/frag_depth2.spv", "C:/textures/texture.jpg", *device, descriptorSetLayout, descriptorPool, *physicalDevice, *commandPool, *graphicsQueue);

}

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

Vertex Shader:

#version 450

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;


void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
    fragColor = inColor;
}

Fragment Shader:

#version 450

layout(binding = 1) uniform sampler2D texSampler;

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1,0,1,1);
}

The application starts and shows a black window, but the terrain is not being rendered. Do you know where the issue is?