I am building a 3d renderer in C++ using Vulkan.hpp. I currently have code that handles classes for Textures, TextureArrays and Cubemaps. I now want to have a class to manage CubemapArrays that follows a similar template to the Cubemap class, but does so in a way the handles Cubemap arrays. As I am looking to extend cubemap functionality to handle multiple cubemaps in a single structure, this will need to handle all of the functions in the Cubemap class, but with the appropriate inputs to select the relevant array layer. Based on the following header and source code for the Cubemap class, can you generate both the header and source code for a CubemapArray class that would achieve this? For further reference, I have included the current header code for the TextureArray class to inform how I typically handle array layer related parameters.

Cubemap.h:

#pragma once

#include <vulkan/vulkan.hpp>
#include <array>
#include <string>

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

    void LoadFromFiles(const std::array<std::string, 6>& filenames, vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue);
    void InitializeBlankImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height);
    void InitializeBlankHDRImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height);
    void InitializeBlankHDRImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height, vk::SampleCountFlagBits samples);
    void InitializeBlankHDRMipMapImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height);
    vk::ImageView GetImageView() const;
    vk::Image GetImage() const;
    vk::Sampler GetSampler() const;
    void TransitionImageLayout(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageSubresourceRange subresourceRange);
    void Cleanup();

    void CopyImageToLayer(vk::Image sourceImage, int layerIndex, vk::Extent3D extent);

    void CopyDepthToLayer(vk::Image sourceImage, int layerIndex, vk::Extent3D extent);

    uint32_t mipLevels;

    uint32_t getWidth() const { return width; }
    uint32_t getHeight() const { return height; }
    vk::Format getFormat() const { return format; }
    vk::SampleCountFlagBits getSampleCount() const { return samples; }
    vk::ImageUsageFlags getImageUsage() const { return usage; }
    vk::ImageAspectFlags getImageAspect() const { return aspect; }
    uint32_t getMipLevels() const { return mipLevels; }

private:
    vk::Device device;
    vk::Image image;
    vk::DeviceMemory imageMemory;
    vk::ImageView imageView;
    vk::Sampler sampler;
    vk::PhysicalDevice physicalDevice;
    vk::CommandPool commandPool;
    vk::Queue graphicsQueue;
    bool initialized = false;

    uint32_t width;
    uint32_t height;
    vk::Format format;
    vk::SampleCountFlagBits samples;
    vk::ImageUsageFlags usage;
    vk::ImageAspectFlags aspect;

    void CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, vk::SampleCountFlagBits numSamples, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties);
    void CopyBufferToImage(vk::Buffer buffer, uint32_t width, uint32_t height, int layerIndex);
    void CreateImageView(vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels);
    void CreateSampler(uint32_t mipLevels);
    void GenerateMipmaps(uint32_t width, uint32_t height, uint32_t mipLevels);
    vk::CommandBuffer BeginSingleTimeCommands(vk::CommandPool commandPool, vk::Device device);
    void EndSingleTimeCommands(vk::CommandPool commandPool, vk::Queue graphicsQueue, vk::Device device, vk::CommandBuffer commandBuffer);
};


Cubemap.cpp:

#include "Cubemap.h"
#include "stb_image.h"
#include "BufferUtils.h"

Cubemap::Cubemap()
    : device(), image(), imageMemory(), imageView(), sampler()
{
}

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

