added chunk rendering

This commit is contained in:
JISAUAY 2025-11-11 16:05:05 -06:00
parent 20f38ad3a9
commit 3ba2d75cff
9 changed files with 235 additions and 151 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -23,6 +23,8 @@ add_executable(VoxelEngine
src/World.cpp src/World.cpp
src/Mesh.h src/Mesh.h
src/Mesh.cpp src/Mesh.cpp
src/Chunk.h
src/Chunk.cpp
) )
# --- Configure GLAD (from your src/ and include/ folders) --- # --- Configure GLAD (from your src/ and include/ folders) ---

View File

@ -6,9 +6,15 @@ Currently, however, the program only renders a single cube.
![a wireframe cube](.attachments/wireframe-cube.png) ![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. World now renders as a single mesh! Much faster than rendering cube by cube.
![cube world](.attachments/world-render.png) ![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)

108
src/Chunk.cpp Normal file
View File

@ -0,0 +1,108 @@
#include "Chunk.h"
#include <vector>
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<float> &vbo, std::vector<unsigned int> &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<float> vboData;
std::vector<unsigned int> 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>();
mesh->init();
}
// Now, upload this data to our mesh object
mesh->uploadData(vboData, eboData);
}

43
src/Chunk.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef CHUNK_H
#define CHUNK_H
#include <unordered_set>
#include <memory>
#include <glm/glm.hpp>
#include "Mesh.h"
const int CHUNK_SIZE = 32;
// Hash function for glm::ivec3 to use with unordered_set
namespace std {
template <>
struct hash<glm::ivec3> {
size_t operator()(const glm::ivec3& v) const {
// Combine hash values of x, y, z components
size_t h1 = hash<int>()(v.x);
size_t h2 = hash<int>()(v.y);
size_t h3 = hash<int>()(v.z);
// Use a simple hash combination algorithm
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
}
class Chunk {
public:
std::unordered_set<glm::ivec3> voxels;
Chunk();
Mesh* getMesh(); // Returns pointer, can be null if not generated yet
void generateMesh();
private:
std::unique_ptr<Mesh> mesh; // Nullable mesh
};
#endif CHUNK_H

View File

@ -1,3 +1,5 @@
#include <iostream>
#include "Mesh.h" #include "Mesh.h"
Mesh::Mesh() : m_VAO(0), m_VBO(0), m_EBO(0), m_indexCount(0) {} 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<float> &vboData, const std::vector<unsigned int> &eboData) { void Mesh::uploadData(const std::vector<float> &vboData, const std::vector<unsigned int> &eboData) {
m_indexCount = eboData.size(); 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); glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
glBufferData(GL_ARRAY_BUFFER, vboData.size() * sizeof(float), vboData.data(), GL_STATIC_DRAW); glBufferData(GL_ARRAY_BUFFER, vboData.size() * sizeof(float), vboData.data(), GL_STATIC_DRAW);
glBindVertexArray(m_VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, eboData.size() * sizeof(unsigned int), eboData.data(), GL_STATIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, eboData.size() * sizeof(unsigned int), eboData.data(), GL_STATIC_DRAW);
glBindVertexArray(0); glBindVertexArray(0);

View File

@ -1,98 +1,22 @@
#include "World.h" #include "World.h"
#include <vector>
const int VERTEX_SIZE = 6; World::World() {}
const float topFace[24] = { void World::flip_voxel(glm::ivec3 position) {
// x, y, z, nx, ny, nz glm::ivec3 chunk_position = position / CHUNK_SIZE;
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, glm::ivec3 local_position = position % CHUNK_SIZE;
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<float> &vbo, std::vector<unsigned int> &ebo) { Chunk * chunk = get_or_create_chunk(chunk_position);
unsigned int vIndex = vbo.size() / VERTEX_SIZE; if (chunk->voxels.count(local_position) != 0) {
for (int i = 0; i < 4; i++) { chunk->voxels.erase(chunk->voxels.find(local_position));
const float* v = &faceVertices[i * VERTEX_SIZE]; } else {
vbo.push_back(v[0] + pos.x); chunk->voxels.insert(local_position);
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);
}
World::World() {
m_mesh.init();
}
Mesh& World::getMesh() {
return m_mesh;
}
void World::generateMesh() {
std::vector<float> vboData;
std::vector<unsigned int> 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);
} }
} }
// Now, upload this data to our mesh object Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) {
m_mesh.uploadData(vboData, eboData); if (this->chunks.count(chunk_position) == 0) {
this->chunks[chunk_position] = Chunk();
}
return &this->chunks[chunk_position];
} }

