small-projects/cpp-voxel-engine/VoxelGame.cpp

209 lines
7.6 KiB
C++
Raw Normal View History

2025-04-06 03:14:06 +00:00
#define STB_IMAGE_IMPLEMENTATION
2025-04-06 02:03:23 +00:00
#include "VoxelGame.h"
#include <iostream>
2025-04-06 02:09:28 +00:00
#include <GL/glew.h>
2025-04-06 03:14:06 +00:00
#include <GL/glu.h>
2025-04-06 02:03:23 +00:00
#include "stb_image.h"
2025-04-06 03:14:06 +00:00
#include <cmath>
#include <unordered_set>
#include "imgui.h"
#include "GreedyMesher.h"
namespace {
// --- Perlin Noise Helpers ---
float fade_(float t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
float lerp_(float a, float b, float t) {
return a + t * (b - a);
}
float grad_(int hash, float x, float y) {
int h = hash & 7;
float u = h < 4 ? x : y;
float v = h < 4 ? y : x;
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}
float perlin(float x, float y) {
static int p[512];
static bool init = false;
if (!init) {
int perm[256] = {
151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,
140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,
247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,
57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,
74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,
60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,
65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,
200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,
52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,
207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,
119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,
129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,
218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,
81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,
184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,
222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};
for (int i = 0; i < 256; i++) {
p[i] = perm[i];
p[i + 256] = perm[i];
}
init = true;
}
int X = int(floor(x)) & 255;
int Y = int(floor(y)) & 255;
x -= floor(x);
y -= floor(y);
float u = fade_(x);
float v = fade_(y);
int A = p[X] + Y, B = p[X + 1] + Y;
return lerp_( v,
lerp_( u, grad_(p[A], x, y), grad_(p[A+1], x-1, y) ),
lerp_( u, grad_(p[B], x, y-1), grad_(p[B+1], x-1, y-1) )
);
}
}
// Helper to safely query voxel (returns 0 if out of bounds)
inline int voxelAt(const Chunk& c, int x, int y, int z) {
if (x<0||y<0||z<0||x>=16||y>=16||z>=16) return 0;
return c.voxels[x][y][z];
}
2025-04-06 02:03:23 +00:00
VoxelGame::VoxelGame()
2025-04-06 03:14:06 +00:00
: textureGrass(0), textureDirt(0), textureWood(0),
totalChunksLoaded(0), totalChunksEverLoaded(0)
2025-04-06 02:03:23 +00:00
{
}
VoxelGame::~VoxelGame() {
glDeleteTextures(1, &textureGrass);
glDeleteTextures(1, &textureDirt);
glDeleteTextures(1, &textureWood);
}
2025-04-06 03:14:06 +00:00
2025-04-06 02:03:23 +00:00
bool VoxelGame::init() {
2025-04-06 03:14:06 +00:00
if (!loadTextures()) return false;
// Initial camera position
float cam[3] = {32,32,80};
updateChunks(cam);
// Build mesh for each chunk
for (auto &p : chunks) {
p.second.mesh = GreedyMesher::mesh(p.second.voxels);
}
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
2025-04-06 02:03:23 +00:00
return true;
}
bool VoxelGame::loadTextures() {
2025-04-06 03:14:06 +00:00
int w,h,n;
unsigned char* data;
data = stbi_load("grass.jpg",&w,&h,&n,4);
if (!data) { std::cerr<<"grass.jpg load fail\n"; return false; }
glGenTextures(1,&textureGrass); glBindTexture(GL_TEXTURE_2D,textureGrass);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_UNSIGNED_BYTE,data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
data = stbi_load("dirt.jpg",&w,&h,&n,4);
if (!data) { std::cerr<<"dirt.jpg load fail\n"; return false; }
glGenTextures(1,&textureDirt); glBindTexture(GL_TEXTURE_2D,textureDirt);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_UNSIGNED_BYTE,data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
data = stbi_load("wood.png",&w,&h,&n,4);
if (!data) { std::cerr<<"wood.png load fail\n"; return false; }
glGenTextures(1,&textureWood); glBindTexture(GL_TEXTURE_2D,textureWood);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_UNSIGNED_BYTE,data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
2025-04-06 02:03:23 +00:00
return true;
}
2025-04-06 03:14:06 +00:00
Chunk VoxelGame::generateChunk(int cx,int cz) {
Chunk c; c.x=cx; c.z=cz;
c.voxels.assign(16, std::vector<std::vector<int>>(16, std::vector<int>(16,0)));
float scale=0.1f;
for(int x=0;x<16;x++)for(int z=0;z<16;z++){
float wx = cx*16 + x, wz = cz*16 + z;
float n = perlin(wx*scale, wz*scale);
int h = int(((n+1)*0.5f)*16);
h = std::clamp(h,0,16);
for(int y=0;y<h;y++){
c.voxels[x][y][z] = (y==h-1?1:2);
2025-04-06 02:03:23 +00:00
}
}
2025-04-06 03:14:06 +00:00
return c;
2025-04-06 02:03:23 +00:00
}
2025-04-06 03:14:06 +00:00
void VoxelGame::updateChunks(const float cam[3]) {
int cx = int(floor(cam[0]/16)), cz=int(floor(cam[2]/16));
int r=2;
std::unordered_set<ChunkKey,ChunkKeyHash> keep;
for(int dx=-r;dx<=r;dx++)for(int dz=-r;dz<=r;dz++){
ChunkKey key{cx+dx,cz+dz};
keep.insert(key);
if(chunks.find(key)==chunks.end()){
Chunk nc = generateChunk(key.x,key.z);
nc.mesh = GreedyMesher::mesh(nc.voxels);
chunks[key]=std::move(nc);
totalChunksEverLoaded++;
}
}
for(auto it=chunks.begin();it!=chunks.end();){
if(!keep.count(it->first)) it=chunks.erase(it);
else ++it;
}
totalChunksLoaded = chunks.size();
2025-04-06 02:03:23 +00:00
}
2025-04-06 03:14:06 +00:00
void VoxelGame::update(float dt,const float cam[3]){
updateChunks(cam);
2025-04-06 02:03:23 +00:00
}
2025-04-06 03:14:06 +00:00
void VoxelGame::render(){
for(auto &p:chunks){
float ox = p.first.x*16, oz = p.first.z*16;
for(auto &q:p.second.mesh){
// shift by chunk offset
float v0[3]={q.x+ox,q.y,q.z+oz},
v1[3]={q.x+q.du[0]+ox,q.y+q.du[1],q.z+q.du[2]+oz},
v2[3]={q.x+q.du[0]+q.dv[0]+ox,q.y+q.du[1]+q.dv[1],q.z+q.du[2]+q.dv[2]+oz},
v3[3]={q.x+q.dv[0]+ox,q.y+q.dv[1],q.z+q.dv[2]+oz};
unsigned int tex = (q.textureID==1?textureGrass:(q.textureID==2?textureDirt:textureWood));
glBindTexture(GL_TEXTURE_2D,tex);
glBegin(GL_QUADS);
glTexCoord2f(0,0); glVertex3fv(v0);
glTexCoord2f(1,0); glVertex3fv(v1);
glTexCoord2f(1,1); glVertex3fv(v2);
glTexCoord2f(0,1); glVertex3fv(v3);
glEnd();
}
}
2025-04-06 02:03:23 +00:00
}
2025-04-06 03:14:06 +00:00
void VoxelGame::debugUI(){
2025-04-06 02:03:23 +00:00
ImGui::Begin("Debug");
2025-04-06 03:14:06 +00:00
ImGui::Text("Chunks Loaded: %d", totalChunksLoaded);
ImGui::Text("Chunks Ever Loaded: %d", totalChunksEverLoaded);
static bool wf=false;
if(ImGui::Checkbox("Wireframe",&wf))
glPolygonMode(GL_FRONT_AND_BACK, wf?GL_LINE:GL_FILL);
2025-04-06 02:03:23 +00:00
ImGui::End();
}