diff --git a/cpp-voxel-engine/.vscode/settings.json b/cpp-voxel-engine/.vscode/settings.json index c56e9f1..f78383e 100644 --- a/cpp-voxel-engine/.vscode/settings.json +++ b/cpp-voxel-engine/.vscode/settings.json @@ -4,6 +4,7 @@ "*.js": "javascript", "*.c": "c", "*.scene": "yaml", - "vector": "cpp" + "vector": "cpp", + "cmath": "cpp" } } \ No newline at end of file diff --git a/cpp-voxel-engine/VoxelGame.cpp b/cpp-voxel-engine/VoxelGame.cpp index 03ae775..8a3ba35 100644 --- a/cpp-voxel-engine/VoxelGame.cpp +++ b/cpp-voxel-engine/VoxelGame.cpp @@ -1,208 +1,569 @@ #define STB_IMAGE_IMPLEMENTATION #include "VoxelGame.h" + #include -#include -#include -#include "stb_image.h" #include -#include +#include +#include +#include "stb_image.h" #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) ) - ); - } +#include +//---------------------------------------------------------------------- +// Perlin noise + +static std::vector perm,p; +static std::once_flag initFlag; +static void initPerlin(){ + perm.resize(256); + std::iota(perm.begin(),perm.end(),0); + std::mt19937 gen(1337); + std::shuffle(perm.begin(),perm.end(),gen); + p.resize(512); + for(int i=0;i<512;++i) p[i]=perm[i&255]; } - -// 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; +static inline float fade(float t){ return t*t*t*(t*(t*6-15)+10); } +static inline float lerp_(float a,float b,float t){ return a+t*(b-a); } +static inline float grad(int h,float x,float y,float z){ + h&=15; float u=h<8?x:y; float v=h<4?y:(h==12||h==14?x:z); + return ((h&1)?-u:u)+((h&2)?-v:v); +} +static float perlin(float x,float y,float z){ + std::call_once(initFlag,initPerlin); + int X=int(floor(x))&255, Y=int(floor(y))&255, Z=int(floor(z))&255; + x-=floor(x); y-=floor(y); z-=floor(z); + float u=fade(x), v=fade(y), w=fade(z); + int A=p[X]+Y, AA=p[A]+Z, AB=p[A+1]+Z; + int B=p[X+1]+Y, BA=p[B]+Z, BB=p[B+1]+Z; + return lerp_( + lerp_( lerp_(grad(p[AA],x,y,z), grad(p[BA],x-1,y,z), u), + lerp_(grad(p[AB],x,y-1,z), grad(p[BB],x-1,y-1,z), u), v), + lerp_( lerp_(grad(p[AA+1],x,y,z-1), grad(p[BA+1],x-1,y,z-1), u), + lerp_(grad(p[AB+1],x,y-1,z-1), grad(p[BB+1],x-1,y-1,z-1), u), v), + w + ); +} +static inline int voxelAt(const Chunk &c,int x,int y,int z){ + if(x<0||y<0||z<0||x>=CHUNK_SIZE||y>=CHUNK_HEIGHT||z>=CHUNK_SIZE) return 0; return c.voxels[x][y][z]; } +//---------------------------------------------------------------------- +// Greedy mesh with normals -VoxelGame::VoxelGame() - : textureGrass(0), textureDirt(0), textureWood(0), - totalChunksLoaded(0), totalChunksEverLoaded(0) +void VoxelGame::greedyMesh(const Chunk &chunk, + std::vector &vertices, + std::vector &indices) { -} - -VoxelGame::~VoxelGame() { - glDeleteTextures(1, &textureGrass); - glDeleteTextures(1, &textureDirt); - glDeleteTextures(1, &textureWood); -} - - -bool VoxelGame::init() { - 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); + const int dims[3]={CHUNK_SIZE,CHUNK_HEIGHT,CHUNK_SIZE}; + for(int d=0;d<3;d++){ + int u=(d+1)%3, v=(d+2)%3; + int x[3]={0,0,0}, q[3]={0,0,0}; q[d]=1; + for(x[d]=-1;x[d] mask(dims[u]*dims[v],0); + for(x[v]=0;x[v]=0) a=voxelAt(chunk,p0[0],p0[1],p0[2]); + if(x[d] lk(queueMutex); + stopWorkers=true; + } + queueCV.notify_all(); + for(auto &t:workers) t.join(); + + glDeleteTextures(1,&textureDirt); + glDeleteTextures(1,&textureGrass); + glDeleteTextures(1,&textureWood); + glDeleteFramebuffers(NUM_CASCADES,depthFBO); + glDeleteTextures(NUM_CASCADES,depthTex); + glDeleteProgram(depthProg); + glDeleteProgram(mainProg); + for(auto &pr:meshes) destroyMesh(pr.second.get()); +} + +//---------------------------------------------------------------------- + +bool VoxelGame::init(int w,int h){ + screenW=w; screenH=h; + if(!loadTextures()||!loadShaders()) return false; + initShadowMaps(); glEnable(GL_DEPTH_TEST); - glEnable(GL_TEXTURE_2D); + glEnable(GL_CULL_FACE); return true; } -bool VoxelGame::loadTextures() { - 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); +//---------------------------------------------------------------------- + +bool VoxelGame::loadTextures(){ + auto load=[&](const char*f,GLuint &t){ + int W,H,N; unsigned char*d=stbi_load(f,&W,&H,&N,4); + if(!d) return false; + glGenTextures(1,&t); glBindTexture(GL_TEXTURE_2D,t); + 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,d); + glGenerateMipmap(GL_TEXTURE_2D); + stbi_image_free(d); + return true; + }; + return load("dirt.jpg",textureDirt) + && load("grass.jpg",textureGrass) + && load("wood.png",textureWood); +} + +//---------------------------------------------------------------------- + +bool VoxelGame::loadShaders(){ + auto compile=[&](const char*s,GLenum t){ + GLuint sh=glCreateShader(t); + glShaderSource(sh,1,&s,nullptr); + glCompileShader(sh); + return sh; + }; + // depth shader + const char* dvsrc = R"( + #version 330 core + layout(location=0) in vec3 aPos; + uniform mat4 uLightSpace,uModel; + void main(){ gl_Position=uLightSpace*uModel*vec4(aPos,1); } + )", *dfsrc = R"( + #version 330 core + void main(){} + )"; + GLuint dv=compile(dvsrc,GL_VERTEX_SHADER), + df=compile(dfsrc,GL_FRAGMENT_SHADER); + depthProg=glCreateProgram(); + glAttachShader(depthProg,dv); glAttachShader(depthProg,df); + glLinkProgram(depthProg); + glDeleteShader(dv); glDeleteShader(df); + + // scene shader + const char* svsrc = R"( + #version 330 core + layout(location=0) in vec3 aPos; + layout(location=1) in vec3 aNorm; + layout(location=2) in vec2 aUV; + uniform mat4 uModel,uView,uProj,uLightSpace[3]; + out vec3 FragPos,Normal; + out vec2 UV; + out vec4 FragPosLight[3]; + void main(){ + FragPos = vec3(uModel*vec4(aPos,1)); + Normal = mat3(transpose(inverse(uModel)))*aNorm; + UV = aUV; + for(int i=0;i<3;i++) + FragPosLight[i] = uLightSpace[i]*vec4(FragPos,1); + gl_Position = uProj*uView*vec4(FragPos,1); + } + )", *sfsrc = R"( + #version 330 core + in vec3 FragPos,Normal; + in vec2 UV; + in vec4 FragPosLight[3]; + uniform sampler2D uTexDirt,uTexGrass; + uniform sampler2DShadow uShadowMap[3]; + uniform float cascadeSplit[3]; + uniform vec3 lightDir,viewPos; + float CalcShadow(int idx){ + vec3 proj = FragPosLight[idx].xyz/FragPosLight[idx].w; + proj = proj*0.5+0.5; + float bias=0.005,sum=0; + for(int x=-1;x<=1;x++)for(int y=-1;y<=1;y++) + sum += texture(uShadowMap[idx], proj.xy+vec2(x,y)*0.001, proj.z-bias); + sum/=9; return proj.z>1?1:sum; + } + out vec4 FragColor; + void main(){ + float ndl = max(dot(normalize(Normal),-lightDir),0); + vec3 base = Normal.y>0.9?texture(uTexGrass,UV).rgb + :texture(uTexDirt,UV).rgb; + float d = length(viewPos-FragPos); + int idx = d>cascadeSplit[1]?2:d>cascadeSplit[0]?1:0; + float sh=CalcShadow(idx); + vec3 amb=0.2*base, dif=(1-sh)*ndl*base; + FragColor=vec4(amb+dif,1); + } + )"; + GLuint sv=compile(svsrc,GL_VERTEX_SHADER), + sf=compile(sfsrc,GL_FRAGMENT_SHADER); + mainProg=glCreateProgram(); + glAttachShader(mainProg,sv); glAttachShader(mainProg,sf); + glLinkProgram(mainProg); + glDeleteShader(sv); glDeleteShader(sf); + return true; } -Chunk VoxelGame::generateChunk(int cx,int cz) { - Chunk c; c.x=cx; c.z=cz; - c.voxels.assign(16, std::vector>(16, std::vector(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>(CHUNK_HEIGHT, + std::vector(CHUNK_SIZE, 0))); + + const float scale = 0.05f; // controls feature size + for (int x = 0; x < CHUNK_SIZE; ++x) { + for (int z = 0; z < CHUNK_SIZE; ++z) { + // world‐space coords + float wx = (cx * CHUNK_SIZE + x) * scale; + float wz = (cz * CHUNK_SIZE + z) * scale; + // perlin → [-1,1] → [0,1] + float n = (perlin(wx, 0.0f, wz) + 1.0f) * 0.5f; + // height [1,CHUNK_HEIGHT] + int h = 1 + int(n * (CHUNK_HEIGHT - 1)); + h = std::clamp(h, 1, CHUNK_HEIGHT); + // fill from y=0 up to y 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++; + +void VoxelGame::update(float dt, glm::vec3 const& camPos){ + lastDeltaTime=dt; + fps=1.0f/dt; + frameTimeSum+=dt; frameCount++; + + int old=totalChunksEverLoaded; + double t0=glfwGetTime(); + updateChunks(camPos); + double t1=glfwGetTime(); + lastChunkGenTime=t1-t0; + chunkGenCount=totalChunksEverLoaded-old; + totalChunkGenTime+=lastChunkGenTime*chunkGenCount; +} + +//---------------------------------------------------------------------------- +// Called each frame to load/unload chunks around the camera and enqueue new ones for meshing. +void VoxelGame::updateChunks(glm::vec3 const& camPos) { + int cx = int(std::floor(camPos.x / CHUNK_SIZE)); + int cz = int(std::floor(camPos.z / CHUNK_SIZE)); + int r = renderDistance; + + // Track which chunks to keep + std::unordered_map keep; + for (int dx = -r; dx <= r; ++dx) { + for (int dz = -r; dz <= r; ++dz) { + ChunkKey k{cx + dx, cz + dz}; + keep[k] = true; + // If not already generated, create and enqueue for meshing + if (!chunks.count(k)) { + chunks[k] = generateChunk(k.x, k.z); + ++totalChunksEverLoaded; + { + std::lock_guard lk(queueMutex); + toBuild.push(k); + } + queueCV.notify_one(); + } } } - for(auto it=chunks.begin();it!=chunks.end();){ - if(!keep.count(it->first)) it=chunks.erase(it); - else ++it; + + // Unload chunks that are out of range + for (auto it = chunks.begin(); it != chunks.end(); ) { + if (!keep.count(it->first)) { + // Also destroy its GPU mesh if present + if (meshes.count(it->first)) { + destroyMesh(meshes[it->first].get()); + meshes.erase(it->first); + } + it = chunks.erase(it); + } else { + ++it; + } } - totalChunksLoaded = chunks.size(); + + totalChunksLoaded = int(chunks.size()); } -void VoxelGame::update(float dt,const float cam[3]){ - updateChunks(cam); -} +//---------------------------------------------------------------------------- +// Worker thread: copies chunk data, runs greedyMesh on CPU, then enqueues RawMesh for GPU upload. +void VoxelGame::workerLoop() { + while (true) { + ChunkKey key; + { + std::unique_lock lk(queueMutex); + queueCV.wait(lk, [&]{ return stopWorkers || !toBuild.empty(); }); + if (stopWorkers && toBuild.empty()) return; + key = toBuild.front(); + toBuild.pop(); + } -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(); + // Copy voxel data under lock + Chunk chunkCopy; + { + std::lock_guard lk(queueMutex); + auto it = chunks.find(key); + if (it == chunks.end()) continue; + chunkCopy = it->second; + } + + // Phase 1: CPU‐side greedy meshing + RawMesh rm; + rm.key = key; + greedyMesh(chunkCopy, rm.verts, rm.indices); + + // Enqueue for main‐thread GPU upload + { + std::lock_guard lk(queueMutex); + readyToUpload.push(std::move(rm)); } } } + +//---------------------------------------------------------------------- +// computeLightSpace: full frustum corners approach + +static void computeLightSpace(glm::mat4 const& view, glm::mat4 const& proj, + glm::vec3 const& lightDir, + glm::mat4 out[3], + float splits[3]) +{ + glm::mat4 inv = glm::inverse(proj * view); + glm::vec4 corners[8] = { + {-1,-1,-1,1},{ 1,-1,-1,1},{ 1, 1,-1,1},{-1, 1,-1,1}, + {-1,-1, 1,1},{ 1,-1, 1,1},{ 1, 1, 1,1},{-1, 1, 1,1} + }; + for(int i=0;i<3;i++){ + float near = (i==0? 1.0f : splits[i-1]); + float far = splits[i]; + std::vector frustCorners; + for(int c=0;c<8;c++){ + glm::vec4 pt = inv * corners[c]; + pt /= pt.w; + frustCorners.push_back(pt); + } + // light view + glm::mat4 lightView = glm::lookAt(-lightDir*100.0f, glm::vec3(0), glm::vec3(0,1,0)); + // compute AABB + glm::vec3 mn(FLT_MAX), mx(-FLT_MAX); + for(auto &pt: frustCorners){ + glm::vec4 lp = lightView * pt; + mn = glm::min(mn, glm::vec3(lp)); + mx = glm::max(mx, glm::vec3(lp)); + } + out[i] = glm::ortho(mn.x,mx.x,mn.y,mx.y,-mx.z-50.0f,-mn.z+50.0f) * lightView; + } +} + +//---------------------------------------------------------------------- + +void VoxelGame::render(glm::mat4 const& view, glm::mat4 const& proj){ + computeLightSpace(view,proj,lightDir,lightSpaceMat,cascadeSplits); + + // 1) Depth passes + glEnable(GL_DEPTH_TEST); + for(int i=0;ivao); + glDrawElements(GL_TRIANGLES,m->indexCount,GL_UNSIGNED_INT,0); + } + glBindVertexArray(0); +} + +//---------------------------------------------------------------------- + void VoxelGame::debugUI(){ ImGui::Begin("Debug"); - ImGui::Text("Chunks Loaded: %d", totalChunksLoaded); - ImGui::Text("Chunks Ever Loaded: %d", totalChunksEverLoaded); + + ImGui::Text("FPS: %.1f", fps); + ImGui::Text("Delta Time: %.3f ms", lastDeltaTime*1000.0f); + ImGui::Text("Avg Frame: %.3f ms", (frameTimeSum/frameCount)*1000.0); + ImGui::Separator(); + + ImGui::Text("Last Chunk Gen: %.3f ms", lastChunkGenTime*1000.0); + ImGui::Text("Chunks this gen: %d", chunkGenCount); + ImGui::Text("Avg Chunk Gen: %.3f ms", + (chunkGenCount?(totalChunkGenTime/chunkGenCount)*1000.0:0.0)); + ImGui::Separator(); + + ImGui::Text("Chunks Loaded: %d", totalChunksLoaded); + ImGui::Text("Chunks Ever: %d", totalChunksEverLoaded); + ImGui::Separator(); + + ImGui::Text("Faces Drawn: %d", lastFaceCount); + ImGui::Text("GL Draw Calls: %d", lastGLCalls); + ImGui::Separator(); + + ImGui::SliderInt("Render Distance",&renderDistance,1,64); + + ImGui::Separator(); + ImGui::Text("Shadow Maps:"); + for(int i=0;ivbo); + glDeleteBuffers(1,&m->ibo); + glDeleteVertexArrays(1,&m->vao); +} diff --git a/cpp-voxel-engine/VoxelGame.h b/cpp-voxel-engine/VoxelGame.h index de42dd6..42e642f 100644 --- a/cpp-voxel-engine/VoxelGame.h +++ b/cpp-voxel-engine/VoxelGame.h @@ -1,58 +1,117 @@ -#ifndef VOXELGAME_H -#define VOXELGAME_H +#pragma once #include - -#include #include -#include "GreedyMesher.h" +#include +#include +#include +#include +#include +#include +#include -// Structure representing a chunk (16×16×16 voxels) -struct Chunk { - int x, z; // Chunk grid coordinates - // 3D voxel data: dimensions [16][16][16] - std::vector>> voxels; - // **Merged mesh faces** produced by the greedy mesher - std::vector mesh; -}; +#include +#include + +// world dimensions +static constexpr int CHUNK_SIZE = 16; +static constexpr int CHUNK_HEIGHT = 64; -// A key for identifying a chunk by its grid coordinates. struct ChunkKey { - int x, z; - bool operator==(const ChunkKey &other) const { - return x == other.x && z == other.z; + int x,z; + bool operator==(ChunkKey const& o) const { return x==o.x && z==o.z; } +}; +struct ChunkKeyHash { + std::size_t operator()(ChunkKey const& k) const noexcept { + return std::hash()(((long long)k.x<<32)|(unsigned)k.z); } }; -struct ChunkKeyHash { - std::size_t operator()(const ChunkKey &key) const { - return std::hash()(key.x) ^ (std::hash()(key.z) << 1); - } +struct Chunk { + int x,z; + // voxels[x][y][z], y ∈ [0,CHUNK_HEIGHT) + std::vector>> voxels; +}; + +struct ChunkMesh { + GLuint vao,vbo,ibo; + GLsizei indexCount; +}; + +struct Vertex { + glm::vec3 pos; + glm::vec3 normal; + glm::vec2 uv; +}; + +struct RawMesh { + ChunkKey key; + std::vector verts; + std::vector indices; }; class VoxelGame { public: VoxelGame(); ~VoxelGame(); - - bool init(); - void update(float deltaTime, const float cameraPos[3]); - void render(); - void debugUI(); - -private: - bool loadTextures(); - Chunk generateChunk(int cx, int cz); - void updateChunks(const float cameraPos[3]); - void drawCube(float x, float y, float z, int voxelType); - - std::unordered_map chunks; - int totalChunksLoaded; - int totalChunksEverLoaded; - - unsigned int textureGrass; - unsigned int textureDirt; - unsigned int textureWood; -}; -#endif // VOXELGAME_H + // initialize with screen size (for cascades) + bool init(int screenW,int screenH); + void update(float dt, glm::vec3 const& camPos); + void render(glm::mat4 const& view, glm::mat4 const& proj); + void debugUI(); + +private: + // chunk generation + Chunk generateChunk(int cx,int cz); + void updateChunks(glm::vec3 const& camPos); + + // CPU meshing + void workerLoop(); + void greedyMesh(Chunk const&, std::vector&, std::vector&); + void destroyMesh(ChunkMesh*); + + std::unordered_map chunks; + std::unordered_map,ChunkKeyHash> meshes; + + std::mutex queueMutex; + std::condition_variable queueCV; + std::queue toBuild; + std::queue readyToUpload; + bool stopWorkers=false; + std::vector workers; + + // stats + int totalChunksLoaded=0, totalChunksEverLoaded=0; + int chunkGenCount=0; + double totalChunkGenTime=0, lastChunkGenTime=0; + float fps=0, lastDeltaTime=0; + double frameTimeSum=0; int frameCount=0; + int lastFaceCount=0, lastGLCalls=0; + + // textures + GLuint textureDirt, textureGrass, textureWood; + + // cascaded shadow maps + static constexpr int NUM_CASCADES = 3; + static constexpr int SHADOW_MAP_SIZE = 1024; + GLuint depthFBO[NUM_CASCADES]; + GLuint depthTex[NUM_CASCADES]; + float cascadeSplits[NUM_CASCADES]; + glm::mat4 lightSpaceMat[NUM_CASCADES]; + + // shaders + GLuint depthProg, mainProg; + + // light & camera + glm::vec3 lightDir = glm::normalize(glm::vec3(1.0f, -1.0f, 0.5f)); + glm::vec3 cameraPos; + + int screenW, screenH; + int renderDistance = 2; + + bool loadTextures(); + bool loadShaders(); + void initShadowMaps(); + void renderScene(GLuint prog); +}; diff --git a/cpp-voxel-engine/VoxelGame.o b/cpp-voxel-engine/VoxelGame.o index e5c5726..b15f3b5 100644 Binary files a/cpp-voxel-engine/VoxelGame.o and b/cpp-voxel-engine/VoxelGame.o differ diff --git a/cpp-voxel-engine/a b/cpp-voxel-engine/a new file mode 100644 index 0000000..30ee2f4 --- /dev/null +++ b/cpp-voxel-engine/a @@ -0,0 +1,74 @@ +static void computeLightSpace(glm::mat4 view, glm::mat4 proj, + glm::vec3 lightDir, + glm::mat4 out[3]){ +glm::mat4 inv = glm::inverse(proj*view); +glm::vec4 corners[8] = { +{-1,-1,-1,1},{1,-1,-1,1},{1,1,-1,1},{-1,1,-1,1}, +{-1,-1, 1,1},{1,-1, 1,1},{1,1, 1,1},{-1,1, 1,1} +}; +for(int i=0;i<3;i++){ +glm::mat4 lightView = glm::lookAt(-lightDir*100.0f,glm::vec3(0),glm::vec3(0,1,0)); +glm::vec3 mn(FLT_MAX), mx(-FLT_MAX); +for(auto &c: corners){ +glm::vec4 wc = inv * c; wc/=wc.w; +glm::vec4 lc = lightView * wc; +mn = glm::min(mn,glm::vec3(lc)); +mx = glm::max(mx,glm::vec3(lc)); +} +out[i] = glm::ortho(mn.x,mx.x,mn.y,mx.y,-mx.z-50.0f,-mn.z+50.0f) * lightView; +} +} + + +void VoxelGame::render(glm::mat4 const& view, glm::mat4 const& proj){ + computeLightSpace(view,proj,lightDir,lightSpaceMat); + + // 1) depth passes + for(int i=0;i #include + #include #include -#include + +#include +#include + #include "imgui.h" -#include "imgui_internal.h" // For Im_PI #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" + #include "VoxelGame.h" -// Global camera variables. -float yaw = -90.0f; -float pitch = 0.0f; -float lastX = 400.0f, lastY = 300.0f; -bool firstMouse = true; -float cameraPos[3] = {32.0f, 32.0f, 80.0f}; -float cameraFront[3] = {0.0f, 0.0f, -1.0f}; -float cameraUp[3] = {0.0f, 1.0f, 0.0f}; +// camera globals +static glm::vec3 cameraPos = {32.0f, 32.0f, 80.0f}; +static glm::vec3 cameraFront = {0.0f, 0.0f, -1.0f}; +static glm::vec3 cameraUp = {0.0f, 1.0f, 0.0f}; +static float yaw = -90.0f, pitch = 0.0f; +static bool firstMouse = true; +static float lastX, lastY; -void mouse_callback(GLFWwindow* window, double xpos, double ypos) { +static void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if (glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_NORMAL) - return; + return; // do not rotate if cursor is free + if (firstMouse) { - lastX = static_cast(xpos); - lastY = static_cast(ypos); + lastX = float(xpos); + lastY = float(ypos); firstMouse = false; } - float xoffset = static_cast(xpos) - lastX; - float yoffset = lastY - static_cast(ypos); - lastX = static_cast(xpos); - lastY = static_cast(ypos); - - float sensitivity = 0.1f; - xoffset *= sensitivity; - yoffset *= sensitivity; - - yaw += xoffset; - pitch += yoffset; - if (pitch > 89.0f) pitch = 89.0f; - if (pitch < -89.0f) pitch = -89.0f; - - float radYaw = yaw * static_cast(IM_PI) / 180.0f; - float radPitch = pitch * static_cast(IM_PI) / 180.0f; - - cameraFront[0] = cos(radYaw) * cos(radPitch); - cameraFront[1] = sin(radPitch); - cameraFront[2] = sin(radYaw) * cos(radPitch); - float len = std::sqrt(cameraFront[0]*cameraFront[0] + - cameraFront[1]*cameraFront[1] + - cameraFront[2]*cameraFront[2]); - cameraFront[0] /= len; - cameraFront[1] /= len; - cameraFront[2] /= len; + float dx = float(xpos) - lastX; + float dy = lastY - float(ypos); + lastX = float(xpos); + lastY = float(ypos); + + const float sensitivity = 0.1f; + dx *= sensitivity; + dy *= sensitivity; + + yaw += dx; + pitch += dy; + pitch = std::clamp(pitch, -89.0f, 89.0f); + + float ry = glm::radians(yaw); + float rp = glm::radians(pitch); + cameraFront = glm::normalize(glm::vec3( + cos(ry)*cos(rp), + sin(rp), + sin(ry)*cos(rp) + )); } -GLFWwindow* initWindow(int width, int height, const char* title) { - if (!glfwInit()) { - std::cerr << "Failed to initialize GLFW\n"; - return nullptr; - } +static GLFWwindow* initWindow(int w, int h, const char* title) { + if (!glfwInit()) return nullptr; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE); - GLFWwindow* window = glfwCreateWindow(width, height, title, nullptr, nullptr); - if (!window) { - std::cerr << "Failed to create GLFW window\n"; + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); +#endif + GLFWwindow* win = glfwCreateWindow(w, h, title, nullptr, nullptr); + if (!win) { glfwTerminate(); return nullptr; } + glfwMakeContextCurrent(win); + glewExperimental = GL_TRUE; + if (glewInit() != GLEW_OK) { + glfwDestroyWindow(win); glfwTerminate(); return nullptr; } - glfwMakeContextCurrent(window); - glfwSetCursorPosCallback(window, mouse_callback); - glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - - glewExperimental = GL_TRUE; - if (glewInit() != GLEW_OK) { - std::cerr << "GLEW Error\n"; - return nullptr; - } - return window; + glViewport(0, 0, w, h); + glfwSetCursorPosCallback(win, mouse_callback); + // start with cursor disabled + glfwSetInputMode(win, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + lastX = w / 2.0f; + lastY = h / 2.0f; + return win; } int main() { - GLFWwindow* window = initWindow(800, 600, "Voxel Game"); - if (!window) - return -1; - + const int SCR_W = 1280, SCR_H = 720; + GLFWwindow* window = initWindow(SCR_W, SCR_H, "Voxel CSM"); + if (!window) return -1; + + // ImGui IMGUI_CHECKVERSION(); ImGui::CreateContext(); - ImGuiIO &io = ImGui::GetIO(); (void)io; - ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); - + ImGui::StyleColorsDark(); + + // Our game VoxelGame game; - if (!game.init()) { - std::cerr << "Failed to initialize game\n"; + if (!game.init(SCR_W, SCR_H)) { + std::cerr << "Failed to init VoxelGame\n"; return -1; } - - float deltaTime = 0.0f, lastFrame = 0.0f; - static bool cursorEnabled = false; - static bool f1Pressed = false; - + + float lastFrame = 0.0f; + bool cursorEnabled = false; + bool f1Pressed = false; + while (!glfwWindowShouldClose(window)) { - float currentFrame = static_cast(glfwGetTime()); - deltaTime = currentFrame - lastFrame; - lastFrame = currentFrame; + float now = float(glfwGetTime()); + float dt = now - lastFrame; + lastFrame = now; + glfwPollEvents(); - - // Toggle cursor lock with F1 (if ImGui isn’t capturing the mouse). - if (glfwGetKey(window, GLFW_KEY_F1) == GLFW_PRESS && !ImGui::GetIO().WantCaptureMouse) { + + // Toggle cursor with F1 (when ImGui not capturing) + ImGuiIO& io = ImGui::GetIO(); + if (glfwGetKey(window, GLFW_KEY_F1) == GLFW_PRESS && !io.WantCaptureMouse) { if (!f1Pressed) { cursorEnabled = !cursorEnabled; - glfwSetInputMode(window, GLFW_CURSOR, cursorEnabled ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED); + glfwSetInputMode(window, GLFW_CURSOR, + cursorEnabled ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED); f1Pressed = true; - firstMouse = true; + firstMouse = true; // reset for mouse callback } } else if (glfwGetKey(window, GLFW_KEY_F1) == GLFW_RELEASE) { f1Pressed = false; } - - float cameraSpeed = 5.0f * deltaTime; - if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { - cameraPos[0] += cameraFront[0] * cameraSpeed; - cameraPos[1] += cameraFront[1] * cameraSpeed; - cameraPos[2] += cameraFront[2] * cameraSpeed; - } - if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { - cameraPos[0] -= cameraFront[0] * cameraSpeed; - cameraPos[1] -= cameraFront[1] * cameraSpeed; - cameraPos[2] -= cameraFront[2] * cameraSpeed; - } - if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { - float right[3] = { - cameraFront[1]*cameraUp[2] - cameraFront[2]*cameraUp[1], - cameraFront[2]*cameraUp[0] - cameraFront[0]*cameraUp[2], - cameraFront[0]*cameraUp[1] - cameraFront[1]*cameraUp[0] - }; - float rLen = std::sqrt(right[0]*right[0] + right[1]*right[1] + right[2]*right[2]); - right[0] /= rLen; right[1] /= rLen; right[2] /= rLen; - cameraPos[0] -= right[0] * cameraSpeed; - cameraPos[1] -= right[1] * cameraSpeed; - cameraPos[2] -= right[2] * cameraSpeed; - } - if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { - float right[3] = { - cameraFront[1]*cameraUp[2] - cameraFront[2]*cameraUp[1], - cameraFront[2]*cameraUp[0] - cameraFront[0]*cameraUp[2], - cameraFront[0]*cameraUp[1] - cameraFront[1]*cameraUp[0] - }; - float rLen = std::sqrt(right[0]*right[0] + right[1]*right[1] + right[2]*right[2]); - right[0] /= rLen; right[1] /= rLen; right[2] /= rLen; - cameraPos[0] += right[0] * cameraSpeed; - cameraPos[1] += right[1] * cameraSpeed; - cameraPos[2] += right[2] * cameraSpeed; - } - + + // WASD movement + float speed = 5.0f * dt; + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + cameraPos += cameraFront * speed; + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + cameraPos -= cameraFront * speed; + glm::vec3 right = glm::normalize(glm::cross(cameraFront, cameraUp)); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + cameraPos -= right * speed; + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + cameraPos += right * speed; + + // Build view/proj + glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp); + glm::mat4 proj = glm::perspective(glm::radians(45.0f), + float(SCR_W)/SCR_H, 0.1f, 200.0f); + + // Update & render + game.update(dt, cameraPos); + game.render(view, proj); + + // Debug UI ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - gluPerspective(45.0, 800.0/600.0, 0.1, 200.0); - - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - float centerX = cameraPos[0] + cameraFront[0]; - float centerY = cameraPos[1] + cameraFront[1]; - float centerZ = cameraPos[2] + cameraFront[2]; - gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], - centerX, centerY, centerZ, - cameraUp[0], cameraUp[1], cameraUp[2]); - - game.update(deltaTime, cameraPos); - game.render(); game.debugUI(); - ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(window); } - + + // Cleanup ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); diff --git a/cpp-voxel-engine/main.o b/cpp-voxel-engine/main.o index e9d5dfd..4d90b5f 100644 Binary files a/cpp-voxel-engine/main.o and b/cpp-voxel-engine/main.o differ diff --git a/cpp-voxel-engine/voxelgame.exe b/cpp-voxel-engine/voxelgame.exe index 5f40676..279fc09 100644 Binary files a/cpp-voxel-engine/voxelgame.exe and b/cpp-voxel-engine/voxelgame.exe differ