diff --git a/.gitignore b/.gitignore index 659bda7..e0f0f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ includes/ # Object files *.o *.obj +*.blend diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f95620..ebf4ca8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,6 +209,75 @@ endif() set_target_properties(testCPUBunny PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/output) +# Debug BVH executable +add_executable(debugBVH + "Marching Cubes/debugBVH.cpp" + shader.cpp + mesh.cpp + model.cpp + ${GEOMETRY_SOURCES} + ${GLAD_SOURCES} +) + +target_include_directories(debugBVH PRIVATE + ${CMAKE_SOURCE_DIR}/includes + ${CMAKE_SOURCE_DIR}/includes/glad + ${CMAKE_SOURCE_DIR}/includes/glm + ${CMAKE_SOURCE_DIR} +) + +target_link_directories(debugBVH PRIVATE ${CMAKE_SOURCE_DIR}/lib/assimp/lib) +target_link_libraries(debugBVH + OpenGL::GL + glfw + assimp-vc143-mt + zlibstatic +) + +if(WIN32 AND MSVC) + target_compile_definitions(debugBVH PRIVATE + WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS) + target_compile_options(debugBVH PRIVATE /external:W0 /external:anglebrackets /external:templates-) +endif() + +set_target_properties(debugBVH PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/output) + +# Rubik's Cube executable +add_executable(RubiksCube + "Rubiks/window.cpp" + "Rubiks/RubiksCube.cpp" + "Rubiks/input.cpp" + "Rubiks/mouse_selector.cpp" + shader.cpp + mesh.cpp + model.cpp + ${GEOMETRY_SOURCES} + ${GLAD_SOURCES} +) + +target_include_directories(RubiksCube PRIVATE + ${CMAKE_SOURCE_DIR}/includes + ${CMAKE_SOURCE_DIR}/includes/glad + ${CMAKE_SOURCE_DIR}/includes/glm + ${CMAKE_SOURCE_DIR} +) + +target_link_directories(RubiksCube PRIVATE ${CMAKE_SOURCE_DIR}/lib/assimp/lib) +target_link_libraries(RubiksCube + OpenGL::GL + glfw + assimp-vc143-mt + zlibstatic +) + +if(WIN32 AND MSVC) + target_compile_definitions(RubiksCube PRIVATE + WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS) + target_compile_options(RubiksCube PRIVATE /external:W0 /external:anglebrackets /external:templates-) +endif() + +set_target_properties(RubiksCube PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/output) + # Platform-specific compiler settings if(WIN32 AND MSVC) set_property(TARGET ${PROJECT_NAME} PROPERTY VS_USER_PROPS "") @@ -314,4 +383,16 @@ if(WIN32) "${SFML_BIN_DIR}/sfml-system-d-2.dll" "${SFML_BIN_DIR}/openal32.dll" $) + + # Copy Assimp DLL for debugBVH + add_custom_command(TARGET debugBVH POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_SOURCE_DIR}/lib/assimp/bin/assimp-vc143-mt.dll" + $) + + # Copy Assimp DLL for RubiksCube + add_custom_command(TARGET RubiksCube POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_SOURCE_DIR}/lib/assimp/bin/assimp-vc143-mt.dll" + $) endif() diff --git a/Marching Cubes/debugBVH.cpp b/Marching Cubes/debugBVH.cpp new file mode 100644 index 0000000..932935d --- /dev/null +++ b/Marching Cubes/debugBVH.cpp @@ -0,0 +1,521 @@ +#include +#include +#include +#include +#include + +#include "GPUMarchCubes.h" +#include "../model.h" +#include "../shader.h" +#include "../camera.h" +#include +#include +#include +#include + +// Window settings +const unsigned int SCR_WIDTH = 1200; +const unsigned int SCR_HEIGHT = 900; + +int MAX_DEPTH = 10; +bool showBVH = true; +bool showModel = true; +bool needsRebuild = false; +int currentViewDepth = 0; + +// Camera +Camera camera(glm::vec3(0.0f, 0.5f, 2.0f)); +float lastX = SCR_WIDTH / 2.0f; +float lastY = SCR_HEIGHT / 2.0f; +bool firstMouse = true; + +// Timing +float deltaTime = 0.0f; +float lastFrame = 0.0f; + +// Callbacks +void framebuffer_size_callback(GLFWwindow* window, int width, int height); +void mouse_callback(GLFWwindow* window, double xpos, double ypos); +void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); +void processInput(GLFWwindow *window); + +struct Triangle { + glm::vec3 v0, v1, v2; + glm::vec3 normal; +}; + +struct BoundingBox { + glm::vec3 min = glm::vec3(1e10f); + glm::vec3 max = glm::vec3(-1e10f); + glm::vec3 center = (min + max) * 0.5f; + unsigned int VAO = 0; + unsigned int VBO = 0; + unsigned int EBO = 0; + glm::mat4 modelMatrix = glm::mat4(1.0f); + + void update(const glm::vec3& point) { + min = glm::min(min, point); + max = glm::max(max, point); + center = (min + max) * 0.5f; + } + + void update(const Triangle& tri) { + update(tri.v0); + update(tri.v1); + update(tri.v2); + } + + ~BoundingBox(){ + if(VAO) glDeleteVertexArrays(1, &VAO); + if(VBO) glDeleteBuffers(1, &VBO); + if(EBO) glDeleteBuffers(1, &EBO); + } + + void setupRender() { + if (VAO != 0) return; // Already setup + + glm::vec3 size = max - min; + float halfWidth = size.x / 2.0f; + float halfHeight = size.y / 2.0f; + float halfDepth = size.z / 2.0f; + + GLfloat vertices[] = { + -halfWidth, -halfHeight, -halfDepth, + halfWidth, -halfHeight, -halfDepth, + halfWidth, halfHeight, -halfDepth, + -halfWidth, halfHeight, -halfDepth, + -halfWidth, -halfHeight, halfDepth, + halfWidth, -halfHeight, halfDepth, + halfWidth, halfHeight, halfDepth, + -halfWidth, halfHeight, halfDepth, + }; + + GLuint indices[] = { + 0, 1, 1, 2, 2, 3, 3, 0, // Back face + 4, 5, 5, 6, 6, 7, 7, 4, // Front face + 0, 4, 1, 5, 2, 6, 3, 7 // Connecting lines + }; + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(0); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + + void render() { + setupRender(); + + glm::mat4 transform = glm::translate(glm::mat4(1.0f), center); + + glBindVertexArray(VAO); + glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + } +}; + +struct Node { + BoundingBox bbox; + std::vector triangles; + Node* childA; + Node* childB; + bool isLeaf; + + ~Node() { + delete childA; + delete childB; + } +}; + +class BVH { +public: + BoundingBox rootBBox; + Node* root; + + BVH(const std::vector& triangles) { + root = buildBVH(triangles, 0); + rootBBox = root->bbox; + } + + ~BVH() { + delete root; + } + + void renderBVH(Shader& shader, int targetDepth) { + if (root) { + renderNodeRecursive(root, shader, 0, targetDepth); + } + } + +private: + void renderNodeRecursive(Node* node, Shader& shader, int currentDepth, int targetDepth) { + if (!node) return; + if (currentDepth == targetDepth) { + glm::vec3 color; + switch (currentDepth % 6) { + case 0: color = glm::vec3(1.0f, 0.0f, 0.0f); break; // Red - Root + case 1: color = glm::vec3(0.0f, 1.0f, 0.0f); break; // Green - Level 1 + case 2: color = glm::vec3(0.0f, 0.0f, 1.0f); break; // Blue - Level 2 + case 3: color = glm::vec3(1.0f, 1.0f, 0.0f); break; // Yellow - Level 3 + case 4: color = glm::vec3(1.0f, 0.0f, 1.0f); break; // Magenta - Level 4 + case 5: color = glm::vec3(0.0f, 1.0f, 1.0f); break; // Cyan - Level 5 + } + + // Set model matrix to center the box at its center + glm::mat4 boxModel = glm::translate(glm::mat4(1.0f), node->bbox.center); + shader.setMat4("model", boxModel); + shader.setVec3("color", color); + + node->bbox.render(); + } + + // Continue traversing children + if (currentDepth < targetDepth && !node->isLeaf) { + if (node->childA) renderNodeRecursive(node->childA, shader, currentDepth + 1, targetDepth); + if (node->childB) renderNodeRecursive(node->childB, shader, currentDepth + 1, targetDepth); + } + } + Node* buildBVH(const std::vector& triangles, int depth) { + Node* node = new Node(); + for (const auto& tri : triangles) { + node->bbox.update(tri); + } + + if (triangles.size() <= 2 || depth >= MAX_DEPTH) { + node->triangles = triangles; + node->isLeaf = true; + node->childA = nullptr; + node->childB = nullptr; + return node; + } + + // Split along longest axis + glm::vec3 extent = node->bbox.max - node->bbox.min; + int axis = 0; + if (extent.y > extent.x && extent.y > extent.z) axis = 1; + else if (extent.z > extent.x) axis = 2; + + float splitPos = node->bbox.min[axis] + extent[axis] * 0.5f; + + std::vector leftTris, rightTris; + for (const auto& tri : triangles) { + float centroid = (tri.v0[axis] + tri.v1[axis] + tri.v2[axis]) / 3.0f; + if (centroid < splitPos) leftTris.push_back(tri); + else rightTris.push_back(tri); + } + + // Where all triangles go to one side + if (leftTris.empty() || rightTris.empty()) { + leftTris.clear(); + rightTris.clear(); + for (size_t i = 0; i < triangles.size(); ++i) { + if (i < triangles.size() / 2) leftTris.push_back(triangles[i]); + else rightTris.push_back(triangles[i]); + } + } + + node->childA = buildBVH(leftTris, depth + 1); + node->childB = buildBVH(rightTris, depth + 1); + node->isLeaf = false; + return node; + } +}; + +int main() +{ + // Initialize GLFW + if (!glfwInit()) { + std::cerr << "Failed to initialize GLFW" << std::endl; + return -1; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "GPU Marching Cubes - Stanford Bunny", NULL, NULL); + if (window == NULL) { + std::cerr << "Failed to create GLFW window" << std::endl; + glfwTerminate(); + return -1; + } + + glfwMakeContextCurrent(window); + glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); + glfwSetCursorPosCallback(window, mouse_callback); + glfwSetScrollCallback(window, scroll_callback); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { + std::cerr << "Failed to initialize GLAD" << std::endl; + return -1; + } + + // Configure OpenGL + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Load shaders + std::cout << "Loading shaders..." << std::endl; + Shader modelShader("shaders/vertex.vs", "shaders/simple_fragment.fs"); + Shader boxShader("shaders/box.vs", "shaders/box.fs"); + std::cout << "Shaders loaded successfully" << std::endl; + + + // Load the Stanford bunny model + std::cout << "Loading Stanford bunny model..." << std::endl; + Model bunnyModel("models/stanford-bunny/source/bunny.obj"); + std::cout << "Model loaded with " << bunnyModel.meshes.size() << " mesh(es)" << std::endl; + + BoundingBox boundingBox; + // Collect all triangles and compute bounding box + std::vector allTriangles; + for (const auto& mesh : bunnyModel.meshes) { + for (size_t i = 0; i < mesh.indices.size(); i += 3) { + Triangle tri; + tri.v0 = glm::vec3(mesh.vertices[mesh.indices[i]].Position); + tri.v1 = glm::vec3(mesh.vertices[mesh.indices[i + 1]].Position); + tri.v2 = glm::vec3(mesh.vertices[mesh.indices[i + 2]].Position); + glm::vec3 edge1 = tri.v1 - tri.v0; + glm::vec3 edge2 = tri.v2 - tri.v0; + tri.normal = glm::normalize(glm::cross(edge1, edge2)); + allTriangles.push_back(tri); + boundingBox.update(tri); + } + } + + std::cout << "Building BVH with " << allTriangles.size() << " triangles..." << std::endl; + BVH* bvh = new BVH(allTriangles); + std::cout << "BVH construction completed" << std::endl; + + // Print controls + std::cout << "\n=== CONTROLS ===" << std::endl; + std::cout << "WASD + Mouse: Camera movement" << std::endl; + std::cout << "B: Toggle BVH visualization" << std::endl; + std::cout << "M: Toggle model visibility" << std::endl; + std::cout << "UP/DOWN arrows: Change BVH depth level to view" << std::endl; + std::cout << "LEFT/RIGHT arrows: Change BVH construction depth (rebuilds BVH)" << std::endl; + std::cout << "Current BVH construction depth: " << MAX_DEPTH << std::endl; + std::cout << "Current view depth: " << currentViewDepth << std::endl; + std::cout << "===============\n" << std::endl; + + // Render loop + std::cout << "Starting render loop..." << std::endl; + while (!glfwWindowShouldClose(window)) { + // Per-frame time logic + float currentFrame = static_cast(glfwGetTime()); + deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; + + // Input + processInput(window); + + // Rebuild BVH if depth changed + if (needsRebuild) { + delete bvh; + bvh = new BVH(allTriangles); + std::cout << "BVH rebuilt with depth " << MAX_DEPTH << std::endl; + needsRebuild = false; + } + + // Render + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Set up matrices + glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); + glm::mat4 view = camera.GetViewMatrix(); + glm::mat4 model = glm::mat4(1.0f); + + // Render BVH Bounding Boxes + if (showBVH) { + boxShader.use(); + boxShader.setMat4("projection", projection); + boxShader.setMat4("view", view); + bvh->renderBVH(boxShader, currentViewDepth); + } + + // Render the mesh + if (showModel) { + modelShader.use(); + modelShader.setMat4("projection", projection); + modelShader.setMat4("view", view); + modelShader.setMat4("model", model); + + // Set lighting + modelShader.setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f)); + modelShader.setVec3("viewPos", camera.Position); + modelShader.setVec3("dirLight.direction", glm::vec3(-0.2f, -1.0f, -0.3f)); + modelShader.setVec3("dirLight.ambient", glm::vec3(0.6f, 0.6f, 0.6f)); + modelShader.setVec3("dirLight.diffuse", glm::vec3(0.8f, 0.8f, 0.8f)); + modelShader.setVec3("dirLight.specular", glm::vec3(1.0f, 1.0f, 1.0f)); + + // Set material + modelShader.setVec3("material.ambient", glm::vec3(0.2f, 0.2f, 0.2f)); + modelShader.setVec3("material.diffuse", glm::vec3(0.8f, 0.8f, 0.8f)); + modelShader.setVec3("material.specular", glm::vec3(1.0f, 1.0f, 1.0f)); + modelShader.setFloat("material.shininess", 32.0f); + + // Render + bunnyModel.Draw(modelShader); + } + + // Swap buffers and poll events + glfwSwapBuffers(window); + glfwPollEvents(); + } + + // Cleanup + std::cout << "Cleaning up..." << std::endl; + delete bvh; + bunnyModel.meshes.clear(); + glfwTerminate(); + std::cout << "Test completed successfully!" << std::endl; + return 0; +} + +// Input callback functions +void processInput(GLFWwindow *window) +{ + static bool bKeyPressed = false; + static bool mKeyPressed = false; + static bool upKeyPressed = false; + static bool downKeyPressed = false; + static bool leftKeyPressed = false; + static bool rightKeyPressed = false; + + if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) + glfwSetWindowShouldClose(window, true); + + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.ProcessKeyboard(FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.ProcessKeyboard(BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.ProcessKeyboard(LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.ProcessKeyboard(RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_C) == GLFW_PRESS) + camera.ProcessKeyboard(UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) + camera.ProcessKeyboard(DOWN, deltaTime); + + // Toggle BVH visibility with 'B' key + if (glfwGetKey(window, GLFW_KEY_B) == GLFW_PRESS) { + if (!bKeyPressed) { + showBVH = !showBVH; + std::cout << "BVH visualization: " << (showBVH ? "ON" : "OFF") << std::endl; + bKeyPressed = true; + } + } else { + bKeyPressed = false; + } + + // Toggle Model visibility with 'M' key + if (glfwGetKey(window, GLFW_KEY_M) == GLFW_PRESS) { + if (!mKeyPressed) { + showModel = !showModel; + std::cout << "Model visualization: " << (showModel ? "ON" : "OFF") << std::endl; + mKeyPressed = true; + } + } else { + mKeyPressed = false; + } + + // Increase view depth with UP arrow + if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) { + if (!upKeyPressed) { + if (currentViewDepth < MAX_DEPTH) { + currentViewDepth++; + std::cout << "Viewing depth level: " << currentViewDepth << std::endl; + } else { + std::cout << "Cannot view beyond max construction depth (" << MAX_DEPTH << ")" << std::endl; + } + upKeyPressed = true; + } + } else { + upKeyPressed = false; + } + + // Decrease view depth with DOWN arrow + if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) { + if (!downKeyPressed && currentViewDepth > 0) { + currentViewDepth--; + std::cout << "Viewing depth level: " << currentViewDepth << std::endl; + downKeyPressed = true; + } + } else { + downKeyPressed = false; + } + + // Increase construction depth with RIGHT arrow (rebuilds BVH) + if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS) { + if (!rightKeyPressed) { + MAX_DEPTH++; + std::cout << "BVH construction depth: " << MAX_DEPTH << " (rebuilding...)" << std::endl; + needsRebuild = true; + rightKeyPressed = true; + } + } else { + rightKeyPressed = false; + } + + // Decrease construction depth with LEFT arrow (rebuilds BVH) + if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS) { + if (!leftKeyPressed && MAX_DEPTH > 0) { + MAX_DEPTH--; + // Ensure view depth doesn't exceed construction depth + if (currentViewDepth > MAX_DEPTH) { + currentViewDepth = MAX_DEPTH; + } + std::cout << "BVH construction depth: " << MAX_DEPTH << " (rebuilding...)" << std::endl; + needsRebuild = true; + leftKeyPressed = true; + } + } else { + leftKeyPressed = false; + } +} + +void framebuffer_size_callback(GLFWwindow* window, int width, int height) +{ + glViewport(0, 0, width, height); +} + +void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) +{ + float xpos = static_cast(xposIn); + float ypos = static_cast(yposIn); + + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; + + lastX = xpos; + lastY = ypos; + + camera.ProcessMouseMovement(xoffset, yoffset); +} + +void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) +{ + camera.ProcessMouseScroll(static_cast(yoffset)); +} diff --git a/Marching Cubes/testGPU.cpp b/Marching Cubes/testGPU.cpp index 7d66bb1..4983b6e 100644 --- a/Marching Cubes/testGPU.cpp +++ b/Marching Cubes/testGPU.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // Window settings const unsigned int SCR_WIDTH = 1200; @@ -23,6 +24,14 @@ float lastX = SCR_WIDTH / 2.0f; float lastY = SCR_HEIGHT / 2.0f; bool firstMouse = true; +int MAX_DEPTH = 10; +bool showBVH = true; +bool showModel = true; + +// Key state tracking for debouncing +bool bKeyPressed = false; +bool mKeyPressed = false; + // Timing float deltaTime = 0.0f; float lastFrame = 0.0f; @@ -33,6 +42,106 @@ void mouse_callback(GLFWwindow* window, double xpos, double ypos); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); void processInput(GLFWwindow *window); +struct Triangle { + glm::vec3 v0, v1, v2; + glm::vec3 normal; +}; + +struct BoundingBox { + glm::vec3 min = glm::vec3(1e10f); + glm::vec3 max = glm::vec3(-1e10f); + glm::vec3 center = (min + max) * 0.5f; + unsigned int VAO = 0; + unsigned int VBO = 0; + unsigned int EBO = 0; + glm::mat4 modelMatrix = glm::mat4(1.0f); + + void update(const glm::vec3& point) { + min = glm::min(min, point); + max = glm::max(max, point); + center = (min + max) * 0.5f; + } + + void update(const Triangle& tri) { + update(tri.v0); + update(tri.v1); + update(tri.v2); + } + + ~BoundingBox(){ + if(VAO) glDeleteVertexArrays(1, &VAO); + if(VBO) glDeleteBuffers(1, &VBO); + if(EBO) glDeleteBuffers(1, &EBO); + } + + void setupRender() { + if (VAO != 0) return; // Already setup + + glm::vec3 size = max - min; + float halfWidth = size.x / 2.0f; + float halfHeight = size.y / 2.0f; + float halfDepth = size.z / 2.0f; + + GLfloat vertices[] = { + -halfWidth, -halfHeight, -halfDepth, + halfWidth, -halfHeight, -halfDepth, + halfWidth, halfHeight, -halfDepth, + -halfWidth, halfHeight, -halfDepth, + -halfWidth, -halfHeight, halfDepth, + halfWidth, -halfHeight, halfDepth, + halfWidth, halfHeight, halfDepth, + -halfWidth, halfHeight, halfDepth, + }; + + GLuint indices[] = { + 0, 1, 1, 2, 2, 3, 3, 0, // Back face + 4, 5, 5, 6, 6, 7, 7, 4, // Front face + 0, 4, 1, 5, 2, 6, 3, 7 // Connecting lines + }; + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(0); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + + void render() { + setupRender(); + + glm::mat4 transform = glm::translate(glm::mat4(1.0f), center); + + glBindVertexArray(VAO); + glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + } +}; + +struct Node { + BoundingBox bbox; + std::vector triangles; + Node* childA; + Node* childB; + bool isLeaf; + + ~Node() { + delete childA; + delete childB; + } +}; + // Helper to compute distance from point to triangle and return closest point float distanceToTriangle(const glm::vec3& p, const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, glm::vec3* outClosestPoint = nullptr) { glm::vec3 edge0 = v1 - v0; @@ -95,19 +204,155 @@ float distanceToTriangle(const glm::vec3& p, const glm::vec3& v0, const glm::vec return glm::distance(p, closest); } +class BVH { +public: + BoundingBox rootBBox; + Node* root; + + BVH(const std::vector& triangles) { + root = buildBVH(triangles, 0); + rootBBox = root->bbox; + } + + ~BVH() { + delete root; + } + + void renderBVH(Shader& shader, int targetDepth) { + if (root) { + renderNodeRecursive(root, shader, 0, targetDepth); + } + } + + float queryDistance(const glm::vec3& point, glm::vec3* outClosestPoint = nullptr, glm::vec3* outNormal = nullptr) { + if (!root) return 1e10f; + + float bestDist = 1e10f; + glm::vec3 bestPoint, bestNormal; + queryDistanceRecursive(root, point, bestDist, bestPoint, bestNormal); + + if (outClosestPoint) *outClosestPoint = bestPoint; + if (outNormal) *outNormal = bestNormal; + return bestDist; + } + +private: + void queryDistanceRecursive(Node* node, const glm::vec3& point, + float& bestDist, glm::vec3& bestPoint, glm::vec3& bestNormal) { + if (!node) return; + + // Early exit if bounding box is too far + float boxDist = distanceToBox(point, node->bbox.min, node->bbox.max); + if (boxDist > bestDist) return; + + if (node->isLeaf) { + // Test all triangles in this leaf + for (const auto& tri : node->triangles) { + glm::vec3 closestPoint; + float dist = distanceToTriangle(point, tri.v0, tri.v1, tri.v2, &closestPoint); + if (dist < bestDist) { + bestDist = dist; + bestPoint = closestPoint; + bestNormal = tri.normal; + } + } + } else { + // Recurse to children + queryDistanceRecursive(node->childA, point, bestDist, bestPoint, bestNormal); + queryDistanceRecursive(node->childB, point, bestDist, bestPoint, bestNormal); + } + } + + float distanceToBox(const glm::vec3& point, const glm::vec3& boxMin, const glm::vec3& boxMax) { + glm::vec3 closest = glm::clamp(point, boxMin, boxMax); + return glm::distance(point, closest); + } + + void renderNodeRecursive(Node* node, Shader& shader, int currentDepth, int targetDepth) { + if (!node) return; + if (currentDepth == targetDepth) { + glm::vec3 color; + switch (currentDepth % 6) { + case 0: color = glm::vec3(1.0f, 0.0f, 0.0f); break; // Red - Root + case 1: color = glm::vec3(0.0f, 1.0f, 0.0f); break; // Green - Level 1 + case 2: color = glm::vec3(0.0f, 0.0f, 1.0f); break; // Blue - Level 2 + case 3: color = glm::vec3(1.0f, 1.0f, 0.0f); break; // Yellow - Level 3 + case 4: color = glm::vec3(1.0f, 0.0f, 1.0f); break; // Magenta - Level 4 + case 5: color = glm::vec3(0.0f, 1.0f, 1.0f); break; // Cyan - Level 5 + } + + // Set model matrix to center the box at its center + glm::mat4 boxModel = glm::translate(glm::mat4(1.0f), node->bbox.center); + shader.setMat4("model", boxModel); + shader.setVec3("color", color); + + node->bbox.render(); + } + + // Continue traversing children + if (currentDepth < targetDepth && !node->isLeaf) { + if (node->childA) renderNodeRecursive(node->childA, shader, currentDepth + 1, targetDepth); + if (node->childB) renderNodeRecursive(node->childB, shader, currentDepth + 1, targetDepth); + } + } + Node* buildBVH(const std::vector& triangles, int depth) { + Node* node = new Node(); + for (const auto& tri : triangles) { + node->bbox.update(tri); + } + + if (triangles.size() <= 2 || depth >= MAX_DEPTH) { + node->triangles = triangles; + node->isLeaf = true; + node->childA = nullptr; + node->childB = nullptr; + return node; + } + + // Split along longest axis + glm::vec3 extent = node->bbox.max - node->bbox.min; + int axis = 0; + if (extent.y > extent.x && extent.y > extent.z) axis = 1; + else if (extent.z > extent.x) axis = 2; + + float splitPos = node->bbox.min[axis] + extent[axis] * 0.5f; + + std::vector leftTris, rightTris; + for (const auto& tri : triangles) { + float centroid = (tri.v0[axis] + tri.v1[axis] + tri.v2[axis]) / 3.0f; + if (centroid < splitPos) leftTris.push_back(tri); + else rightTris.push_back(tri); + } + + // Where all triangles go to one side + if (leftTris.empty() || rightTris.empty()) { + leftTris.clear(); + rightTris.clear(); + for (size_t i = 0; i < triangles.size(); ++i) { + if (i < triangles.size() / 2) leftTris.push_back(triangles[i]); + else rightTris.push_back(triangles[i]); + } + } + + node->childA = buildBVH(leftTris, depth + 1); + node->childB = buildBVH(rightTris, depth + 1); + node->isLeaf = false; + return node; + } +}; + // Helper function to generate a signed distance field from a mesh std::vector generateSDFFromMesh( const Model& model, int gridSizeX, int gridSizeY, int gridSizeZ, - glm::vec3& outBoundsMin, glm::vec3& outBoundsMax) + glm::vec3& outBoundsMin, glm::vec3& outBoundsMax, + BVH* modelBVH = nullptr) { // Collect all triangles with their normals - struct Triangle { - glm::vec3 v0, v1, v2; - glm::vec3 normal; - }; std::vector triangles; - + + // Create Bounding Box for the model + outBoundsMin = glm::vec3(1e10f); outBoundsMax = glm::vec3(-1e10f); @@ -170,17 +415,25 @@ std::vector generateSDFFromMesh( ); // Find minimum distance to any triangle and track closest - float minDist = 1e10f; + float minDist; glm::vec3 closestPoint; glm::vec3 closestNormal; - for (const auto& tri : triangles) { - glm::vec3 currentClosest; - float dist = distanceToTriangle(gridPos, tri.v0, tri.v1, tri.v2, ¤tClosest); - if (dist < minDist) { - minDist = dist; - closestPoint = currentClosest; - closestNormal = tri.normal; + if (modelBVH) { + // Use BVH for fast distance queries + minDist = modelBVH->queryDistance(gridPos, &closestPoint, &closestNormal); + } else { + // Fallback to brute force + minDist = 1e10f; + + for (const auto& tri : triangles) { + glm::vec3 currentClosest; + float dist = distanceToTriangle(gridPos, tri.v0, tri.v1, tri.v2, ¤tClosest); + if (dist < minDist) { + minDist = dist; + closestPoint = currentClosest; + closestNormal = tri.normal; + } } } @@ -251,12 +504,34 @@ int main() // Load shaders std::cout << "Loading shaders..." << std::endl; Shader marchShader("shaders/vertex.vs", "shaders/simple_fragment.fs"); + Shader boxShader("shaders/box.vs", "shaders/box.fs"); std::cout << "Shaders loaded successfully" << std::endl; // Load the Stanford bunny model std::cout << "Loading Stanford bunny model..." << std::endl; Model bunnyModel("models/stanford-bunny/source/bunny.obj"); std::cout << "Model loaded with " << bunnyModel.meshes.size() << " mesh(es)" << std::endl; + + BoundingBox boundingBox; + // Collect all triangles and compute bounding box + std::vector allTriangles; + for (const auto& mesh : bunnyModel.meshes) { + for (size_t i = 0; i < mesh.indices.size(); i += 3) { + Triangle tri; + tri.v0 = glm::vec3(mesh.vertices[mesh.indices[i]].Position); + tri.v1 = glm::vec3(mesh.vertices[mesh.indices[i + 1]].Position); + tri.v2 = glm::vec3(mesh.vertices[mesh.indices[i + 2]].Position); + glm::vec3 edge1 = tri.v1 - tri.v0; + glm::vec3 edge2 = tri.v2 - tri.v0; + tri.normal = glm::normalize(glm::cross(edge1, edge2)); + allTriangles.push_back(tri); + boundingBox.update(tri); + } + } + + std::cout << "Building BVH with " << allTriangles.size() << " triangles..." << std::endl; + BVH* bvh = new BVH(allTriangles); + std::cout << "BVH construction completed" << std::endl; // Set grid resolution (higher = more detail) int gridSizeX = 80; @@ -264,9 +539,15 @@ int main() int gridSizeZ = 80; // Generate SDF from bunny model first to get bounds - std::cout << "Generating signed distance field from bunny model..." << std::endl; + std::cout << "Generating signed distance field from bunny model using BVH acceleration..." << std::endl; glm::vec3 boundsMin, boundsMax; - std::vector sdfGrid = generateSDFFromMesh(bunnyModel, gridSizeX, gridSizeY, gridSizeZ, boundsMin, boundsMax); + + auto startTime = std::chrono::high_resolution_clock::now(); + std::vector sdfGrid = generateSDFFromMesh(bunnyModel, gridSizeX, gridSizeY, gridSizeZ, boundsMin, boundsMax, bvh); + auto endTime = std::chrono::high_resolution_clock::now(); + + auto duration = std::chrono::duration_cast(endTime - startTime); + std::cout << "SDF generation completed in " << duration.count() << " ms using BVH acceleration" << std::endl; if (sdfGrid.empty()) { std::cerr << "Failed to generate SDF grid" << std::endl; @@ -362,37 +643,48 @@ int main() // Render glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - + // Set up matrices glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); glm::mat4 view = camera.GetViewMatrix(); glm::mat4 model = glm::mat4(1.0f); - model = glm::rotate(model, currentFrame * glm::radians(30.0f), glm::vec3(0.0f, 1.0f, 0.0f)); + // model = glm::rotate(model, currentFrame * glm::radians(30.0f), glm::vec3(0.0f, 1.0f, 0.0f)); // Model is already in world space, no transformation needed - // Render the marched mesh - marchShader.use(); - marchShader.setMat4("projection", projection); - marchShader.setMat4("view", view); - marchShader.setMat4("model", model); - - // Set lighting - marchShader.setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f)); - marchShader.setVec3("viewPos", camera.Position); - marchShader.setVec3("dirLight.direction", glm::vec3(-0.2f, -1.0f, -0.3f)); - marchShader.setVec3("dirLight.ambient", glm::vec3(0.6f, 0.6f, 0.6f)); - marchShader.setVec3("dirLight.diffuse", glm::vec3(0.8f, 0.8f, 0.8f)); - marchShader.setVec3("dirLight.specular", glm::vec3(1.0f, 1.0f, 1.0f)); - - // Set material - marchShader.setVec3("material.ambient", glm::vec3(0.2f, 0.2f, 0.2f)); - marchShader.setVec3("material.diffuse", glm::vec3(0.8f, 0.8f, 0.8f)); - marchShader.setVec3("material.specular", glm::vec3(1.0f, 1.0f, 1.0f)); - marchShader.setFloat("material.shininess", 32.0f); + if (showModel) { + // Render the marched mesh + marchShader.use(); + marchShader.setMat4("projection", projection); + marchShader.setMat4("view", view); + marchShader.setMat4("model", model); + + // Set lighting + marchShader.setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f)); + marchShader.setVec3("viewPos", camera.Position); + marchShader.setVec3("dirLight.direction", glm::vec3(-0.2f, -1.0f, -0.3f)); + marchShader.setVec3("dirLight.ambient", glm::vec3(0.6f, 0.6f, 0.6f)); + marchShader.setVec3("dirLight.diffuse", glm::vec3(0.8f, 0.8f, 0.8f)); + marchShader.setVec3("dirLight.specular", glm::vec3(1.0f, 1.0f, 1.0f)); + + // Set material + marchShader.setVec3("material.ambient", glm::vec3(0.2f, 0.2f, 0.2f)); + marchShader.setVec3("material.diffuse", glm::vec3(0.8f, 0.8f, 0.8f)); + marchShader.setVec3("material.specular", glm::vec3(1.0f, 1.0f, 1.0f)); + marchShader.setFloat("material.shininess", 32.0f); + + // Render + glBindVertexArray(marchVAO); + glDrawElements(GL_TRIANGLES, static_cast(indices.size()), GL_UNSIGNED_INT, 0); + } + + // Render BVH Bounding Boxes + if (showBVH) { + boxShader.use(); + boxShader.setMat4("projection", projection); + boxShader.setMat4("view", view); + bvh->renderBVH(boxShader, MAX_DEPTH); + } - // Render - glBindVertexArray(marchVAO); - glDrawElements(GL_TRIANGLES, static_cast(indices.size()), GL_UNSIGNED_INT, 0); // Swap buffers and poll events glfwSwapBuffers(window); @@ -428,6 +720,22 @@ void processInput(GLFWwindow *window) camera.ProcessKeyboard(UP, deltaTime); if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) camera.ProcessKeyboard(DOWN, deltaTime); + + // Handle B key with debouncing + bool bCurrentlyPressed = glfwGetKey(window, GLFW_KEY_B) == GLFW_PRESS; + if (bCurrentlyPressed && !bKeyPressed) { + showBVH = !showBVH; + std::cout << "BVH display: " << (showBVH ? "ON" : "OFF") << std::endl; + } + bKeyPressed = bCurrentlyPressed; + + // Handle M key with debouncing + bool mCurrentlyPressed = glfwGetKey(window, GLFW_KEY_M) == GLFW_PRESS; + if (mCurrentlyPressed && !mKeyPressed) { + showModel = !showModel; + std::cout << "Model display: " << (showModel ? "ON" : "OFF") << std::endl; + } + mKeyPressed = mCurrentlyPressed; } void framebuffer_size_callback(GLFWwindow* window, int width, int height) diff --git a/Rubiks/RCube.mtl b/Rubiks/RCube.mtl new file mode 100644 index 0000000..236ef3a --- /dev/null +++ b/Rubiks/RCube.mtl @@ -0,0 +1,12 @@ +# Blender 4.5.0 MTL File: 'RCube.blend' +# www.blender.org + +newmtl Material +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.800000 0.800000 0.800000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/Rubiks/RubiksCube.cpp b/Rubiks/RubiksCube.cpp new file mode 100644 index 0000000..2df70db --- /dev/null +++ b/Rubiks/RubiksCube.cpp @@ -0,0 +1,398 @@ +#include "RubiksCube.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace { +const std::array kTargetColors = { + glm::vec3(1.0f, 0.0f, 0.0f), // index 0 -> red (restore) + glm::vec3(1.0f, 0.6f, 0.0f), // index 1 -> orange (left) + glm::vec3(1.0f, 1.0f, 1.0f), // index 2 -> white (up) + glm::vec3(1.0f, 1.0f, 0.0f), // index 3 -> yellow (bottom) + glm::vec3(0.0f, 1.0f, 0.0f), // index 4 -> green (front/side) + glm::vec3(0.0f, 0.2f, 1.0f) // index 5 -> blue (back) +}; + +struct FacePivot { + glm::vec3 position; // Center of the rotating face + glm::vec3 axis; // Rotation axis (normal to face) +}; + +const std::array kFacePivots = { + // 6 outer faces + FacePivot{{+1, 0, 0}, {1, 0, 0}}, // Right + FacePivot{{-1, 0, 0}, {-1, 0, 0}}, // Left + FacePivot{{0, +1, 0}, {0, 1, 0}}, // Up + FacePivot{{0, -1, 0}, {0, -1, 0}}, // Down + FacePivot{{0, 0, +1}, {0, 0, 1}}, // Front + FacePivot{{0, 0, -1}, {0, 0, -1}}, // Back + // 3 middle slices (M, E, S) + FacePivot{{0, 0, 0}, {1, 0, 0}}, // M (between L/R) + FacePivot{{0, 0, 0}, {0, 1, 0}}, // E (between U/D) + FacePivot{{0, 0, 0}, {0, 0, 1}}, // S (between F/B) +}; +} + + +RubiksCube::RubiksCube(const std::string& modelPath, + const std::string& faceTexturePath, + const glm::vec3& cubeCenter, + float cubieScaleValue, + float cubieSpacingValue) + : model(modelPath.c_str()), + center(cubeCenter), + cubieScale(cubieScaleValue), + cubieSpacing(cubieSpacingValue) { + buildCubieOffsets(); + initCubieFaceColors(); + if (!faceTexturePath.empty()) { + loadFaceTextures(faceTexturePath); + } +} + +RubiksCube::~RubiksCube() { + releaseTextures(); +} + +void RubiksCube::applyMaterial(Shader& shader) const { + if (faceTexturesAvailable && faceTexturesEnabled) { + shader.setBool("useFaceTextures", true); + // Texture units 10-15 so mesh textures remain undisturbed + for (int i = 0; i < static_cast(faceTextures.size()); ++i) { + glActiveTexture(GL_TEXTURE10 + i); + glBindTexture(GL_TEXTURE_2D, faceTextures[i]); + shader.setInt("faceTextures[" + std::to_string(i) + "]", 10 + i); + } + glActiveTexture(GL_TEXTURE0); + } else { + shader.setBool("useFaceTextures", false); + } +} + +void RubiksCube::draw(Shader& shader, const std::vector* cubieIDs) { + // Precompute the active face rotation once per frame + glm::mat4 animRotation4(1.0f); + if (animating && animatingFaceIndex >= 0) { + const auto& pivot = kFacePivots[animatingFaceIndex]; + animRotation4 = glm::rotate(glm::mat4(1.0f), glm::radians(currentAngle), pivot.axis); + } + + for (size_t i = 0; i < cubieOffsets.size(); ++i) { + const auto& offset = cubieOffsets[i]; + glm::mat4 modelMatrix = getCubieModelMatrix(offset); + + bool isAnimatingCubie = false; + if (animating) { + isAnimatingCubie = std::find(animatingCubieIndices.begin(), + animatingCubieIndices.end(), i) + != animatingCubieIndices.end(); + if (isAnimatingCubie) { + glm::mat4 toOrigin = glm::translate(glm::mat4(1.0f), -center); + glm::mat4 fromOrigin = glm::translate(glm::mat4(1.0f), center); + modelMatrix = fromOrigin * animRotation4 * toOrigin * modelMatrix; + } + } + + shader.setMat4("model", modelMatrix); + + const auto& faceColors = cubieFaceColors[i]; + for (int f = 0; f < 6; ++f) { + shader.setInt("faceColorIndex[" + std::to_string(f) + "]", faceColors.colorIndex[f]); + } + + if (cubieIDs && i < cubieIDs->size()) { + shader.setInt("cubieID", static_cast((*cubieIDs)[i])); + } else { + shader.setInt("cubieID", -1); + } + model.Draw(shader); + } +} + +glm::mat4 RubiksCube::getCubieModelMatrix(const glm::vec3& offset) const { + glm::mat4 modelMatrix = glm::mat4(1.0f); + glm::vec3 worldPos = center + offset * cubieSpacing; + modelMatrix = glm::translate(modelMatrix, worldPos); + modelMatrix = glm::scale(modelMatrix, glm::vec3(cubieScale)); + return modelMatrix; +} + +void RubiksCube::setFaceTexturesEnabled(bool enabled) { + faceTexturesEnabled = enabled; +} + +void RubiksCube::buildCubieOffsets() { + cubieOffsets.reserve(26); + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + for (int z = -1; z <= 1; ++z) { + if (x == 0 && y == 0 && z == 0) { + continue; + } + cubieOffsets.emplace_back(static_cast(x), + static_cast(y), + static_cast(z)); + } + } + } +} + +void RubiksCube::initCubieFaceColors() { + // Face indices: 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z + cubieFaceColors.resize(cubieOffsets.size()); + + for (size_t i = 0; i < cubieOffsets.size(); ++i) { + const auto& pos = cubieOffsets[i]; + CubieFaces& faces = cubieFaceColors[i]; + + faces.colorIndex[0] = (pos.x > 0.5f) ? 0 : -1; + faces.colorIndex[1] = (pos.x < -0.5f) ? 1 : -1; + faces.colorIndex[2] = (pos.y > 0.5f) ? 2 : -1; + faces.colorIndex[3] = (pos.y < -0.5f) ? 3 : -1; + faces.colorIndex[4] = (pos.z > 0.5f) ? 4 : -1; + faces.colorIndex[5] = (pos.z < -0.5f) ? 5 : -1; + } +} + +void RubiksCube::rotateCubieFaceColors(size_t cubieIndex, int axis, bool clockwise) { + // axis: 0=X, 1=Y, 2=Z + CubieFaces& faces = cubieFaceColors[cubieIndex]; + std::array oldColors = faces.colorIndex; + + if (axis == 0) { + if (clockwise) { + faces.colorIndex[4] = oldColors[2]; + faces.colorIndex[3] = oldColors[4]; + faces.colorIndex[5] = oldColors[3]; + faces.colorIndex[2] = oldColors[5]; + } else { + faces.colorIndex[5] = oldColors[2]; + faces.colorIndex[3] = oldColors[5]; + faces.colorIndex[4] = oldColors[3]; + faces.colorIndex[2] = oldColors[4]; + } + } else if (axis == 1) { + if (clockwise) { + faces.colorIndex[5] = oldColors[0]; + faces.colorIndex[1] = oldColors[5]; + faces.colorIndex[4] = oldColors[1]; + faces.colorIndex[0] = oldColors[4]; + } else { + faces.colorIndex[4] = oldColors[0]; + faces.colorIndex[1] = oldColors[4]; + faces.colorIndex[5] = oldColors[1]; + faces.colorIndex[0] = oldColors[5]; + } + } else { + if (clockwise) { + faces.colorIndex[2] = oldColors[0]; + faces.colorIndex[1] = oldColors[2]; + faces.colorIndex[3] = oldColors[1]; + faces.colorIndex[0] = oldColors[3]; + } else { + faces.colorIndex[3] = oldColors[0]; + faces.colorIndex[1] = oldColors[3]; + faces.colorIndex[2] = oldColors[1]; + faces.colorIndex[0] = oldColors[2]; + } + } +} + +void RubiksCube::loadFaceTextures(const std::string& texturePath) { + int width = 0; + int height = 0; + int channels = 0; + + stbi_set_flip_vertically_on_load(false); + unsigned char* img = stbi_load(texturePath.c_str(), &width, &height, &channels, 0); + if (!img) { + std::cerr << "Failed to load Rubik's base tile texture: " << texturePath << std::endl; + return; + } + + const int pixelCount = width * height; + GLenum format = (channels == 4) ? GL_RGBA : GL_RGB; + const int stride = (format == GL_RGBA) ? 4 : 3; + + std::vector recolored(pixelCount * stride, 255); + glGenTextures(static_cast(faceTextures.size()), faceTextures.data()); + + for (int face = 0; face < static_cast(faceTextures.size()); ++face) { + for (int i = 0; i < pixelCount; ++i) { + int srcIdx = i * channels; + int dstIdx = i * stride; + + unsigned char r = img[srcIdx]; + unsigned char g = (channels >= 2) ? img[srcIdx + 1] : img[srcIdx]; + unsigned char b = (channels >= 3) ? img[srcIdx + 2] : img[srcIdx]; + unsigned char a = (channels == 4) ? img[srcIdx + 3] : 255; + + float brightness = std::max({r, g, b}) / 255.0f; + brightness = std::min(1.0f, brightness * 1.2f); + glm::vec3 tinted = kTargetColors[face] * brightness; + + recolored[dstIdx] = static_cast(glm::clamp(tinted.r, 0.0f, 1.0f) * 255.0f); + recolored[dstIdx + 1] = static_cast(glm::clamp(tinted.g, 0.0f, 1.0f) * 255.0f); + recolored[dstIdx + 2] = static_cast(glm::clamp(tinted.b, 0.0f, 1.0f) * 255.0f); + if (format == GL_RGBA) { + recolored[dstIdx + 3] = a; + } + } + + glBindTexture(GL_TEXTURE_2D, faceTextures[face]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // Ensure pixel storage alignment matches our tightly-packed recolored buffer + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, recolored.data()); + glGenerateMipmap(GL_TEXTURE_2D); + + // Debug: print a central pixel of the recolored texture to verify channel values + try { + if (!recolored.empty() && width > 0 && height > 0) { + int sx = width / 2; + int sy = height / 2; + int sampleIdx = sy * width + sx; + int dstIdx = sampleIdx * stride; + if (dstIdx + 2 < static_cast(recolored.size())) { + int r = static_cast(recolored[dstIdx]); + int g = static_cast(recolored[dstIdx + 1]); + int b = static_cast(recolored[dstIdx + 2]); + std::cout << "[RubiksCube] face=" << face << " centerPixel=(" << r << "," << g << "," << b << ")" << std::endl; + } + } + } catch (...) {} + } + + stbi_image_free(img); + faceTexturesAvailable = true; +} + +void RubiksCube::releaseTextures() { + if (faceTexturesAvailable) { + glDeleteTextures(static_cast(faceTextures.size()), faceTextures.data()); + faceTexturesAvailable = false; + } +} + +void RubiksCube::rotateFace(int faceIndex, float angle){ + const auto& pivot = kFacePivots[faceIndex]; + glm::mat4 rotation4 = glm::rotate(glm::mat4(1.0f), glm::radians(angle), pivot.axis); + + int axis; + bool clockwise; + + if (std::abs(pivot.axis.x) > 0.5f) axis = 0; + else if (std::abs(pivot.axis.y) > 0.5f) axis = 1; + else axis = 2; + + float axisSign = (axis == 0) ? pivot.axis.x : ((axis == 1) ? pivot.axis.y : pivot.axis.z); + clockwise = (angle * axisSign) > 0.0f; + + for (size_t i = 0; i < cubieOffsets.size(); ++i) { + if (cubieOnFace(cubieOffsets[i], faceIndex)) { + glm::vec4 pos = rotation4 * glm::vec4(cubieOffsets[i], 1.0f); + cubieOffsets[i] = glm::vec3(pos); + + cubieOffsets[i].x = std::round(cubieOffsets[i].x); + cubieOffsets[i].y = std::round(cubieOffsets[i].y); + cubieOffsets[i].z = std::round(cubieOffsets[i].z); + + rotateCubieFaceColors(i, axis, clockwise); + } + } +} + +void RubiksCube::startFaceRotation(int faceIndex, float angle, float duration) { + if (animating) return; // Don't start new animation while one is in progress + + animating = true; + animatingFaceIndex = faceIndex; + targetAngle = angle; + currentAngle = 0.0f; + animationDuration = duration; + animationTime = 0.0f; + + // Store indices of cubies that will be animated + animatingCubieIndices.clear(); + for (size_t i = 0; i < cubieOffsets.size(); ++i) { + if (cubieOnFace(cubieOffsets[i], faceIndex)) { + animatingCubieIndices.push_back(i); + } + } +} + +bool RubiksCube::updateAnimation(float deltaTime) { + if (!animating) return false; + + animationTime += deltaTime; + + // Use smooth easing (ease-out cubic) + float t = std::min(animationTime / animationDuration, 1.0f); + float easedT = 1.0f - (1.0f - t) * (1.0f - t) * (1.0f - t); + + currentAngle = targetAngle * easedT; + + // Animation complete + if (t >= 1.0f) { + // Apply the final rotation and snap + rotateFace(animatingFaceIndex, targetAngle); + + // Reset animation state + animating = false; + animatingFaceIndex = -1; + targetAngle = 0.0f; + currentAngle = 0.0f; + animationTime = 0.0f; + animatingCubieIndices.clear(); + + return true; // Animation just completed + } + + return false; +} + +int RubiksCube::findCubieAtPosition(const glm::vec3& position) const { + const float epsilon = 0.5f; + for (size_t i = 0; i < cubieOffsets.size(); ++i) { + if (glm::length(cubieOffsets[i] - position) < epsilon) { + return static_cast(i); + } + } + return -1; +} + +bool RubiksCube::cubieOnFace(const glm::vec3& offset, int faceIndex) { + const float epsilon = 0.1f; // Tolerance for floating-point comparison + + switch (faceIndex) { + case 0: // Right face (x == +1) + return std::abs(offset.x - 1.0f) < epsilon; + case 1: // Left face (x == -1) + return std::abs(offset.x + 1.0f) < epsilon; + case 2: // Up face (y == +1) + return std::abs(offset.y - 1.0f) < epsilon; + case 3: // Down face (y == -1) + return std::abs(offset.y + 1.0f) < epsilon; + case 4: // Front face (z == +1) + return std::abs(offset.z - 1.0f) < epsilon; + case 5: // Back face (z == -1) + return std::abs(offset.z + 1.0f) < epsilon; + case 6: // M slice (x == 0, middle between L/R) + return std::abs(offset.x) < epsilon; + case 7: // E slice (y == 0, middle between U/D) + return std::abs(offset.y) < epsilon; + case 8: // S slice (z == 0, middle between F/B) + return std::abs(offset.z) < epsilon; + default: + return false; + } +} diff --git a/Rubiks/RubiksCube.h b/Rubiks/RubiksCube.h new file mode 100644 index 0000000..69082ca --- /dev/null +++ b/Rubiks/RubiksCube.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "shader.h" +#include "model.h" +#include "globals.h" + +// Face indices: 0=+X(Right), 1=-X(Left), 2=+Y(Up), 3=-Y(Down), 4=+Z(Front), 5=-Z(Back) +// Color indices: 0=Red, 1=Orange, 2=White, 3=Yellow, 4=Green, 5=Blue, -1=Black(internal) +struct CubieFaces { + std::array colorIndex; + + CubieFaces() : colorIndex{-1, -1, -1, -1, -1, -1} {} +}; + +class RubiksCube { +public: + RubiksCube(const std::string& modelPath, + const std::string& faceTexturePath, + const glm::vec3& cubeCenter, + float cubieScale, + float cubieSpacing); + ~RubiksCube(); + + RubiksCube(const RubiksCube&) = delete; + RubiksCube& operator=(const RubiksCube&) = delete; + RubiksCube(RubiksCube&&) = delete; + RubiksCube& operator=(RubiksCube&&) = delete; + + void applyMaterial(Shader& shader) const; + // draw optionally accepts a vector of cubie IDs parallel to getCubieOffsets() + void draw(Shader& shader, const std::vector* cubieIDs = nullptr); + + const Model& getModel() const { return model; } + const std::vector& getCubieOffsets() const { return cubieOffsets; } + glm::mat4 getCubieModelMatrix(const glm::vec3& offset) const; + void setFaceTexturesEnabled(bool enabled); + bool areFaceTexturesEnabled() const { return faceTexturesAvailable && faceTexturesEnabled; } + + // Instant rotation (snaps immediately) + void rotateFace(int faceIndex, float angle); + + // Animated rotation methods + void startFaceRotation(int faceIndex, float targetAngle, float duration = 0.3f); + bool updateAnimation(float deltaTime); // Returns true if animation just completed + bool isAnimating() const { return animating; } + float getAnimationProgress() const { return animating ? currentAngle / targetAngle : 0.0f; } + int getAnimatingFace() const { return animatingFaceIndex; } + float getCurrentAnimationAngle() const { return currentAngle; } + + // Get the position that was selected before rotation started + glm::vec3 getLastSelectedPosition() const { return lastSelectedPosition; } + void setLastSelectedPosition(const glm::vec3& pos) { lastSelectedPosition = pos; } + + // Find cubie index at a given position + int findCubieAtPosition(const glm::vec3& position) const; + +private: + void buildCubieOffsets(); + void initCubieFaceColors(); + void rotateCubieFaceColors(size_t cubieIndex, int axis, bool clockwise); + void loadFaceTextures(const std::string& texturePath); + void releaseTextures(); + bool cubieOnFace(const glm::vec3& offset, int faceIndex); + + Model model; + glm::vec3 center; + float cubieScale; + float cubieSpacing; + std::vector cubieOffsets; + std::vector cubieFaceColors; + std::vector animStartFaceColors; + std::array faceTextures{}; + bool faceTexturesAvailable{false}; + bool faceTexturesEnabled{true}; + + // Animation state + bool animating{false}; + int animatingFaceIndex{-1}; + float targetAngle{0.0f}; + float currentAngle{0.0f}; + float animationDuration{0.3f}; + float animationTime{0.0f}; + std::vector animatingCubieIndices; + glm::vec3 lastSelectedPosition{0.0f}; +}; diff --git a/Rubiks/globals.h b/Rubiks/globals.h new file mode 100644 index 0000000..7d90956 --- /dev/null +++ b/Rubiks/globals.h @@ -0,0 +1,8 @@ +#ifndef GLOBALS_H +#define GLOBALS_H + +#include + +using ID = int16_t; + +#endif // GLOBALS_H \ No newline at end of file diff --git a/Rubiks/input.cpp b/Rubiks/input.cpp new file mode 100644 index 0000000..4603361 --- /dev/null +++ b/Rubiks/input.cpp @@ -0,0 +1,96 @@ +#include "input.h" + +#include +#include + +namespace { +constexpr std::array, 3> kMouseButtonMap = {{ + {"left", GLFW_MOUSE_BUTTON_LEFT}, + {"middle", GLFW_MOUSE_BUTTON_MIDDLE}, + {"right", GLFW_MOUSE_BUTTON_RIGHT}, +}}; +} + +Input::Input(GLFWwindow* windowPtr) + : window(windowPtr) { + mouseButtons["left"] = false; + mouseButtons["middle"] = false; + mouseButtons["right"] = false; + keyState.fill(false); +} + +void Input::setWindow(GLFWwindow* windowPtr) { + window = windowPtr; +} + +void Input::reset() { + keysDown.clear(); + keysUp.clear(); + keysHeld.clear(); +} + +void Input::update() { + if (!window) { + return; + } + + reset(); + glfwPollEvents(); + + quit = glfwWindowShouldClose(window) != 0; + + for (int key = 0; key <= GLFW_KEY_LAST; ++key) { + int state = glfwGetKey(window, key); + bool isPressed = (state == GLFW_PRESS || state == GLFW_REPEAT); + if (isPressed) { + keysHeld.push_back(key); + if (!keyState[key]) { + keysDown.push_back(key); + } + } else if (keyState[key]) { + keysUp.push_back(key); + } + keyState[key] = isPressed; + } + + double xpos = 0.0; + double ypos = 0.0; + glfwGetCursorPos(window, &xpos, &ypos); + mousePos = { static_cast(xpos), static_cast(ypos) }; + + for (const auto& [name, button] : kMouseButtonMap) { + mouseButtons[name] = glfwGetMouseButton(window, button) == GLFW_PRESS; + } +} + +bool Input::isKeyDown(int key) const { + return std::find(keysDown.begin(), keysDown.end(), key) != keysDown.end(); +} + +bool Input::isKeyUp(int key) const { + return std::find(keysUp.begin(), keysUp.end(), key) != keysUp.end(); +} + +bool Input::isKeyHeld(int key) const { + return std::find(keysHeld.begin(), keysHeld.end(), key) != keysHeld.end(); +} + +const std::map& Input::getMouseButtons() const { + return mouseButtons; +} + +std::pair Input::getMousePos() const { + return mousePos; +} + +void Input::select(ID id) { + if (id >= 0) { + selectedID = id; + isSelecting = true; + } +} + +void Input::deselect() { + selectedID = -1; + isSelecting = false; +} diff --git a/Rubiks/input.h b/Rubiks/input.h new file mode 100644 index 0000000..fd4eb74 --- /dev/null +++ b/Rubiks/input.h @@ -0,0 +1,52 @@ +#ifndef INPUT_H +#define INPUT_H + +#include + +#include +#include +#include +#include +#include + +#include "globals.h" + +class Input { +public: + explicit Input(GLFWwindow* window = nullptr); + ~Input() = default; + + void setWindow(GLFWwindow* window); + + void reset(); + void update(); + + bool isKeyDown(int key) const; + bool isKeyUp(int key) const; + bool isKeyHeld(int key) const; + + const std::map& getMouseButtons() const; + std::pair getMousePos() const; + + bool shouldQuit() const { return quit; } + + void select(ID id); + void deselect(); + bool hasSelection() const { return selectedID >= 0; } + ID getSelectedID() const { return selectedID; } + +private: + GLFWwindow* window{nullptr}; + bool quit{false}; + std::pair mousePos{0, 0}; + std::map mouseButtons; + std::vector keysHeld; + std::vector keysDown; + std::vector keysUp; + std::array keyState{}; + + bool isSelecting{false}; + ID selectedID{-1}; +}; + +#endif // INPUT_H \ No newline at end of file diff --git a/Rubiks/mouse_selector.cpp b/Rubiks/mouse_selector.cpp new file mode 100644 index 0000000..3f14c6b --- /dev/null +++ b/Rubiks/mouse_selector.cpp @@ -0,0 +1,330 @@ +/** + * @file mouse_selector.cpp + * @brief Implements camera-based raycasting for 3D object selection. + * + * This module provides functionality to shoot rays from the camera through + * screen coordinates and test for intersections with 3D objects. The process: + * + * 1. Convert 2D screen coordinates to normalized device coordinates (NDC) + * 2. Unproject the NDC point through inverse projection/view matrices + * 3. Compute the ray direction in world space + * 4. Test ray against all registered selectable objects using triangle intersection + * 5. Return the closest hit object + */ + +#include "mouse_selector.h" + +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr float kEpsilon = 1e-6f; +} + +MouseSelector::MouseSelector(Camera& cam, float nearPlaneDistance, float farPlaneDistance) + : camera(&cam), nearPlane(nearPlaneDistance), farPlane(farPlaneDistance) {} + +ID MouseSelector::addSelectable(const Model& model, const glm::mat4& transform) { + Selectable selectable{ nextId++, &model, nullptr, transform }; + selectables.push_back(selectable); + return selectable.id; +} + +ID MouseSelector::addSelectable(const Mesh& mesh, const glm::mat4& transform) { + Selectable selectable{ nextId++, nullptr, &mesh, transform }; + selectables.push_back(selectable); + return selectable.id; +} + +bool MouseSelector::removeSelectable(ID id) { + auto it = std::remove_if(selectables.begin(), selectables.end(), [id](const Selectable& selectable) { + return selectable.id == id; + }); + if (it != selectables.end()) { + selectables.erase(it, selectables.end()); + if (selectedId && *selectedId == id) { + selectedId.reset(); + } + return true; + } + return false; +} + +void MouseSelector::updateSelectableTransform(ID id, const glm::mat4& transform) { + for (auto& selectable : selectables) { + if (selectable.id == id) { + selectable.transform = transform; + break; + } + } +} + +void MouseSelector::clearSelection() { + selectedId.reset(); +} + +std::optional MouseSelector::getSelection() const { + return selectedId; +} + +/** + * @brief Processes mouse input to perform object selection via raycasting. + * + * On a left mouse button click (rising edge), this method: + * 1. Retrieves the current mouse position in screen coordinates + * 2. Computes a ray from the camera position through the mouse location + * 3. Tests the ray against all registered selectable objects + * 4. Selects the closest intersected object (if any) + * + * @param input The input handler containing mouse state + * @param screenSize The current screen dimensions (width, height) + */ +void MouseSelector::handleSelection(const Input& input, const std::pair& screenSize) { + auto buttons = input.getMouseButtons(); + bool leftPressed = false; + bool middlePressed = false; + if (auto it = buttons.find("left"); it != buttons.end()) leftPressed = it->second; + if (auto it2 = buttons.find("middle"); it2 != buttons.end()) middlePressed = it2->second; + + // Only process selection on left-button rising edge, ignore during camera drag + if (leftPressed && !mousePressedLastFrame && !middlePressed) { + auto mousePos = input.getMousePos(); + + // Step 1: Compute ray from camera through screen point + glm::vec3 rayDir = calculateRayDirection(screenSize, mousePos); + glm::vec3 rayOrigin = camera->Position; + +#ifdef DEBUG_SELECTION + std::cout << "[MouseSelector] Click at (" << mousePos.first << "," << mousePos.second << ")" + << " rayOrigin=(" << rayOrigin.x << "," << rayOrigin.y << "," << rayOrigin.z << ")" + << " rayDir=(" << rayDir.x << "," << rayDir.y << "," << rayDir.z << ")" << std::endl; +#endif + + // Step 2: Test ray against all selectable objects + std::optional> closest; + + for (const auto& selectable : selectables) { + std::optional> result; + if (selectable.model) { + result = testModel(rayOrigin, rayDir, selectable); + } else if (selectable.mesh) { + result = testMesh(rayOrigin, rayDir, selectable); + } + + if (!result) continue; + + // Step 3: Track closest intersection + if (!closest || result->second < closest->second) { + closest = result; + } + } + + // Step 4: Update selection state + if (closest) { + selectedId = closest->first; + } else { + selectedId.reset(); + } + } + + mousePressedLastFrame = leftPressed; +} + +/** + * @brief Computes a ray direction from screen coordinates through the camera. + * + * This is the core of screen-to-world raycasting: + * + * 1. Convert screen coordinates to Normalized Device Coordinates (NDC): + * - X: [-1, 1] from left to right + * - Y: [-1, 1] from bottom to top (Y is inverted from screen coords) + * + * 2. Create a clip-space point at the near plane (z = -1) + * + * 3. Unproject to eye space using inverse projection matrix + * + * 4. Transform to world space using inverse view matrix + * + * 5. Normalize to get a unit direction vector + * + * @param screenSize Current viewport dimensions (width, height) + * @param mousePos Mouse position in screen coordinates (origin at top-left) + * @return Normalized ray direction in world space + */ +glm::vec3 MouseSelector::calculateRayDirection(const std::pair& screenSize, + const std::pair& mousePos) const { + // Step 1: Convert screen coordinates to NDC [-1, 1] + float ndcX = (2.0f * mousePos.first) / screenSize.first - 1.0f; + float ndcY = 1.0f - (2.0f * mousePos.second) / screenSize.second; // Y inverted + + // Step 2: Create clip-space ray (at near plane, pointing into screen) + glm::vec4 rayClip(ndcX, ndcY, -1.0f, 1.0f); + + // Step 3: Build projection and view matrices + glm::mat4 projection = glm::perspective( + glm::radians(camera->Zoom), + static_cast(screenSize.first) / static_cast(screenSize.second), + nearPlane, + farPlane); + glm::mat4 view = camera->GetViewMatrix(); + + // Step 4: Unproject from clip space to eye space + glm::vec4 rayEye = glm::inverse(projection) * rayClip; + rayEye = glm::vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); // Direction, not point + + // Step 5: Transform from eye space to world space + glm::vec4 worldRay4 = glm::inverse(view) * rayEye; + glm::vec3 worldRay = glm::normalize(glm::vec3(worldRay4)); + + return worldRay; +} + +/** + * @brief Tests ray intersection with a triangle using the Möller–Trumbore algorithm. + * + * This algorithm efficiently computes ray-triangle intersection without + * explicitly computing the plane equation. It uses barycentric coordinates + * to determine if the intersection point lies within the triangle. + * + * Mathematical basis: + * - A point P on a ray: P = O + t*D (origin O, direction D, parameter t) + * - A point in triangle: P = (1-u-v)*V0 + u*V1 + v*V2 + * - Solve for t, u, v using Cramer's rule + * + * @param rayOrigin Ray origin point (camera position) + * @param rayDir Normalized ray direction + * @param v0, v1, v2 Triangle vertices in world space + * @return Pair of (hit success, distance along ray if hit) + */ +std::pair MouseSelector::rayIntersectsTriangle(const glm::vec3& rayOrigin, + const glm::vec3& rayDir, + const glm::vec3& v0, + const glm::vec3& v1, + const glm::vec3& v2) const { + // Compute edge vectors from v0 + glm::vec3 edge1 = v1 - v0; + glm::vec3 edge2 = v2 - v0; + + // Begin calculating determinant (also used to calculate u parameter) + glm::vec3 h = glm::cross(rayDir, edge2); + float det = glm::dot(edge1, h); + + // If determinant is near zero, ray lies in plane of triangle + if (std::abs(det) < kEpsilon) { + return {false, 0.0f}; + } + + float invDet = 1.0f / det; + + // Calculate distance from v0 to ray origin + glm::vec3 s = rayOrigin - v0; + + // Calculate u parameter and test bounds + float u = invDet * glm::dot(s, h); + if (u < 0.0f || u > 1.0f) { + return {false, 0.0f}; + } + + // Prepare to test v parameter + glm::vec3 q = glm::cross(s, edge1); + + // Calculate v parameter and test bounds + float v = invDet * glm::dot(rayDir, q); + if (v < 0.0f || u + v > 1.0f) { + return {false, 0.0f}; + } + + // Calculate t (distance along ray to intersection) + float t = invDet * glm::dot(edge2, q); + + // Ray intersection (t must be positive - in front of camera) + if (t > kEpsilon) { + return {true, t}; + } + + return {false, 0.0f}; +} + +/** + * @brief Tests ray intersection with all meshes in a Model. + * + * Iterates through each mesh in the model and finds the closest intersection. + * + * @param rayOrigin Ray origin (camera position) + * @param rayDir Normalized ray direction + * @param selectable The selectable containing the model to test + * @return Optional pair of (ID, distance) if hit, nullopt otherwise + */ +std::optional> MouseSelector::testModel(const glm::vec3& rayOrigin, + const glm::vec3& rayDir, + const Selectable& selectable) { + if (!selectable.model) { + return std::nullopt; + } + + std::optional> closest; + for (const auto& mesh : selectable.model->meshes) { + Selectable temp{selectable.id, nullptr, &mesh, selectable.transform}; + auto result = testMesh(rayOrigin, rayDir, temp); + if (result && (!closest || result->second < closest->second)) { + closest = result; + } + } + return closest; +} + +/** + * @brief Tests ray intersection with all triangles in a Mesh. + * + * For each triangle in the mesh: + * 1. Transform vertices to world space using the selectable's transform matrix + * 2. Test ray-triangle intersection + * 3. Track the closest intersection point + * + * @param rayOrigin Ray origin (camera position) + * @param rayDir Normalized ray direction + * @param selectable The selectable containing the mesh to test + * @return Optional pair of (ID, distance) if hit, nullopt otherwise + */ +std::optional> MouseSelector::testMesh(const glm::vec3& rayOrigin, + const glm::vec3& rayDir, + const Selectable& selectable) { + if (!selectable.mesh) { + return std::nullopt; + } + + const auto& mesh = *selectable.mesh; + float closestDistance = std::numeric_limits::max(); + bool hit = false; + + // Iterate through triangles (3 indices per triangle) + for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) { + // Get vertex data + Vertex v0 = mesh.vertices[mesh.indices[i]]; + Vertex v1 = mesh.vertices[mesh.indices[i + 1]]; + Vertex v2 = mesh.vertices[mesh.indices[i + 2]]; + + // Transform vertices to world space + glm::vec3 p0 = glm::vec3(selectable.transform * glm::vec4(v0.Position, 1.0f)); + glm::vec3 p1 = glm::vec3(selectable.transform * glm::vec4(v1.Position, 1.0f)); + glm::vec3 p2 = glm::vec3(selectable.transform * glm::vec4(v2.Position, 1.0f)); + + // Test intersection + auto [intersects, distance] = rayIntersectsTriangle(rayOrigin, rayDir, p0, p1, p2); + if (intersects && distance < closestDistance) { + closestDistance = distance; + hit = true; + lastIntersectionPoint = rayOrigin + rayDir * distance; + } + } + + if (hit) { + return std::make_pair(selectable.id, closestDistance); + } + return std::nullopt; +} \ No newline at end of file diff --git a/Rubiks/mouse_selector.h b/Rubiks/mouse_selector.h new file mode 100644 index 0000000..3cc45f3 --- /dev/null +++ b/Rubiks/mouse_selector.h @@ -0,0 +1,148 @@ +/** + * @file mouse_selector.h + * @brief Camera-based raycasting system for 3D object selection. + * + * This module implements raycasting from the camera through screen coordinates + * to enable mouse-based selection of 3D objects. The raycasting pipeline: + * + * 1. Screen coordinates → Normalized Device Coordinates (NDC) + * 2. NDC → Eye space via inverse projection matrix + * 3. Eye space → World space via inverse view matrix + * 4. Ray-triangle intersection testing using Möller–Trumbore algorithm + * + * Usage: + * MouseSelector selector(camera); + * ID id = selector.addSelectable(model, transform); + * selector.handleSelection(input, screenSize); + * if (auto selected = selector.getSelection()) { ... } + */ + +#ifndef MOUSE_SELECTOR_H +#define MOUSE_SELECTOR_H + +#include + +#include +#include +#include + +#include "../camera.h" +#include "../mesh.h" +#include "../model.h" +#include "globals.h" +#include "input.h" + +/** + * @brief Raycasting-based mouse selection system for 3D objects. + * + * Provides functionality to: + * - Register 3D objects (Models or Meshes) as selectable + * - Compute rays from screen coordinates through the camera + * - Test ray intersections with triangles + * - Track and report selected objects + */ +class MouseSelector { +public: + /** + * @brief Represents a selectable 3D object. + */ + struct Selectable { + ID id; ///< Unique identifier for this selectable + const Model* model{nullptr}; ///< Pointer to Model (if model-based) + const Mesh* mesh{nullptr}; ///< Pointer to Mesh (if mesh-based) + glm::mat4 transform{1.0f}; ///< World transform matrix + }; + + /** + * @brief Constructs a MouseSelector with camera reference. + * @param camera Reference to the camera for ray computation + * @param nearPlane Near clipping plane distance + * @param farPlane Far clipping plane distance + */ + MouseSelector(Camera& camera, float nearPlane = 0.1f, float farPlane = 100.0f); + + /// @name Selectable Management + /// @{ + + /** Registers a Model as selectable, returns unique ID */ + ID addSelectable(const Model& model, const glm::mat4& transform = glm::mat4(1.0f)); + + /** Registers a Mesh as selectable, returns unique ID */ + ID addSelectable(const Mesh& mesh, const glm::mat4& transform = glm::mat4(1.0f)); + + /** Removes a selectable by ID, returns true if found */ + bool removeSelectable(ID id); + + /** Updates the world transform of a selectable */ + void updateSelectableTransform(ID id, const glm::mat4& transform); + /// @} + + /// @name Selection State + /// @{ + + /** Clears current selection */ + void clearSelection(); + + /** Programmatically sets selection to given ID */ + void setSelection(ID id) { selectedId = id; } + + /** Returns currently selected ID, or nullopt if none */ + std::optional getSelection() const; + /// @} + + /** + * @brief Processes mouse input and performs raycasting selection. + * + * Call each frame to handle mouse-based object selection. + * On left-click, shoots a ray through the cursor and selects + * the closest intersected object. + */ + void handleSelection(const Input& input, const std::pair& screenSize); + + /** Returns all registered selectables (for debugging/iteration) */ + const std::vector& getSelectables() const { return selectables; } + +private: + Camera* camera; ///< Camera for ray computation + float nearPlane; ///< Near clipping plane + float farPlane; ///< Far clipping plane + ID nextId{0}; ///< Counter for generating unique IDs + std::vector selectables; ///< All registered selectables + std::optional selectedId; ///< Currently selected object ID + glm::vec3 lastIntersectionPoint{0.0f}; ///< Last ray-triangle hit point + bool mousePressedLastFrame{false}; ///< For detecting click rising edge + + /// @name Raycasting Implementation + /// @{ + + /** + * @brief Computes world-space ray direction from screen coordinates. + * + * Unprojects screen position through inverse projection and view matrices. + */ + glm::vec3 calculateRayDirection(const std::pair& screenSize, + const std::pair& mousePos) const; + + /** + * @brief Tests ray-triangle intersection using Möller–Trumbore algorithm. + * @return Pair of (hit, distance) where distance is along ray if hit + */ + std::pair rayIntersectsTriangle(const glm::vec3& rayOrigin, + const glm::vec3& rayDir, + const glm::vec3& v0, + const glm::vec3& v1, + const glm::vec3& v2) const; + + /** Tests ray against all meshes in a Model */ + std::optional> testModel(const glm::vec3& rayOrigin, + const glm::vec3& rayDir, + const Selectable& selectable); + + /** Tests ray against all triangles in a Mesh */ + std::optional> testMesh(const glm::vec3& rayOrigin, + const glm::vec3& rayDir, + const Selectable& selectable); + /// @} +}; + +#endif // MOUSE_SELECTOR_H \ No newline at end of file diff --git a/Rubiks/window.cpp b/Rubiks/window.cpp new file mode 100644 index 0000000..f4886fa --- /dev/null +++ b/Rubiks/window.cpp @@ -0,0 +1,567 @@ +#include +#include + +#include +#include +#include + +#include "shader.h" +#include "camera.h" +#include "model.h" +#include "RubiksCube.h" +#include "input.h" +#include "mouse_selector.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr unsigned int SCR_WIDTH = 800; +constexpr unsigned int SCR_HEIGHT = 600; + +class InfiniteGridRenderer { +public: + InfiniteGridRenderer(); + ~InfiniteGridRenderer(); + + void render(const glm::mat4& projection, const glm::mat4& view, const glm::vec3& cameraPos); + +private: + Shader shader; + unsigned int vao{0}; + + float gridSize = 100.0f; + float minPixelsBetweenCells = 2.0f; + float cellSize = 0.025f; + glm::vec4 colorThin{0.5f, 0.5f, 0.5f, 1.0f}; + glm::vec4 colorThick{0.0f, 0.0f, 0.0f, 1.0f}; + float alpha = 0.5f; +}; + +class RubiksApplication { +public: + RubiksApplication(); + ~RubiksApplication(); + + int run(); + void scramble(int depth); + +private: + void initWindow(); + void initGLAD(); + void initInputSystem(); + void initCallbacks(); + void initRenderState(); + void mainLoop(); + void updateDeltaTime(); + void processInput(); + void renderScene(); + void onFramebufferResize(int width, int height); + void onMouseMove(double xpos, double ypos); + void onScroll(double yoffset); + + static void framebuffer_size_callback(GLFWwindow* window, int width, int height); + static void mouse_callback(GLFWwindow* window, double xpos, double ypos); + static void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); + + inline static const std::array kAxisDirections = { + glm::vec3( 1.0f, 0.0f, 0.0f), + glm::vec3(-1.0f, 0.0f, 0.0f), + glm::vec3( 0.0f, 1.0f, 0.0f), + glm::vec3( 0.0f, -1.0f, 0.0f), + glm::vec3( 0.0f, 0.0f, 1.0f), + glm::vec3( 0.0f, 0.0f, -1.0f) + }; + inline static const glm::vec3 kAmbientStrength{0.08f}; + inline static const glm::vec3 kDiffuseStrength{0.55f}; + inline static const glm::vec3 kSpecularStrength{0.25f}; + + GLFWwindow* window{nullptr}; + bool glfwInitialized{false}; + + Camera camera; + float lastX; + float lastY; + bool firstMouse{true}; + float deltaTime{0.0f}; + float lastFrame{0.0f}; + bool middleMouseDownLastFrame{false}; + int lastSelectedID{-2}; + int debugDisplayMode{0}; + bool faceTexturesEnabled{true}; + + std::unique_ptr cubeShader; + std::unique_ptr gridRenderer; + std::unique_ptr rubiksCube; + std::unique_ptr input; + std::unique_ptr mouseSelector; + std::vector cubieIDs; +}; + +InfiniteGridRenderer::InfiniteGridRenderer() + : shader("shaders/infinite_grid.vs", "shaders/infinite_grid.fs") { + glGenVertexArrays(1, &vao); +} + +InfiniteGridRenderer::~InfiniteGridRenderer() { + if (vao != 0) { + glDeleteVertexArrays(1, &vao); + vao = 0; + } +} + +void InfiniteGridRenderer::render(const glm::mat4& projection, const glm::mat4& view, const glm::vec3& cameraPos) { + shader.use(); + shader.setMat4("gVP", projection * view); + shader.setVec3("gCameraWorldPos", cameraPos); + shader.setFloat("gGridSize", gridSize); + shader.setFloat("gGridMinPixelsBetweenCells", minPixelsBetweenCells); + shader.setFloat("gGridCellSize", cellSize); + shader.setVec4("gGridColorThin", colorThin); + shader.setVec4("gGridColorThick", colorThick); + shader.setFloat("gGridAlpha", alpha); + + glDepthMask(GL_FALSE); + glBindVertexArray(vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); + glDepthMask(GL_TRUE); +} + +RubiksApplication::RubiksApplication() + : camera(glm::vec3(3.0f, 3.0f, 5.0f), glm::vec3(0.0f, 1.0f, 0.0f), -135.0f, -25.0f), + lastX(static_cast(SCR_WIDTH) / 2.0f), + lastY(static_cast(SCR_HEIGHT) / 2.0f) {} + +RubiksApplication::~RubiksApplication() { + rubiksCube.reset(); + gridRenderer.reset(); + cubeShader.reset(); + + if (window) { + glfwDestroyWindow(window); + window = nullptr; + } + if (glfwInitialized) { + glfwTerminate(); + } +} + +int RubiksApplication::run() { + initWindow(); + initGLAD(); + initInputSystem(); + initCallbacks(); + initRenderState(); + mainLoop(); + return 0; +} + +void RubiksApplication::initWindow() { + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW"); + } + glfwInitialized = true; + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + + window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Rubik's Cube", nullptr, nullptr); + if (window == nullptr) { + throw std::runtime_error("Failed to create GLFW window"); + } + + glfwMakeContextCurrent(window); + glfwSetWindowUserPointer(window, this); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} + +void RubiksApplication::initInputSystem() { + input = std::make_unique(window); +} + +void RubiksApplication::initGLAD() { + if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) { + throw std::runtime_error("Failed to initialize GLAD"); + } +} + +void RubiksApplication::initCallbacks() { + glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); + glfwSetCursorPosCallback(window, mouse_callback); + glfwSetScrollCallback(window, scroll_callback); +} + +void RubiksApplication::initRenderState() { + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + cubeShader = std::make_unique("shaders/vertex.vs", "shaders/rubiks.fs"); + cubeShader->use(); + cubeShader->setInt("texture_diffuse1", 0); + cubeShader->setBool("material.useTexture", false); + cubeShader->setBool("useFaceTextures", false); + + gridRenderer = std::make_unique(); + rubiksCube = std::make_unique( + "Rubiks/RCube.obj", + "img/textures/Rubiks Col.png", + glm::vec3(0.0f, 3.0f, 0.0f), + 0.3f, + 0.58f); + rubiksCube->setFaceTexturesEnabled(faceTexturesEnabled); + + mouseSelector = std::make_unique(camera); + cubieIDs.clear(); + cubieIDs.reserve(rubiksCube->getCubieOffsets().size()); + for (const auto& offset : rubiksCube->getCubieOffsets()) { + glm::mat4 transform = rubiksCube->getCubieModelMatrix(offset); + ID id = mouseSelector->addSelectable(rubiksCube->getModel(), transform); + cubieIDs.push_back(id); + } +} + +void RubiksApplication::mainLoop() { + while (!glfwWindowShouldClose(window)) { + updateDeltaTime(); + if (input) { + input->update(); + bool middlePressed = false; + auto mb = input->getMouseButtons(); + if (auto it = mb.find("middle"); it != mb.end()) middlePressed = it->second; + if (middlePressed && !middleMouseDownLastFrame) { + // entering drag mode, reset firstMouse to avoid jump on first movement + firstMouse = true; + } + if (middlePressed) { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } else { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + middleMouseDownLastFrame = middlePressed; + + // Toggle raw base-color display (no lighting) with G + if (input->isKeyDown(GLFW_KEY_G)) { + debugDisplayMode = (debugDisplayMode == 1) ? 0 : 1; + } + // Toggle sticker face textures with T + if (input->isKeyDown(GLFW_KEY_T)) { + faceTexturesEnabled = !faceTexturesEnabled; + if (rubiksCube) { + rubiksCube->setFaceTexturesEnabled(faceTexturesEnabled); + } + std::cout << "[Rubiks] Face textures " + << (faceTexturesEnabled ? "enabled" : "disabled") << std::endl; + } + } + processInput(); + + // Update Rubik's cube animation + if (rubiksCube) { + bool animationCompleted = rubiksCube->updateAnimation(deltaTime); + + // After rotation completes, update selection to the cubie now at the previously selected position + if (animationCompleted && mouseSelector) { + glm::vec3 targetPos = rubiksCube->getLastSelectedPosition(); + int newCubieIndex = rubiksCube->findCubieAtPosition(targetPos); + + if (newCubieIndex >= 0 && newCubieIndex < static_cast(cubieIDs.size())) { + // Update selection to the new cubie at the same position + mouseSelector->setSelection(cubieIDs[newCubieIndex]); + + // Update all selectable transforms to match new cubie positions + const auto& offsets = rubiksCube->getCubieOffsets(); + for (size_t i = 0; i < offsets.size() && i < cubieIDs.size(); ++i) { + glm::mat4 transform = rubiksCube->getCubieModelMatrix(offsets[i]); + mouseSelector->updateSelectableTransform(cubieIDs[i], transform); + } + } + } + } + + if (mouseSelector && input) { + mouseSelector->handleSelection(*input, {SCR_WIDTH, SCR_HEIGHT}); + } + + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + renderScene(); + + glfwSwapBuffers(window); + if (!input) { + glfwPollEvents(); + } + } +} + +void RubiksApplication::scramble(int depth){ + Camera scrambleCam; + std::array camPos = { + glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), + glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f), + glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, 0.0f, -1.0f) + }; + + + +} + +void RubiksApplication::updateDeltaTime() { + float currentFrame = static_cast(glfwGetTime()); + deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; +} + +void RubiksApplication::processInput() { + if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { + glfwSetWindowShouldClose(window, true); + } + + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.ProcessKeyboard(FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.ProcessKeyboard(BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.ProcessKeyboard(LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.ProcessKeyboard(RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_C) == GLFW_PRESS) + camera.ProcessKeyboard(UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) + camera.ProcessKeyboard(DOWN, deltaTime); + + // Handle Rubik's cube face rotations with arrow keys + if (rubiksCube && !rubiksCube->isAnimating() && input) { + int selectedID = -1; + if (mouseSelector) { + if (auto s = mouseSelector->getSelection(); s) { + selectedID = static_cast(*s); + } + } + + // Find the cubie offset for the selected ID + glm::vec3 selectedOffset(0.0f); + bool hasSelection = false; + if (selectedID >= 0 && selectedID < static_cast(cubieIDs.size())) { + for (size_t i = 0; i < cubieIDs.size(); ++i) { + if (static_cast(cubieIDs[i]) == selectedID) { + selectedOffset = rubiksCube->getCubieOffsets()[i]; + hasSelection = true; + break; + } + } + } + + if (hasSelection) { + // Camera vectors for determining visual directions + glm::vec3 camForward = camera.Front; // Where camera looks + glm::vec3 camRight = camera.Right; // Camera's right direction + glm::vec3 camUp = camera.Up; // Camera's up direction + + // Round offset to nearest integer for comparison + int x = static_cast(std::round(selectedOffset.x)); + int y = static_cast(std::round(selectedOffset.y)); + int z = static_cast(std::round(selectedOffset.z)); + + int faceIndex = -1; + float angle = 90.0f; + + if (input->isKeyDown(GLFW_KEY_UP) || input->isKeyDown(GLFW_KEY_DOWN)) { + // UP/DOWN: Rotate around an axis parallel to camera's RIGHT vector + // This makes pieces move up/down visually + + // Find which world axis camera.Right is most aligned with + float dotX = std::abs(camRight.x); + float dotZ = std::abs(camRight.z); + + if (dotX > dotZ) { + // Camera right is along X, so rotate around X axis + // Use X-slices: Right(0), Left(1), M(6) + if (x == 1) faceIndex = 0; + else if (x == -1) faceIndex = 1; + else faceIndex = 6; + + // Rotation direction: positive X rotation moves +Y toward +Z + // For "UP" to move pieces upward visually, we need to consider + // which way the camera is looking (camForward) + // If looking toward -Z (front face), UP should rotate negatively around X + // If looking toward +Z (back face), UP should rotate positively around X + angle = (camForward.z > 0) ? 90.0f : -90.0f; + + // Also need to flip based on which side of the X axis we're rotating + // Right face (x=1) rotates with positive X axis + // Left face (x=-1) rotates with negative X axis (per kFacePivots) + // But M slice (x=0) uses positive X axis + if (faceIndex == 1) angle = -angle; // Left face has inverted axis + + } else { + // Camera right is along Z, so rotate around Z axis + // Use Z-slices: Front(4), Back(5), S(8) + if (z == 1) faceIndex = 4; + else if (z == -1) faceIndex = 5; + else faceIndex = 8; + + // Rotation direction: positive Z rotation moves +X toward +Y + // If looking toward -X, UP should rotate positively around Z + // If looking toward +X (red face), UP should rotate negatively around Z + angle = (camForward.x > 0) ? -90.0f : 90.0f; + + // Back face has inverted axis + if (faceIndex == 5) angle = -angle; + } + + // Flip for DOWN key + if (input->isKeyDown(GLFW_KEY_DOWN)) { + angle = -angle; + } + } + else if (input->isKeyDown(GLFW_KEY_LEFT) || input->isKeyDown(GLFW_KEY_RIGHT)) { + // LEFT/RIGHT: Rotate around Y axis (world up) + // Use Y-slices: Up(2), Down(3), E(7) + if (y == 1) faceIndex = 2; + else if (y == -1) faceIndex = 3; + else faceIndex = 7; + + angle = -90.0f; + + if (faceIndex == 3) angle = -angle; + + // Flip for RIGHT key + if (input->isKeyDown(GLFW_KEY_RIGHT)) { + angle = -angle; + } + } + + if (faceIndex >= 0) { + rubiksCube->setLastSelectedPosition(selectedOffset); + rubiksCube->startFaceRotation(faceIndex, angle, 0.25f); + } + } + } +} + +void RubiksApplication::renderScene() { + glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), + static_cast(SCR_WIDTH) / static_cast(SCR_HEIGHT), + 0.1f, + 100.0f); + glm::mat4 view = camera.GetViewMatrix(); + + if (gridRenderer) { + gridRenderer->render(projection, view, camera.Position); + } + + if (!cubeShader || !rubiksCube) { + return; + } + + cubeShader->use(); + cubeShader->setVec3("material.ambient", glm::vec3(0.3f)); + cubeShader->setVec3("material.diffuse", glm::vec3(1.0f)); + cubeShader->setVec3("material.specular", glm::vec3(0.3f)); + cubeShader->setFloat("material.shininess", 32.0f); + cubeShader->setVec3("lightColor", glm::vec3(1.0f)); + cubeShader->setVec3("viewPos", camera.Position); + + cubeShader->setInt("dirLightCount", static_cast(kAxisDirections.size())); + for (int i = 0; i < static_cast(kAxisDirections.size()); ++i) { + std::string baseName = "dirLights[" + std::to_string(i) + "]"; + cubeShader->setVec3(baseName + ".direction", kAxisDirections[i]); + cubeShader->setVec3(baseName + ".ambient", kAmbientStrength); + cubeShader->setVec3(baseName + ".diffuse", kDiffuseStrength); + cubeShader->setVec3(baseName + ".specular", kSpecularStrength); + } + + cubeShader->setMat4("projection", projection); + cubeShader->setMat4("view", view); + cubeShader->setInt("debugDisplayMode", debugDisplayMode); + + // Provide the cubie ID mapping so the shader can highlight selected cubie + int sel = -1; + if (mouseSelector) { + if (auto s = mouseSelector->getSelection(); s) sel = static_cast(*s); + } + cubeShader->setInt("selectedID", sel); + // Debug output: print when selection changes + if (sel != lastSelectedID) { + if (sel >= 0) std::cout << "Selected ID: " << sel << std::endl; + else std::cout << "Selection cleared" << std::endl; + lastSelectedID = sel; + } + rubiksCube->applyMaterial(*cubeShader); + rubiksCube->draw(*cubeShader, &cubieIDs); +} + +void RubiksApplication::onFramebufferResize(int width, int height) { + glViewport(0, 0, width, height); +} + +void RubiksApplication::onMouseMove(double xposIn, double yposIn) { + float xpos = static_cast(xposIn); + float ypos = static_cast(yposIn); + + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + // Only process camera movement while middle mouse is held (query GLFW directly to avoid races) + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) { + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; + lastX = xpos; + lastY = ypos; + camera.ProcessMouseMovement(xoffset, yoffset); + } else { + // update last positions so we don't get a jump when entering drag + lastX = xpos; + lastY = ypos; + } +} + +void RubiksApplication::onScroll(double yoffset) { + camera.ProcessMouseScroll(static_cast(yoffset)); +} + +void RubiksApplication::framebuffer_size_callback(GLFWwindow* window, int width, int height) { + if (auto* app = static_cast(glfwGetWindowUserPointer(window))) { + app->onFramebufferResize(width, height); + } +} + +void RubiksApplication::mouse_callback(GLFWwindow* window, double xpos, double ypos) { + if (auto* app = static_cast(glfwGetWindowUserPointer(window))) { + app->onMouseMove(xpos, ypos); + } +} + +void RubiksApplication::scroll_callback(GLFWwindow* window, double /*xoffset*/, double yoffset) { + if (auto* app = static_cast(glfwGetWindowUserPointer(window))) { + app->onScroll(yoffset); + } +} + +} // namespace + +int main() { + try { + RubiksApplication app; + return app.run(); + } catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << std::endl; + return -1; + } +} \ No newline at end of file diff --git a/SPHFluid/3D/GPUFluidMesh.cpp b/SPHFluid/3D/GPUFluidMesh.cpp new file mode 100644 index 0000000..e69de29 diff --git a/SPHFluid/3D/GPUFluidMesh.h b/SPHFluid/3D/GPUFluidMesh.h new file mode 100644 index 0000000..e69de29 diff --git a/SPHFluid/3D/window3D.cpp b/SPHFluid/3D/window3D.cpp index 8653f7a..6fac819 100644 --- a/SPHFluid/3D/window3D.cpp +++ b/SPHFluid/3D/window3D.cpp @@ -186,13 +186,27 @@ int main() { boxShader.setVec3("color", glm::vec3(1.0f, 1.0f, 0.0f)); // Yellow color for the box boundingBox.Render(view, projection); - // Render Particles - particleShader.use(); - particleShader.setMat4("projection", projection); - particleShader.setMat4("view", view); - particleShader.setVec3("lightPos", glm::vec3(10.0, 20.0, 10.0)); - particleShader.setVec3("viewPos", camera.Position); - particleDisplay.Render(view, projection); + // // Render Particles + // particleShader.use(); + // particleShader.setMat4("projection", projection); + // particleShader.setMat4("view", view); + // particleShader.setVec3("lightPos", glm::vec3(10.0, 20.0, 10.0)); + // particleShader.setVec3("viewPos", camera.Position); + // particleDisplay.Render(view, projection); + + // After particle rendering or instead of it: + static Shader meshShader("SPHFluid/shaders/fluidMesh3d.vs", "SPHFluid/shaders/fluidMesh3d.fs"); + meshShader.use(); + meshShader.setMat4("projection", projection); + meshShader.setMat4("view", view); + glm::mat4 meshModel = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, worldYOffset, 0.0f)); + meshShader.setMat4("model", meshModel); + meshShader.setVec3("fluidColor", glm::vec3(0.2f, 0.5f, 0.9f)); + meshShader.setVec3("lightPos", glm::vec3(10.0, 20.0, 10.0)); + meshShader.setVec3("viewPos", camera.Position); + + glEnable(GL_CULL_FACE); + glDisable(GL_CULL_FACE); glfwSwapBuffers(window); glfwPollEvents(); diff --git a/SPHFluid/GPUFluidMesh.h b/SPHFluid/GPUFluidMesh.h new file mode 100644 index 0000000..e69de29 diff --git a/SPHFluid/shaders/ClearDensity.compute b/SPHFluid/shaders/ClearDensity.compute new file mode 100644 index 0000000..e69de29 diff --git a/SPHFluid/shaders/CubeMarching.compute b/SPHFluid/shaders/CubeMarching.compute new file mode 100644 index 0000000..b192dc9 --- /dev/null +++ b/SPHFluid/shaders/CubeMarching.compute @@ -0,0 +1,247 @@ +#version 430 + +struct Vertex { + vec3 Position; + float pad0; // padding to match std430 (vec3 -> 16 bytes) + vec3 Normal; + float pad1; + vec2 TexCoord; + float pad2; + float pad3; // total size now 48 bytes to match CPU-side struct +}; + +struct Triangle { + uint vertexIndices[3]; +}; + +struct GridCell { + Vertex vertices[8]; + float cornerValues[8]; +}; + +layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; + +layout(binding = 0, r32f) uniform readonly image3D densityTexture; + +layout(std430, binding = 1) writeonly buffer VertexBuffer { + Vertex vertices[]; +}; + +layout(std430, binding = 2) writeonly buffer IndexBuffer { + uint indices[]; +}; + +layout(std430, binding = 3) readonly buffer edgeTableBuffer { + int edgeTable[256]; +}; + +layout(std430, binding = 4) readonly buffer triTableBuffer { + int triTable[4096]; +}; + +layout(std430, binding = 5) readonly buffer edgeToVertexBuffer { + uvec2 edgeToVertex[12]; +}; + +layout(std430, binding = 6) buffer AtomicCounters { + uint vertexCount; + uint indexCount; +}; + +// Uniforms +const uint CORNER_COUNT = 8u; + +uniform float isolevel; +uniform uint sizeX; +uniform uint sizeY; +uniform uint sizeZ; +uniform vec3 boundsMin; +uniform vec3 boundsMax; + +float getValueAt(uint x, uint y, uint z) { + if (x >= sizeX || y >= sizeY || z >= sizeZ) return 0.0; + return imageLoad(densityTexture, ivec3(x, y, z)).r; +} + +// Simple bounds check for a cube that needs (x, x+1), (y, y+1), (z, z+1) +bool cubeHasNeighbors(uint x, uint y, uint z) { + return (x + 1u < sizeX) && (y + 1u < sizeY) && (z + 1u < sizeZ); +} + +void getCubeIndex(in GridCell cell, out uint cubeIndex) { + cubeIndex = 0u; + for (uint i = 0u; i < CORNER_COUNT; ++i) { + if (cell.cornerValues[i] < isolevel) { + cubeIndex |= (1u << i); + } + } +} + +void interpolateVertices(in GridCell cell, in uint cubeIndex, out Vertex interpolated[12]) { + // Initialize all vertices + for (int i = 0; i < 12; ++i) { + interpolated[i].Position = vec3(0.0); + interpolated[i].Normal = vec3(0.0); + interpolated[i].TexCoord = vec2(0.0); + } + + int intersectionKey = edgeTable[cubeIndex]; + for (uint ii = 0u; ii < 12u; ++ii) { + if ((intersectionKey & (1 << int(ii))) != 0) { + int i = int(ii); + uint v1 = edgeToVertex[i].x; + uint v2 = edgeToVertex[i].y; + float val1 = cell.cornerValues[v1]; + float val2 = cell.cornerValues[v2]; + float denom = val2 - val1; + float t = (abs(denom) < 1e-6) ? 0.5 : (isolevel - val1) / denom; + + Vertex vert1 = cell.vertices[v1]; + Vertex vert2 = cell.vertices[v2]; + interpolated[i].Position = mix(vert1.Position, vert2.Position, t); + interpolated[i].Normal = vec3(0.0); + interpolated[i].TexCoord = vert1.TexCoord * (1.0 - t) + vert2.TexCoord * t; + } + } +} + +void generateTriangles(in Vertex edgeVertices[12], in uint cubeIndex, out Triangle triangles[5], out uint triangleCount) { + triangleCount = 0u; + uint tableOffset = cubeIndex * 16u; + + for (uint i = 0u; i < 16u; i += 3u) { + int a = triTable[tableOffset + i]; + if (a == -1) break; // Sentinel value + int b = triTable[tableOffset + i + 1u]; + int c = triTable[tableOffset + i + 2u]; + + Triangle tri; + tri.vertexIndices[0] = uint(a); + tri.vertexIndices[1] = uint(b); + tri.vertexIndices[2] = uint(c); + triangles[triangleCount] = tri; + triangleCount++; + if (triangleCount >= 5u) break; + } +} + +void calculateFaceNormal(in Vertex verts[3], out vec3 normal) { + vec3 edge1 = verts[1].Position - verts[0].Position; + vec3 edge2 = verts[2].Position - verts[0].Position; + vec3 crossProd = cross(edge1, edge2); + float len = length(crossProd); + if (len > 1e-8) { + normal = crossProd / len; + } else { + normal = vec3(0.0, 1.0, 0.0); + } +} + +void processCube(uint x, uint y, uint z) { + // Bounds: require neighbors at +1 in each axis + if (!cubeHasNeighbors(x, y, z)) { + return; + } + + GridCell cell; + // Fill corner scalar values (order: bottom face z, then top face z+1) + cell.cornerValues[0] = getValueAt(x, y, z); + cell.cornerValues[1] = getValueAt(x + 1u, y, z); + cell.cornerValues[2] = getValueAt(x + 1u, y + 1u, z); + cell.cornerValues[3] = getValueAt(x, y + 1u, z); + cell.cornerValues[4] = getValueAt(x, y, z + 1u); + cell.cornerValues[5] = getValueAt(x + 1u, y, z + 1u); + cell.cornerValues[6] = getValueAt(x + 1u, y + 1u, z + 1u); + cell.cornerValues[7] = getValueAt(x, y + 1u, z + 1u); + + // Fill vertex positions - convert from grid space to world space + float fx = float(x); + float fy = float(y); + float fz = float(z); + + // Grid with N points has N-1 cells, so we divide by (size - 1) not size + float gridSizeX = max(1.0, float(sizeX - 1u)); + float gridSizeY = max(1.0, float(sizeY - 1u)); + float gridSizeZ = max(1.0, float(sizeZ - 1u)); + + vec3 gridScale = (boundsMax - boundsMin) / vec3(gridSizeX, gridSizeY, gridSizeZ); + + // Convert grid positions to world positions + cell.vertices[0].Position = boundsMin + vec3(fx, fy, fz) * gridScale; + cell.vertices[1].Position = boundsMin + vec3(fx + 1.0, fy, fz) * gridScale; + cell.vertices[2].Position = boundsMin + vec3(fx + 1.0, fy + 1.0, fz) * gridScale; + cell.vertices[3].Position = boundsMin + vec3(fx, fy + 1.0, fz) * gridScale; + cell.vertices[4].Position = boundsMin + vec3(fx, fy, fz + 1.0) * gridScale; + cell.vertices[5].Position = boundsMin + vec3(fx + 1.0, fy, fz + 1.0) * gridScale; + cell.vertices[6].Position = boundsMin + vec3(fx + 1.0, fy + 1.0, fz + 1.0) * gridScale; + cell.vertices[7].Position = boundsMin + vec3(fx, fy + 1.0, fz + 1.0) * gridScale; + + // Normal placeholders + for (int i = 0; i < 8; ++i) { + cell.vertices[i].Normal = vec3(0.0); + } + + // Texcoords normalized across grid + cell.vertices[0].TexCoord = vec2(fx / gridSizeX, fy / gridSizeY); + cell.vertices[1].TexCoord = vec2((fx + 1.0) / gridSizeX, fy / gridSizeY); + cell.vertices[2].TexCoord = vec2((fx + 1.0) / gridSizeX, (fy + 1.0) / gridSizeY); + cell.vertices[3].TexCoord = vec2(fx / gridSizeX, (fy + 1.0) / gridSizeY); + cell.vertices[4].TexCoord = vec2(fx / gridSizeX, fy / gridSizeY); + cell.vertices[5].TexCoord = vec2((fx + 1.0) / gridSizeX, fy / gridSizeY); + cell.vertices[6].TexCoord = vec2((fx + 1.0) / gridSizeX, (fy + 1.0) / gridSizeY); + cell.vertices[7].TexCoord = vec2(fx / gridSizeX, (fy + 1.0) / gridSizeY); + + // Compute cube index and interpolate edge vertices + uint cubeIndex; + getCubeIndex(cell, cubeIndex); + + // Early exit for empty/full cubes + if (cubeIndex == 0u || cubeIndex == 255u) { + return; + } + + Vertex edgeVertices[12]; + interpolateVertices(cell, cubeIndex, edgeVertices); + + Triangle triangles[5]; + uint triCount; + generateTriangles(edgeVertices, cubeIndex, triangles, triCount); + + if (triCount == 0u) return; + + // Reserve space in SSBOs atomically (3 vertices per triangle) + uint vertsNeeded = triCount * 3u; + uint baseVertex = atomicAdd(vertexCount, vertsNeeded); + uint baseIndex = atomicAdd(indexCount, vertsNeeded); + + // Write out vertices and indices + for (uint t = 0u; t < triCount; ++t) { + Vertex triVerts[3]; + triVerts[0] = edgeVertices[triangles[t].vertexIndices[0]]; + triVerts[1] = edgeVertices[triangles[t].vertexIndices[1]]; + triVerts[2] = edgeVertices[triangles[t].vertexIndices[2]]; + + vec3 faceNormal; + calculateFaceNormal(triVerts, faceNormal); + + // Apply normal to all vertices (flat shading) + triVerts[0].Normal = faceNormal; + triVerts[1].Normal = faceNormal; + triVerts[2].Normal = faceNormal; + + // Write vertices and indices + for (uint j = 0u; j < 3u; ++j) { + uint outVertexIdx = baseVertex + t * 3u + j; + vertices[outVertexIdx] = triVerts[j]; + indices[baseIndex + t * 3u + j] = outVertexIdx; + } + } +} + +void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + uint z = gl_GlobalInvocationID.z; + + processCube(x, y, z); +} \ No newline at end of file diff --git a/SPHFluid/shaders/FluidSim-3D.compute b/SPHFluid/shaders/FluidSim-3D.compute index f03cdfc..caf93d3 100644 --- a/SPHFluid/shaders/FluidSim-3D.compute +++ b/SPHFluid/shaders/FluidSim-3D.compute @@ -6,6 +6,7 @@ const int CalculateDensitiesKernel = 2; const int CalculatePressureForcesKernel = 3; const int CalculateViscosityKernel = 4; const int UpdatePositionsKernel = 5; +const int WriteDensityTextureKernel = 6; layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; @@ -35,6 +36,8 @@ layout(std430, binding = 2) restrict buffer StartIndicesBuffer { uint startIndices[]; }; +layout(binding = 3, r32f) uniform image3D densityTexture; + // Uniforms uniform int numParticles; uniform float deltaTime; @@ -57,6 +60,8 @@ uniform float spikyPow3Factor; uniform float spikyPow2DerivativeFactor; uniform float spikyPow3DerivativeFactor; +const uint3 densityMapSize = uint3(64, 64, 64); + uniform int currentKernel; const ivec3 offsets3D[27] = ivec3[]( ivec3(-1, -1, -1), ivec3(-1, -1, 0), ivec3(-1, -1, 1), @@ -204,6 +209,16 @@ void HandleCollisions(inout Particle particle) { } } +void WriteDensityToTexture() { + uvec3 texSize = uvec3(64, 64, 64); + for (uint i = 0; i < numParticles; i++) { + vec3 pos = particles[i].predictedPosition + (boundsSize * 0.5); + ivec3 texCoord = ivec3(clamp(floor(pos / boundsSize * vec3(texSize)), vec3(0), vec3(texSize - 1))); + + // Thread-safe accumulation + imageAtomicAdd(densityTexture, texCoord, particles[i].density); + } +} void main() { uint index = gl_GlobalInvocationID.x; @@ -319,4 +334,9 @@ void main() { particles[index].position = newPosition; HandleCollisions(particles[index]); } + else if (currentKernel == WriteDensityKernel) { + if (index == 0) { + WriteDensityToTexture(); + } + } } \ No newline at end of file diff --git a/SPHFluid/shaders/fluid_mesh.fs b/SPHFluid/shaders/fluid_mesh.fs new file mode 100644 index 0000000..e69de29 diff --git a/SPHFluid/shaders/fluid_mesh.vs b/SPHFluid/shaders/fluid_mesh.vs new file mode 100644 index 0000000..e69de29 diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/camera.h b/camera.h index e9ef51c..322c371 100644 --- a/camera.h +++ b/camera.h @@ -19,7 +19,7 @@ enum Camera_Movement // Default camera values const float YAW = -90.0f; const float PITCH = 0.0f; -const float SPEED = 5.0f; +const float SPEED = 2.0f; const float SENSITIVITY = 0.1f; const float ZOOM = 45.0f; diff --git a/geometry/cube.h b/geometry/cube.h new file mode 100644 index 0000000..7047c80 --- /dev/null +++ b/geometry/cube.h @@ -0,0 +1,134 @@ +#ifndef CUBOID_H +#define CUBOID_H +#include "geometry_data.h" + +class Cuboid : public GeometryData { + public: + Cuboid(float width, float height, float depth){ + float halfWidth = width / 2.0f; + float halfHeight = height / 2.0f; + float halfDepth = depth / 2.0f; + + // 24 vertices (4 per face, 6 faces) with proper normals for each face + float vertices[] = { + // Back face (z = -halfDepth) + -halfWidth, -halfHeight, -halfDepth, + halfWidth, -halfHeight, -halfDepth, + halfWidth, halfHeight, -halfDepth, + -halfWidth, halfHeight, -halfDepth, + + // Front face (z = halfDepth) + -halfWidth, -halfHeight, halfDepth, + halfWidth, -halfHeight, halfDepth, + halfWidth, halfHeight, halfDepth, + -halfWidth, halfHeight, halfDepth, + + // Left face (x = -halfWidth) + -halfWidth, -halfHeight, -halfDepth, + -halfWidth, -halfHeight, halfDepth, + -halfWidth, halfHeight, halfDepth, + -halfWidth, halfHeight, -halfDepth, + + // Right face (x = halfWidth) + halfWidth, -halfHeight, -halfDepth, + halfWidth, -halfHeight, halfDepth, + halfWidth, halfHeight, halfDepth, + halfWidth, halfHeight, -halfDepth, + + // Bottom face (y = -halfHeight) + -halfWidth, -halfHeight, -halfDepth, + halfWidth, -halfHeight, -halfDepth, + halfWidth, -halfHeight, halfDepth, + -halfWidth, -halfHeight, halfDepth, + + // Top face (y = halfHeight) + -halfWidth, halfHeight, -halfDepth, + halfWidth, halfHeight, -halfDepth, + halfWidth, halfHeight, halfDepth, + -halfWidth, halfHeight, halfDepth, + }; + + // Indices for triangles (6 faces * 2 triangles * 3 vertices) + unsigned int indices[] = { + // Back face + 0, 1, 2, 2, 3, 0, + // Front face + 4, 5, 6, 6, 7, 4, + // Left face + 8, 9, 10, 10, 11, 8, + // Right face + 12, 13, 14, 14, 15, 12, + // Bottom face + 16, 17, 18, 18, 19, 16, + // Top face + 20, 21, 22, 22, 23, 20, + }; + + // Normals for each vertex + float normals[] = { + // Back face + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + + // Front face + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + + // Left face + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + + // Right face + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + + // Bottom face + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + + // Top face + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + }; + + float textureCoords[] = { + // Back face + 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + // Front face + 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + // Left face + 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + // Right face + 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + // Bottom face + 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + // Top face + 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + }; + + addAttribute("v_pos", 3, std::vector(vertices, vertices + sizeof(vertices) / sizeof(float))); + addAttribute("v_norm", 3, std::vector(normals, normals + sizeof(normals) / sizeof(float))); + addAttribute("v_uv", 2, std::vector(textureCoords, textureCoords + sizeof(textureCoords) / sizeof(float))); + + setIndices(std::vector(indices, indices + sizeof(indices) / sizeof(unsigned int))); + } + + Cuboid(float length) : Cuboid(length, length, length) { + // Delegate to the main constructor + } + ~Cuboid(){} +}; + +#endif \ No newline at end of file diff --git a/img/textures/Rubiks Col.png b/img/textures/Rubiks Col.png new file mode 100644 index 0000000..bb0d76f Binary files /dev/null and b/img/textures/Rubiks Col.png differ diff --git a/mesh.cpp b/mesh.cpp index d45f875..93188df 100644 --- a/mesh.cpp +++ b/mesh.cpp @@ -40,6 +40,9 @@ void Mesh::setupMesh () { void Mesh::Draw(Shader &shader) { + const bool hasTextures = !textures.empty(); + shader.setBool("material.useTexture", hasTextures); + unsigned int diffuseNr = 1; unsigned int specularNr = 1; for(unsigned int i = 0; i < textures.size(); i++) @@ -53,7 +56,7 @@ void Mesh::Draw(Shader &shader) else if(name == "texture_specular") number = std::to_string(specularNr++); - shader.setInt(("material." + name + number).c_str(), i); + shader.setInt((name + number).c_str(), static_cast(i)); glBindTexture(GL_TEXTURE_2D, textures[i].id); } glActiveTexture(GL_TEXTURE0); diff --git a/shader.cpp b/shader.cpp index 9482b2b..d4fb0e2 100644 --- a/shader.cpp +++ b/shader.cpp @@ -205,6 +205,10 @@ void Shader::setFloat(const std::string &name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } +void Shader::setMat3(const std::string &name, const glm::mat3 value) const{ + int Loc = glGetUniformLocation(ID, name.c_str()); + glUniformMatrix3fv(Loc, 1, GL_FALSE, glm::value_ptr(value)); +} void Shader::setMat4(const std::string &name, const glm::mat4 value) const{ int Loc = glGetUniformLocation(ID, name.c_str()); glUniformMatrix4fv(Loc, 1, GL_FALSE, glm::value_ptr(value)); diff --git a/shader.h b/shader.h index 13168b0..92aa143 100644 --- a/shader.h +++ b/shader.h @@ -24,6 +24,7 @@ class Shader{ void setVec3(const std::string &name, glm::vec3 value) const; void setVec4(const std::string &name, glm::vec4 value) const; void setFloat(const std::string &name, float value) const; + void setMat3(const std::string &name, const glm::mat3 value) const; void setMat4(const std::string &name, const glm::mat4 value) const; }; diff --git a/shaders/box.fs b/shaders/box.fs new file mode 100644 index 0000000..e55d54f --- /dev/null +++ b/shaders/box.fs @@ -0,0 +1,9 @@ +#version 330 core +out vec4 FragColor; + +uniform vec3 color; + +void main() +{ + FragColor = vec4(color, 1.0); +} diff --git a/shaders/box.vs b/shaders/box.vs new file mode 100644 index 0000000..cacee3f --- /dev/null +++ b/shaders/box.vs @@ -0,0 +1,11 @@ +#version 330 core +layout(location = 0) in vec3 aPos; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +void main() +{ + gl_Position = projection * view * model * vec4(aPos, 1.0); +} diff --git a/shaders/rubiks.fs b/shaders/rubiks.fs new file mode 100644 index 0000000..92e2358 --- /dev/null +++ b/shaders/rubiks.fs @@ -0,0 +1,212 @@ +#version 330 core +out vec4 FragColor; + +in vec3 Normal; +in vec2 TexCoords; +in vec3 FragPos; +in vec3 LocalPos; +in vec3 LocalNormal; + +// Simple lighting uniforms +uniform vec3 viewPos; + +struct Material { + vec3 ambient; + vec3 diffuse; + vec3 specular; + float shininess; + bool useTexture; +}; +uniform Material material; + +struct DirLight { + vec3 direction; + vec3 ambient; + vec3 diffuse; + vec3 specular; +}; + +const int MAX_DIR_LIGHTS = 6; +uniform DirLight dirLights[MAX_DIR_LIGHTS]; +uniform int dirLightCount; + +uniform sampler2D texture_diffuse1; +uniform sampler2D faceTextures[6]; +uniform bool useFaceTextures; +uniform int debugDisplayMode; // 0 = normal lighting, 1 = show baseColor (unlit) +uniform int cubieID; +uniform int selectedID; + +// NEW: Direct face color indices (set per cubie from CPU) +// Each element is the color index for that geometric face, or -1 for black/internal +// Face indices: 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z +uniform int faceColorIndex[6]; + +// Standard Rubik's Cube face colors +// Color indices: 0=Red, 1=Orange, 2=White, 3=Yellow, 4=Green, 5=Blue +const vec3 kFaceColors[6] = vec3[6]( + vec3(1.0, 0.0, 0.0), // 0: red + vec3(1.0, 0.5, 0.0), // 1: orange + vec3(1.0, 1.0, 1.0), // 2: white + vec3(1.0, 1.0, 0.0), // 3: yellow + vec3(0.0, 1.0, 0.0), // 4: green + vec3(0.0, 0.2, 1.0) // 5: blue +); + +// Determine which geometric face this fragment is on and get UV coordinates +// Returns the face index (0-5), or -1 if near an edge/seam (black plastic) +int getGeometricFace(vec3 localPos, vec3 normal, out vec2 uv) +{ + vec3 n = normalize(normal); + vec3 p = localPos; + vec3 ap = abs(p); + float maxAxis = max(ap.x, max(ap.y, ap.z)); + + if (maxAxis < 1e-4) + { + p = n; + ap = abs(p); + maxAxis = max(ap.x, max(ap.y, ap.z)); + } + + vec3 projected = p / maxAxis; + vec3 aprRaw = abs(projected); + vec3 apr = aprRaw + vec3(0.0003, 0.0002, 0.0001); // tie-breaker + + // Determine current geometric face + int currentFace; + if (apr.x > apr.y && apr.x > apr.z) + { + if (projected.x > 0.0) + { + currentFace = 0; // +X + uv = vec2(-projected.z, projected.y); + } + else + { + currentFace = 1; // -X + uv = vec2(projected.z, projected.y); + } + } + else if (apr.y > apr.z) + { + if (projected.y > 0.0) + { + currentFace = 2; // +Y (top) + uv = vec2(projected.x, -projected.z); + } + else + { + currentFace = 3; // -Y (bottom) + uv = vec2(projected.x, projected.z); + } + } + else + { + if (projected.z > 0.0) + { + currentFace = 4; // +Z + uv = vec2(projected.x, projected.y); + } + else + { + currentFace = 5; // -Z + uv = vec2(-projected.x, projected.y); + } + } + + // Detect if we're near an edge where two axes compete -> black plastic seam + float largest = max(aprRaw.x, max(aprRaw.y, aprRaw.z)); + float secondLargest = 0.0; + if (largest == aprRaw.x) + secondLargest = max(aprRaw.y, aprRaw.z); + else if (largest == aprRaw.y) + secondLargest = max(aprRaw.x, aprRaw.z); + else + secondLargest = max(aprRaw.x, aprRaw.y); + + if (largest - secondLargest < 0.08) + { + return -1; // edge/corner -> black plastic + } + + // Map UV from [-1,1] to [0,1] + uv = uv * 0.5 + 0.5; + uv = clamp(uv, vec2(0.0), vec2(1.0)); + + return currentFace; +} + +// Get the color for this fragment based on face mapping +vec3 getCubieColor(vec3 localPos, vec3 normal) +{ + vec2 uv; + int geometricFace = getGeometricFace(localPos, normal, uv); + + // Edge/seam -> black plastic + if (geometricFace < 0) + { + return vec3(0.02); + } + + // Get the color index for this geometric face + int colorIdx = faceColorIndex[geometricFace]; + + // -1 means internal/black + if (colorIdx < 0) + { + return vec3(0.02); + } + + // Textures ON: sample from per-face texture + if (useFaceTextures) + { + return texture(faceTextures[colorIdx], uv).rgb; + } + + // Textures OFF: use solid face colors + return kFaceColors[colorIdx]; +} + +void main() +{ + // Normalize the normal vector + vec3 norm = normalize(Normal); + vec3 viewDir = normalize(viewPos - FragPos); + + // Get base color for this cubie face + vec3 baseColor = getCubieColor(LocalPos, LocalNormal); + + // Debug shortcut: render the base color directly (no lighting) to inspect textures/mapping + if (debugDisplayMode == 1) + { + FragColor = vec4(baseColor, 1.0); + return; + } + + vec3 result = vec3(0.0); + for (int i = 0; i < dirLightCount && i < MAX_DIR_LIGHTS; ++i) + { + vec3 lightDir = normalize(-dirLights[i].direction); + + vec3 ambient = dirLights[i].ambient * baseColor; + + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = dirLights[i].diffuse * diff * baseColor; + + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + vec3 specular = dirLights[i].specular * spec * material.specular; + + result += ambient + diffuse + specular; + } + + // Highlight if this cubie is selected + if (cubieID >= 0 && cubieID == selectedID) + { + vec3 highlight = vec3(1.0, 0.0, 1.0); + result = mix(result, highlight, 0.35); + } + + FragColor = vec4(result, 1.0); +} diff --git a/shaders/vertex.vs b/shaders/vertex.vs index 21fd6d4..02acb22 100644 --- a/shaders/vertex.vs +++ b/shaders/vertex.vs @@ -6,6 +6,8 @@ layout (location = 2) in vec2 aTexCoord; out vec2 TexCoords; out vec3 Normal; out vec3 FragPos; +out vec3 LocalPos; +out vec3 LocalNormal; uniform mat4 model; @@ -19,8 +21,10 @@ void main() gl_Position = projection * view * model * vec4(aPos, 1.0); TexCoords = vec2(aTexCoord.x, aTexCoord.y); FragPos = vec3(model * vec4(aPos, 1.0)); + LocalPos = aPos; // Transform normals to world space using the normal matrix // The normal matrix is the transpose of the inverse of the upper-left 3x3 of the model matrix Normal = mat3(transpose(inverse(model))) * aNormal; + LocalNormal = aNormal; } \ No newline at end of file