This commit is contained in:
Mars 2024-11-15 21:25:10 -05:00
parent 2f779d28ac
commit 4c82bbe275
9 changed files with 694 additions and 158 deletions

View file

@ -0,0 +1,8 @@
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}

View file

@ -0,0 +1,11 @@
#version 450
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}

View file

@ -1,12 +1,47 @@
#version 450
layout(binding = 1) uniform sampler2D texSampler;
layout(binding = 3) uniform sampler2D texSampler;
layout(binding = 1) uniform LightInfo {
vec3 position;
vec3 color;
float ambient_strength;
float specular_strength;
} light;
layout(binding = 2) uniform CameraInfo {
vec3 position;
} camera;
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(location = 2) in vec3 fragNormal;
layout(location = 3) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(texSampler, fragTexCoord);
// Get base color from texture
vec4 texColor = texture(texSampler, fragTexCoord);
// Normalize vectors
vec3 normal = normalize(fragNormal);
vec3 lightDir = normalize(light.position - fragWorldPos);
vec3 viewDir = normalize(camera.position - fragWorldPos);
vec3 reflectDir = reflect(-lightDir, normal);
// Ambient
vec3 ambient = light.ambient_strength * light.color;
// Diffuse
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * light.color;
// Specular
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = light.specular_strength * spec * light.color;
// Combine lighting components
vec3 lighting = (ambient + diffuse + specular);
outColor = vec4(lighting * texColor.rgb, texColor.a);
}

View file

@ -1,20 +1,28 @@
#version 450
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
mat4 model;
mat4 view;
mat4 proj;
} ubo;
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 3) in vec3 inNormal;
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;
layout(location = 2) out vec3 fragNormal;
layout(location = 3) out vec3 fragWorldPos;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
vec4 worldPos = ubo.model * vec4(inPosition, 1.0);
gl_Position = ubo.proj * ubo.view * worldPos;
// Transform normal to world space
fragNormal = mat3(transpose(inverse(ubo.model))) * inNormal;
fragWorldPos = worldPos.xyz;
fragColor = inColor;
fragTexCoord = inTexCoord;
}

View file

