diff --git a/.attachments/perlin-noise.png b/.attachments/perlin-noise.png new file mode 100644 index 0000000..0ff27cd Binary files /dev/null and b/.attachments/perlin-noise.png differ diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fb3a3a..885eebb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,8 @@ add_executable(VoxelEngine src/Mesh.cpp src/Chunk.h src/Chunk.cpp + src/Player.h + src/Perlin.h ) # --- Configure GLAD (from your src/ and include/ folders) --- diff --git a/README.md b/README.md index cbc3e76..52bf32f 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,9 @@ Woah! The camera doesn't just phase through placed blocks anymore! The camera ca Final part of the blog post tutorial. Changing the data structure of the Chunk's to store voxels in a 1d array instead of a HashMap. ![Hi written on a dirt plane in stone.](.attachments/data-structure-redo.png) + +## 0.9.0 : Perlin Noise - 11/24/25 + +Woah! Look at that, not a flat plane of dirt. This update adds variation to terrian height with a height map generated by perlin noise. Unfortunately, this tanks FPS. So I need to make the engine _faster_. + +![Rolling green hills](.attachments/perlin-noise.png) diff --git a/shaders/cube_frag.glsl b/shaders/cube_frag.glsl index ebcc600..3540730 100644 --- a/shaders/cube_frag.glsl +++ b/shaders/cube_frag.glsl @@ -17,6 +17,8 @@ void main() // Dirt - brownish color *= vec3(0.6, 0.4, 0.2); } else if (v_VoxelKind == 2.0) { + color *= vec3(0.2, 0.8, 0.2); + } else if (v_VoxelKind == 3.0) { // Stone - grayish (keep as is) } diff --git a/src/Chunk.cpp b/src/Chunk.cpp index 6049230..5843e6f 100644 --- a/src/Chunk.cpp +++ b/src/Chunk.cpp @@ -51,10 +51,10 @@ void addFace(glm::ivec3 pos, const float *faceVertices, float voxelKind, std::ve 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; - } + // 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); @@ -77,9 +77,10 @@ Chunk::Chunk() { } Mesh* Chunk::getMesh() { - // Generate mesh on first access if not already generated - if (!mesh && !this->isEmpty()) { + // Generate mesh on first access if not already generated, or if dirty + if ((!mesh || mesh_dirty) && !this->isEmpty()) { generateMesh(); + mesh_dirty = false; } return mesh.get(); } @@ -142,7 +143,3 @@ void Chunk::generateMesh() { // Now, upload this data to our mesh object mesh->uploadData(vboData, eboData); } - -void Chunk::regenerateMesh() { - this->generateMesh(); -} diff --git a/src/Chunk.h b/src/Chunk.h index 17ca20e..8962756 100644 --- a/src/Chunk.h +++ b/src/Chunk.h @@ -15,6 +15,7 @@ enum class VoxelKind : uint8_t { Air, Dirt, + Grass, Stone }; @@ -45,7 +46,6 @@ class Chunk { Mesh* getMesh(); // Returns pointer, can be null if not generated yet void generateMesh(); - void regenerateMesh(); // Accessors inline VoxelKind get(int x, int y, int z) const { @@ -60,11 +60,13 @@ class Chunk { inline void set(int x, int y, int z, VoxelKind v) { data[index(x, y, z)] = v; empty_checked = false; // Invalidate cache + mesh_dirty = true; // Mark mesh as needing regeneration } inline void set(glm::ivec3 pos, VoxelKind v) { // std::cout << "Setting Pos (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; data[index(pos.x, pos.y, pos.z)] = v; empty_checked = false; // Invalidate cache + mesh_dirty = true; // Mark mesh as needing regeneration } // Other @@ -86,6 +88,7 @@ class Chunk { std::unique_ptr mesh; // Nullable mesh mutable bool is_empty = true; // Cache empty state mutable bool empty_checked = false; // Track if we've checked + bool mesh_dirty = true; // Track if mesh needs regeneration static inline constexpr int index(int x, int y, int z) noexcept { return x + (y * SIZE) + (z * SIZE * SIZE); diff --git a/src/Perlin.h b/src/Perlin.h new file mode 100644 index 0000000..95af9f2 --- /dev/null +++ b/src/Perlin.h @@ -0,0 +1,87 @@ +#ifndef PERLIN_H +#define PERLIN_H + +#include +#include +#include + +#include + + +// Doesn't need to be super fast because assumedly I'm not making Perlin noise _that_ often +std::vector makePermutation() { + std::vector permutation; + + for (int i = 0; i < 256; i++) { + permutation.push_back(i); + } + + std::random_device rd; + std::mt19937 g(rd()); + + std::shuffle(permutation.begin(), permutation.end(), g); + + for (int i = 0; i < 256; i++) { + permutation.push_back(permutation[i]); + } + + return permutation; +} + +glm::vec2 getConstantVector(int v) { + int h = v & 3; + + if (h == 0) { + return glm::vec2(1.0f, 1.0f); + } else if (h == 1) { + return glm::vec2(-1.0f, 1.0f); + } else if (h == 2) { + return glm::vec2(-1.0f, -1.0f); + } + + return glm::vec2(1.0f, -1.0f); +} + +float fade(float t) { + return ((6 * t - 15) * t + 10) * t * t * t; +} + +// linear interpolation +float lerp(float t, float a1, float a2) { + return a1 + t * (a2 - a1); +} + +float perlinNoise2D(const float x, const float y, const std::vector& permutation) { + const int X = (int)floorf(x) & 255; + const int Y = (int)floorf(y) & 255; + + const float xf = x - floorf(x); + const float yf = y - floorf(y); + + const glm::vec2 topRight = glm::vec2(xf - 1.0f, yf - 1.0f); + const glm::vec2 topLeft = glm::vec2(xf, yf - 1.0f); + const glm::vec2 bottomRight = glm::vec2(xf - 1.0f, yf); + const glm::vec2 bottomLeft = glm::vec2(xf, yf); + + const int valueTopRight = permutation[permutation[X+1]+Y+1]; + const int valueTopLeft = permutation[permutation[X]+Y+1]; + const int valueBottomRight = permutation[permutation[X+1]+Y]; + const int valueBottomLeft = permutation[permutation[X]+Y]; + + const auto dotTopRight = glm::dot(topRight, getConstantVector(valueTopRight)); + const auto dotTopLeft = glm::dot(topLeft, getConstantVector(valueTopLeft)); + const auto dotBottomRight = glm::dot(bottomRight, getConstantVector(valueBottomRight)); + const auto dotBottomLeft = glm::dot(bottomLeft, getConstantVector(valueBottomLeft)); + + const float u = fade(xf); + const float v = fade(yf); + + return lerp( + u, + lerp(v, dotBottomLeft, dotTopLeft), + lerp(v, dotBottomRight, dotTopRight) + ); +} + +#endif + diff --git a/src/World.cpp b/src/World.cpp index 3939eee..97e32bf 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -1,4 +1,5 @@ #include "World.h" +#include "Perlin.h" #include @@ -20,7 +21,9 @@ glm::ivec3 floor_divide(glm::ivec3 value, int divisor) { return result; } -World::World() {} +World::World() { + this->noise_permutation = makePermutation(); +} void World::set_voxel(glm::ivec3 position, VoxelKind kind) { glm::ivec3 chunk_position = floor_divide(position, CHUNK_SIZE); @@ -32,8 +35,8 @@ void World::set_voxel(glm::ivec3 position, VoxelKind kind) { Chunk * chunk = get_or_create_chunk(chunk_position); chunk->set(local_position, kind); - - chunk->regenerateMesh(); + + // No need to call regenerateMesh() - the dirty flag will trigger regeneration on next getMesh() } Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { @@ -41,25 +44,38 @@ Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { // Fill in new chunk with a flat plane of voxels at y=0 auto [it, inserted] = this->chunks.try_emplace(chunk_position); Chunk& chunk = it->second; + if (inserted) { - if (chunk_position.y == 0) { - for (int z = 0; z < CHUNK_SIZE; z++) { + + const float scale = 0.05f; // smaller = smoother, larger = more variation + const float heightMultiplier = 20.0f; // Maximum terrian height variation + const int baseHeight = 0; // Sea Level + + for (int z = 0; z < CHUNK_SIZE; z++) { + for (int x = 0; x < CHUNK_SIZE; x++) { + glm::ivec3 world_pos = chunk_position * CHUNK_SIZE + glm::ivec3(x, 0, z); + + float noise = perlinNoise2D(world_pos.x * scale, world_pos.z * scale, this->noise_permutation); + + float normalizedNoise = (noise + 1.0f) * 0.5f; + int terrainHeight = baseHeight + (int)(normalizedNoise * heightMultiplier); + 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 * CHUNK_SIZE + offset; + glm::ivec3 offset = glm::ivec3(x, y, z); + glm::ivec3 world_voxel_position = chunk_position * CHUNK_SIZE + offset; - VoxelKind kind; - if (world_voxel_position.y == 0) { - kind = VoxelKind::Dirt; - } else if (world_voxel_position.y < 0) { - kind = VoxelKind::Stone; - } else { - kind = VoxelKind::Air; - } - - chunk.set(offset, kind); + VoxelKind kind; + if (world_voxel_position.y < terrainHeight - 3) { + kind = VoxelKind::Stone; + } else if (world_voxel_position.y < terrainHeight) { + kind = VoxelKind::Dirt; // Near surface + } else if (world_voxel_position.y == terrainHeight) { + kind = VoxelKind::Grass; // Surface block (could be grass) + } else { + kind = VoxelKind::Air; // Above terrain } + + chunk.set(offset, kind); } } } diff --git a/src/World.h b/src/World.h index 9993f68..bd003a2 100644 --- a/src/World.h +++ b/src/World.h @@ -3,6 +3,7 @@ #include #include +#include #include @@ -22,6 +23,9 @@ class World VoxelKind get_voxel(glm::ivec3 position); Chunk* get_or_create_chunk(glm::ivec3 chunk_position); + + private: + std::vector noise_permutation; }; #endif WORLD_H diff --git a/src/main.cpp b/src/main.cpp index 9e578da..9db3d37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,14 +31,14 @@ const float Z_NEAR = 0.1f; bool WIREFRAME_MODE = !true; const float FOV = 110.0f; -const std::string VERSION = "0.8.0"; +const std::string VERSION = "0.9.0"; -const int VIEW_DISTANCE = 4; // In Chunks +const int VIEW_DISTANCE = 4; bool W_pressed, S_pressed, A_pressed, D_pressed = false; glm::mat4 projection = glm::mat4(1.0f); -Player player = Player(CAMERA_SPEED, glm::vec3(0.0f, 1.85f, 0.0f)); +Player player = Player(CAMERA_SPEED, glm::vec3(0.0f, 20.0f, 0.0f)); Camera camera = Camera(&player); // Targeted block for highlighting @@ -436,7 +436,7 @@ int main() { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); shader.setVec3("u_Color", glm::vec3(0.5, 0.5, 0.5)); - for (int y = -VIEW_DISTANCE; y < VIEW_DISTANCE; y++) { + for (int y = -2; y < 2; y++) { // Reduced from -VIEW_DISTANCE to VIEW_DISTANCE for (int z = -VIEW_DISTANCE; z < VIEW_DISTANCE; z++) { for (int x = -VIEW_DISTANCE; x < VIEW_DISTANCE; x++) { glm::ivec3 chunk_offset = glm::ivec3(x, y, z); @@ -466,7 +466,7 @@ int main() { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Just the lines shader.setVec3("u_Color", glm::vec3(1.0, 1.0, 1.0)); - for (int y = -VIEW_DISTANCE; y < VIEW_DISTANCE; y++) { + for (int y = -2; y < 2; y++) { // Reduced from -VIEW_DISTANCE to VIEW_DISTANCE for (int z = -VIEW_DISTANCE; z < VIEW_DISTANCE; z++) { for (int x = -VIEW_DISTANCE; x < VIEW_DISTANCE; x++) { glm::ivec3 chunk_offset = glm::ivec3(x, y, z);