void Cubemap::LoadFromFiles(const std::array<std::string, 6>& filenames, vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    int width, height, channels;
    stbi_uc* pixels = stbi_load(filenames[0].c_str(), &width, &height, &channels, STBI_rgb_alpha);

    if (!pixels)
    {
        throw std::runtime_error("Failed to load cubemap face!");
    }

    uint32_t mipLevels = 1;

    vk::DeviceSize imageSize = width * height * 4;
    std::vector<vk::Buffer> stagingBuffers(6);
    std::vector<vk::DeviceMemory> stagingBufferMemories(6);

    for (int i = 0; i < 6; ++i)
    {
        stbi_uc* facePixels = stbi_load(filenames[i].c_str(), &width, &height, &channels, STBI_rgb_alpha);
        if (!facePixels)
        {
            throw std::runtime_error("Failed to load cubemap face!");
        }

        BufferUtils::CreateBuffer(device, physicalDevice, imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffers[i], stagingBufferMemories[i]);

        void* bufferData;
        device.mapMemory(stagingBufferMemories[i], 0, imageSize, {}, &bufferData);
        memcpy(bufferData, facePixels, static_cast<size_t>(imageSize));
        device.unmapMemory(stagingBufferMemories[i]);
        stbi_image_free(facePixels);
    }

    CreateImage(width, height, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal,
        vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal);

    CreateImageView(vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mipLevels);
    CreateSampler(mipLevels);

    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = mipLevels;
    subresourceRange.baseArrayLayer = 0;
    subresourceRange.layerCount = 6;

    TransitionImageLayout(vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, subresourceRange);

    for (int i = 0; i < 6; ++i)
    {
        CopyBufferToImage(stagingBuffers[i], width, height, i);
    }

    TransitionImageLayout(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresourceRange);

    for (int i = 0; i < 6; ++i)
    {
        device.destroyBuffer(stagingBuffers[i], nullptr);
        device.freeMemory(stagingBufferMemories[i], nullptr);
    }
}

// New method to initialize blank images for shadows
void Cubemap::InitializeBlankImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    //uint32_t width = width; // Standard size for shadow maps, adjust as needed
    //uint32_t height = height;
    uint32_t mipLevels = 1;

    CreateImage(width, height, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eD32Sfloat, vk::ImageTiling::eOptimal,
        vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal);

    CreateImageView(vk::Format::eD32Sfloat, vk::ImageAspectFlagBits::eDepth, mipLevels);
    CreateSampler(mipLevels);

    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eDepth;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = mipLevels;
    subresourceRange.baseArrayLayer = 0;
    subresourceRange.layerCount = 6;

    TransitionImageLayout(vk::ImageLayout::eUndefined, vk::ImageLayout::eDepthStencilAttachmentOptimal, subresourceRange);
}

// New method to initialize blank HDR images irradiance
void Cubemap::InitializeBlankHDRImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    //uint32_t width = width; // Standard size for shadow maps, adjust as needed
    //uint32_t height = height;
    uint32_t mipLevels = 1;

    CreateImage(width, height, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR16G16B16A16Sfloat, vk::ImageTiling::eOptimal,
        vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal);

    CreateImageView(vk::Format::eR16G16B16A16Sfloat, vk::ImageAspectFlagBits::eColor, mipLevels);
    CreateSampler(mipLevels);

    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = mipLevels;
    subresourceRange.baseArrayLayer = 0;
    subresourceRange.layerCount = 6;

    TransitionImageLayout(vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, subresourceRange);
    TransitionImageLayout(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresourceRange);
}

// New method to initialize blank HDR images irradiance
void Cubemap::InitializeBlankHDRMipMapImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    //uint32_t width = width; // Standard size for shadow maps, adjust as needed
    //uint32_t height = height;
    mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1;

    CreateImage(width, height, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR16G16B16A16Sfloat, vk::ImageTiling::eOptimal,
        vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal);

    CreateImageView(vk::Format::eR16G16B16A16Sfloat, vk::ImageAspectFlagBits::eColor, mipLevels);
    CreateSampler(mipLevels);


    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = mipLevels;
    subresourceRange.baseArrayLayer = 0;
    subresourceRange.layerCount = 6;

    TransitionImageLayout(vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, subresourceRange);

    GenerateMipmaps(width, height, mipLevels);

    //TransitionImageLayout(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresourceRange);
}

void Cubemap::Cleanup()
{
    if (!initialized) {
        return;
    }

    if (sampler) {
        device.destroySampler(sampler, nullptr);
        sampler = nullptr;
    }

    if (imageView) {
        device.destroyImageView(imageView, nullptr);
        imageView = nullptr;
    }

    if (image) {
        device.destroyImage(image, nullptr);
        image = nullptr;
    }

    if (imageMemory) {
        device.freeMemory(imageMemory, nullptr);
        imageMemory = nullptr;
    }

    initialized = false;
}

vk::ImageView Cubemap::GetImageView() const
{
    return imageView;
}

vk::Image Cubemap::GetImage() const
{
    return image;
}

vk::Sampler Cubemap::GetSampler() const
{
    return sampler;
}

