2025-04-06 03:14:06 +00:00
|
|
|
|
#define STB_IMAGE_IMPLEMENTATION
|
2025-04-06 02:03:23 +00:00
|
|
|
|
#include "VoxelGame.h"
|
2025-04-06 04:20:54 +00:00
|
|
|
|
|
2025-04-06 02:03:23 +00:00
|
|
|
|
#include <iostream>
|
2025-04-06 03:14:06 +00:00
|
|
|
|
#include <cmath>
|
2025-04-06 04:20:54 +00:00
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
|
#include <glm/gtc/type_ptr.hpp>
|
|
|
|
|
#include "stb_image.h"
|
2025-04-06 03:14:06 +00:00
|
|
|
|
#include "imgui.h"
|
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
#include <GLFW/glfw3.h>
|
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
// Perlin noise
|
2025-04-06 03:14:06 +00:00
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
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;
|
2025-04-06 03:14:06 +00:00
|
|
|
|
return c.voxels[x][y][z];
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
// Greedy mesh with normals
|
2025-04-06 02:03:23 +00:00
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
void VoxelGame::greedyMesh(const Chunk &chunk,
|
|
|
|
|
std::vector<Vertex> &vertices,
|
|
|
|
|
std::vector<uint32_t> &indices)
|
2025-04-06 02:03:23 +00:00
|
|
|
|
{
|
2025-04-06 04:20:54 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-06 02:03:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
// Constructor / Destructor
|
2025-04-06 02:03:23 +00:00
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
VoxelGame::VoxelGame(): cameraPos(32.0f,32.0f,80.0f) {
|
|
|
|
|
for(int i=0;i<4;i++)
|
|
|
|
|
workers.emplace_back(&VoxelGame::workerLoop,this);
|
|
|
|
|
}
|
2025-04-06 03:14:06 +00:00
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
VoxelGame::~VoxelGame(){
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lk(queueMutex);
|
|
|
|
|
stopWorkers=true;
|
2025-04-06 03:14:06 +00:00
|
|
|
|
}
|
2025-04-06 04:20:54 +00:00
|
|
|
|
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();
|
2025-04-06 03:14:06 +00:00
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
2025-04-06 04:20:54 +00:00
|
|
|
|
glEnable(GL_CULL_FACE);
|
2025-04-06 02:03:23 +00:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2025-04-06 02:03:23 +00:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
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 04:20:54 +00:00
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
2025-04-06 03:14:06 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-06 04:20:54 +00:00
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
}
|
2025-04-06 03:14:06 +00:00
|
|
|
|
}
|
2025-04-06 04:20:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
// 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);
|
2025-04-06 03:14:06 +00:00
|
|
|
|
}
|
2025-04-06 04:20:54 +00:00
|
|
|
|
// 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;
|
2025-04-06 03:14:06 +00:00
|
|
|
|
}
|
2025-04-06 02:03:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 04:20:54 +00:00
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
|
|
|
|
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 04:20:54 +00:00
|
|
|
|
|
|
|
|
|
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();
|
2025-04-06 03:14:06 +00:00
|
|
|
|
static bool wf=false;
|
|
|
|
|
if(ImGui::Checkbox("Wireframe",&wf))
|
2025-04-06 04:20:54 +00:00
|
|
|
|
glPolygonMode(GL_FRONT_AND_BACK,wf?GL_LINE:GL_FILL);
|
|
|
|
|
|
2025-04-06 02:03:23 +00:00
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
2025-04-06 04:20:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void VoxelGame::destroyMesh(ChunkMesh* m){
|
|
|
|
|
glDeleteBuffers(1,&m->vbo);
|
|
|
|
|
glDeleteBuffers(1,&m->ibo);
|
|
|
|
|
glDeleteVertexArrays(1,&m->vao);
|
|
|
|
|
}
|