perlin noise generation and height maps
This commit is contained in:
parent
15aba4f901
commit
561df24953
BIN
.attachments/perlin-noise.png
Normal file
BIN
.attachments/perlin-noise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -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) ---
|
||||
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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_.
|
||||
|
||||

|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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> 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);
|
||||
|
||||
87
src/Perlin.h
Normal file
87
src/Perlin.h
Normal file
@ -0,0 +1,87 @@
|
||||
#ifndef PERLIN_H
|
||||
#define PERLIN_H
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
|
||||
// Doesn't need to be super fast because assumedly I'm not making Perlin noise _that_ often
|
||||
std::vector<int> makePermutation() {
|
||||
std::vector<int> 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<int>& 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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "World.h"
|
||||
#include "Perlin.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
@ -22,6 +23,9 @@ class World
|
||||
VoxelKind get_voxel(glm::ivec3 position);
|
||||
|
||||
Chunk* get_or_create_chunk(glm::ivec3 chunk_position);
|
||||
|
||||
private:
|
||||
std::vector<int> noise_permutation;
|
||||
};
|
||||
|
||||
#endif WORLD_H
|
||||
|
||||
10
src/main.cpp
10
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user