void Cubemap::CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, vk::SampleCountFlagBits numSamples, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties)
{
    vk::ImageCreateInfo imageInfo{};
    imageInfo.imageType = vk::ImageType::e2D;
    imageInfo.extent.width = width;
    imageInfo.extent.height = height;
    imageInfo.extent.depth = 1;
    imageInfo.mipLevels = mipLevels;
    imageInfo.arrayLayers = 6;
    imageInfo.format = format;
    imageInfo.tiling = tiling;
    imageInfo.initialLayout = vk::ImageLayout::eUndefined;
    imageInfo.usage = usage;
    imageInfo.sharingMode = vk::SharingMode::eExclusive;
    imageInfo.samples = numSamples;
    imageInfo.flags = vk::ImageCreateFlagBits::eCubeCompatible;

    if (device.createImage(&imageInfo, nullptr, &image) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create cubemap image!");
    }

    vk::MemoryRequirements memRequirements;
    device.getImageMemoryRequirements(image, &memRequirements);

    vk::MemoryAllocateInfo allocInfo{};
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = BufferUtils::FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);

    if (device.allocateMemory(&allocInfo, nullptr, &imageMemory) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to allocate cubemap image memory!");
    }

    device.bindImageMemory(image, imageMemory, 0);

    this->width = width;
    this->height = height;
    this->format = format;
    this->samples = numSamples;
    this->usage = usage;
    this->mipLevels = mipLevels;
}

void Cubemap::TransitionImageLayout(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageSubresourceRange subresourceRange)
{
    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    vk::ImageMemoryBarrier barrier{};
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.image = image;
    barrier.subresourceRange = subresourceRange;

    vk::PipelineStageFlags sourceStage;
    vk::PipelineStageFlags destinationStage;

    // Transition from Undefined to TransferDstOptimal
    if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) {
        barrier.srcAccessMask = {};
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;

        sourceStage = vk::PipelineStageFlagBits::eTopOfPipe;
        destinationStage = vk::PipelineStageFlagBits::eTransfer;
    }
    // Transition from TransferDstOptimal to ShaderReadOnlyOptimal
    else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
        barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

        sourceStage = vk::PipelineStageFlagBits::eTransfer;
        destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
    }
    // Transition from ShaderReadOnlyOptimal to TransferDstOptimal 
    else if (oldLayout == vk::ImageLayout::eShaderReadOnlyOptimal && newLayout == vk::ImageLayout::eTransferDstOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead;
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;

        sourceStage = vk::PipelineStageFlagBits::eFragmentShader;
        destinationStage = vk::PipelineStageFlagBits::eTransfer;
    }
    // Transition from Undefined to DepthStencilAttachmentOptimal
    else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
        barrier.srcAccessMask = {};
        barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;

        sourceStage = vk::PipelineStageFlagBits::eTopOfPipe;
        destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
    }
    // Transition from DepthStencilAttachmentOptimal to ShaderReadOnlyOptimal
    else if (oldLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
        barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

        sourceStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
        destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
    }
    // Transition from DepthStencilAttachmentOptimal to TransferDstOptimal
    else if (oldLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal && newLayout == vk::ImageLayout::eTransferDstOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;

        sourceStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
        destinationStage = vk::PipelineStageFlagBits::eTransfer;
    }
    // Transition from TransferDstOptimal to DepthStencilAttachmentOptimal
    else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; 
        barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;

        sourceStage = vk::PipelineStageFlagBits::eTransfer;
        destinationStage =  vk::PipelineStageFlagBits::eEarlyFragmentTests;
    }
    // Transition from DepthStencilAttachmentOptimal to TransferSrcOptimal
    else if (oldLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal && newLayout == vk::ImageLayout::eTransferSrcOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead;

        sourceStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
        destinationStage = vk::PipelineStageFlagBits::eTransfer;
    }
    // Transition from TransferSrcOptimal to DepthStencilAttachmentOptimal
    else if (oldLayout == vk::ImageLayout::eTransferSrcOptimal && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
        barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;

        sourceStage = vk::PipelineStageFlagBits::eTransfer;
        destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
    }
    // Transition from ShaderReadOnlyOptimal back to DepthStencilAttachmentOptimal (if needed)
    else if (oldLayout == vk::ImageLayout::eShaderReadOnlyOptimal && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead;
        barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;

        sourceStage = vk::PipelineStageFlagBits::eFragmentShader;
        destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
    }
    // Transition from ShaderReadOnlyOptimal to TransferSrcOptimal
    else if (oldLayout == vk::ImageLayout::eShaderReadOnlyOptimal && newLayout == vk::ImageLayout::eTransferSrcOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead;
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead;

        sourceStage = vk::PipelineStageFlagBits::eFragmentShader;
        destinationStage = vk::PipelineStageFlagBits::eTransfer;
    }
    // Transition from TransferSrcOptimal to ShaderReadOnlyOptimal
    else if (oldLayout == vk::ImageLayout::eTransferSrcOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
        barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

        sourceStage = vk::PipelineStageFlagBits::eTransfer;
        destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
    }
    else {
        throw std::invalid_argument("Unsupported layout transition!");
    }

    commandBuffer.pipelineBarrier(
        sourceStage, destinationStage,
        {},
        0, nullptr,
        0, nullptr,
        1, &barrier
    );

    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);
}

