diff --git a/.attachments/fixed-raycast.png b/.attachments/fixed-raycast.png new file mode 100644 index 0000000..eacff4b Binary files /dev/null and b/.attachments/fixed-raycast.png differ diff --git a/README.md b/README.md index 660f799..f5d939e 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,9 @@ Different voxel types are now supported in the data structures and passed to the When placing a block, the program now raycasts to place where the camera is looking. ![a place of brown dirt with word Hi built out of stone.](.attachments/raycasting_test.png) + +### 0.6.1 : Raycast Bug Fixes - 11/17/25 + +It took me a couple of days to fix the ray casting. I forgot that my mesh is offset by 0.5, so 3/4 raycasts looked at least a block off of what the camera was looking at. + +![Hello, written in blocks on a dirt plane](.attachments/fixed-raycast.png) diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 0000000..508130d --- /dev/null +++ b/run.ps1 @@ -0,0 +1 @@ +.\build\Debug\VoxelEngine.exe \ No newline at end of file diff --git a/src/World.cpp b/src/World.cpp index 346f578..c66ff9f 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -71,32 +71,56 @@ std::optional> World::raycast_voxel(glm::vec3 // Normalize direction to ensure consistent behavior direction = glm::normalize(direction); - std::cout << "[RAYCAST] Start: (" << start.x << ", " << start.y << ", " << start.z << ")" << std::endl; - std::cout << "[RAYCAST] Direction: (" << direction.x << ", " << direction.y << ", " << direction.z << ")" << std::endl; + // std::cout << "[RAYCAST] Start: (" << start.x << ", " << start.y << ", " << start.z << ")" << std::endl; + // std::cout << "[RAYCAST] Direction: (" << direction.x << ", " << direction.y << ", " << direction.z << ")" << std::endl; - glm::ivec3 pos = glm::ivec3(std::floor(start.x), std::floor(start.y), std::floor(start.z)); - std::cout << "[RAYCAST] Starting voxel: (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; + // Adjust start position to account for voxels being centered at integer + 0.5 + // (voxel at pos (0,0,0) occupies space from (-0.5,-0.5,-0.5) to (0.5,0.5,0.5)) + glm::vec3 adjusted_start = start + glm::vec3(0.5f, 0.5f, 0.5f); + + glm::ivec3 pos = glm::ivec3(std::floor(adjusted_start.x), std::floor(adjusted_start.y), std::floor(adjusted_start.z)); + // std::cout << "[RAYCAST] Starting voxel: (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; - // (+1 or -1 for each axis) - glm::ivec3 step_dir = glm::sign(direction); - std::cout << "[RAYCAST] Step direction: (" << step_dir.x << ", " << step_dir.y << ", " << step_dir.z << ")" << std::endl; + // (+1, -1, or 0 for each axis) + glm::ivec3 step_dir = glm::ivec3( + direction.x > 0 ? 1 : (direction.x < 0 ? -1 : 0), + direction.y > 0 ? 1 : (direction.y < 0 ? -1 : 0), + direction.z > 0 ? 1 : (direction.z < 0 ? -1 : 0) + ); + // std::cout << "[RAYCAST] Step direction: (" << step_dir.x << ", " << step_dir.y << ", " << step_dir.z << ")" << std::endl; - // How far to step in each axis - const float epsilon = 0.0001f; - glm::vec3 safe_dir = glm::vec3( - abs(direction.x) < epsilon ? epsilon : direction.x, - abs(direction.y) < epsilon ? epsilon : direction.y, - abs(direction.z) < epsilon ? epsilon : direction.z + // Delta: how far along the ray we move for a full voxel step in each axis + glm::vec3 delta = glm::vec3( + step_dir.x != 0 ? std::abs(1.0f / direction.x) : FLT_MAX, + step_dir.y != 0 ? std::abs(1.0f / direction.y) : FLT_MAX, + step_dir.z != 0 ? std::abs(1.0f / direction.z) : FLT_MAX ); - glm::vec3 delta = glm::abs(1.0f / safe_dir); + // Calculate t_max: distance along ray to next voxel boundary for each axis + glm::vec3 t_max; + glm::vec3 fract = adjusted_start - glm::vec3(pos); - glm::vec3 fract = start - glm::vec3(pos); - glm::vec3 t_max = glm::vec3( - (step_dir.x > 0 ? (1.0f - fract.x) : fract.x) * delta.x, - (step_dir.y > 0 ? (1.0f - fract.y) : fract.y) * delta.y, - (step_dir.z > 0 ? (1.0f - fract.z) : fract.z) * delta.z - ); + // For each axis, calculate distance to the next boundary + if (step_dir.x > 0) + t_max.x = (1.0f - fract.x) * delta.x; + else if (step_dir.x < 0) + t_max.x = fract.x * delta.x; + else + t_max.x = FLT_MAX; + + if (step_dir.y > 0) + t_max.y = (1.0f - fract.y) * delta.y; + else if (step_dir.y < 0) + t_max.y = fract.y * delta.y; + else + t_max.y = FLT_MAX; + + if (step_dir.z > 0) + t_max.z = (1.0f - fract.z) * delta.z; + else if (step_dir.z < 0) + t_max.z = fract.z * delta.z; + else + t_max.z = FLT_MAX; float dist = 0.0f; glm::ivec3 last_move = glm::ivec3(0, 0, 0); @@ -107,11 +131,11 @@ std::optional> World::raycast_voxel(glm::vec3 while (dist < max_dist && iterations < max_iterations) { iterations++; - std::cout << "[RAYCAST] Iteration " << iterations << " at voxel (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; + // std::cout << "[RAYCAST] Iteration " << iterations << " at voxel (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; if (this->get_voxel(pos).has_value()) { - std::cout << "[RAYCAST] Hit voxel at (" << pos.x << ", " << pos.y << ", " << pos.z << ") after " << iterations << " iterations" << std::endl; - std::cout << "[RAYCAST] Returning normal: (" << -last_move.x << ", " << -last_move.y << ", " << -last_move.z << ")" << std::endl; + // std::cout << "[RAYCAST] Hit voxel at (" << pos.x << ", " << pos.y << ", " << pos.z << ") after " << iterations << " iterations" << std::endl; + // std::cout << "[RAYCAST] Returning normal: (" << -last_move.x << ", " << -last_move.y << ", " << -last_move.z << ")" << std::endl; return std::make_pair(pos, -last_move); } @@ -134,7 +158,7 @@ std::optional> World::raycast_voxel(glm::vec3 } } - std::cout << "[RAYCAST] No voxel hit after " << iterations << " iterations, dist=" << dist << std::endl; + // std::cout << "[RAYCAST] No voxel hit after " << iterations << " iterations, dist=" << dist << std::endl; return std::nullopt; } diff --git a/src/main.cpp b/src/main.cpp index 7982996..51846a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,8 @@ unsigned int SCR_WIDTH = 1920; unsigned int SCR_HEIGHT = 1080; +const int EDIT_RANGE = 5; + // Jump const float JUMP_COOLDOWN = 0.0f; const float JUMP_POWER = 10.0f; // Initial Jump Speed @@ -35,6 +37,9 @@ bool W_pressed, S_pressed, A_pressed, D_pressed = false; glm::mat4 projection = glm::mat4(1.0f); Camera camera = Camera(CAMERA_SPEED, glm::vec3(0.0f, 0.0f, 0.0f)); +// Targeted block for highlighting +std::optional targeted_block = std::nullopt; + // Callback function for when the window is resized // glfw: whenever the window size changed (by OS or user resize) this callback function executes void framebuffer_size_callback(GLFWwindow* window, int width, int height) @@ -60,7 +65,7 @@ World world; void mouse_callback(GLFWwindow * window, double xpos, double ypos) { float x_offset = xpos - last_x; - float y_offset = ypos - last_y; + float y_offset = last_y - ypos; // Inverted Y for typical FPS controls last_x = xpos; last_y = ypos; @@ -83,7 +88,7 @@ void mouse_callback(GLFWwindow * window, double xpos, double ypos) glm::vec3 direction; direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); - direction.y = -sin(glm::radians(pitch)); + direction.y = sin(glm::radians(pitch)); // Positive Y is up direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); camera.target = glm::normalize(direction); } @@ -93,6 +98,34 @@ void processInput(GLFWwindow *window) { float current_time = glfwGetTime(); + if (glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS) { + glm::vec3 eye = camera.getEyePosition(); + glm::mat4 view = camera.getView(); + glm::vec3 forward = -glm::vec3(view[0][2], view[1][2], view[2][2]); // Extract forward from view matrix + + std::cout << "Camera target (from pitch/yaw): (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; + std::cout << "Camera forward (from view matrix): (" << forward.x << ", " << forward.y << ", " << forward.z << ")" << std::endl; + std::cout << "Match: " << (glm::length(camera.target - forward) < 0.01f ? "YES" : "NO") << std::endl; + } + + // Update targeted block every frame for highlight + std::optional> raycast_result = + world.raycast_voxel(camera.getEyePosition(), camera.target, EDIT_RANGE); + if (raycast_result.has_value()) { + targeted_block = raycast_result.value().first; + + // Debug: print occasionally + static int frame_count = 0; + if (frame_count % 60 == 0) { + std::cout << "Camera eye: (" << camera.getEyePosition().x << ", " << camera.getEyePosition().y << ", " << camera.getEyePosition().z << ")" << std::endl; + std::cout << "Camera target dir: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; + std::cout << "Highlighted block: (" << targeted_block.value().x << ", " << targeted_block.value().y << ", " << targeted_block.value().z << ")" << std::endl; + } + frame_count++; + } else { + targeted_block = std::nullopt; + } + if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { // if (mouse_lock) { // glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); @@ -182,10 +215,10 @@ void processInput(GLFWwindow *window) std::cout << "\n=== LEFT CLICK (PLACE) ===" << std::endl; std::cout << "Camera eye pos: (" << camera.getEyePosition().x << ", " << camera.getEyePosition().y << ", " << camera.getEyePosition().z << ")" << std::endl; std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; - + std::optional> raycast_result = - world.raycast_voxel(camera.getEyePosition(), camera.target, 3); - + world.raycast_voxel(camera.getEyePosition(), camera.target, EDIT_RANGE); + if (raycast_result.has_value()) { auto [target_block, normal] = raycast_result.value(); std::cout << "Hit block at: (" << target_block.x << ", " << target_block.y << ", " << target_block.z << ")" << std::endl; @@ -207,10 +240,10 @@ void processInput(GLFWwindow *window) std::cout << "\n=== RIGHT CLICK (REMOVE) ===" << std::endl; std::cout << "Camera eye pos: (" << camera.getEyePosition().x << ", " << camera.getEyePosition().y << ", " << camera.getEyePosition().z << ")" << std::endl; std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; - + std::optional> raycast_result = - world.raycast_voxel(camera.getEyePosition(), camera.target, 3); - + world.raycast_voxel(camera.getEyePosition(), camera.target, EDIT_RANGE); + if (raycast_result.has_value()) { auto [target_block, normal] = raycast_result.value(); std::cout << "Hit block at: (" << target_block.x << ", " << target_block.y << ", " << target_block.z << ")" << std::endl; @@ -236,7 +269,7 @@ int main() { glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 2. --- Create a Window --- - GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Voxel Engine 0.6.0", NULL, NULL); + GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Voxel Engine 0.6.1", NULL, NULL); if (window == NULL) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); @@ -278,10 +311,10 @@ int main() { unsigned int crosshairVAO, crosshairVBO; glGenVertexArrays(1, &crosshairVAO); glGenBuffers(1, &crosshairVBO); - + glBindVertexArray(crosshairVAO); glBindBuffer(GL_ARRAY_BUFFER, crosshairVBO); - + // Crosshair vertices in NDC (Normalized Device Coordinates: -1 to 1) float crosshairSize = 0.02f; // Size in NDC float crosshairVertices[] = { @@ -292,12 +325,65 @@ int main() { 0.0f, -crosshairSize, 0.0f, 0.0f, crosshairSize, 0.0f }; - + glBufferData(GL_ARRAY_BUFFER, sizeof(crosshairVertices), crosshairVertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); + // Setup highlight box (cube outline) + unsigned int highlightVAO, highlightVBO, highlightEBO; + glGenVertexArrays(1, &highlightVAO); + glGenBuffers(1, &highlightVBO); + glGenBuffers(1, &highlightEBO); + + glBindVertexArray(highlightVAO); + glBindBuffer(GL_ARRAY_BUFFER, highlightVBO); + + // Cube outline vertices (slightly larger than 1 unit cube for visibility) + float offset = 0.001f; // Slight offset to prevent z-fighting + float highlightVertices[] = { + // 8 corners of a cube + -0.5f - offset, -0.5f - offset, -0.5f - offset, // 0 + 0.5f + offset, -0.5f - offset, -0.5f - offset, // 1 + 0.5f + offset, 0.5f + offset, -0.5f - offset, // 2 + -0.5f - offset, 0.5f + offset, -0.5f - offset, // 3 + -0.5f - offset, -0.5f - offset, 0.5f + offset, // 4 + 0.5f + offset, -0.5f - offset, 0.5f + offset, // 5 + 0.5f + offset, 0.5f + offset, 0.5f + offset, // 6 + -0.5f - offset, 0.5f + offset, 0.5f + offset // 7 + }; + + // Indices for the 12 edges of the cube + unsigned int highlightIndices[] = { + // Bottom face + 0, 1, 1, 2, 2, 3, 3, 0, + // Top face + 4, 5, 5, 6, 6, 7, 7, 4, + // Vertical edges + 0, 4, 1, 5, 2, 6, 3, 7 + }; + + glBufferData(GL_ARRAY_BUFFER, sizeof(highlightVertices), highlightVertices, GL_STATIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, highlightEBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(highlightIndices), highlightIndices, GL_STATIC_DRAW); + glBindVertexArray(0); + + // Setup debug ray + unsigned int debugRayVAO, debugRayVBO; + glGenVertexArrays(1, &debugRayVAO); + glGenBuffers(1, &debugRayVBO); + + glBindVertexArray(debugRayVAO); + glBindBuffer(GL_ARRAY_BUFFER, debugRayVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6, nullptr, GL_DYNAMIC_DRAW); // 2 points * 3 coords + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + glBindVertexArray(0); + glm::mat4 view; double start_time = glfwGetTime(); double move_time = glfwGetTime(); @@ -400,6 +486,22 @@ int main() { // --- Reset polygon mode for next frame (good practice) --- glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // ---- Draw Highlighted Block ---- + if (targeted_block.has_value()) { + shader.use(); + glm::mat4 model = glm::mat4(1.0f); + // Voxels are centered on integer coordinates, so no need to add 0.5 + model = glm::translate(model, glm::vec3(targeted_block.value())); + glm::mat4 mvp = projection * view * model; + shader.setMat4("u_mvp", mvp); + shader.setVec3("u_Color", glm::vec3(0.0f, 0.0f, 0.0f)); // Black outline + + glLineWidth(2.0f); + glBindVertexArray(highlightVAO); + glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0); // 24 indices = 12 edges + glBindVertexArray(0); + } + // ---- Draw Crosshair ---- glDisable(GL_DEPTH_TEST); // Draw on top of everything shader.use(); @@ -425,4 +527,4 @@ int main() { glfwTerminate(); // Clean up GLFW return 0; -} \ No newline at end of file +}