diff --git a/.attachments/raycasting_test.png b/.attachments/raycasting_test.png new file mode 100644 index 0000000..5c516b7 Binary files /dev/null and b/.attachments/raycasting_test.png differ diff --git a/Notes.md b/Notes.md new file mode 100644 index 0000000..8c5e7a5 --- /dev/null +++ b/Notes.md @@ -0,0 +1,6 @@ + +These are just some notes on the blogpost I was following. While it was great, I thought the author might want to know where some areas were lacking (i would). + +- in the definition of raycast_voxel `step` is never defined, nor explained. I'm pretty sure it's just supposed to be `step_dir` though. +- `get_voxel` returns the type `Voxel?` but `Chunk.voxels` doesn't hold a type named `Voxel` only a HashMap + diff --git a/README.md b/README.md index a64cf02..660f799 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,9 @@ Chunks now generate around the player in a square based on a render distance. Th Different voxel types are now supported in the data structures and passed to the shaders. ![a world of dirt with some stone blocks](.attachments/voxel-types.png) + +## 0.6.0 : Raycasting Placing - 11/13/25 + +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) diff --git a/src/Camera.h b/src/Camera.h index a545a52..fb1d2dd 100644 --- a/src/Camera.h +++ b/src/Camera.h @@ -4,7 +4,7 @@ #include #include -const float CAMERA_HEIGHT = 1.7f; +const float CAMERA_HEIGHT = 1.8f; const float GRAVITY = 18.6f; // Should be in m/s^2 but it doesn't really work out with 9.8 so we double it class Camera @@ -62,11 +62,12 @@ class Camera glm::mat4 getView() { - glm::mat4 v = glm::lookAt(this->position, - this->target + this->position, - this->worldUp); + glm::vec3 real_camera_pos = this->position; + real_camera_pos.y += CAMERA_HEIGHT; - v = glm::translate(v, glm::vec3(0.0f, -CAMERA_HEIGHT, 0.0f)); + glm::mat4 v = glm::lookAt(real_camera_pos, + real_camera_pos + this->target, + this->worldUp); return v; } @@ -92,6 +93,9 @@ class Camera } + glm::vec3 getEyePosition() const { + return position + glm::vec3(0.0f, CAMERA_HEIGHT, 0.0f); + } }; #endif diff --git a/src/Chunk.cpp b/src/Chunk.cpp index b2c35c8..0bc1864 100644 --- a/src/Chunk.cpp +++ b/src/Chunk.cpp @@ -46,9 +46,19 @@ void addFace(glm::ivec3 pos, const float *faceVertices, float voxelKind, std::ve unsigned int vIndex = vbo.size() / VERTEX_SIZE; for (int i = 0; i < 4; i++) { const float* v = &faceVertices[i * 6]; // Face data is still 6 floats per vertex - vbo.push_back(v[0] + pos.x); - vbo.push_back(v[1] + pos.y); - vbo.push_back(v[2] + pos.z); + float world_x = v[0] + pos.x; + float world_y = v[1] + pos.y; + float world_z = v[2] + pos.z; + + // Debug: Print first vertex of first face + if (vbo.empty() && i == 0) { + std::cout << "[MESH] First vertex - Local pos: (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; + std::cout << "[MESH] First vertex - World: (" << world_x << ", " << world_y << ", " << world_z << ")" << std::endl; + } + + vbo.push_back(world_x); + vbo.push_back(world_y); + vbo.push_back(world_z); vbo.push_back(v[3]); vbo.push_back(v[4]); vbo.push_back(v[5]); @@ -117,5 +127,10 @@ void Chunk::generateMesh() { } void Chunk::regenerateMesh() { + std::cout << "[REGENERATE] Regenerating mesh with " << voxels.size() << " voxels" << std::endl; + if (!voxels.empty()) { + auto first = voxels.begin(); + std::cout << "[REGENERATE] First voxel at local: (" << first->first.x << ", " << first->first.y << ", " << first->first.z << ")" << std::endl; + } this->generateMesh(); } diff --git a/src/World.cpp b/src/World.cpp index d68a724..346f578 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -1,10 +1,34 @@ #include "World.h" +#include + +// Helper function to handle negative modulo correctly +glm::ivec3 positive_modulo(glm::ivec3 value, int divisor) { + glm::ivec3 result; + result.x = ((value.x % divisor) + divisor) % divisor; + result.y = ((value.y % divisor) + divisor) % divisor; + result.z = ((value.z % divisor) + divisor) % divisor; + return result; +} + +// Helper function to correctly floor divide for chunk positions +glm::ivec3 floor_divide(glm::ivec3 value, int divisor) { + glm::ivec3 result; + result.x = (int)std::floor((float)value.x / divisor); + result.y = (int)std::floor((float)value.y / divisor); + result.z = (int)std::floor((float)value.z / divisor); + return result; +} + World::World() {} void World::set_voxel(glm::ivec3 position, std::optional kind) { - glm::ivec3 chunk_position = position / CHUNK_SIZE; - glm::ivec3 local_position = position % CHUNK_SIZE; + glm::ivec3 chunk_position = floor_divide(position, CHUNK_SIZE); + glm::ivec3 local_position = positive_modulo(position, CHUNK_SIZE); + + std::cout << "[SET_VOXEL] World pos: (" << position.x << ", " << position.y << ", " << position.z << ")" << std::endl; + std::cout << "[SET_VOXEL] Chunk pos: (" << chunk_position.x << ", " << chunk_position.y << ", " << chunk_position.z << ")" << std::endl; + std::cout << "[SET_VOXEL] Local pos: (" << local_position.x << ", " << local_position.y << ", " << local_position.z << ")" << std::endl; Chunk * chunk = get_or_create_chunk(chunk_position); if (!kind.has_value()) { @@ -22,11 +46,11 @@ Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { // Fill in new chunk with a flat plane of voxels at y=0 std::unordered_map voxels; if (chunk_position.y == 0) { - for (int z = 0; z <= CHUNK_SIZE; z++) { - for (int y = 0; y <= CHUNK_SIZE; y++) { - for (int x = 0; x <= CHUNK_SIZE; x++) { + for (int z = 0; z < CHUNK_SIZE; z++) { + for (int y = 0; y < CHUNK_SIZE; y++) { + for (int x = 0; x < CHUNK_SIZE; x++) { glm::ivec3 offset = glm::ivec3(x, y, z); - glm::ivec3 world_voxel_position = chunk_position * 32 + offset; + glm::ivec3 world_voxel_position = chunk_position * CHUNK_SIZE + offset; if (world_voxel_position.y == 0) { voxels[offset] = VoxelKind::Dirt; @@ -42,3 +66,86 @@ Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { } return &this->chunks[chunk_position]; } + +std::optional> World::raycast_voxel(glm::vec3 start, glm::vec3 direction, float max_dist) { + // 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; + + 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; + + // (+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; + + // 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 + ); + + glm::vec3 delta = glm::abs(1.0f / safe_dir); + + 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 + ); + + float dist = 0.0f; + glm::ivec3 last_move = glm::ivec3(0, 0, 0); + + int iterations = 0; + const int max_iterations = 100; // Safety limit + + while (dist < max_dist && iterations < max_iterations) { + iterations++; + + 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; + return std::make_pair(pos, -last_move); + } + + // step in the axis with the smallest t_max - that's the next voxel boundary + if (t_max.x < t_max.y && t_max.x < t_max.z) { + pos.x += step_dir.x; + last_move = glm::ivec3(step_dir.x, 0, 0); + dist = t_max.x; + t_max.x += delta.x; + } else if (t_max.y < t_max.z) { + pos.y += step_dir.y; + last_move = glm::ivec3(0, step_dir.y, 0); + dist = t_max.y; + t_max.y += delta.y; + } else { + pos.z += step_dir.z; + last_move = glm::ivec3(0, 0, step_dir.z); + dist = t_max.z; + t_max.z += delta.z; + } + } + + std::cout << "[RAYCAST] No voxel hit after " << iterations << " iterations, dist=" << dist << std::endl; + return std::nullopt; +} + +std::optional World::get_voxel(glm::ivec3 position) { + glm::ivec3 chunk_position = floor_divide(position, CHUNK_SIZE); + glm::ivec3 local_position = positive_modulo(position, CHUNK_SIZE); + + Chunk* chunk = this->get_or_create_chunk(chunk_position); + auto it = chunk->voxels.find(local_position); + if (it != chunk->voxels.end()) { + return it->second; + } + return std::nullopt; +} diff --git a/src/World.h b/src/World.h index 82413fd..fa51d65 100644 --- a/src/World.h +++ b/src/World.h @@ -17,6 +17,10 @@ class World void set_voxel(glm::ivec3 position, std::optional kind); + std::optional> raycast_voxel(glm::vec3 start, glm::vec3 direction, float max_dist); + + std::optional get_voxel(glm::ivec3 position); + Chunk* get_or_create_chunk(glm::ivec3 chunk_position); }; diff --git a/src/main.cpp b/src/main.cpp index 29e821a..7982996 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,7 +33,7 @@ const int VIEW_DISTANCE = 4; // In Chunks 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, 1.0f, 3.0f)); +Camera camera = Camera(CAMERA_SPEED, glm::vec3(0.0f, 0.0f, 0.0f)); // Callback function for when the window is resized // glfw: whenever the window size changed (by OS or user resize) this callback function executes @@ -86,10 +86,6 @@ void mouse_callback(GLFWwindow * window, double xpos, double ypos) direction.y = -sin(glm::radians(pitch)); direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); camera.target = glm::normalize(direction); - - if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS) { - world.set_voxel(glm::ivec3(camera.position), VoxelKind::Stone); - } } // process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly @@ -176,6 +172,57 @@ void processInput(GLFWwindow *window) camera.speed = CAMERA_SPEED * 7.5; if(glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_RELEASE) camera.speed = CAMERA_SPEED * 20; + + static bool left_mouse_pressed = false; + static bool right_mouse_pressed = false; + + // Left click - place block + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS && !left_mouse_pressed) { + left_mouse_pressed = true; + 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); + + 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; + std::cout << "Normal: (" << normal.x << ", " << normal.y << ", " << normal.z << ")" << std::endl; + glm::ivec3 place_pos = target_block + normal; + std::cout << "Placing at: (" << place_pos.x << ", " << place_pos.y << ", " << place_pos.z << ")" << std::endl; + world.set_voxel(place_pos, VoxelKind::Stone); + } else { + std::cout << "No block hit within range" << std::endl; + } + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_RELEASE) { + left_mouse_pressed = false; + } + + // Right click - remove block + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS && !right_mouse_pressed) { + right_mouse_pressed = true; + 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); + + 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; + std::cout << "Removing block at: (" << target_block.x << ", " << target_block.y << ", " << target_block.z << ")" << std::endl; + world.set_voxel(target_block, std::nullopt); + } else { + std::cout << "No block hit within range" << std::endl; + } + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_RELEASE) { + right_mouse_pressed = false; + } } int main() { @@ -189,7 +236,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.5.0", NULL, NULL); + GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Voxel Engine 0.6.0", NULL, NULL); if (window == NULL) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); @@ -227,6 +274,30 @@ int main() { // Setup projection matrix projection = glm::perspective(glm::radians(FOV), (float)SCR_WIDTH / (float)SCR_HEIGHT, Z_NEAR, Z_FAR); // This doesn't need to be recalculated every frame + // Setup crosshair + 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[] = { + // Horizontal line + -crosshairSize, 0.0f, 0.0f, + crosshairSize, 0.0f, 0.0f, + // Vertical line + 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); + glm::mat4 view; double start_time = glfwGetTime(); double move_time = glfwGetTime(); @@ -246,15 +317,19 @@ int main() { shader.use(); - if (!debug_printed) { - std::cout << "\n=== RENDER DEBUG ===" << std::endl; - std::cout << "Camera pos: (" << camera.position.x << ", " << camera.position.y << ", " << camera.position.z << ")" << std::endl; - std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; - // std::cout << "Number of chunks to draw: " << world.chunks.size() << std::endl; - } + // if (!debug_printed) { + // std::cout << "\n=== RENDER DEBUG ===" << std::endl; + // std::cout << "Camera pos: (" << camera.position.x << ", " << camera.position.y << ", " << camera.position.z << ")" << std::endl; + // std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; + // // std::cout << "Number of chunks to draw: " << world.chunks.size() << std::endl; + // } // Generate chunks around the camera - glm::ivec3 camera_chunk = glm::ivec3(camera.position) / CHUNK_SIZE; + glm::ivec3 camera_chunk = glm::ivec3( + std::floor(camera.position.x / CHUNK_SIZE), + std::floor(camera.position.y / CHUNK_SIZE), + std::floor(camera.position.z / CHUNK_SIZE) + ); // ---- PASS 1: Draw solid grey cubes ---- glEnable(GL_POLYGON_OFFSET_FILL); @@ -276,15 +351,25 @@ int main() { Chunk* chunk = world.get_or_create_chunk(chunk_pos); Mesh* chunk_mesh = chunk->getMesh(); - if (chunk_mesh) + if (chunk_mesh) { + // Debug: Log when rendering chunks with your placed block + static bool logged_placed_block = false; + if (!logged_placed_block && chunk->voxels.count(glm::ivec3(0, 1, 31)) > 0) { + std::cout << "\n[RENDER] ===== FOUND PLACED BLOCK =====" << std::endl; + std::cout << "[RENDER] Drawing chunk at pos (" << chunk_pos.x << ", " << chunk_pos.y << ", " << chunk_pos.z << ")" << std::endl; + std::cout << "[RENDER] Translation: (" << (chunk_pos.x * CHUNK_SIZE) << ", " << (chunk_pos.y * CHUNK_SIZE) << ", " << (chunk_pos.z * CHUNK_SIZE) << ")" << std::endl; + std::cout << "[RENDER] Final world position should be: (" << (chunk_pos.x * CHUNK_SIZE + 0) << ", " << (chunk_pos.y * CHUNK_SIZE + 1) << ", " << (chunk_pos.z * CHUNK_SIZE + 31) << ")" << std::endl; + logged_placed_block = true; + } chunk_mesh->draw(); + } } } } - if (!debug_printed) { - debug_printed = true; - } + // if (!debug_printed) { + // debug_printed = true; + // } glDisable(GL_POLYGON_OFFSET_FILL); @@ -315,6 +400,16 @@ int main() { // --- Reset polygon mode for next frame (good practice) --- glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // ---- Draw Crosshair ---- + glDisable(GL_DEPTH_TEST); // Draw on top of everything + shader.use(); + shader.setMat4("u_mvp", glm::mat4(1.0f)); // Identity matrix (NDC coordinates) + shader.setVec3("u_Color", glm::vec3(1.0f, 1.0f, 1.0f)); // White + glBindVertexArray(crosshairVAO); + glDrawArrays(GL_LINES, 0, 4); // 4 vertices = 2 lines + glBindVertexArray(0); + glEnable(GL_DEPTH_TEST); + double end_time = glfwGetTime(); float time_since_last_move = end_time - move_time; if (time_since_last_move >= 0.017)