void Cubemap::CreateImageView(vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels)
{
    vk::ImageViewCreateInfo viewInfo{};
    viewInfo.image = image;
    viewInfo.viewType = vk::ImageViewType::eCube;
    viewInfo.format = format;
    viewInfo.subresourceRange.aspectMask = aspectFlags;
    viewInfo.subresourceRange.baseMipLevel = 0;
    viewInfo.subresourceRange.levelCount = mipLevels;
    viewInfo.subresourceRange.baseArrayLayer = 0;
    viewInfo.subresourceRange.layerCount = 6;

    if (device.createImageView(&viewInfo, nullptr, &imageView) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create cubemap image view!");
    }

    this->aspect = aspectFlags;
    this->format = format;
}

void Cubemap::CreateSampler(uint32_t mipLevels)
{
    vk::SamplerCreateInfo samplerInfo{};
    samplerInfo.magFilter = vk::Filter::eLinear;
    samplerInfo.minFilter = vk::Filter::eLinear;
    samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
    samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
    samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
    samplerInfo.anisotropyEnable = VK_TRUE;
    samplerInfo.maxAnisotropy = 16;
    samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite;
    samplerInfo.unnormalizedCoordinates = VK_FALSE;
    samplerInfo.compareEnable = VK_FALSE;
    samplerInfo.compareOp = vk::CompareOp::eAlways;
    samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
    samplerInfo.mipLodBias = 0.0f;
    samplerInfo.minLod = 0.0f;
    samplerInfo.maxLod = static_cast<float>(mipLevels);

    if (device.createSampler(&samplerInfo, nullptr, &sampler) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create cubemap sampler!");
    }
}

void Cubemap::CopyBufferToImage(vk::Buffer buffer, uint32_t width, uint32_t height, int layerIndex)
{
    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    vk::BufferImageCopy region{};
    region.bufferOffset = 0;
    region.bufferRowLength = 0;
    region.bufferImageHeight = 0;
    region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
    region.imageSubresource.mipLevel = 0;
    region.imageSubresource.baseArrayLayer = layerIndex;
    region.imageSubresource.layerCount = 1;
    region.imageOffset = vk::Offset3D{ 0, 0, 0 };
    region.imageExtent = vk::Extent3D{ width, height, 1 };

    commandBuffer.copyBufferToImage(
        buffer,
        image,
        vk::ImageLayout::eTransferDstOptimal,
        1,
        &region
    );

    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);
}

vk::CommandBuffer Cubemap::BeginSingleTimeCommands(vk::CommandPool commandPool, vk::Device device)
{
    vk::CommandBufferAllocateInfo allocInfo{};
    allocInfo.level = vk::CommandBufferLevel::ePrimary;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    vk::CommandBuffer commandBuffer;
    device.allocateCommandBuffers(&allocInfo, &commandBuffer);

    vk::CommandBufferBeginInfo beginInfo{};
    beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;

    commandBuffer.begin(beginInfo);

    return commandBuffer;
}

void Cubemap::EndSingleTimeCommands(vk::CommandPool commandPool, vk::Queue graphicsQueue, vk::Device device, vk::CommandBuffer commandBuffer)
{
    commandBuffer.end();

    vk::SubmitInfo submitInfo{};
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    graphicsQueue.submit(submitInfo, nullptr);
    graphicsQueue.waitIdle();

    device.freeCommandBuffers(commandPool, 1, &commandBuffer);
}