@ -23,6 +23,7 @@
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
// Include custom utility headers
#include "util/crosshair.hpp" // Crosshair definitions
#include "util/shaders.hpp" // Compiled shader code
#include "util/types.hpp" // Custom type definitions
#include "util/unique_image.hpp" // Custom image handling utilities
@ -48,8 +49,10 @@ constexpr f64 CAMERA_SPEED = 1.0;
constexpr i32 MAX_FRAMES_IN_FLIGHT = 2;
// Shader file paths
constexpr const char* VERTEX_SHADER_PATH = "shaders/vertex.glsl";
constexpr const char* FRAGMENT_SHADER_PATH = "shaders/fragment.glsl";
constexpr const char* VERTEX_SHADER_PATH = "shaders/vertex.glsl";
constexpr const char* FRAGMENT_SHADER_PATH = "shaders/fragment.glsl";
constexpr const char* CROSSHAIR_VERTEX_SHADER_PATH = "shaders/crosshair_vertex.glsl";
constexpr const char* CROSSHAIR_FRAGMENT_SHADER_PATH = "shaders/crosshair_fragment.glsl";
// Validation layers for debug builds
#ifndef NDEBUG
@ -127,6 +130,13 @@ class VulkanApp {
vk::UniquePipeline mGraphicsPipeline; ///< Graphics pipeline
vk::UniquePipeline mOldPipeline; ///< Previous graphics pipeline for safe deletion
vk::UniquePipelineLayout mCrosshairPipelineLayout;
vk::UniquePipeline mCrosshairPipeline;
vk::UniqueBuffer mCrosshairVertexBuffer;
vk::UniqueDeviceMemory mCrosshairVertexBufferMemory;
vk::UniqueBuffer mCrosshairIndexBuffer;
vk::UniqueDeviceMemory mCrosshairIndexBufferMemory;
vk::UniqueCommandPool mCommandPool; ///< Command pool for allocating command buffers
vk::UniqueImage mColorImage; ///< Color image
@ -154,11 +164,27 @@ class VulkanApp {
std::vector<vk::UniqueDeviceMemory> mUniformBuffersMemory; ///< Memory for uniform buffers
std::vector<void*> mUniformBuffersMapped; ///< Mapped uniform buffers
std::vector<vk::UniqueBuffer> mLightUniformBuffers; ///< Uniform buffers for light parameters
std::vector<vk::UniqueDeviceMemory> mLightUniformBuffersMemory; ///< Memory for light uniform buffers
std::vector<void*> mLightUniformBuffersMapped; ///< Mapped light uniform buffers
std::vector<vk::UniqueBuffer> mCameraUniformBuffers; ///< Uniform buffers for camera parameters
std::vector<vk::UniqueDeviceMemory> mCameraUniformBuffersMemory; ///< Memory for camera uniform buffers
std::vector<void*> mCameraUniformBuffersMapped; ///< Mapped camera uniform buffers
vk::UniqueDescriptorPool mDescriptorPool; ///< Descriptor pool for the application
vk::UniqueDescriptorPool mImGuiDescriptorPool; ///< Separate descriptor pool for ImGui
std::vector<vk::DescriptorSet> mDescriptorSets; ///< Descriptor sets for binding resources
std::vector<vk::UniqueCommandBuffer> mCommandBuffers; ///< Command buffers for drawing
std::vector<vk::UniqueCommandBuffer> mCommandBuffers; ///< Command buffers for drawing commands
// Light settings
struct {
glm::vec3 position = glm::vec3(2.0F, 2.0F, 2.0F);
glm::vec3 color = glm::vec3(1.0F, 1.0F, 1.0F);
float ambient_strength = 0.1F;
float specular_strength = 0.5F;
} mLightSettings;
std::vector<vk::UniqueSemaphore>
mImageAvailableSemaphores; ///< Signals that an image is available for rendering
@ -222,6 +248,17 @@ class VulkanApp {
alignas(16) glm::mat4 proj; ///< Projection matrix
};
struct LightInfo {
alignas(16) glm::vec3 position; ///< Light position
alignas(16) glm::vec3 color; ///< Light color
alignas(4) float ambient_strength; ///< Ambient strength
alignas(4) float specular_strength; ///< Specular strength
};
struct CameraInfo {
alignas(16) glm::vec3 position; ///< Camera position
};
struct Camera {
glm::dvec3 position;
glm::dvec3 front;
@ -231,19 +268,23 @@ class VulkanApp {
f64 pitch;
Camera()
: position(2.0, 2.0, 2.0),
front(glm::normalize(glm::dvec3(-2.0, -2.0, -2.0))),
: position(2.0, 2.0, 0.5),
front(glm::normalize(glm::dvec3(0.0, 1.0, 0.0))),
up(0.0, 0.0, 1.0),
right(glm::normalize(glm::cross(front, up))),
yaw(-135.0) // -135 degrees to match the initial front vector
,
pitch(-35.26) // -35.26 degrees to match the initial front vector
{
right(glm::normalize(glm::cross(front, glm::dvec3(0.0, 0.0, 1.0)))),
yaw(180.0),
pitch(0.0) {
updateCameraVectors();
}
[[nodiscard]] fn getPosition() const -> glm::dvec3 { return position; }
[[nodiscard]] fn getFront() const -> glm::dvec3 { return front; }
[[nodiscard]] fn getYaw() const -> f64 { return yaw; }
[[nodiscard]] fn getPitch() const -> f64 { return pitch; }
[[nodiscard]] fn getViewMatrix() const -> glm::mat4 {
return glm::lookAt(position, position + front, up);
}
@ -289,6 +330,12 @@ class VulkanApp {
yaw += xoffset * sensitivity;
pitch += yoffset * sensitivity;
// Clamp yaw to [-180, 180] range
if (yaw > 180.0)
yaw -= 360.0;
if (yaw < -180.0)
yaw += 360.0;
// Constrain pitch to avoid camera flipping
if (pitch > 89.0)
pitch = 89.0;
@ -348,9 +395,20 @@ class VulkanApp {
// Set window creation hints
vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone }; // No OpenGL context
// Get the primary monitor and its resolution
vkfw::Monitor primaryMonitor = vkfw::getPrimaryMonitor();
const GLFWvidmode* videoMode = primaryMonitor.getVideoMode();
// Calculate window position to center it
i32 xpos = (videoMode->width - WIDTH) / 2;
i32 ypos = (videoMode->height - HEIGHT) / 2;
// Create the window
mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints);
// Set window position
mWindow->setPos(xpos, ypos);
// Set the user pointer to this instance, allowing us to access it in callbacks
mWindow->setUserPointer(this);
@ -359,22 +417,22 @@ class VulkanApp {
// Set up mouse callback
mWindow->callbacks()->on_cursor_move =
[this](const vkfw::Window& /*window*/, f64 xpos, f64 ypos) -> void {
[this](const vkfw::Window& /*window*/, f64 mouseX, f64 mouseY) -> void {
if (!mCursorCaptured)
return; // Skip camera movement when cursor is not captured
if (mFirstMouse) {
mLastX = xpos;
mLastY = ypos;
mLastX = mouseX;
mLastY = mouseY;
mFirstMouse = false;
return;
}
f64 xoffset = xpos - mLastX;
f64 yoffset = mLastY - ypos; // Reversed since y-coordinates range from bottom to top
f64 xoffset = mouseX - mLastX;
f64 yoffset = mLastY - mouseY; // Reversed since y-coordinates range from bottom to top
mLastX = xpos;
mLastY = ypos;
mLastX = mouseX;
mLastY = mouseY;
mCamera.rotate(-xoffset, yoffset); // Invert xoffset for correct horizontal movement
};
@ -391,6 +449,7 @@ class VulkanApp {
mCursorCaptured = false;
window.set<vkfw::InputMode::eCursor>(vkfw::CursorMode::eNormal);
}
if (key == vkfw::Key::eR && action == vkfw::KeyAction::ePress) {
try {
mDevice->waitIdle();
@ -444,32 +503,36 @@ class VulkanApp {
* others that must be created first.
*/
fn initVulkan() -> void {
createInstance(); // Create the Vulkan instance
setupDebugMessenger(); // Set up debug messaging (validation layers)
createSurface(); // Create the window surface
pickPhysicalDevice(); // Select a suitable GPU
createLogicalDevice(); // Create a logical device from the chosen GPU
createSwapChain(); // Create the swap chain for presenting images
createImageViews(); // Create image views for the swap chain images
createRenderPass(); // Set up the render pass
createDescriptorSetLayout(); // Create the descriptor set layout
createGraphicsPipeline(); // Create the graphics pipeline
createCommandPool(); // Create a command pool for allocating command buffers
createColorResources(); // Create resources for multisampling
createDepthResources(); // Create resources for depth testing
createFramebuffers(); // Create framebuffers for rendering
createTextureImage(); // Load and create the texture image
createTextureImageView(); // Create an image view for the texture
createTextureSampler(); // Create a sampler for the texture
loadModel(); // Load the 3D model
createVertexBuffer(); // Create a buffer for vertex data
createIndexBuffer(); // Create a buffer for index data
createUniformBuffers(); // Create uniform buffers for shader parameters
createDescriptorPool(); // Create a descriptor pool
createDescriptorSets(); // Allocate and update descriptor sets
createCommandBuffers(); // Create command buffers for rendering commands
createSyncObjects(); // Create synchronization objects (semaphores and fences)
initImGui(); // Initialize Dear ImGui for GUI rendering
createInstance(); // Create the Vulkan instance
setupDebugMessenger(); // Set up debug messaging (validation layers)
createSurface(); // Create the window surface
pickPhysicalDevice(); // Select a suitable GPU
createLogicalDevice(); // Create a logical device from the chosen GPU
createSwapChain(); // Create the swap chain for presenting images
createImageViews(); // Create image views for the swap chain images
createRenderPass(); // Set up the render pass
createDescriptorSetLayout(); // Create the descriptor set layout
createGraphicsPipeline(); // Create the graphics pipeline
createCrosshairPipeline(); // Create the crosshair pipeline
createCommandPool(); // Create a command pool for allocating command buffers
createColorResources(); // Create resources for multisampling
createDepthResources(); // Create resources for depth testing
createFramebuffers(); // Create framebuffers for rendering
createTextureImage(); // Load and create the texture image
createTextureImageView(); // Create an image view for the texture
createTextureSampler(); // Create a sampler for the texture
loadModel(); // Load the 3D model
createVertexBuffer(); // Create a buffer for vertex data
createIndexBuffer(); // Create a buffer for index data
createUniformBuffers(); // Create uniform buffers for shader parameters
createLightUniformBuffers(); // Create uniform buffers for light parameters
createCameraUniformBuffers(); // Create uniform buffers for camera parameters
createDescriptorPool(); // Create a descriptor pool
createDescriptorSets(); // Allocate and update descriptor sets
createCommandBuffers(); // Create command buffers for rendering commands
createCrosshairBuffers(); // Create crosshair buffers
createSyncObjects(); // Create synchronization objects (semaphores and fences)
initImGui(); // Initialize Dear ImGui for GUI rendering
}
/**
@ -564,6 +627,107 @@ class VulkanApp {
++frameCounter;
vkfw::pollEvents();
// Start the ImGui frame
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Create ImGui window with controls
ImGui::Begin("Controls");
// Camera Position
auto pos = mCamera.getPosition();
ImGui::Text("Camera Position: (%.2f, %.2f, %.2f)", pos.x, pos.y, pos.z);
// Camera Settings
if (ImGui::CollapsingHeader("Camera Settings", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::SliderFloat("Camera Speed", &mCameraSpeed, 0.1F, 10.0F, "%.1f");
ImGui::SliderFloat("Field of View", &mFieldOfView, 45.0F, 120.0F, "%.1f");
if (ImGui::Button("Reset Camera")) {
mCamera = Camera(); // Reset to default position
}
}
// Rendering Settings
if (ImGui::CollapsingHeader("Rendering Settings", ImGuiTreeNodeFlags_DefaultOpen)) {
// Wireframe Mode
if (ImGui::Checkbox("Wireframe Mode", &mWireframeMode)) {
recreateSwapChain();
}
// Line Width (only in wireframe mode)
static float LineWidth = 1.0F;
if (mWireframeMode) {
if (ImGui::SliderFloat("Line Width", &LineWidth, 1.0F, 5.0F, "%.1f")) {
// TODO: Update line width in pipeline
}
}
}
// Controls Help
if (ImGui::CollapsingHeader("Controls", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::BulletText("Use mouse to look around");
ImGui::BulletText("WASD to move horizontally");
ImGui::BulletText("Space/Shift to move up/down");
ImGui::BulletText("ESC to toggle mouse capture");
ImGui::BulletText("Tab to toggle this menu");
}
// Performance Metrics
if (ImGui::CollapsingHeader("Performance", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text(
"Application average %.3f ms/frame (%.1f FPS)",
static_cast<f64>(1000.0F / ImGui::GetIO().Framerate),
static_cast<f64>(ImGui::GetIO().Framerate)
);
// Memory Usage
ImGui::Separator();
ImGui::Text("Memory Usage:");
ImGui::Text(
"Vertex Buffer: %.2f MB",
(static_cast<double>(mVertices.size()) * static_cast<double>(sizeof(Vertex))) / (1024.0 * 1024.0)
);
ImGui::Text(
"Index Buffer: %.2f MB",
(static_cast<double>(mIndices.size()) * static_cast<double>(sizeof(uint32_t))) / (1024.0 * 1024.0)
);
// Camera Information
ImGui::Separator();
ImGui::Text("Camera Information:");
ImGui::Text(
"Position: (%.2f, %.2f, %.2f)",
mCamera.getPosition().x,
mCamera.getPosition().y,
mCamera.getPosition().z
);
ImGui::Text(
"Front Vector: (%.2f, %.2f, %.2f)", mCamera.getFront().x, mCamera.getFront().y, mCamera.getFront().z
);
ImGui::Text("Yaw: %.2f°, Pitch: %.2f°", mCamera.getYaw(), mCamera.getPitch());
// Light Controls
ImGui::Separator();
ImGui::Text("Light Controls:");
// Light Position
ImGui::DragFloat3("Light Position", &mLightSettings.position.x, 0.1F, -10.0F, 10.0F);
// Light Color
ImGui::ColorEdit3("Light Color", &mLightSettings.color.x);
// Light Strengths
ImGui::SliderFloat("Ambient Strength", &mLightSettings.ambient_strength, 0.0F, 1.0F);
ImGui::SliderFloat("Specular Strength", &mLightSettings.specular_strength, 0.0F, 1.0F);
}
ImGui::End();
// Render ImGui
ImGui::Render();
drawFrame();
}
@ -603,13 +767,17 @@ class VulkanApp {
createImageViews();
createRenderPass();
createGraphicsPipeline();
createCrosshairPipeline();
createColorResources();
createDepthResources();
createFramebuffers();
createUniformBuffers();
createLightUniformBuffers();
createCameraUniformBuffers();
createDescriptorPool();
createDescriptorSets();
createCommandBuffers();
createCrosshairBuffers();
mImagesInFlight.resize(mSwapChainImages.size(), nullptr);
}
@ -981,15 +1149,36 @@ class VulkanApp {
.pImmutableSamplers = nullptr,
};
vk::DescriptorSetLayoutBinding samplerLayoutBinding {
vk::DescriptorSetLayoutBinding lightLayoutBinding {
.binding = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eFragment,
.pImmutableSamplers = nullptr,
};
vk::DescriptorSetLayoutBinding cameraLayoutBinding {
.binding = 2,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eFragment,
.pImmutableSamplers = nullptr,
};
vk::DescriptorSetLayoutBinding samplerLayoutBinding {
.binding = 3,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eFragment,
.pImmutableSamplers = nullptr,
};
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = { uboLayoutBinding, samplerLayoutBinding };
std::array<vk::DescriptorSetLayoutBinding, 4> bindings = {
uboLayoutBinding,
lightLayoutBinding,
cameraLayoutBinding,
samplerLayoutBinding,
};
vk::DescriptorSetLayoutCreateInfo layoutInfo {
.bindingCount = static_cast<u32>(bindings.size()),
@ -1033,7 +1222,7 @@ class VulkanApp {
};
vk::VertexInputBindingDescription bindingDescription = Vertex::getBindingDescription();
std::array<vk::VertexInputAttributeDescription, 3> attributeDescriptions =
std::array<vk::VertexInputAttributeDescription, 4> attributeDescriptions =
Vertex::getAttributeDescriptions();
vk::PipelineVertexInputStateCreateInfo vertexInputInfo {
@ -1132,6 +1321,152 @@ class VulkanApp {
mGraphicsPipeline = std::move(graphicsPipelineValue);
}
fn createCrosshairPipeline() -> void {
// Create pipeline layout (no descriptor sets or push constants needed)
vk::PipelineLayoutCreateInfo pipelineLayoutInfo {};
mCrosshairPipelineLayout = mDevice->createPipelineLayoutUnique(pipelineLayoutInfo);
// Load shaders
auto vertShaderCode =
ShaderCompiler::getCompiledShader(CROSSHAIR_VERTEX_SHADER_PATH, shaderc_vertex_shader);
auto fragShaderCode =
ShaderCompiler::getCompiledShader(CROSSHAIR_FRAGMENT_SHADER_PATH, shaderc_fragment_shader);
vk::UniqueShaderModule vertShaderModule = createShaderModule(vertShaderCode);
vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode);
vk::PipelineShaderStageCreateInfo vertShaderStageInfo { .stage = vk::ShaderStageFlagBits::eVertex,
.module = vertShaderModule.get(),
.pName = "main" };
vk::PipelineShaderStageCreateInfo fragShaderStageInfo { .stage = vk::ShaderStageFlagBits::eFragment,
.module = fragShaderModule.get(),
.pName = "main" };
std::array shaderStages = { vertShaderStageInfo, fragShaderStageInfo };
// Vertex input
auto bindingDescription = CrosshairVertex::getBindingDescription();
auto attributeDescriptions = CrosshairVertex::getAttributeDescriptions();
vk::PipelineVertexInputStateCreateInfo vertexInputInfo {
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &bindingDescription,
.vertexAttributeDescriptionCount = static_cast<u32>(attributeDescriptions.size()),
.pVertexAttributeDescriptions = attributeDescriptions.data()
};
// Input assembly
vk::PipelineInputAssemblyStateCreateInfo inputAssembly { .topology = vk::PrimitiveTopology::eLineList,
.primitiveRestartEnable = false };
// Viewport and scissor
vk::PipelineViewportStateCreateInfo viewportState { .viewportCount = 1, .scissorCount = 1 };
// Rasterization
vk::PipelineRasterizationStateCreateInfo rasterizer { .depthClampEnable = false,
.rasterizerDiscardEnable = false,
.polygonMode = vk::PolygonMode::eFill,
.cullMode = vk::CullModeFlagBits::eNone,
.frontFace = vk::FrontFace::eCounterClockwise,
.depthBiasEnable = false,
.lineWidth = 1.0F };
// Multisampling
vk::PipelineMultisampleStateCreateInfo multisampling { .rasterizationSamples = mMsaaSamples,
.sampleShadingEnable = false };
// Color blending
vk::PipelineColorBlendAttachmentState colorBlendAttachment {
.blendEnable = false,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA
};
vk::PipelineColorBlendStateCreateInfo colorBlending { .logicOpEnable = false,
.attachmentCount = 1,
.pAttachments = &colorBlendAttachment };
// Dynamic state
std::array dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor };
vk::PipelineDynamicStateCreateInfo dynamicState { .dynamicStateCount =
static_cast<u32>(dynamicStates.size()),
.pDynamicStates = dynamicStates.data() };
// Depth and stencil
vk::PipelineDepthStencilStateCreateInfo depthStencil { .depthTestEnable = false,
.depthWriteEnable = false,
.depthCompareOp = vk::CompareOp::eLess,
.depthBoundsTestEnable = false,
.stencilTestEnable = false };
// Create the pipeline
vk::GraphicsPipelineCreateInfo pipelineInfo { .stageCount = static_cast<u32>(shaderStages.size()),
.pStages = shaderStages.data(),
.pVertexInputState = &vertexInputInfo,
.pInputAssemblyState = &inputAssembly,
.pViewportState = &viewportState,
.pRasterizationState = &rasterizer,
.pMultisampleState = &multisampling,
.pDepthStencilState = &depthStencil,
.pColorBlendState = &colorBlending,
.pDynamicState = &dynamicState,
.layout = mCrosshairPipelineLayout.get(),
.renderPass = mRenderPass.get(),
.subpass = 0 };
mCrosshairPipeline = mDevice->createGraphicsPipelineUnique(nullptr, pipelineInfo).value;
}
fn createCrosshairBuffers() -> void {
// Create vertex buffer
vk::DeviceSize bufferSize = sizeof(crosshairVertices[0]) * crosshairVertices.size();
auto stagingBuffer = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
);
void* data = mDevice->mapMemory(stagingBuffer.second.get(), 0, bufferSize);
memcpy(data, crosshairVertices.data(), bufferSize);
mDevice->unmapMemory(stagingBuffer.second.get());
auto [vertexBuffer, vertexBufferMemory] = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eDeviceLocal
);
copyBuffer(stagingBuffer.first.get(), vertexBuffer.get(), bufferSize);
mCrosshairVertexBuffer = std::move(vertexBuffer);
mCrosshairVertexBufferMemory = std::move(vertexBufferMemory);
// Create index buffer
bufferSize = sizeof(crosshairIndices[0]) * crosshairIndices.size();
auto stagingBufferIndices = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
);
data = mDevice->mapMemory(stagingBufferIndices.second.get(), 0, bufferSize);
memcpy(data, crosshairIndices.data(), bufferSize);
mDevice->unmapMemory(stagingBufferIndices.second.get());
auto [indexBuffer, indexBufferMemory] = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer,
vk::MemoryPropertyFlagBits::eDeviceLocal
);
copyBuffer(stagingBufferIndices.first.get(), indexBuffer.get(), bufferSize);
mCrosshairIndexBuffer = std::move(indexBuffer);
mCrosshairIndexBufferMemory = std::move(indexBufferMemory);
}
/**
* @brief Creates framebuffers for the swap chain images.
*
@ -1710,10 +2045,15 @@ class VulkanApp {
attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 2)],
},
.color = { 1.0F, 1.0F, 1.0F },
.tex_coord = {
.texCoord = {
attrib.texcoords[static_cast<u32>((2 * index.texcoord_index) + 0)],
1.0F - attrib.texcoords[static_cast<u32>((2 * index.texcoord_index) + 1)],
},
.normal = {
attrib.normals[static_cast<u32>((3 * index.normal_index) + 0)],
attrib.normals[static_cast<u32>((3 * index.normal_index) + 1)],
attrib.normals[static_cast<u32>((3 * index.normal_index) + 2)],
},
};
if (!uniqueVertices.contains(vertex)) {
@ -1803,14 +2143,52 @@ class VulkanApp {
mUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
mUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);
for (usize i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
std::tie(mUniformBuffers[i], mUniformBuffersMemory[i]) = createBuffer(
for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
std::tie(mUniformBuffers[idx], mUniformBuffersMemory[idx]) = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eUniformBuffer,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
);
mUniformBuffersMapped[i] = mDevice->mapMemory(mUniformBuffersMemory[i].get(), 0, bufferSize);
mUniformBuffersMapped[idx] = mDevice->mapMemory(mUniformBuffersMemory[idx].get(), 0, bufferSize);
}
}
fn createLightUniformBuffers() -> void {
vk::DeviceSize bufferSize = sizeof(LightInfo);
mLightUniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
mLightUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
mLightUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);
for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
std::tie(mLightUniformBuffers[idx], mLightUniformBuffersMemory[idx]) = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eUniformBuffer,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
);
mLightUniformBuffersMapped[idx] =
mDevice->mapMemory(mLightUniformBuffersMemory[idx].get(), 0, bufferSize);
}
}
fn createCameraUniformBuffers() -> void {
vk::DeviceSize bufferSize = sizeof(CameraInfo);
mCameraUniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
mCameraUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
mCameraUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);
for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
std::tie(mCameraUniformBuffers[idx], mCameraUniformBuffersMemory[idx]) = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eUniformBuffer,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
);
mCameraUniformBuffersMapped[idx] =
mDevice->mapMemory(mCameraUniformBuffersMemory[idx].get(), 0, bufferSize);
}
}
@ -1821,7 +2199,15 @@ class VulkanApp {
* The pool is sized to accommodate the number of frames in flight.
*/
fn createDescriptorPool() -> void {
std::array<vk::DescriptorPoolSize, 2> poolSizes = {
std::array<vk::DescriptorPoolSize, 4> poolSizes = {
vk::DescriptorPoolSize {
.type = vk::DescriptorType::eUniformBuffer,
.descriptorCount = MAX_FRAMES_IN_FLIGHT,
},
vk::DescriptorPoolSize {
.type = vk::DescriptorType::eUniformBuffer,
.descriptorCount = MAX_FRAMES_IN_FLIGHT,
},
vk::DescriptorPoolSize {
.type = vk::DescriptorType::eUniformBuffer,
.descriptorCount = MAX_FRAMES_IN_FLIGHT,
@ -1858,33 +2244,61 @@ class VulkanApp {
mDescriptorSets = mDevice->allocateDescriptorSets(allocInfo);
for (usize i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vk::DescriptorBufferInfo bufferInfo {
.buffer = mUniformBuffers[i].get(),
for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
vk::DescriptorBufferInfo uboBufferInfo {
.buffer = mUniformBuffers[idx].get(),
.offset = 0,
.range = sizeof(UniformBufferObject),
};
vk::DescriptorBufferInfo lightBufferInfo {
.buffer = mLightUniformBuffers[idx].get(),
.offset = 0,
.range = sizeof(LightInfo),
};
vk::DescriptorBufferInfo cameraBufferInfo {
.buffer = mCameraUniformBuffers[idx].get(),
.offset = 0,
.range = sizeof(CameraInfo),
};
vk::DescriptorImageInfo imageInfo {
.sampler = mTextureSampler.get(),
.imageView = mTextureImageView.get(),
.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal,
};
std::array<vk::WriteDescriptorSet, 2> descriptorWrites = {
std::array<vk::WriteDescriptorSet, 4> descriptorWrites = {
vk::WriteDescriptorSet {
.dstSet = mDescriptorSets[i],
.dstSet = mDescriptorSets[idx],
.dstBinding = 0,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &bufferInfo,
.pBufferInfo = &uboBufferInfo,
},
vk::WriteDescriptorSet {
.dstSet = mDescriptorSets[i],
.dstSet = mDescriptorSets[idx],
.dstBinding = 1,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &lightBufferInfo,
},
vk::WriteDescriptorSet {
.dstSet = mDescriptorSets[idx],
.dstBinding = 2,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &cameraBufferInfo,
},
vk::WriteDescriptorSet {
.dstSet = mDescriptorSets[idx],
.dstBinding = 3,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &imageInfo,
},
@ -2170,6 +2584,23 @@ class VulkanApp {
// Copy the uniform buffer object to the mapped memory
memcpy(mUniformBuffersMapped[mCurrentFrame], &ubo, sizeof(ubo));
LightInfo lightInfo {
.position = mLightSettings.position,
.color = mLightSettings.color,
.ambient_strength = mLightSettings.ambient_strength,
.specular_strength = mLightSettings.specular_strength,
};
// Copy the light uniform buffer object to the mapped memory
memcpy(mLightUniformBuffersMapped[mCurrentFrame], &lightInfo, sizeof(lightInfo));
CameraInfo cameraInfo {
.position = glm::vec3(mCamera.getPosition()), // Use actual camera position
};
// Copy the camera uniform buffer object to the mapped memory
memcpy(mCameraUniformBuffersMapped[mCurrentFrame], &cameraInfo, sizeof(cameraInfo));
// Example: Add extra clones with different translations
std::vector<glm::mat4> modelMatrices = { glm::translate(glm::mat4(1.0F), glm::vec3(2.0F, 0.0F, 0.0F)),
glm::translate(glm::mat4(1.0F), glm::vec3(-2.0F, 0.0F, 0.0F)),
@ -2195,53 +2626,19 @@ class VulkanApp {
commandBuffer.drawIndexed(static_cast<u32>(mIndices.size()), 1, 0, 0, 0);
}
// Only render ImGui when cursor is not captured
if (!mCursorCaptured) {
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Draw the crosshair
commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mCrosshairPipeline.get());
// Create ImGui window with useful controls
ImGui::Begin("Settings", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
std::array<vk::Buffer, 1> vertexBuffers = { mCrosshairVertexBuffer.get() };
std::array<vk::DeviceSize, 1> offsets = { 0 };
commandBuffer.bindVertexBuffers(0, 1, vertexBuffers.data(), offsets.data());
commandBuffer.bindIndexBuffer(mCrosshairIndexBuffer.get(), 0, vk::IndexType::eUint16);
// Set initial window size (this will be the minimum size due to AlwaysAutoResize)
ImGui::SetWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
// Draw the crosshair
commandBuffer.drawIndexed(static_cast<u32>(crosshairIndices.size()), 1, 0, 0, 0);
// Camera settings
if (ImGui::CollapsingHeader("Camera")) {
ImGui::SliderFloat("Camera Speed", &mCameraSpeed, 0.1F, 5.0F);
glm::dvec3 pos = mCamera.getPosition();
ImGui::Text("Position: (%.2f, %.2f, %.2f)", pos.x, pos.y, pos.z);
}
// Rendering settings
if (ImGui::CollapsingHeader("Rendering")) {
if (ImGui::Checkbox("Wireframe Mode", &mWireframeMode)) {
// Wait for all operations to complete
mDevice->waitIdle();
// Store the old pipeline for deletion after the next frame
if (mGraphicsPipeline)
mOldPipeline = std::move(mGraphicsPipeline);
// Recreate the pipeline
createGraphicsPipeline();
}
ImGui::SliderFloat("Field of View", &mFieldOfView, 45.0F, 120.0F);
}
// Performance metrics
if (ImGui::CollapsingHeader("Performance")) {
const f32 framerate = ImGui::GetIO().Framerate;
ImGui::Text("%.1f FPS", static_cast<f64>(framerate));
ImGui::Text("%.3f ms/frame", static_cast<f64>(1000.0F / framerate));
}
ImGui::End();
ImGui::Render();
// Render ImGui if we have a draw data (ImGui::Render was called)
if (ImGui::GetDrawData()) {
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer);
}

49
src/util/crosshair.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
#include <array>
#define VULKAN_HPP_NO_CONSTRUCTORS
#include <vulkan/vulkan.hpp>
#include "types.hpp"
struct CrosshairVertex {
vec2 pos;
vec3 color;
static fn getBindingDescription() -> vk::VertexInputBindingDescription {
return { .binding = 0, .stride = sizeof(CrosshairVertex), .inputRate = vk::VertexInputRate::eVertex };
}
static fn getAttributeDescriptions() -> std::array<vk::VertexInputAttributeDescription, 2> {
return {
vk::VertexInputAttributeDescription { .location = 0,
.binding = 0,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(CrosshairVertex, pos) },
vk::VertexInputAttributeDescription { .location = 1,
.binding = 0,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(CrosshairVertex, color) }
};
}
};
// Crosshair vertices (in normalized device coordinates)
constexpr f32 CROSSHAIR_SIZE = 0.02F;
constexpr std::array<CrosshairVertex, 4> crosshairVertices = {
// Horizontal line
CrosshairVertex { { -CROSSHAIR_SIZE * (9.0F / 16.0F), 0.0F }, { 1.0F, 1.0F, 1.0F } }, // Left
CrosshairVertex { { CROSSHAIR_SIZE * (9.0F / 16.0F), 0.0F }, { 1.0F, 1.0F, 1.0F } }, // Right
// Vertical line
CrosshairVertex { { 0.0F, -CROSSHAIR_SIZE }, { 1.0F, 1.0F, 1.0F } }, // Bottom
CrosshairVertex { { 0.0F, CROSSHAIR_SIZE }, { 1.0F, 1.0F, 1.0F } } // Top
};
// Indices for drawing the crosshair lines
constexpr std::array<u16, 4> crosshairIndices = {
0,
1, // Horizontal line
2,
3 // Vertical line
};

View file

@ -1,7 +1,7 @@
/**
* @file types.hpp
* @brief Core type definitions and aliases for the project.
*
*
* This file provides a centralized location for type definitions used throughout
* the project. It includes fixed-width integer types, floating-point types, and
* commonly used GLM vector types. The type aliases are designed to improve code
@ -12,8 +12,8 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <glm/glm.hpp>
#include <string>
#define fn auto

View file

@ -1,13 +1,14 @@
/**
* @file unique_image.hpp
* @brief Provides RAII wrapper for image loading and management.
*
*
* This file implements a RAII-compliant image handling class that uses stb_image
* for loading various image formats. It ensures proper resource management and
* provides a safe interface for image data access.
*/
#include <filesystem>
#include <fmt/format.h>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
@ -17,7 +18,7 @@
namespace stb {
/**
* @brief RAII wrapper for image data loaded via stb_image.
*
*
* This class provides safe resource management for loaded images, ensuring proper
* cleanup of image data. It supports move semantics but prevents copying to maintain
* single ownership of image resources.
@ -26,41 +27,41 @@ namespace stb {
public:
/**
* @brief Constructs a UniqueImage by loading from file.
*
*
* @param path Filesystem path to the image file.
* @throws std::runtime_error If image loading fails.
*
*
* Automatically loads the image data from the specified file using stb_image.
* The image data is stored in RGBA format with 8 bits per channel.
*/
UniqueImage(const std::filesystem::path& path) { load(path.string().c_str()); }
// Prevent copying to maintain single ownership
UniqueImage(const UniqueImage&) = delete;
fn operator=(const UniqueImage&) -> UniqueImage& = delete;
UniqueImage(const UniqueImage&) = delete;
fn operator=(const UniqueImage&)->UniqueImage& = delete;
/**
* @brief Move constructor for transferring image ownership.
*
*
* @param other Source UniqueImage to move from.
*
*
* Transfers ownership of image data from another UniqueImage instance,
* leaving the source in a valid but empty state.
*/
UniqueImage(UniqueImage&& other) noexcept
: mData(other.mData),
mWidth(static_cast<i32>(other.mWidth)),
mHeight(static_cast<i32>(other.mHeight)),
: mData(other.mData),
mWidth(static_cast<i32>(other.mWidth)),
mHeight(static_cast<i32>(other.mHeight)),
mChannels(static_cast<i32>(other.mChannels)) {
other.mData = nullptr;
}
/**
* @brief Move assignment operator for transferring image ownership.
*
*
* @param other Source UniqueImage to move from.
* @return Reference to this object.
*
*
* Safely transfers ownership of image data, ensuring proper cleanup of
* existing resources before the transfer.
*/
@ -80,7 +81,7 @@ namespace stb {
/**
* @brief Destructor that ensures proper cleanup of image resources.
*
*
* Automatically frees the image data using stb_image_free when the
* object goes out of scope.
*/
@ -92,7 +93,7 @@ namespace stb {
/**
* @brief Get raw pointer to image data.
* @return Pointer to the raw image data in memory.
*
*
* The data is stored in row-major order with either RGB or RGBA format,
* depending on the source image.
*/
@ -119,10 +120,10 @@ namespace stb {
private:
/**
* @brief Internal helper function to load image data.
*
*
* @param path Path to the image file.
* @throws std::runtime_error If image loading fails.
*
*
* Uses stb_image to load the image data, automatically detecting the
* format and number of channels from the file.
*/

View file

@ -1,9 +1,9 @@
/**
* @file vertex.hpp
* @brief Defines the vertex structure and its associated utilities for 3D rendering.
*
*
* This file contains the Vertex structure used for 3D model representation in the Vulkan
* graphics pipeline. It includes position, color, and texture coordinate data, along with
* graphics pipeline. It includes position, color, texture coordinate, and normal data, along with
* Vulkan-specific descriptors for vertex input handling.
*/
@ -18,80 +18,107 @@
#include "types.hpp"
/**
* @brief Represents a vertex in 3D space with color and texture information.
*
* @brief Represents a vertex in 3D space with color, texture, and normal information.
*
* This structure defines a vertex with all its attributes required for rendering,
* including position in 3D space, RGB color, and texture coordinates. It also
* including position in 3D space, RGB color, texture coordinates, and normal vector. It also
* provides methods for Vulkan vertex input configuration.
*/
struct Vertex {
vec3 pos; ///< Position of the vertex in 3D space (x, y, z)
vec3 color; ///< RGB color values, each component in range [0, 1]
vec2 tex_coord; ///< Texture coordinates (u, v) for texture mapping
glm::vec3 pos; ///< Position of the vertex in 3D space (x, y, z)
glm::vec3 color; ///< RGB color values, each component in range [0, 1]
glm::vec2 texCoord; ///< Texture coordinates (u, v) for texture mapping
glm::vec3 normal; ///< Normal vector of the vertex
/**
* @brief Provides the vertex binding description for Vulkan.
*
*
* @return vk::VertexInputBindingDescription Describes how to bind vertex data to GPU memory.
*
*
* The binding description specifies:
* - Binding index (0)
* - Stride (size of one vertex)
* - Input rate (per-vertex data)
*/
static fn getBindingDescription() -> vk::VertexInputBindingDescription {
return { .binding = 0, .stride = sizeof(Vertex), .inputRate = vk::VertexInputRate::eVertex };
return vk::VertexInputBindingDescription {
.binding = 0,
.stride = sizeof(Vertex),
.inputRate = vk::VertexInputRate::eVertex,
};
}
/**
* @brief Provides attribute descriptions for vertex data interpretation.
*
* @return std::array<vk::VertexInputAttributeDescription, 3> Array of descriptions for position, color, and texture coordinates.
*
*
* @return std::array<vk::VertexInputAttributeDescription, 4> Array of descriptions for position, color, texture coordinates, and normal.
*
* The attribute descriptions specify:
* - Location indices (0 for position, 1 for color, 2 for texture coordinates)
* - Location indices (0 for position, 1 for color, 2 for texture coordinates, 3 for normal)
* - Binding point (0)
* - Data format (R32G32B32 for vec3, R32G32 for vec2)
* - Offset of each attribute in the vertex structure
*/
static fn getAttributeDescriptions() -> std::array<vk::VertexInputAttributeDescription, 3> {
static fn getAttributeDescriptions() -> std::array<vk::VertexInputAttributeDescription, 4> {
return {
vk::VertexInputAttributeDescription { 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) },
vk::VertexInputAttributeDescription { 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) },
vk::VertexInputAttributeDescription { 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, tex_coord) }
vk::VertexInputAttributeDescription {
.location = 0,
.binding = 0,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, pos),
},
vk::VertexInputAttributeDescription {
.location = 1,
.binding = 0,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, color),
},
vk::VertexInputAttributeDescription {
.location = 2,
.binding = 0,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(Vertex, texCoord),
},
vk::VertexInputAttributeDescription {
.location = 3,
.binding = 0,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, normal),
},
};
}
/**
* @brief Compares two vertices for equality.
*
*
* @param other The vertex to compare with.
* @return bool True if vertices are identical in position, color, and texture coordinates.
* @return bool True if vertices are identical in position, color, texture coordinates, and normal.
*/
fn operator==(const Vertex& other) const -> bool {
return pos == other.pos && color == other.color && tex_coord == other.tex_coord;
fn operator==(const Vertex& other) const->bool {
return pos == other.pos && color == other.color && texCoord == other.texCoord && normal == other.normal;
}
};
namespace std {
/**
* @brief Hash function specialization for Vertex type.
*
*
* This specialization allows Vertex objects to be used as keys in unordered containers.
* The hash combines position, color, and texture coordinate data using bit operations
* The hash combines position, color, texture coordinate, and normal data using bit operations
* to create a unique hash value.
*/
template <>
struct hash<Vertex> {
/**
* @brief Computes hash value for a vertex.
*
*
* @param vertex The vertex to hash.
* @return size_t Hash value combining all vertex attributes.
*/
fn operator()(Vertex const& vertex) const -> size_t {
return ((hash<vec3>()(vertex.pos) ^ (hash<vec3>()(vertex.color) << 1)) >> 1) ^
(hash<vec2>()(vertex.tex_coord) << 1);
fn operator()(Vertex const& vertex) const->size_t {
return ((hash<glm::vec3>()(vertex.pos) ^ (hash<glm::vec3>()(vertex.color) << 1)) >> 1) ^
(hash<glm::vec2>()(vertex.texCoord) << 1) ^
(hash<glm::vec3>()(vertex.normal) << 2);
}
};
}