#define STB_IMAGE_IMPLEMENTATION #include "VoxelGame.h" #include <iostream> #include <cmath> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> #include "stb_image.h" #include "imgui.h" #include <GLFW/glfw3.h> //---------------------------------------------------------------------- // Perlin noise static std::vector<int> 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<Vertex> &vertices, std::vector<uint32_t> &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]<dims[d];){ std::vector<int> mask(dims[u]*dims[v],0); for(x[v]=0;x[v]<dims[v];++x[v]){ for(x[u]=0;x[u]<dims[u];++x[u]){ int a=0,b=0; int p0[3]={x[0],x[1],x[2]}; int p1[3]={x[0]+q[0],x[1]+q[1],x[2]+q[2]}; if(x[d]>=0) a=voxelAt(chunk,p0[0],p0[1],p0[2]); if(x[d]<dims[d]-1) b=voxelAt(chunk,p1[0],p1[1],p1[2]); mask[x[u]+dims[u]*x[v]] = ((a!=0)!=(b!=0)) ? ((a!=0)?a:-b) : 0; } } ++x[d]; for(int j=0;j<dims[v];j++){ for(int i=0;i<dims[u];){ int c=mask[i+dims[u]*j]; if(!c){ ++i; continue; } int w=1; while(i+w<dims[u] && mask[i+w+dims[u]*j]==c) ++w; int h=1; bool done=false; while(j+h<dims[v]){ for(int k=0;k<w;k++) if(mask[i+k+dims[u]*(j+h)]!=c){ done=true; break; } if(done) break; ++h; } int du[3]={0,0,0}, dv[3]={0,0,0}; du[u]=w; dv[v]=h; int pos[3]={0,0,0}; pos[d]=x[d]; pos[u]=i; pos[v]=j; float fx=pos[0], fy=pos[1], fz=pos[2]; float px[4]={fx, fx+du[0], fx+du[0]+dv[0], fx+dv[0]}; float py[4]={fy, fy+du[1], fy+du[1]+dv[1], fy+dv[1]}; float pz[4]={fz, fz+du[2], fz+du[2]+dv[2], fz+dv[2]}; glm::vec3 U={px[1]-px[0],py[1]-py[0],pz[1]-pz[0]}; glm::vec3 V={px[3]-px[0],py[3]-py[0],pz[3]-pz[0]}; glm::vec3 N=glm::normalize(glm::cross(U,V)); float duv=float(w), dvv=float(h); Vertex v0{{px[0],py[0],pz[0]},N,{0,0}}; Vertex v1{{px[1],py[1],pz[1]},N,{duv,0}}; Vertex v2{{px[2],py[2],pz[2]},N,{duv,dvv}}; Vertex v3{{px[3],py[3],pz[3]},N,{0,dvv}}; if(c<0){ std::swap(v0,v1); std::swap(v2,v3); } uint32_t base=vertices.size(); vertices.insert(vertices.end(),{v0,v1,v2,v3}); indices.insert(indices.end(),{base,base+1,base+2,base,base+2,base+3}); lastFaceCount++; for(int l=0;l<h;++l) for(int k=0;k<w;++k) mask[i+k+dims[u]*(j+l)] = 0; i+=w; } } } } } //---------------------------------------------------------------------- // Constructor / Destructor VoxelGame::VoxelGame(): cameraPos(32.0f,32.0f,80.0f) { for(int i=0;i<4;i++) workers.emplace_back(&VoxelGame::workerLoop,this); } VoxelGame::~VoxelGame(){ { std::lock_guard<std::mutex> 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<NUM_CASCADES;i++){ glBindTexture(GL_TEXTURE_2D,depthTex[i]); glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT, SHADOW_MAP_SIZE,SHADOW_MAP_SIZE, 0,GL_DEPTH_COMPONENT,GL_FLOAT,nullptr); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER); float bc[4]={1,1,1,1}; glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,bc); glBindFramebuffer(GL_FRAMEBUFFER,depthFBO[i]); glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,depthTex[i],0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); } glBindFramebuffer(GL_FRAMEBUFFER,0); float near=1.0f, far=200.0f, lambda=0.5f; for(int i=0;i<NUM_CASCADES;i++){ float si=(i+1)/(float)NUM_CASCADES; float log = near*std::pow(far/near,si); float uni = near + (far-near)*si; cascadeSplits[i] = lambda*log + (1-lambda)*uni; } } Chunk VoxelGame::generateChunk(int cx, int cz) { Chunk c; c.x = cx; c.z = cz; c.voxels.assign(CHUNK_SIZE, std::vector<std::vector<int>>(CHUNK_HEIGHT, std::vector<int>(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<h for (int y = 0; y < h; ++y) { // grass at top, dirt below c.voxels[x][y][z] = (y == h - 1 ? 1 : 2); } } } return c; } 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<ChunkKey,bool,ChunkKeyHash> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<glm::vec4> 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;i<NUM_CASCADES;i++){ glViewport(0,0,SHADOW_MAP_SIZE,SHADOW_MAP_SIZE); glBindFramebuffer(GL_FRAMEBUFFER,depthFBO[i]); glClear(GL_DEPTH_BUFFER_BIT); glUseProgram(depthProg); glUniformMatrix4fv(glGetUniformLocation(depthProg,"uLightSpace"), 1,GL_FALSE,glm::value_ptr(lightSpaceMat[i])); renderScene(depthProg); } glBindFramebuffer(GL_FRAMEBUFFER,0); // 2) Main pass glViewport(0,0,screenW,screenH); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glUseProgram(mainProg); glUniformMatrix4fv(glGetUniformLocation(mainProg,"uView"), 1,GL_FALSE,glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(mainProg,"uProj"), 1,GL_FALSE,glm::value_ptr(proj)); for(int i=0;i<NUM_CASCADES;i++){ char buf[32]; sprintf(buf,"uLightSpace[%d]",i); glUniformMatrix4fv(glGetUniformLocation(mainProg,buf), 1,GL_FALSE,glm::value_ptr(lightSpaceMat[i])); } glUniform1fv(glGetUniformLocation(mainProg,"cascadeSplit"), NUM_CASCADES,cascadeSplits); glUniform3fv(glGetUniformLocation(mainProg,"lightDir"), 1,glm::value_ptr(lightDir)); glUniform3fv(glGetUniformLocation(mainProg,"viewPos"), 1,glm::value_ptr(cameraPos)); // bind textures glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D,textureDirt); glUniform1i(glGetUniformLocation(mainProg,"uTexDirt"),0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D,textureGrass); glUniform1i(glGetUniformLocation(mainProg,"uTexGrass"),1); for(int i=0;i<NUM_CASCADES;i++){ glActiveTexture(GL_TEXTURE2+i); glBindTexture(GL_TEXTURE_2D,depthTex[i]); char buf[32]; sprintf(buf,"uShadowMap[%d]",i); glUniform1i(glGetUniformLocation(mainProg,buf),2+i); } renderScene(mainProg); } //---------------------------------------------------------------------- void VoxelGame::renderScene(GLuint prog){ for(auto &pr:meshes){ glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(pr.first.x*16.0f,0,pr.first.z*16.0f)); glUniformMatrix4fv(glGetUniformLocation(prog,"uModel"), 1,GL_FALSE,glm::value_ptr(model)); ChunkMesh*m=pr.second.get(); glBindVertexArray(m->vao); 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;i<NUM_CASCADES;i++){ ImGui::Text(" Cascade %d", i); ImGui::Image((intptr_t)depthTex[i], ImVec2(128,128), ImVec2(0,1),ImVec2(1,0)); } ImGui::Separator(); static bool wf=false; if(ImGui::Checkbox("Wireframe",&wf)) glPolygonMode(GL_FRONT_AND_BACK,wf?GL_LINE:GL_FILL); ImGui::End(); } void VoxelGame::destroyMesh(ChunkMesh* m){ glDeleteBuffers(1,&m->vbo); glDeleteBuffers(1,&m->ibo); glDeleteVertexArrays(1,&m->vao); }