void Cubemap::GenerateMipmaps(uint32_t width, uint32_t height, uint32_t mipLevels)
{
    vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(vk::Format::eR8G8B8A8Srgb);

    if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear)) {
        throw std::runtime_error("Texture image format does not support linear filtering!");
    }

    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    vk::ImageMemoryBarrier barrier{};
    barrier.image = image;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 6;
    barrier.subresourceRange.levelCount = 1;

    int32_t mipWidth = width;
    int32_t mipHeight = height;

    for (uint32_t i = 1; i < mipLevels; i++) {
        barrier.subresourceRange.baseMipLevel = i - 1;
        barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
        barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal;
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead;

        commandBuffer.pipelineBarrier(
            vk::PipelineStageFlagBits::eTransfer,
            vk::PipelineStageFlagBits::eTransfer,
            vk::DependencyFlags{},
            0, nullptr,
            0, nullptr,
            1, &barrier
        );

        vk::ImageBlit blit{};
        blit.srcOffsets[0] = vk::Offset3D{ 0, 0, 0 };
        blit.srcOffsets[1] = vk::Offset3D{ static_cast<int32_t>(mipWidth), static_cast<int32_t>(mipHeight), 1 };
        blit.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
        blit.srcSubresource.mipLevel = i - 1;
        blit.srcSubresource.baseArrayLayer = 0;
        blit.srcSubresource.layerCount = 6;
        blit.dstOffsets[0] = vk::Offset3D{ 0, 0, 0 };
        blit.dstOffsets[1] = vk::Offset3D{ static_cast<int32_t>(mipWidth / 2), static_cast<int32_t>(mipHeight / 2), 1 };
        blit.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
        blit.dstSubresource.mipLevel = i;
        blit.dstSubresource.baseArrayLayer = 0;
        blit.dstSubresource.layerCount = 6;

        commandBuffer.blitImage(
            image, vk::ImageLayout::eTransferSrcOptimal,
            image, vk::ImageLayout::eTransferDstOptimal,
            1, &blit, vk::Filter::eLinear
        );

        barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
        barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
        barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

        commandBuffer.pipelineBarrier(
            vk::PipelineStageFlagBits::eTransfer,
            vk::PipelineStageFlagBits::eFragmentShader,
            vk::DependencyFlags{},
            0, nullptr,
            0, nullptr,
            1, &barrier
        );

        mipWidth /= 2;
        mipHeight /= 2;
    }

    barrier.subresourceRange.baseMipLevel = mipLevels - 1;
    barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
    barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
    barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
    barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

    commandBuffer.pipelineBarrier(
        vk::PipelineStageFlagBits::eTransfer,
        vk::PipelineStageFlagBits::eFragmentShader,
        vk::DependencyFlags{},
        0, nullptr,
        0, nullptr,
        1, &barrier
    );

    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);
}

void Cubemap::CopyImageToLayer(vk::Image sourceImage, int layerIndex, vk::Extent3D extent) {
    // Transition cubemap layer to TRANSFER_DST_OPTIMAL
    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = 1;
    subresourceRange.baseArrayLayer = layerIndex;
    subresourceRange.layerCount = 1;

    TransitionImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferDstOptimal, subresourceRange);

    // Create command buffer for the copy operation
    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    // Setup copy region parameters
    vk::ImageCopy copyRegion{};
    copyRegion.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
    copyRegion.srcSubresource.mipLevel = 0;
    copyRegion.srcSubresource.baseArrayLayer = 0; // Source is a 2D image
    copyRegion.srcSubresource.layerCount = 1;
    copyRegion.srcOffset = vk::Offset3D{ 0, 0, 0 };

    copyRegion.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
    copyRegion.dstSubresource.mipLevel = 0;
    copyRegion.dstSubresource.baseArrayLayer = layerIndex;
    copyRegion.dstSubresource.layerCount = 1;
    copyRegion.dstOffset = vk::Offset3D{ 0, 0, 0 };
    copyRegion.extent = extent;

    // Perform the actual image copy
    commandBuffer.copyImage(
        sourceImage,
        vk::ImageLayout::eTransferSrcOptimal, // Source must be in this layout
        image,
        vk::ImageLayout::eTransferDstOptimal,
        1,
        &copyRegion
    );

    // Submit the copy command and wait for completion
    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);

    // Transition cubemap layer back to SHADER_READ_ONLY_OPTIMAL
    TransitionImageLayout(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresourceRange);
}


