small-projects/cpp-voxel-engine/VoxelGame.cpp
OusmBlueNinja 58962e52fc Broken
2025-04-05 23:20:54 -05:00

570 lines
20 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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) {
// worldspace 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: CPUside greedy meshing
RawMesh rm;
rm.key = key;
greedyMesh(chunkCopy, rm.verts, rm.indices);
// Enqueue for mainthread 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);
}