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.

As a start, this engine will render a basic spinning box with uniform color. However, it will then expand to cover the full functionality of the game engine.

The code uses a Texture class in order to load textures for use in the game. The header and source file for this class look like this:

Header:

#pragma once

#include <vulkan/vulkan.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" // Include the stb_image header
#include <string>

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

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

	VkImageView GetImageView() const;
	VkSampler GetSampler() const;

private:
	VkDevice device;
	VkImage image;
	VkDeviceMemory imageMemory;
	VkImageView imageView;
	VkSampler sampler;

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


Source:

#include "Texture.h"
#include <iostream>

Texture::Texture()
	: device(VK_NULL_HANDLE), image(VK_NULL_HANDLE), imageMemory(VK_NULL_HANDLE), imageView(VK_NULL_HANDLE), sampler(VK_NULL_HANDLE)
{
}

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

void Texture::LoadFromFile(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
    this->device = device;

    // Load image from file using stb_image
    int width, height, channels;
    stbi_uc* pixels = stbi_load(filename.c_str(), &width, &height, &channels, STBI_rgb_alpha);
    if (!pixels)
    {
        throw std::runtime_error("Failed to load texture image!");
    }

    // Calculate the number of mip levels
    uint32_t mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1;

    // Create a buffer to store the image data
    VkDeviceSize imageSize = width * height * 4;
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    // Create and fill the buffer
    // …

    // Copy image data to the buffer
    void* bufferData;
    vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &bufferData);
    memcpy(bufferData, pixels, static_cast<size_t>(imageSize));
    vkUnmapMemory(device, stagingBufferMemory);

    // Free the stb_image buffer
    stbi_image_free(pixels);

    // Create vkImage, copy buffer to image, and create imageView and sampler
    // …

    CreateImage(width, height, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL,
        VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
    CreateImageView(VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels);
    CreateSampler(mipLevels);

    CopyBufferToImage(stagingBuffer, width, height);

    // Cleanup the staging buffer and staging buffer memory
    // …
}

void Texture::Cleanup()
{
	vkDestroySampler(device, sampler, nullptr);
	vkDestroyImageView(device, imageView, nullptr);
	vkDestroyImage(device, image, nullptr);
	vkFreeMemory(device, imageMemory, nullptr);
}

VkImageView Texture::GetImageView() const
{
	return imageView;
}

VkSampler Texture::GetSampler() const
{
	return sampler;
}

void Texture::CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties)
{
	// Create VkImage object
	// …

	// Allocate and bind memory for the image
	// …

	// Transition the image layout to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
	// …
}

void Texture::TransitionImageLayout(VkImageLayout oldLayout, VkImageLayout newLayout)
{
	// Create a one-time-use command buffer and record image layout transition commands
	// …
}

void Texture::CreateImageView(VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels)
{
	// Create VkImageView object
	// …
}

void Texture::CreateSampler(uint32_t mipLevels)
{
	// Create VkSampler object
	// …
}

void Texture::CopyBufferToImage(VkBuffer buffer, uint32_t width, uint32_t height)
{
	// Create a one-time-use command buffer and record buffer to image copy commands
	// …
}

Based on this code structure, what would the completed methods look like for CreateImage, TransitionImageLayout, CreateImageView, CreateSampler and CopyBufferToImage?