View File

@ -1,41 +1,22 @@
#ifndef WORLD_H #ifndef WORLD_H
#define CAMERA_H #define WORLD_H
#include <unordered_set> #include <unordered_map>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "Mesh.h" #include "Chunk.h"
// Hash function for glm::ivec3 to use with unordered_set
namespace std {
template <>
struct hash<glm::ivec3> {
size_t operator()(const glm::ivec3& v) const {
// Combine hash values of x, y, z components
size_t h1 = hash<int>()(v.x);
size_t h2 = hash<int>()(v.y);
size_t h3 = hash<int>()(v.z);
// Use a simple hash combination algorithm
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
}
class World class World
{ {
public: public:
std::unordered_set<glm::ivec3> voxels; std::unordered_map<glm::ivec3, Chunk> chunks;
World(); World();
void generateMesh(); void flip_voxel(glm::ivec3 position);
Mesh& getMesh(); Chunk* get_or_create_chunk(glm::ivec3 chunk_position);
private:
Mesh m_mesh;
}; };
#endif WORLD_H #endif WORLD_H

View File

@ -1,5 +1,4 @@
#include <iostream> #include <iostream>
#include <unordered_set>
#include <glad/glad.h> // Must be included before GLFW #include <glad/glad.h> // Must be included before GLFW
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
@ -171,28 +170,6 @@ void processInput(GLFWwindow *window)
camera.speed = CAMERA_SPEED * 20; 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() { int main() {
// 1. --- Initialize GLFW --- // 1. --- Initialize GLFW ---
if (!glfwInit()) { if (!glfwInit()) {
@ -231,16 +208,15 @@ int main() {
Object3D cube = Object3D("objs/cube.obj"); Object3D cube = Object3D("objs/cube.obj");
// Place cubes in the world // Place cubes in the world
for (int z = 0; z <= 32; z++) { for (int z = 0; z <= 256; z++) {
for (int x = 0; x <= 32; x++) { for (int x = 0; x <= 256; x++) {
world.voxels.insert(glm::ivec3(x, -1, z)); world.flip_voxel(glm::ivec3(x, -1, z));
} }
} }
world.voxels.insert(glm::ivec3(7, 0, 13)); world.flip_voxel(glm::ivec3(7, 0, 13));
std::cout << "Generating world mesh..." << std::endl; std::cout << "World created with " << world.chunks.size() << " chunks" << std::endl;
world.generateMesh(); // Mesh generation now happens automatically when first rendering
std::cout << "Mesh generated!" << std::endl;
if (WIREFRAME_MODE) if (WIREFRAME_MODE)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
@ -257,6 +233,7 @@ int main() {
double move_time = glfwGetTime(); double move_time = glfwGetTime();
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
bool debug_printed = false;
// 6. --- The Render Loop --- // 6. --- The Render Loop ---
while (!glfwWindowShouldClose(window)) { while (!glfwWindowShouldClose(window)) {
@ -269,9 +246,13 @@ int main() {
view = camera.getView(); view = camera.getView();
shader.use(); shader.use();
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 mvp = projection * view * model; if (!debug_printed) {
shader.setMat4("u_mvp", mvp); 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 ---- // ---- PASS 1: Draw solid grey cubes ----
glEnable(GL_POLYGON_OFFSET_FILL); glEnable(GL_POLYGON_OFFSET_FILL);
@ -279,7 +260,28 @@ int main() {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
shader.setVec3("u_Color", glm::vec3(0.5, 0.5, 0.5)); 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); glDisable(GL_POLYGON_OFFSET_FILL);
@ -287,7 +289,19 @@ int main() {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Just the lines glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Just the lines
shader.setVec3("u_Color", glm::vec3(1.0, 1.0, 1.0)); 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) --- // --- Reset polygon mode for next frame (good practice) ---
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);