void Cubemap::CopyDepthToLayer(vk::Image sourceImage, int layerIndex, vk::Extent3D extent) {
    // Transition cubemap layer to TRANSFER_DST_OPTIMAL
    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eDepth;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = 1;
    subresourceRange.baseArrayLayer = layerIndex;
    subresourceRange.layerCount = 1;

    TransitionImageLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eTransferDstOptimal, subresourceRange);

    // Create command buffer for the copy operation
    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    // Setup copy region parameters
    vk::ImageCopy copyRegion{};
    copyRegion.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth;
    copyRegion.srcSubresource.mipLevel = 0;
    copyRegion.srcSubresource.baseArrayLayer = 0; // Source is a 2D image
    copyRegion.srcSubresource.layerCount = 1;
    copyRegion.srcOffset = vk::Offset3D{ 0, 0, 0 };

    copyRegion.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth;
    copyRegion.dstSubresource.mipLevel = 0;
    copyRegion.dstSubresource.baseArrayLayer = layerIndex;
    copyRegion.dstSubresource.layerCount = 1;
    copyRegion.dstOffset = vk::Offset3D{ 0, 0, 0 };
    copyRegion.extent = extent;

    // Perform the actual image copy
    commandBuffer.copyImage(
        sourceImage,
        vk::ImageLayout::eTransferSrcOptimal, // Source must be in this layout
        image,
        vk::ImageLayout::eTransferDstOptimal,
        1,
        &copyRegion
    );

    // Submit the copy command and wait for completion
    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);

    // Transition cubemap layer back to SHADER_READ_ONLY_OPTIMAL
    TransitionImageLayout(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, subresourceRange);
}


TextureArray.h:

#pragma once

#include <vulkan/vulkan.hpp>
#include "BufferUtils.h"
#include "Texture.h"
#include <string>

#define KHRONOS_STATIC
#include <ktx.h>
#include <ktxvulkan.h>

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

    void Initialize(uint32_t layerCount, uint32_t width, uint32_t height, uint32_t mipLevels, vk::Format format,
        vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue);
    void Cleanup();

    void CopyFromTexture(const Texture& texture, uint32_t layer, uint32_t mipLevels);
    vk::ImageView GetImageView() const;
    vk::Sampler GetSampler() const;
    void LoadKTXToLayer(const std::string& filename, uint32_t layer);

    enum class Channel {
        R = 0,
        G = 1,
        B = 2,
        A = 3
    };

    void CopyFromTexture(
        const Texture& texture,
        uint32_t layer,
        uint32_t mipLevels,
        Channel sourceChannel,
        Channel targetChannel
    );

private:
    vk::Device device;
    vk::Image image;
    vk::DeviceMemory imageMemory;
    vk::ImageView imageView;
    vk::Sampler sampler;
    vk::PhysicalDevice physicalDevice;
    vk::CommandPool commandPool;
    vk::Queue graphicsQueue;
    bool initialized = false;

    vk::Format format;       // Store format of the texture array
    uint32_t mipLevels;      // Store number of mipmap levels

    uint32_t width;
    uint32_t height;

    void CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, uint32_t arrayLayers,
        vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties);
    void TransitionImageLayout(vk::CommandBuffer commandBuffer, vk::Image image, vk::ImageLayout oldLayout,
        vk::ImageLayout newLayout, uint32_t mipLevel, uint32_t baseArrayLayer, uint32_t layerCount);
    void CreateImageView(vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels, uint32_t arrayLayers);
    void CreateSampler(uint32_t mipLevels);
    vk::CommandBuffer BeginSingleTimeCommands();
    void EndSingleTimeCommands(vk::CommandBuffer commandBuffer);

    // Helper function to create a staging buffer
    void CreateStagingBuffer(vk::DeviceSize size, vk::Buffer& buffer, vk::DeviceMemory& memory);
    void CleanupStagingBuffer(vk::Buffer& buffer, vk::DeviceMemory& memory);
};