#define STB_IMAGE_IMPLEMENTATION #include "VoxelGame.h" #include #include #include #include #include "stb_image.h" #include "imgui.h" #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]; } 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 void VoxelGame::greedyMesh(const Chunk &chunk, std::vector &vertices, std::vector &indices) { 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_CULL_FACE); return true; } //---------------------------------------------------------------------- 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; } //---------------------------------------------------------------------- void VoxelGame::initShadowMaps(){ glGenFramebuffers(NUM_CASCADES,depthFBO); glGenTextures(NUM_CASCADES,depthTex); for(int i=0;i>(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 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(); } } } // 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 = int(chunks.size()); } //---------------------------------------------------------------------------- // 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(); } // 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("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); }