diff --git a/.attachments/chunk-render.png b/.attachments/chunk-render.png new file mode 100644 index 0000000..0a0e789 Binary files /dev/null and b/.attachments/chunk-render.png differ diff --git a/CMakeLists.txt b/CMakeLists.txt index ad4cd12..5fb3a3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,8 @@ add_executable(VoxelEngine src/World.cpp src/Mesh.h src/Mesh.cpp + src/Chunk.h + src/Chunk.cpp ) # --- Configure GLAD (from your src/ and include/ folders) --- diff --git a/README.md b/README.md index 3f945bd..475ce8a 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,15 @@ Currently, however, the program only renders a single cube. ![a wireframe cube](.attachments/wireframe-cube.png) -## World Render - 11/10/25 +## World Mesh Rendering - 11/10/25 World now renders as a single mesh! Much faster than rendering cube by cube. ![cube world](.attachments/world-render.png) +## Chunk Mesh Rendering - 11/11/25 + +Instead of making the _entire_ world a single mesh, now the world is made up of 32x32x32 "Chunks" which are areas of the world that are a single mesh. + +![cube world (but in chunks!)](.attachments/chunk-render.png) + diff --git a/src/Chunk.cpp b/src/Chunk.cpp new file mode 100644 index 0000000..208087c --- /dev/null +++ b/src/Chunk.cpp @@ -0,0 +1,108 @@ +#include "Chunk.h" +#include + +const int VERTEX_SIZE = 6; + +const float topFace[24] = { + // x, y, z, nx, ny, nz + -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, + -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f +}; +const float bottomFace[24] = { + -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, + 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, + -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f +}; +const float rightFace[24] = { + 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f +}; +const float leftFace[24] = { + -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f +}; +const float frontFace[24] = { + -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f +}; +const float backFace[24] = { + -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f +}; + +void addFace(glm::ivec3 pos, const float *faceVertices, std::vector &vbo, std::vector &ebo) { + unsigned int vIndex = vbo.size() / VERTEX_SIZE; + for (int i = 0; i < 4; i++) { + const float* v = &faceVertices[i * VERTEX_SIZE]; + vbo.push_back(v[0] + pos.x); + vbo.push_back(v[1] + pos.y); + vbo.push_back(v[2] + pos.z); + vbo.push_back(v[3]); + vbo.push_back(v[4]); + vbo.push_back(v[5]); + } + ebo.push_back(vIndex + 0); + ebo.push_back(vIndex + 1); + ebo.push_back(vIndex + 2); + ebo.push_back(vIndex + 0); + ebo.push_back(vIndex + 2); + ebo.push_back(vIndex + 3); +} + +Chunk::Chunk() { + // mesh is initially null (nullptr) +} + +Mesh* Chunk::getMesh() { + // Generate mesh on first access if not already generated + if (!mesh && !voxels.empty()) { + generateMesh(); + } + return mesh.get(); +} + +void Chunk::generateMesh() { + std::vector vboData; + std::vector eboData; + + for (const glm::ivec3 pos : voxels) { + if (voxels.count(pos + glm::ivec3(0, 1, 0)) == 0) { + addFace(pos, topFace, vboData, eboData); + } + if (voxels.count(pos + glm::ivec3(0, -1, 0)) == 0) { + addFace(pos, bottomFace, vboData, eboData); + } + if (voxels.count(pos + glm::ivec3(1, 0, 0)) == 0) { + addFace(pos, rightFace, vboData, eboData); + } + if (voxels.count(pos + glm::ivec3(-1, 0, 0)) == 0) { + addFace(pos, leftFace, vboData, eboData); + } + if (voxels.count(pos + glm::ivec3(0, 0, 1)) == 0) { + addFace(pos, frontFace, vboData, eboData); + } + if (voxels.count(pos + glm::ivec3(0, 0, -1)) == 0) { + addFace(pos, backFace, vboData, eboData); + } + } + + // Create and initialize mesh if it doesn't exist + if (!mesh) { + mesh = std::make_unique(); + mesh->init(); + } + + // Now, upload this data to our mesh object + mesh->uploadData(vboData, eboData); +} diff --git a/src/Chunk.h b/src/Chunk.h new file mode 100644 index 0000000..32e0de0 --- /dev/null +++ b/src/Chunk.h @@ -0,0 +1,43 @@ +#ifndef CHUNK_H +#define CHUNK_H + +#include +#include + +#include + +#include "Mesh.h" + +const int CHUNK_SIZE = 32; + +// Hash function for glm::ivec3 to use with unordered_set +namespace std { + template <> + struct hash { + size_t operator()(const glm::ivec3& v) const { + // Combine hash values of x, y, z components + size_t h1 = hash()(v.x); + size_t h2 = hash()(v.y); + size_t h3 = hash()(v.z); + + // Use a simple hash combination algorithm + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; +} + +class Chunk { + public: + std::unordered_set voxels; + + Chunk(); + + Mesh* getMesh(); // Returns pointer, can be null if not generated yet + + void generateMesh(); + + private: + std::unique_ptr mesh; // Nullable mesh +}; + +#endif CHUNK_H diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 4653497..e429e4f 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -1,3 +1,5 @@ +#include + #include "Mesh.h" Mesh::Mesh() : m_VAO(0), m_VBO(0), m_EBO(0), m_indexCount(0) {} @@ -38,10 +40,14 @@ void Mesh::init() { void Mesh::uploadData(const std::vector &vboData, const std::vector &eboData) { m_indexCount = eboData.size(); + std::cout << "Uploading mesh: " << vboData.size() << " floats, " << eboData.size() << " indices" << std::endl; + + // Bind VAO first, then upload buffer data + glBindVertexArray(m_VAO); + glBindBuffer(GL_ARRAY_BUFFER, m_VBO); glBufferData(GL_ARRAY_BUFFER, vboData.size() * sizeof(float), vboData.data(), GL_STATIC_DRAW); - glBindVertexArray(m_VAO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, eboData.size() * sizeof(unsigned int), eboData.data(), GL_STATIC_DRAW); glBindVertexArray(0); diff --git a/src/World.cpp b/src/World.cpp index a6f7bdc..3ad6728 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -1,98 +1,22 @@ #include "World.h" -#include -const int VERTEX_SIZE = 6; +World::World() {} -const float topFace[24] = { - // x, y, z, nx, ny, nz - -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, - 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, - 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, - -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f -}; -const float bottomFace[24] = { - -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, - 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, - 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, - -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f -}; -const float rightFace[24] = { - 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, - 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, - 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, - 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f -}; -const float leftFace[24] = { - -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, - -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, - -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, - -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f -}; -const float frontFace[24] = { - -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, - 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, - 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, - -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f -}; -const float backFace[24] = { - -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, - 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, - 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, - -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f -}; +void World::flip_voxel(glm::ivec3 position) { + glm::ivec3 chunk_position = position / CHUNK_SIZE; + glm::ivec3 local_position = position % CHUNK_SIZE; -void addFace(glm::ivec3 pos, const float *faceVertices, std::vector &vbo, std::vector &ebo) { - unsigned int vIndex = vbo.size() / VERTEX_SIZE; - for (int i = 0; i < 4; i++) { - const float* v = &faceVertices[i * VERTEX_SIZE]; - vbo.push_back(v[0] + pos.x); - vbo.push_back(v[1] + pos.y); - vbo.push_back(v[2] + pos.z); - vbo.push_back(v[3]); - vbo.push_back(v[4]); - vbo.push_back(v[5]); + Chunk * chunk = get_or_create_chunk(chunk_position); + if (chunk->voxels.count(local_position) != 0) { + chunk->voxels.erase(chunk->voxels.find(local_position)); + } else { + chunk->voxels.insert(local_position); } - ebo.push_back(vIndex + 0); - ebo.push_back(vIndex + 1); - ebo.push_back(vIndex + 2); - ebo.push_back(vIndex + 0); - ebo.push_back(vIndex + 2); - ebo.push_back(vIndex + 3); } -World::World() { - m_mesh.init(); -} - -Mesh& World::getMesh() { - return m_mesh; -} - -void World::generateMesh() { - std::vector vboData; - std::vector eboData; - - for (const glm::ivec3 pos : voxels) { - if (voxels.count(pos + glm::ivec3(0, 1, 0)) == 0) { - addFace(pos, topFace, vboData, eboData); - } - if (voxels.count(pos + glm::ivec3(0, -1, 0)) == 0) { - addFace(pos, bottomFace, vboData, eboData); - } - if (voxels.count(pos + glm::ivec3(1, 0, 0)) == 0) { - addFace(pos, rightFace, vboData, eboData); - } - if (voxels.count(pos + glm::ivec3(-1, 0, 0)) == 0) { - addFace(pos, leftFace, vboData, eboData); - } - if (voxels.count(pos + glm::ivec3(0, 0, 1)) == 0) { - addFace(pos, frontFace, vboData, eboData); - } - if (voxels.count(pos + glm::ivec3(0, 0, -1)) == 0) { - addFace(pos, backFace, vboData, eboData); - } +Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { + if (this->chunks.count(chunk_position) == 0) { + this->chunks[chunk_position] = Chunk(); } - - // Now, upload this data to our mesh object - m_mesh.uploadData(vboData, eboData); + return &this->chunks[chunk_position]; } diff --git a/src/World.h b/src/World.h index 3367d1b..927ac8f 100644 --- a/src/World.h +++ b/src/World.h @@ -1,41 +1,22 @@ #ifndef WORLD_H -#define CAMERA_H +#define WORLD_H -#include +#include #include -#include "Mesh.h" - -// Hash function for glm::ivec3 to use with unordered_set -namespace std { - template <> - struct hash { - size_t operator()(const glm::ivec3& v) const { - // Combine hash values of x, y, z components - size_t h1 = hash()(v.x); - size_t h2 = hash()(v.y); - size_t h3 = hash()(v.z); - - // Use a simple hash combination algorithm - return h1 ^ (h2 << 1) ^ (h3 << 2); - } - }; -} +#include "Chunk.h" class World { public: - std::unordered_set voxels; + std::unordered_map chunks; World(); - void generateMesh(); + void flip_voxel(glm::ivec3 position); - Mesh& getMesh(); - - private: - Mesh m_mesh; + Chunk* get_or_create_chunk(glm::ivec3 chunk_position); }; #endif WORLD_H diff --git a/src/main.cpp b/src/main.cpp index e11c433..c080291 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,4 @@ #include -#include #include // Must be included before GLFW #include @@ -171,28 +170,6 @@ void processInput(GLFWwindow *window) camera.speed = CAMERA_SPEED * 20; } -// Draw a cube at some x, y, z -// [UPDATED] Added a new 'color' parameter -void draw_cube(glm::ivec3 coords, Shader& shader, unsigned int VAO, size_t indexCount, const glm::mat4& view, const glm::mat4& projection, const glm::vec3& color) { - // Create model matrix for positioning the cube - glm::mat4 model = glm::mat4(1.0f); - model = glm::translate(model, glm::vec3(coords)); - - model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f)); - - // Calculate MVP matrix - glm::mat4 mvp = projection * view * model; - - // Set the uniform and draw - shader.use(); - shader.setMat4("u_mvp", mvp); - // [NEW] Set the color uniform in the fragment shader - shader.setVec3("u_Color", color); - - glBindVertexArray(VAO); - glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr); -} - int main() { // 1. --- Initialize GLFW --- if (!glfwInit()) { @@ -231,16 +208,15 @@ int main() { Object3D cube = Object3D("objs/cube.obj"); // Place cubes in the world - for (int z = 0; z <= 32; z++) { - for (int x = 0; x <= 32; x++) { - world.voxels.insert(glm::ivec3(x, -1, z)); + for (int z = 0; z <= 256; z++) { + for (int x = 0; x <= 256; x++) { + world.flip_voxel(glm::ivec3(x, -1, z)); } } - world.voxels.insert(glm::ivec3(7, 0, 13)); - std::cout << "Generating world mesh..." << std::endl; - world.generateMesh(); - std::cout << "Mesh generated!" << std::endl; + world.flip_voxel(glm::ivec3(7, 0, 13)); + std::cout << "World created with " << world.chunks.size() << " chunks" << std::endl; + // Mesh generation now happens automatically when first rendering if (WIREFRAME_MODE) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); @@ -257,6 +233,7 @@ int main() { double move_time = glfwGetTime(); glEnable(GL_DEPTH_TEST); + bool debug_printed = false; // 6. --- The Render Loop --- while (!glfwWindowShouldClose(window)) { @@ -269,9 +246,13 @@ int main() { view = camera.getView(); shader.use(); - glm::mat4 model = glm::mat4(1.0f); - glm::mat4 mvp = projection * view * model; - shader.setMat4("u_mvp", mvp); + + 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; + } // ---- PASS 1: Draw solid grey cubes ---- glEnable(GL_POLYGON_OFFSET_FILL); @@ -279,7 +260,28 @@ int main() { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); shader.setVec3("u_Color", glm::vec3(0.5, 0.5, 0.5)); - world.getMesh().draw(); + for (auto& [chunk_position, chunk] : world.chunks) { + + glm::ivec3 chunk_offset = chunk_position * CHUNK_SIZE; + glm::mat4 model = glm::mat4(1.0f); + model = glm::translate(model, glm::vec3(chunk_offset)); + glm::mat4 mvp = projection * view * model; + shader.setMat4("u_mvp", mvp); + + if (!debug_printed) { + std::cout << "Drawing chunk at (" << chunk_position.x << ", " << chunk_position.y << ", " << chunk_position.z << ")" << std::endl; + std::cout << "Chunk offset: (" << chunk_offset.x << ", " << chunk_offset.y << ", " << chunk_offset.z << ")" << std::endl; + } + + Mesh* chunkMesh = chunk.getMesh(); + if (chunkMesh) { + chunkMesh->draw(); + } + } + + if (!debug_printed) { + debug_printed = true; + } glDisable(GL_POLYGON_OFFSET_FILL); @@ -287,7 +289,19 @@ int main() { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Just the lines shader.setVec3("u_Color", glm::vec3(1.0, 1.0, 1.0)); - world.getMesh().draw(); + for (auto& [chunk_position, chunk] : world.chunks) { + + glm::ivec3 chunk_offset = chunk_position * CHUNK_SIZE; + glm::mat4 model = glm::mat4(1.0f); + model = glm::translate(model, glm::vec3(chunk_offset)); + glm::mat4 mvp = projection * view * model; + shader.setMat4("u_mvp", mvp); + + Mesh* chunkMesh = chunk.getMesh(); + if (chunkMesh) { + chunkMesh->draw(); + } + } // --- Reset polygon mode for next frame (good practice) --- glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);