diff --git a/shaders/fragment.glsl b/shaders/fragment.glsl new file mode 100644 index 0000000..25ed3e0 --- /dev/null +++ b/shaders/fragment.glsl @@ -0,0 +1,12 @@ +#version 450 + +layout(binding = 1) uniform sampler2D texSampler; + +layout(location = 0) in vec3 fragColor; +layout(location = 1) in vec2 fragTexCoord; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = texture(texSampler, fragTexCoord); +} diff --git a/shaders/vertex.glsl b/shaders/vertex.glsl new file mode 100644 index 0000000..6a4ed5f --- /dev/null +++ b/shaders/vertex.glsl @@ -0,0 +1,20 @@ +#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 = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragColor; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + fragColor = inColor; + fragTexCoord = inTexCoord; +} diff --git a/src/main.cpp b/src/main.cpp index de02b9c..f3cee8f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,45 +47,9 @@ constexpr f64 CAMERA_SPEED = 1.0; // Maximum number of frames that can be processed concurrently constexpr i32 MAX_FRAMES_IN_FLIGHT = 2; -// Vertex shader -constexpr const char* vertShaderSrc = R"glsl( - #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 = 2) in vec2 inTexCoord; - - layout(location = 0) out vec3 fragColor; - layout(location = 1) out vec2 fragTexCoord; - - void main() { - gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); - fragColor = inColor; - fragTexCoord = inTexCoord; - } -)glsl"; - -// Fragment shader -constexpr const char* fragShaderSrc = R"glsl( - #version 450 - - layout(binding = 1) uniform sampler2D texSampler; - - layout(location = 0) in vec3 fragColor; - layout(location = 1) in vec2 fragTexCoord; - - layout(location = 0) out vec4 outColor; - - void main() { - outColor = texture(texSampler, fragTexCoord); - } -)glsl"; +// Shader file paths +constexpr const char* VERTEX_SHADER_PATH = "shaders/vertex.glsl"; +constexpr const char* FRAGMENT_SHADER_PATH = "shaders/fragment.glsl"; // Validation layers for debug builds #ifndef NDEBUG @@ -129,9 +93,9 @@ class VulkanApp { } // Shut down ImGui - // ImGui_ImplVulkan_Shutdown(); - // ImGui_ImplGlfw_Shutdown(); - // ImGui::DestroyContext(); + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); } private: @@ -161,6 +125,7 @@ class VulkanApp { vk::UniqueDescriptorSetLayout mDescriptorSetLayout; ///< Descriptor set layout vk::UniquePipelineLayout mPipelineLayout; ///< Pipeline layout vk::UniquePipeline mGraphicsPipeline; ///< Graphics pipeline + vk::UniquePipeline mOldPipeline; ///< Previous graphics pipeline for safe deletion vk::UniqueCommandPool mCommandPool; ///< Command pool for allocating command buffers @@ -199,6 +164,7 @@ class VulkanApp { mImageAvailableSemaphores; ///< Signals that an image is available for rendering std::vector mRenderFinishedSemaphores; ///< Signals that rendering has finished std::vector mInFlightFences; ///< Ensures CPU-GPU synchronization + std::vector mImagesInFlight; ///< Tracks which fences are in use by which swap chain images bool mFramebufferResized = false; ///< Flag indicating if the framebuffer was resized u32 mCurrentFrame = 0; ///< Index of the current frame being rendered @@ -211,6 +177,11 @@ class VulkanApp { f64 mLastY = HEIGHT / 2.0; ///< Last mouse Y position bool mCursorCaptured = true; ///< Flag indicating if cursor is captured + // ImGui-related state + f32 mCameraSpeed = CAMERA_SPEED; ///< Current camera speed + f32 mFieldOfView = 90.0F; ///< Current field of view + bool mWireframeMode = false; ///< Wireframe rendering mode + /** * @brief Struct to store queue family indices. * @@ -256,8 +227,8 @@ class VulkanApp { glm::dvec3 front; glm::dvec3 up; glm::dvec3 right; - double yaw; - double pitch; + f64 yaw; + f64 pitch; Camera() : position(2.0, 2.0, 2.0), @@ -277,20 +248,44 @@ class VulkanApp { return glm::lookAt(position, position + front, up); } - fn moveForward(f64 deltaTime) -> void { position += front * CAMERA_SPEED * deltaTime; } + fn moveForward(f64 deltaTime) -> void { + // Project front vector onto horizontal plane by zeroing Z component + glm::dvec3 horizontalFront = front; + horizontalFront.z = 0.0; + horizontalFront = glm::normalize(horizontalFront); + position += horizontalFront * CAMERA_SPEED * deltaTime; + } - fn moveBackward(f64 deltaTime) -> void { position -= front * CAMERA_SPEED * deltaTime; } + fn moveBackward(f64 deltaTime) -> void { + // Project front vector onto horizontal plane by zeroing Z component + glm::dvec3 horizontalFront = front; + horizontalFront.z = 0.0; + horizontalFront = glm::normalize(horizontalFront); + position -= horizontalFront * CAMERA_SPEED * deltaTime; + } - fn moveLeft(f64 deltaTime) -> void { position -= right * CAMERA_SPEED * deltaTime; } + fn moveLeft(f64 deltaTime) -> void { + // Project right vector onto horizontal plane by zeroing Z component + glm::dvec3 horizontalRight = right; + horizontalRight.z = 0.0; + horizontalRight = glm::normalize(horizontalRight); + position -= horizontalRight * CAMERA_SPEED * deltaTime; + } - fn moveRight(f64 deltaTime) -> void { position += right * CAMERA_SPEED * deltaTime; } + fn moveRight(f64 deltaTime) -> void { + // Project right vector onto horizontal plane by zeroing Z component + glm::dvec3 horizontalRight = right; + horizontalRight.z = 0.0; + horizontalRight = glm::normalize(horizontalRight); + position += horizontalRight * CAMERA_SPEED * deltaTime; + } fn moveUp(f64 deltaTime) -> void { position += glm::dvec3(0.0, 0.0, 1.0) * CAMERA_SPEED * deltaTime; } fn moveDown(f64 deltaTime) -> void { position -= glm::dvec3(0.0, 0.0, 1.0) * CAMERA_SPEED * deltaTime; } - fn rotate(double xoffset, double yoffset) -> void { - const double sensitivity = 0.1; + fn rotate(f64 xoffset, f64 yoffset) -> void { + const f64 sensitivity = 0.1; yaw += xoffset * sensitivity; pitch += yoffset * sensitivity; @@ -320,23 +315,20 @@ class VulkanApp { Camera mCamera; ///< Camera object - static fn processInput(vkfw::Window& window, Camera& camera, const f64& deltaTime) -> void { + static fn processInput(vkfw::Window& window, Camera& camera, const f32& deltaTime, const f32& cameraSpeed) + -> void { if (window.getKey(vkfw::Key::eW) == vkfw::eTrue) - camera.moveForward(deltaTime); + camera.moveForward(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eA) == vkfw::eTrue) - camera.moveLeft(deltaTime); + camera.moveLeft(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eS) == vkfw::eTrue) - camera.moveBackward(deltaTime); + camera.moveBackward(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eD) == vkfw::eTrue) - camera.moveRight(deltaTime); + camera.moveRight(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eSpace) == vkfw::eTrue) - camera.moveUp(deltaTime); + camera.moveUp(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eLeftShift) == vkfw::eTrue) - camera.moveDown(deltaTime); - - fmt::println( - "New position: {} {} {}", camera.getPosition()[0], camera.getPosition()[1], camera.getPosition()[2] - ); + camera.moveDown(static_cast(deltaTime * cameraSpeed)); } /** @@ -399,6 +391,13 @@ class VulkanApp { mCursorCaptured = false; window.set(vkfw::CursorMode::eNormal); } + if (key == vkfw::Key::eR && action == vkfw::KeyAction::ePress) { + try { + mDevice->waitIdle(); + createGraphicsPipeline(); + fmt::println("Shaders reloaded successfully!"); + } catch (const std::exception& e) { fmt::println(stderr, "Failed to reload shaders: {}", e.what()); } + } }; // Set up mouse button callback for re-capture @@ -410,9 +409,12 @@ class VulkanApp { ) -> void { if (button == vkfw::MouseButton::eLeft && action == vkfw::MouseButtonAction::ePress && !mCursorCaptured) { - mCursorCaptured = true; - mFirstMouse = true; // Reset first mouse flag to avoid jumps - window.set(vkfw::CursorMode::eDisabled); + // Only capture cursor if click is not on ImGui window + if (!ImGui::GetIO().WantCaptureMouse) { + mCursorCaptured = true; + mFirstMouse = true; // Reset first mouse flag to avoid jumps + window.set(vkfw::CursorMode::eDisabled); + } } }; @@ -467,7 +469,7 @@ class VulkanApp { 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 + initImGui(); // Initialize Dear ImGui for GUI rendering } /** @@ -477,8 +479,6 @@ class VulkanApp { * sets up the style, initializes ImGui for GLFW and Vulkan, and creates a descriptor pool for ImGui. */ fn initImGui() -> void { - // Temporarily disabled ImGui initialization - /* // Create ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -532,7 +532,6 @@ class VulkanApp { }; ImGui_ImplVulkan_Init(&initInfo); - */ } /** @@ -552,7 +551,7 @@ class VulkanApp { deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; - processInput(mWindow.get(), mCamera, deltaTime); + processInput(mWindow.get(), mCamera, static_cast(deltaTime), mCameraSpeed); mView = mCamera.getViewMatrix(); if (currentFrame - lastFpsUpdate > 1.0) { @@ -590,27 +589,29 @@ class VulkanApp { * It cleans up the old swap chain and creates a new one with updated properties. */ fn recreateSwapChain() -> void { - // Get the new width and height - auto [width, height] = mWindow->getFramebufferSize(); - - // If the width or height are 0, wait for events + i32 width = 0, height = 0; while (width == 0 || height == 0) { std::tie(width, height) = mWindow->getFramebufferSize(); vkfw::waitEvents(); } - // Wait for the device to finish mDevice->waitIdle(); - // Clean up the swap chain cleanupSwapChain(); - // Create a new swap chain createSwapChain(); createImageViews(); + createRenderPass(); + createGraphicsPipeline(); createColorResources(); createDepthResources(); createFramebuffers(); + createUniformBuffers(); + createDescriptorPool(); + createDescriptorSets(); + createCommandBuffers(); + + mImagesInFlight.resize(mSwapChainImages.size(), nullptr); } /** @@ -772,22 +773,24 @@ class VulkanApp { // Set the queue priority f32 queuePriority = 1.0F; - // For each unique queue family, create a new queue - for (const u32& queueFamily : uniqueQueueFamilies) { - vk::DeviceQueueCreateInfo queueCreateInfo { + // For each unique queue family, create a queue create info + queueCreateInfos.reserve(uniqueQueueFamilies.size()); + + for (u32 queueFamily : uniqueQueueFamilies) + queueCreateInfos.push_back({ .queueFamilyIndex = queueFamily, .queueCount = 1, .pQueuePriorities = &queuePriority, - }; + }); - queueCreateInfos.emplace_back(queueCreateInfo); - } - - // Enable anisotropic filtering + // Enable required features vk::PhysicalDeviceFeatures deviceFeatures { + .fillModeNonSolid = vk::True, // Required for wireframe rendering + .wideLines = vk::True, // Required for line width > 1.0 .samplerAnisotropy = vk::True, }; + // Create the logical device vk::DeviceCreateInfo createInfo { .queueCreateInfoCount = static_cast(queueCreateInfos.size()), .pQueueCreateInfos = queueCreateInfos.data(), @@ -796,8 +799,9 @@ class VulkanApp { .pEnabledFeatures = &deviceFeatures, }; - // Create the logical device and set the graphics and present queues - mDevice = mPhysicalDevice.createDeviceUnique(createInfo); + mDevice = mPhysicalDevice.createDeviceUnique(createInfo); + + // Get the graphics and present queues mGraphicsQueue = mDevice->getQueue(qfIndices.graphics_family.value(), 0); mPresentQueue = mDevice->getQueue(qfIndices.present_family.value(), 0); } @@ -1004,9 +1008,9 @@ class VulkanApp { */ fn createGraphicsPipeline() -> void { std::vector vertShaderCode = - ShaderCompiler::getCompiledShader(vertShaderSrc, shaderc_shader_kind::shaderc_vertex_shader, "vert"); + ShaderCompiler::getCompiledShader(VERTEX_SHADER_PATH, shaderc_shader_kind::shaderc_vertex_shader); std::vector fragShaderCode = - ShaderCompiler::getCompiledShader(fragShaderSrc, shaderc_shader_kind::shaderc_fragment_shader, "frag"); + ShaderCompiler::getCompiledShader(FRAGMENT_SHADER_PATH, shaderc_shader_kind::shaderc_fragment_shader); vk::UniqueShaderModule vertShaderModule = createShaderModule(vertShaderCode); vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode); @@ -1052,11 +1056,11 @@ class VulkanApp { vk::PipelineRasterizationStateCreateInfo rasterizer { .depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False, - .polygonMode = vk::PolygonMode::eFill, + .polygonMode = mWireframeMode ? vk::PolygonMode::eLine : vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eBack, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = vk::False, - .lineWidth = 1.0F, + .lineWidth = mWireframeMode ? 2.0F : 1.0F, // Thicker lines in wireframe mode }; vk::PipelineMultisampleStateCreateInfo multisampling { @@ -1083,7 +1087,7 @@ class VulkanApp { .logicOp = vk::LogicOp::eCopy, .attachmentCount = 1, .pAttachments = &colorBlendAttachment, - .blendConstants = std::array { 0.0F, 0.0F, 0.0F, 0.0F }, + .blendConstants = std::array { 0.0F, 0.0F, 0.0F, 0.0F }, }; std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; @@ -2083,31 +2087,22 @@ class VulkanApp { * This function records drawing commands into the given command buffer. */ fn recordCommandBuffer(const vk::CommandBuffer& commandBuffer, const u32& imageIndex) -> void { - // Define the command buffer begin info - vk::CommandBufferBeginInfo beginInfo {}; - // Begin the command buffer - commandBuffer.begin(beginInfo); + commandBuffer.begin({ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }); - // Define the render pass begin info - std::array clearValues { - // Set the color buffer to black - vk::ClearValue { .color = { .uint32 = std::array { 0, 0, 0, 255 } } }, - // Set the depth buffer to 1.0F - vk::ClearValue { .depthStencil = { .depth = 1.0F, .stencil = 0 } }, + // Define clear values for color and depth + std::array clearValues = { + vk::ClearValue { .color = { std::array { 0.0F, 0.0F, 0.0F, 1.0F } } }, + vk::ClearValue { .depthStencil = { 1.0F, 0 } }, }; - // Define the render pass info + // Begin the render pass vk::RenderPassBeginInfo renderPassInfo { - // Render pass itself - .renderPass = mRenderPass.get(), - // Current framebuffer - .framebuffer = mSwapChainFramebuffers[imageIndex].get(), - // Render area (entire framebuffer) - .renderArea = { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent }, - // Clear values for the attachments + .renderPass = mRenderPass.get(), + .framebuffer = mSwapChainFramebuffers[imageIndex].get(), + .renderArea = { .offset = { 0, 0 }, .extent = mSwapChainExtent }, .clearValueCount = static_cast(clearValues.size()), - .pClearValues = clearValues.data(), + .pClearValues = clearValues.data() }; // Begin the render pass @@ -2162,7 +2157,7 @@ class VulkanApp { .view = mView, // Projection matrix - glm::perspective(fov, aspect, near, far) .proj = glm::perspective( - glm::radians(90.0F), + glm::radians(mFieldOfView), static_cast(mSwapChainExtent.width) / static_cast(mSwapChainExtent.height), 0.1F, 100.0F @@ -2176,11 +2171,9 @@ class VulkanApp { memcpy(mUniformBuffersMapped[mCurrentFrame], &ubo, sizeof(ubo)); // Example: Add extra clones with different translations - std::vector 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)), - glm::translate(glm::mat4(1.0F), glm::vec3(0.0F, 2.0F, 0.0F)) - }; + std::vector 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)), + glm::translate(glm::mat4(1.0F), glm::vec3(0.0F, 2.0F, 0.0F)) }; for (const auto& modelMatrix : modelMatrices) { // Update model matrix for each clone @@ -2202,15 +2195,55 @@ class VulkanApp { commandBuffer.drawIndexed(static_cast(mIndices.size()), 1, 0, 0, 0); } - // ImGui_ImplVulkan_NewFrame(); - // ImGui_ImplGlfw_NewFrame(); - // ImGui::NewFrame(); + // Only render ImGui when cursor is not captured + if (!mCursorCaptured) { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); - // Your ImGui code here - // ImGui::ShowDemoWindow(); + // Create ImGui window with useful controls + ImGui::Begin("Settings", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - // ImGui::Render(); - // ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer); + // Set initial window size (this will be the minimum size due to AlwaysAutoResize) + ImGui::SetWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver); + + // 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(framerate)); + ImGui::Text("%.3f ms/frame", static_cast(1000.0F / framerate)); + } + + ImGui::End(); + + ImGui::Render(); + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer); + } // End the render pass commandBuffer.endRenderPass(); @@ -2274,7 +2307,7 @@ class VulkanApp { .view = mView, // Projection matrix - glm::perspective(fov, aspect, near, far) .proj = glm::perspective( - glm::radians(90.0F), + glm::radians(mFieldOfView), static_cast(mSwapChainExtent.width) / static_cast(mSwapChainExtent.height), 0.1F, 100.0F @@ -2308,6 +2341,12 @@ class VulkanApp { if (result != vk::Result::eSuccess) throw std::runtime_error("Failed to wait for fences!"); + // Clear old pipeline if it exists + if (mOldPipeline) { + mDevice->waitIdle(); // Wait for all operations to complete + mOldPipeline.reset(); + } + // Acquire the next image from the swap chain auto [imageIndexResult, imageIndexValue] = mDevice->acquireNextImageKHR( mSwapChain.get(), UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame].get(), nullptr @@ -2349,7 +2388,7 @@ class VulkanApp { .pSignalSemaphores = &mRenderFinishedSemaphores[mCurrentFrame].get(), }; - // Submit the graphics queue + // Submit the command buffer mGraphicsQueue.submit(submitInfo, mInFlightFences[mCurrentFrame].get()); vk::PresentInfoKHR presentInfo { @@ -2375,7 +2414,7 @@ class VulkanApp { // Increment the current frame (or loop back to 0) mCurrentFrame = (mCurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT; - } catch (vk::OutOfDateKHRError& /*err*/) { + } catch (const vk::SystemError& /*err*/) { // Recreate the swap chain if it's out of date mFramebufferResized = false; recreateSwapChain(); diff --git a/src/util/shaders.hpp b/src/util/shaders.hpp index 13b23b7..b6d43a4 100644 --- a/src/util/shaders.hpp +++ b/src/util/shaders.hpp @@ -1,53 +1,95 @@ +/** + * @file shaders.hpp + * @brief SPIR-V shader compilation and caching system. + * + * This file provides functionality for compiling GLSL shaders to SPIR-V and + * managing a shader cache system. It supports automatic recompilation when + * source files are modified and efficient caching of compiled shaders. + */ + #pragma once #include #include #include +#include #include #include #include #include "types.hpp" +/** + * @brief Handles shader compilation and caching operations. + * + * This class provides static methods for compiling GLSL shaders to SPIR-V + * and managing a cache system. It automatically detects when shaders need + * to be recompiled based on file timestamps and provides efficient caching + * of compiled shader binaries. + */ class ShaderCompiler { public: ShaderCompiler() = default; /** - * @brief Compiles or loads a cached SPIR-V shader from a file. + * @brief Compiles or retrieves a cached SPIR-V shader. * - * @param shaderSource The source code of the shader. - * @param kind The type of shader being compiled (vertex, fragment, etc.). - * @param shaderName The name used for caching the compiled shader. - * @return std::vector A vector containing the compiled SPIR-V code. - * @throws std::runtime_error if shader compilation fails. + * @param shaderPath Path to the GLSL shader source file + * @param kind Type of shader (vertex, fragment, compute, etc.) + * @return std::vector Compiled SPIR-V binary code + * @throws std::runtime_error If shader compilation fails or file is not found * - * This function attempts to load a shader from the cache. If the shader - * is not found in the cache, it compiles the shader from the source code - * and saves the result to the cache for future use. + * This function performs the following steps: + * 1. Checks if a cached version exists and is up-to-date + * 2. Loads from cache if available and valid + * 3. Otherwise, compiles the shader from source + * 4. Caches the newly compiled shader for future use + * 5. Returns the SPIR-V binary code */ - static fn getCompiledShader( - const char* shaderSource, - const shaderc_shader_kind& kind, - const string& shaderName - ) -> std::vector { + static fn getCompiledShader(const std::filesystem::path& shaderPath, const shaderc_shader_kind& kind) + -> std::vector { using namespace std; - const filesystem::path cacheFile = getCacheFilePath(shaderName); + // Convert to absolute path if relative + filesystem::path absPath = filesystem::absolute(shaderPath); - // Try loading from cache first - vector spirvCode = loadCachedShader(cacheFile); + if (!filesystem::exists(absPath)) + throw runtime_error("Shader file not found: " + absPath.string()); - if (!spirvCode.empty()) { - fmt::println("Loaded shader from cache: {}", cacheFile.string()); - return spirvCode; + const string shaderName = absPath.stem().string(); + const filesystem::path cacheFile = getCacheFilePath(shaderName); + + // Check if we need to recompile by comparing timestamps + if (filesystem::exists(cacheFile)) { + const auto sourceTime = filesystem::last_write_time(absPath); + const auto cacheTime = filesystem::last_write_time(cacheFile); + + if (cacheTime >= sourceTime) { + // Cache is up to date, load it + vector spirvCode = loadCachedShader(cacheFile); + if (!spirvCode.empty()) { + fmt::println("Loaded shader from cache: {}", cacheFile.string()); + return spirvCode; + } + } } - // Cache miss, compile the shader - spirvCode = compileShader(shaderSource, kind); + // Need to compile the shader + fmt::println("Compiling shader: {}", absPath.string()); + + // Read shader source + ifstream file(absPath); + if (!file) + throw runtime_error("Failed to open shader file: " + absPath.string()); + + string shaderSource((istreambuf_iterator(file)), istreambuf_iterator()); + file.close(); + + // Compile the shader + vector spirvCode = compileShader(shaderSource.c_str(), kind); if (spirvCode.empty()) - throw runtime_error("Shader compilation failed for: " + shaderName); + throw runtime_error("Shader compilation failed for: " + absPath.string()); // Cache the compiled SPIR-V binary saveCompiledShader(spirvCode, cacheFile.string()); @@ -56,13 +98,17 @@ class ShaderCompiler { private: /** - * @brief Generates the cache file path based on the operating system. + * @brief Determines the platform-specific shader cache directory. * - * @param shaderName The name used for the shader file. - * @return string The full path to the cache file. + * @param shaderName Base name of the shader file + * @return std::filesystem::path Full path to the cache file * - * This function determines the appropriate directory for caching shaders - * based on the operating system and returns the full path for the specified shader name. + * Cache locations: + * - Windows: %LOCALAPPDATA%/VulkanApp/Shaders/ + * - macOS: ~/Library/Application Support/VulkanApp/Shaders/ + * - Linux: ~/.local/share/VulkanApp/Shaders/ + * + * The directory is created if it doesn't exist. */ static fn getCacheFilePath(const string& shaderName) -> std::filesystem::path { using namespace std::filesystem; @@ -76,91 +122,88 @@ class ShaderCompiler { #endif if (!exists(cacheDir)) - create_directories(cacheDir); // Create the directory if it doesn't exist + create_directories(cacheDir); return cacheDir / (shaderName + ".spv"); } /** - * @brief Compiles GLSL code to SPIR-V. + * @brief Loads a cached SPIR-V shader from disk. * - * @param source The GLSL source code to compile. - * @param kind The type of shader being compiled. - * @return std::vector A vector containing the compiled SPIR-V code. + * @param cachePath Path to the cached shader file + * @return std::vector SPIR-V binary code, empty if loading fails * - * This function uses the shaderc library to compile GLSL source code into - * SPIR-V binary format. If the compilation fails, an empty vector is returned. + * Reads the binary SPIR-V data from the cache file. Returns an empty + * vector if the file cannot be opened or read properly. */ - static fn compileShader(const char* source, const shaderc_shader_kind& kind) -> std::vector { - using namespace shaderc; - - Compiler compiler; - CompileOptions options; - - SpvCompilationResult result = compiler.CompileGlslToSpv(source, kind, "shader.glsl", "main", options); - - if (result.GetCompilationStatus() != shaderc_compilation_status_success) { - fmt::println(stderr, "Shader compilation failed: {}", result.GetErrorMessage()); + static fn loadCachedShader(const std::filesystem::path& cachePath) -> std::vector { + std::ifstream file(cachePath, std::ios::binary); + if (!file.is_open()) return {}; - } - return { result.cbegin(), result.cend() }; + // Read file size + file.seekg(0, std::ios::end); + const std::streamoff fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // Allocate buffer and read data + std::vector buffer(static_cast(fileSize) / sizeof(u32)); + file.read(std::bit_cast(buffer.data()), fileSize); + + return buffer; } /** - * @brief Loads compiled SPIR-V shader code from a cache file. + * @brief Compiles GLSL source code to SPIR-V. * - * @param cacheFile The path to the cached SPIR-V file. - * @return std::vector A vector containing the loaded SPIR-V code. - * @throws std::runtime_error if the file cannot be read. + * @param source GLSL shader source code + * @param kind Type of shader being compiled + * @return std::vector Compiled SPIR-V binary code * - * This function checks if the specified cache file exists and reads its - * contents into a vector. If the file cannot be opened, an exception is thrown. + * Uses the shaderc library to compile GLSL to SPIR-V. The compilation + * is performed with optimization level set to performance and generates + * debug information in debug builds. */ - static fn loadCachedShader(const std::filesystem::path& cacheFile) -> std::vector { - using namespace std; + static fn compileShader(const char* source, shaderc_shader_kind kind) -> std::vector { + shaderc::Compiler compiler; + shaderc::CompileOptions options; - if (!filesystem::exists(cacheFile)) - return {}; // No cached file + // Set compilation options +#ifdef NDEBUG + options.SetOptimizationLevel(shaderc_optimization_level_performance); +#else + options.SetOptimizationLevel(shaderc_optimization_level_zero); + options.SetGenerateDebugInfo(); +#endif - ifstream file(cacheFile, ios::binary); + // Compile the shader + shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, kind, "shader", options); - // Check if the file was successfully opened - if (!file) - throw runtime_error("Failed to open cached shader file: " + cacheFile.string()); + if (module.GetCompilationStatus() != shaderc_compilation_status_success) + return {}; - usize fileSize = filesystem::file_size(cacheFile); - vector spirvCode(fileSize / sizeof(u32)); - - // Read entire file content into the vector - if (!file.read(bit_cast(spirvCode.data()), static_cast(fileSize))) - throw runtime_error("Failed to read cached shader file: " + cacheFile.string()); - - return spirvCode; + return { module.cbegin(), module.cend() }; } /** - * @brief Saves compiled SPIR-V binary to a cache file. + * @brief Saves compiled SPIR-V code to the cache. * - * @param spirvCode The SPIR-V code to save. - * @param cacheFile The path to the file where the SPIR-V code will be saved. - * @throws std::runtime_error if the file cannot be written. + * @param spirv Compiled SPIR-V binary code + * @param cachePath Path where the cache file should be saved + * @return bool True if save was successful, false otherwise * - * This function writes the compiled SPIR-V binary to the specified file. - * If the file cannot be opened or written, an exception is thrown. + * Writes the SPIR-V binary to disk for future use. Creates any + * necessary parent directories if they don't exist. */ - static fn saveCompiledShader(const std::vector& spirvCode, const string& cacheFile) -> void { - using namespace std; + static fn saveCompiledShader(const std::vector& spirv, const std::string& cachePath) -> bool { + std::ofstream file(cachePath, std::ios::binary); + if (!file.is_open()) + return false; - ofstream file(cacheFile, ios::binary); + file.write( + std::bit_cast(spirv.data()), static_cast(spirv.size() * sizeof(u32)) + ); - // Check if the file was successfully opened - if (!file) - throw runtime_error("Failed to open file for saving shader: " + cacheFile); - - if (!file.write( - bit_cast(spirvCode.data()), static_cast(spirvCode.size() * sizeof(u32)) - )) - throw runtime_error("Failed to save shader to cache: " + cacheFile); + return file.good(); } }; diff --git a/src/util/types.hpp b/src/util/types.hpp index fe0fd7e..57ae50f 100644 --- a/src/util/types.hpp +++ b/src/util/types.hpp @@ -1,128 +1,152 @@ +/** + * @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 + * readability and ensure consistent type usage across the codebase. + */ + #pragma once #include #include #include +#include #define fn auto +// Integer Types + /** - * @typedef u8 - * @brief Represents an 8-bit unsigned integer. - * - * This type alias is used for 8-bit unsigned integers, ranging from 0 to 255. - * It is based on the std::uint8_t type. + * @brief 8-bit unsigned integer. + * @details Range: 0 to 255 + * Used for byte-level operations and color channel values. */ using u8 = std::uint8_t; /** - * @typedef u16 - * @brief Represents a 16-bit unsigned integer. - * - * This type alias is used for 16-bit unsigned integers, ranging from 0 to 65,535. - * It is based on the std::uint16_t type. + * @brief 16-bit unsigned integer. + * @details Range: 0 to 65,535 + * Used for texture coordinates and small indices. */ using u16 = std::uint16_t; /** - * @typedef u32 - * @brief Represents a 32-bit unsigned integer. - * - * This type alias is used for 32-bit unsigned integers, ranging from 0 to 4,294,967,295. - * It is based on the std::uint32_t type. + * @brief 32-bit unsigned integer. + * @details Range: 0 to 4,294,967,295 + * Used for array sizes, indices, and flags. */ using u32 = std::uint32_t; /** - * @typedef u64 - * @brief Represents a 64-bit unsigned integer. - * - * This type alias is used for 64-bit unsigned integers, ranging from 0 to - * 18,446,744,073,709,551,615. It is based on the std::uint64_t type. + * @brief 64-bit unsigned integer. + * @details Range: 0 to 18,446,744,073,709,551,615 + * Used for large indices and timestamps. */ using u64 = std::uint64_t; -// Type Aliases for Signed Integers - /** - * @typedef i8 - * @brief Represents an 8-bit signed integer. - * - * This type alias is used for 8-bit signed integers, ranging from -128 to 127. - * It is based on the std::int8_t type. + * @brief 8-bit signed integer. + * @details Range: -128 to 127 + * Used for small signed values and relative offsets. */ using i8 = std::int8_t; /** - * @typedef i16 - * @brief Represents a 16-bit signed integer. - * - * This type alias is used for 16-bit signed integers, ranging from -32,768 to 32,767. - * It is based on the std::int16_t type. + * @brief 16-bit signed integer. + * @details Range: -32,768 to 32,767 + * Used for medium-range signed values. */ using i16 = std::int16_t; /** - * @typedef i32 - * @brief Represents a 32-bit signed integer. - * - * This type alias is used for 32-bit signed integers, ranging from -2,147,483,648 to 2,147,483,647. - * It is based on the std::int32_t type. + * @brief 32-bit signed integer. + * @details Range: -2,147,483,648 to 2,147,483,647 + * Primary signed integer type for general use. */ using i32 = std::int32_t; /** - * @typedef i64 - * @brief Represents a 64-bit signed integer. - * - * This type alias is used for 64-bit signed integers, ranging from -9,223,372,036,854,775,808 to - * 9,223,372,036,854,775,807. It is based on the std::int64_t type. + * @brief 64-bit signed integer. + * @details Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 + * Used for large signed values and time calculations. */ using i64 = std::int64_t; -// Type Aliases for Floating-Point Numbers +// Floating-Point Types /** - * @typedef f32 - * @brief Represents a 32-bit floating-point number. - * - * This type alias is used for 32-bit floating-point numbers, which follow the IEEE 754 standard. - * It is based on the float type. + * @brief 32-bit floating-point number. + * @details IEEE 754 single-precision + * Used for graphics calculations, positions, and colors. */ using f32 = float; /** - * @typedef f64 - * @brief Represents a 64-bit floating-point number. - * - * This type alias is used for 64-bit floating-point numbers, which follow the IEEE 754 standard. - * It is based on the double type. + * @brief 64-bit floating-point number. + * @details IEEE 754 double-precision + * Used for high-precision calculations and physics. */ using f64 = double; -// Type Aliases for Size Types +// Size Types /** - * @typedef usize - * @brief Represents an unsigned size type. - * - * This type alias is used for representing the size of objects in bytes. - * It is based on the std::size_t type, which is the result type of the sizeof operator. + * @brief Unsigned size type. + * @details Platform-dependent size (32/64-bit) + * Used for memory sizes and container sizes. */ using usize = std::size_t; /** - * @typedef isize - * @brief Represents a signed size type. - * - * This type alias is used for representing pointer differences. - * It is based on the std::ptrdiff_t type, which is the signed integer type returned when - * subtracting two pointers. + * @brief Signed size type. + * @details Platform-dependent size (32/64-bit) + * Used for pointer arithmetic and container differences. */ using isize = std::ptrdiff_t; /** - * @typedef string - * @brief Represents a string. + * @brief String type alias. + * @details Standard string type for text handling. */ using string = std::string; + +// GLM Vector Types + +/** + * @brief 2D vector with 32-bit float components. + * @details Used for 2D positions, texture coordinates. + */ +using vec2 = glm::f32vec2; + +/** + * @brief 3D vector with 32-bit float components. + * @details Used for 3D positions, colors, normals. + */ +using vec3 = glm::f32vec3; + +/** + * @brief 4D vector with 32-bit float components. + * @details Used for homogeneous coordinates, quaternions. + */ +using vec4 = glm::f32vec4; + +/** + * @brief 2D vector with 64-bit float components. + * @details Used for high-precision 2D calculations. + */ +using dvec2 = glm::f64vec2; + +/** + * @brief 3D vector with 64-bit float components. + * @details Used for high-precision 3D positions and directions. + */ +using dvec3 = glm::f64vec3; + +/** + * @brief 4D vector with 64-bit float components. + * @details Used for high-precision homogeneous coordinates. + */ +using dvec4 = glm::f64vec4; diff --git a/src/util/unique_image.hpp b/src/util/unique_image.hpp index 622fe9a..01ea809 100644 --- a/src/util/unique_image.hpp +++ b/src/util/unique_image.hpp @@ -1,3 +1,12 @@ +/** + * @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 #define STB_IMAGE_IMPLEMENTATION @@ -7,45 +16,53 @@ namespace stb { /** - * @brief A class that handles loading and managing image data. - * - * This class uses the stb_image library to load images from the filesystem - * and provides access to the image data, dimensions, and channel count. + * @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. */ class UniqueImage { public: /** - * @brief Constructs a UniqueImage object and loads an image from the specified path. - * - * @param path The filesystem path to the image file to load. + * @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()); } - // Deleted copy constructor to prevent copying. + // Prevent copying to maintain single ownership UniqueImage(const UniqueImage&) = delete; - - // Deleted copy assignment operator to prevent copying. - fn operator=(const UniqueImage&)->UniqueImage& = delete; + fn operator=(const UniqueImage&) -> UniqueImage& = delete; /** - * @brief Move constructor for UniqueImage. - * - * @param other The UniqueImage object from which to move resources. - * - * Transfers ownership of resources from another UniqueImage object. + * @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(other.mWidth), mHeight(other.mHeight), mChannels(other.mChannels) { + : mData(other.mData), + mWidth(static_cast(other.mWidth)), + mHeight(static_cast(other.mHeight)), + mChannels(static_cast(other.mChannels)) { other.mData = nullptr; } /** - * @brief Move assignment operator for UniqueImage. - * - * @param other The UniqueImage object from which to move resources. + * @brief Move assignment operator for transferring image ownership. + * + * @param other Source UniqueImage to move from. * @return Reference to this object. - * - * Transfers ownership of resources from another UniqueImage object. + * + * Safely transfers ownership of image data, ensuring proper cleanup of + * existing resources before the transfer. */ fn operator=(UniqueImage&& other) noexcept -> UniqueImage& { if (this != &other) { @@ -53,18 +70,19 @@ namespace stb { stbi_image_free(mData); mData = other.mData; - mWidth = other.mWidth; - mHeight = other.mHeight; - mChannels = other.mChannels; + mWidth = static_cast(other.mWidth); + mHeight = static_cast(other.mHeight); + mChannels = static_cast(other.mChannels); other.mData = nullptr; } return *this; } /** - * @brief Destructor for UniqueImage. - * - * Frees the image data if it is allocated. + * @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. */ ~UniqueImage() { if (mData) @@ -72,50 +90,51 @@ namespace stb { } /** - * @brief Retrieves the image data. - * - * @return Pointer to the image data in memory. + * @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. */ [[nodiscard]] fn getData() const -> u8* { return mData; } /** - * @brief Retrieves the width of the image. - * - * @return The width of the image in pixels. + * @brief Get image width in pixels. + * @return Width of the image. */ [[nodiscard]] fn getWidth() const -> i32 { return mWidth; } /** - * @brief Retrieves the height of the image. - * - * @return The height of the image in pixels. + * @brief Get image height in pixels. + * @return Height of the image. */ [[nodiscard]] fn getHeight() const -> i32 { return mHeight; } /** - * @brief Retrieves the number of channels in the image. - * - * @return The number of channels in the image (e.g., 3 for RGB, 4 for RGBA). + * @brief Get number of color channels. + * @return Number of channels (e.g., 3 for RGB, 4 for RGBA). */ [[nodiscard]] fn getChannels() const -> i32 { return mChannels; } private: - u8* mData = nullptr; ///< Pointer to the image data. - i32 mWidth = 0; ///< Width of the image in pixels. - i32 mHeight = 0; ///< Height of the image in pixels. - i32 mChannels = 0; ///< Number of channels in the image. - /** - * @brief Loads an image from a file. - * - * @param filename The name of the file from which to load the image. - * @throws std::runtime_error If the image fails to load. + * @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. */ - fn load(const char* filename) -> void { - mData = stbi_load(filename, &mWidth, &mHeight, &mChannels, STBI_rgb_alpha); - + fn load(const char* path) -> void { + mData = stbi_load(path, &mWidth, &mHeight, &mChannels, STBI_rgb_alpha); if (!mData) - throw std::runtime_error("Failed to load image: " + string(stbi_failure_reason())); + throw std::runtime_error(fmt::format("Failed to load texture image: {}", path)); } + + u8* mData = nullptr; ///< Raw image data in memory + i32 mWidth = 0; ///< Image width in pixels + i32 mHeight = 0; ///< Image height in pixels + i32 mChannels = 0; ///< Number of color channels }; } diff --git a/src/util/vertex.hpp b/src/util/vertex.hpp index a3a0ef1..0e2ba69 100644 --- a/src/util/vertex.hpp +++ b/src/util/vertex.hpp @@ -1,3 +1,12 @@ +/** + * @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 + * Vulkan-specific descriptors for vertex input handling. + */ + #define GLM_FORCE_DEPTH_ZERO_TO_ONE #define GLM_ENABLE_EXPERIMENTAL #include @@ -8,45 +17,81 @@ #include "types.hpp" -// Vertex data for the model +/** + * @brief Represents a vertex in 3D space with color and texture 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 + * provides methods for Vulkan vertex input configuration. + */ struct Vertex { - // Position of the vertex - glm::vec3 pos; - // Color of the vertex (in RGB) - glm::vec3 color; - // Texture coordinates of the vertex - glm::vec2 tex_coord; + 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 - // Returns the binding description for 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 }; } - // Returns the attribute descriptions for the vertex + /** + * @brief Provides attribute descriptions for vertex data interpretation. + * + * @return std::array Array of descriptions for position, color, and texture coordinates. + * + * The attribute descriptions specify: + * - Location indices (0 for position, 1 for color, 2 for texture coordinates) + * - Binding point (0) + * - Data format (R32G32B32 for vec3, R32G32 for vec2) + * - Offset of each attribute in the vertex structure + */ static fn getAttributeDescriptions() -> std::array { return { - // Position attribute - vk::VertexInputAttributeDescription { 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) }, - // Color attribute - vk::VertexInputAttributeDescription { 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) }, - // Texture coordinate attribute - vk::VertexInputAttributeDescription { 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, tex_coord) } + 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) } }; } - // Overload the equality operator for the vertex - fn operator==(const Vertex& other) const->bool { + /** + * @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. + */ + fn operator==(const Vertex& other) const -> bool { return pos == other.pos && color == other.color && tex_coord == other.tex_coord; } }; -// Hash function for the vertex 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 + * to create a unique hash value. + */ template <> struct hash { - fn operator()(Vertex const& vertex) const->size_t { - return ((hash()(vertex.pos) ^ (hash()(vertex.color) << 1)) >> 1) ^ - (hash()(vertex.tex_coord) << 1); + /** + * @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()(vertex.pos) ^ (hash()(vertex.color) << 1)) >> 1) ^ + (hash()(vertex.tex_coord) << 1); } }; }