983 lines
43 KiB
C++
983 lines
43 KiB
C++
#include <SDL2/SDL.h>
|
||
#include <GL/glew.h>
|
||
#include <SDL2/SDL_opengl.h>
|
||
#include <glm/glm.hpp>
|
||
#include <glm/gtc/matrix_transform.hpp>
|
||
#include <glm/gtc/type_ptr.hpp>
|
||
|
||
#include <iostream>
|
||
#include <vector>
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <unordered_map>
|
||
#include <unordered_set>
|
||
#include <memory>
|
||
#include <algorithm>
|
||
#include <climits>
|
||
#include <string>
|
||
#include <thread>
|
||
#include <mutex>
|
||
#include <atomic>
|
||
#include <queue>
|
||
#include <deque>
|
||
#include <condition_variable>
|
||
#include <functional>
|
||
|
||
// Build:
|
||
// mkdir build && cd build && cmake .. && make -j$(nproc)
|
||
// ./minicraft
|
||
|
||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||
static const int SCREEN_W = 1280;
|
||
static const int SCREEN_H = 720;
|
||
static const int CHUNK_SIZE = 16;
|
||
static const int WORLD_H = 64;
|
||
static const int RENDER_DIST = 16; // sensible default; easily changed
|
||
static const float MOVE_SPEED = 6.0f;
|
||
static const float MOUSE_SENS = 0.12f;
|
||
static const float GRAVITY = -22.0f;
|
||
static const float JUMP_VEL = 9.0f;
|
||
static const float HW = 0.3f;
|
||
static const float PH = 1.8f;
|
||
static const int HOTBAR_SIZE = 8;
|
||
// How many chunk meshes we build per frame (tune up/down for perf vs pop-in)
|
||
static const int MESH_BUILDS_PER_FRAME = 4;
|
||
// Worker threads for chunk generation
|
||
static const int GEN_THREADS = (int)std::max(1u, std::thread::hardware_concurrency() - 1);
|
||
|
||
// ─── Block types ─────────────────────────────────────────────────────────────
|
||
enum BlockType : uint8_t { AIR=0, GRASS, DIRT, STONE, WOOD, LEAVES, SAND, SNOW, WATER };
|
||
static const int NUM_BLOCK_TYPES = 9;
|
||
|
||
struct BlockColor { float r,g,b; };
|
||
static const BlockColor BLOCK_COLORS[] = {
|
||
{0,0,0},
|
||
{0.40f,0.72f,0.24f},
|
||
{0.55f,0.40f,0.22f},
|
||
{0.50f,0.50f,0.50f},
|
||
{0.45f,0.30f,0.15f},
|
||
{0.22f,0.55f,0.18f},
|
||
{0.88f,0.83f,0.52f},
|
||
{0.92f,0.95f,0.98f},
|
||
{0.18f,0.42f,0.88f},
|
||
};
|
||
static const char* BLOCK_NAMES[] = {
|
||
"Air","Grass","Dirt","Stone","Wood","Leaves","Sand","Snow","Water"
|
||
};
|
||
static const float FACE_SHADE[] = {1.0f, 0.5f, 0.8f, 0.7f, 0.65f, 0.65f};
|
||
|
||
// ─── Perlin Noise ─────────────────────────────────────────────────────────────
|
||
static int P[512];
|
||
static void initNoise(int seed){
|
||
int perm[256]; for(int i=0;i<256;i++) perm[i]=i;
|
||
unsigned rng=(unsigned)seed;
|
||
for(int i=255;i>0;i--){ rng=rng*1664525u+1013904223u; int j=(rng>>16)%(i+1); std::swap(perm[i],perm[j]); }
|
||
for(int i=0;i<512;i++) P[i]=perm[i&255];
|
||
}
|
||
static float fade(float t){return t*t*t*(t*(t*6-15)+10);}
|
||
static float lerp(float a,float b,float t){return a+t*(b-a);}
|
||
static float grad(int h,float x,float y){h&=3;float u=(h<2)?x:y,v=(h<2)?y:x;return((h&1)?-u:u)+((h&2)?-v:v);}
|
||
static float noise2(float x,float y){
|
||
int xi=(int)floorf(x)&255,yi=(int)floorf(y)&255;
|
||
float xf=x-floorf(x),yf=y-floorf(y),u=fade(xf),v=fade(yf);
|
||
int aa=P[P[xi]+yi],ab=P[P[xi]+yi+1],ba=P[P[xi+1]+yi],bb=P[P[xi+1]+yi+1];
|
||
return lerp(lerp(grad(aa,xf,yf),grad(ba,xf-1,yf),u),lerp(grad(ab,xf,yf-1),grad(bb,xf-1,yf-1),u),v);
|
||
}
|
||
static float fbm(float x,float y,int oct=6){
|
||
float val=0,amp=1,freq=1,mx=0;
|
||
for(int i=0;i<oct;i++){val+=noise2(x*freq,y*freq)*amp;mx+=amp;amp*=0.5f;freq*=2.0f;}
|
||
return val/mx;
|
||
}
|
||
|
||
// ─── World chunk ─────────────────────────────────────────────────────────────
|
||
struct Chunk {
|
||
int cx,cz;
|
||
uint8_t blocks[CHUNK_SIZE][WORLD_H][CHUNK_SIZE];
|
||
GLuint VAO=0,VBO=0,EBO=0;
|
||
int indexCount=0;
|
||
// meshReady: the VAO holds a valid (possibly empty) mesh
|
||
// meshDirty: block data changed, needs a rebuild
|
||
bool meshDirty=true, meshReady=false, generated=false;
|
||
Chunk(int cx,int cz):cx(cx),cz(cz){memset(blocks,AIR,sizeof(blocks));}
|
||
~Chunk(){
|
||
if(VAO){ glDeleteVertexArrays(1,&VAO); glDeleteBuffers(1,&VBO); glDeleteBuffers(1,&EBO); }
|
||
}
|
||
};
|
||
struct ChunkKey{
|
||
int x,z;
|
||
bool operator==(const ChunkKey& o)const{return x==o.x&&z==o.z;}
|
||
};
|
||
struct ChunkKeyHash{
|
||
size_t operator()(const ChunkKey& k)const{
|
||
return std::hash<long long>()((long long)(unsigned)k.x<<32|(unsigned)k.z);
|
||
}
|
||
};
|
||
using ChunkMap = std::unordered_map<ChunkKey,std::unique_ptr<Chunk>,ChunkKeyHash>;
|
||
|
||
// ─── Thread pool ─────────────────────────────────────────────────────────────
|
||
struct ThreadPool {
|
||
std::vector<std::thread> workers;
|
||
std::deque<std::function<void()>> tasks;
|
||
std::mutex mtx;
|
||
std::condition_variable cv;
|
||
std::atomic<bool> stop{false};
|
||
std::atomic<int> pending{0};
|
||
|
||
void start(int n){
|
||
for(int i=0;i<n;i++)
|
||
workers.emplace_back([this]{
|
||
while(true){
|
||
std::function<void()> task;
|
||
{ std::unique_lock<std::mutex> lk(mtx);
|
||
cv.wait(lk,[this]{return stop.load()||!tasks.empty();});
|
||
if(stop&&tasks.empty()) return;
|
||
task=std::move(tasks.front()); tasks.pop_front(); }
|
||
task(); --pending;
|
||
}
|
||
});
|
||
}
|
||
void enqueue(std::function<void()> f){
|
||
{ std::lock_guard<std::mutex> lk(mtx); ++pending; tasks.push_back(std::move(f)); }
|
||
cv.notify_one();
|
||
}
|
||
void shutdown(){
|
||
stop=true; cv.notify_all();
|
||
for(auto& w:workers) w.join();
|
||
}
|
||
} POOL;
|
||
|
||
// ─── Global state ─────────────────────────────────────────────────────────────
|
||
static ChunkMap CHUNKS;
|
||
|
||
// Finished-generation queue: workers push here, main thread drains
|
||
struct FinishedChunk { int cx,cz; std::unique_ptr<Chunk> chunk; };
|
||
static std::queue<FinishedChunk> FINISHED_Q;
|
||
static std::mutex FINISHED_MTX;
|
||
|
||
// In-flight set: prevents double-submitting
|
||
static std::unordered_set<ChunkKey,ChunkKeyHash> IN_FLIGHT;
|
||
static std::mutex IN_FLIGHT_MTX;
|
||
|
||
// Dirty mesh queue (ordered set so we don't double-process)
|
||
static std::unordered_set<ChunkKey,ChunkKeyHash> DIRTY_SET;
|
||
|
||
// ─── Chunk generation (worker thread, no GL) ──────────────────────────────────
|
||
static void generateChunkData(Chunk& c){
|
||
int wx0=c.cx*CHUNK_SIZE, wz0=c.cz*CHUNK_SIZE;
|
||
for(int x=0;x<CHUNK_SIZE;x++)
|
||
for(int z=0;z<CHUNK_SIZE;z++){
|
||
float wx=(wx0+x)*0.008f, wz=(wz0+z)*0.008f;
|
||
float cont = fbm(wx*0.4f, wz*0.4f, 4);
|
||
float detail = fbm(wx*2.0f+5.3f, wz*2.0f+1.7f, 5);
|
||
float ridge = 1.0f-fabsf(fbm(wx*0.8f+3.1f, wz*0.8f+8.9f, 4));
|
||
float h = 12.0f + cont*18.0f + detail*6.0f + ridge*14.0f*std::max(0.0f,cont);
|
||
int top = std::max(2, std::min(WORLD_H-2, (int)h));
|
||
bool isSand=(top<16), isSnow=(top>38);
|
||
for(int y=0;y<WORLD_H;y++){
|
||
if(y==0) c.blocks[x][y][z]=STONE;
|
||
else if(y<top-4) c.blocks[x][y][z]=STONE;
|
||
else if(y<top) c.blocks[x][y][z]=isSand?SAND:DIRT;
|
||
else if(y==top) c.blocks[x][y][z]=isSand?SAND:(isSnow?SNOW:GRASS);
|
||
else if(y<=14) c.blocks[x][y][z]=WATER;
|
||
else c.blocks[x][y][z]=AIR;
|
||
}
|
||
if(!isSand&&!isSnow&&top>=16&&top<36&&top+6<WORLD_H){
|
||
unsigned col=(unsigned)((wx0+x)*73856093u^(unsigned)(wz0+z)*19349663u);
|
||
if((col&0xFF)<18){
|
||
int trunk=top+1;
|
||
for(int t=trunk;t<trunk+4&&t<WORLD_H;t++) c.blocks[x][t][z]=WOOD;
|
||
for(int dx=-2;dx<=2;dx++) for(int dz=-2;dz<=2;dz++) for(int dy=trunk+2;dy<=trunk+5;dy++){
|
||
int nx=x+dx, nz=z+dz;
|
||
if(nx>=0&&nx<CHUNK_SIZE&&nz>=0&&nz<CHUNK_SIZE&&dy<WORLD_H)
|
||
if(c.blocks[nx][dy][nz]==AIR) c.blocks[nx][dy][nz]=LEAVES;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
c.generated=true;
|
||
}
|
||
|
||
static void submitChunkGen(int cx,int cz){
|
||
ChunkKey k{cx,cz};
|
||
{ std::lock_guard<std::mutex> lk(IN_FLIGHT_MTX);
|
||
if(IN_FLIGHT.count(k)) return;
|
||
IN_FLIGHT.insert(k); }
|
||
POOL.enqueue([cx,cz,k]{
|
||
auto c=std::make_unique<Chunk>(cx,cz);
|
||
generateChunkData(*c);
|
||
{ std::lock_guard<std::mutex> lk(FINISHED_MTX);
|
||
FINISHED_Q.push({cx,cz,std::move(c)}); }
|
||
{ std::lock_guard<std::mutex> lk(IN_FLIGHT_MTX);
|
||
IN_FLIGHT.erase(k); }
|
||
});
|
||
}
|
||
|
||
// ─── Block accessors (main thread only) ──────────────────────────────────────
|
||
// Returns the block at world coords, using only the chunk's own data when
|
||
// called during mesh building (no cross-chunk queries that might be stale).
|
||
static uint8_t getBlock(int wx,int wy,int wz){
|
||
if(wy<0) return STONE; if(wy>=WORLD_H) return AIR;
|
||
int cx=(int)floorf((float)wx/CHUNK_SIZE);
|
||
int cz=(int)floorf((float)wz/CHUNK_SIZE);
|
||
auto it=CHUNKS.find({cx,cz});
|
||
if(it==CHUNKS.end()||!it->second->generated) return AIR; // treat unknown as AIR so border faces show
|
||
int lx=wx-cx*CHUNK_SIZE, lz=wz-cz*CHUNK_SIZE;
|
||
return it->second->blocks[lx][wy][lz];
|
||
}
|
||
|
||
static void setBlock(int wx,int wy,int wz,uint8_t val){
|
||
if(wy<0||wy>=WORLD_H) return;
|
||
int cx=(int)floorf((float)wx/CHUNK_SIZE);
|
||
int cz=(int)floorf((float)wz/CHUNK_SIZE);
|
||
auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return;
|
||
int lx=wx-cx*CHUNK_SIZE, lz=wz-cz*CHUNK_SIZE;
|
||
it->second->blocks[lx][wy][lz]=val;
|
||
DIRTY_SET.insert({cx,cz});
|
||
// Also dirty the four face-adjacent neighbours
|
||
static const int NX[]={-1,1,0,0}, NZ[]={0,0,-1,1};
|
||
static const int BX[]={ 0,CHUNK_SIZE-1,lx,lx}, BZ[]={lz,lz,0,CHUNK_SIZE-1};
|
||
(void)BX; (void)BZ;
|
||
if(lx==0) DIRTY_SET.insert({cx-1,cz});
|
||
if(lx==CHUNK_SIZE-1) DIRTY_SET.insert({cx+1,cz});
|
||
if(lz==0) DIRTY_SET.insert({cx,cz-1});
|
||
if(lz==CHUNK_SIZE-1) DIRTY_SET.insert({cx,cz+1});
|
||
(void)NX; (void)NZ;
|
||
}
|
||
|
||
// ─── Mesh building (GL, main thread) ─────────────────────────────────────────
|
||
struct Vertex { float x,y,z,r,g,b; };
|
||
|
||
// When building a mesh we need to peek at neighbouring chunks for face culling.
|
||
// Key insight: we query getBlock() which returns AIR for unloaded chunks.
|
||
// This means border faces toward unloaded chunks will be SHOWN (not culled),
|
||
// which is correct — you can see the world edge. Once the neighbour loads,
|
||
// its arrival marks this chunk dirty and it gets rebuilt with proper culling.
|
||
static bool shouldDrawFace(uint8_t cur, int wx, int wy, int wz){
|
||
uint8_t nb = getBlock(wx,wy,wz);
|
||
if(nb==AIR) return true;
|
||
if(nb==WATER||nb==LEAVES) return cur!=nb;
|
||
return false;
|
||
}
|
||
|
||
static void addFace(std::vector<Vertex>& verts, std::vector<GLuint>& idx,
|
||
glm::vec3 v0,glm::vec3 v1,glm::vec3 v2,glm::vec3 v3,
|
||
float r,float g,float b,float shade)
|
||
{
|
||
GLuint base=(GLuint)verts.size();
|
||
float sr=r*shade, sg=g*shade, sb=b*shade;
|
||
verts.push_back({v0.x,v0.y,v0.z,sr,sg,sb});
|
||
verts.push_back({v1.x,v1.y,v1.z,sr,sg,sb});
|
||
verts.push_back({v2.x,v2.y,v2.z,sr,sg,sb});
|
||
verts.push_back({v3.x,v3.y,v3.z,sr,sg,sb});
|
||
idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3});
|
||
}
|
||
|
||
static void buildChunkMesh(Chunk& c){
|
||
std::vector<Vertex> verts;
|
||
std::vector<GLuint> idx;
|
||
verts.reserve(4096);
|
||
idx.reserve(6144);
|
||
|
||
int wx0=c.cx*CHUNK_SIZE, wz0=c.cz*CHUNK_SIZE;
|
||
|
||
for(int x=0;x<CHUNK_SIZE;x++)
|
||
for(int y=0;y<WORLD_H;y++)
|
||
for(int z=0;z<CHUNK_SIZE;z++){
|
||
uint8_t b=c.blocks[x][y][z];
|
||
if(b==AIR) continue;
|
||
float r=BLOCK_COLORS[b].r, g=BLOCK_COLORS[b].g, bl=BLOCK_COLORS[b].b;
|
||
float bx=wx0+x, by=y, bz=wz0+z;
|
||
int wx=wx0+x, wz=wz0+z;
|
||
// +Y top
|
||
if(shouldDrawFace(b,wx,y+1,wz))
|
||
addFace(verts,idx,{bx,by+1,bz+1},{bx+1,by+1,bz+1},{bx+1,by+1,bz},{bx,by+1,bz},r,g,bl,FACE_SHADE[0]);
|
||
// -Y bottom
|
||
if(shouldDrawFace(b,wx,y-1,wz))
|
||
addFace(verts,idx,{bx,by,bz},{bx+1,by,bz},{bx+1,by,bz+1},{bx,by,bz+1},r,g,bl,FACE_SHADE[1]);
|
||
// +Z front
|
||
if(shouldDrawFace(b,wx,y,wz+1))
|
||
addFace(verts,idx,{bx,by,bz+1},{bx+1,by,bz+1},{bx+1,by+1,bz+1},{bx,by+1,bz+1},r,g,bl,FACE_SHADE[2]);
|
||
// -Z back
|
||
if(shouldDrawFace(b,wx,y,wz-1))
|
||
addFace(verts,idx,{bx+1,by,bz},{bx,by,bz},{bx,by+1,bz},{bx+1,by+1,bz},r,g,bl,FACE_SHADE[3]);
|
||
// -X left
|
||
if(shouldDrawFace(b,wx-1,y,wz))
|
||
addFace(verts,idx,{bx,by,bz},{bx,by,bz+1},{bx,by+1,bz+1},{bx,by+1,bz},r,g,bl,FACE_SHADE[4]);
|
||
// +X right
|
||
if(shouldDrawFace(b,wx+1,y,wz))
|
||
addFace(verts,idx,{bx+1,by,bz+1},{bx+1,by,bz},{bx+1,by+1,bz},{bx+1,by+1,bz+1},r,g,bl,FACE_SHADE[5]);
|
||
}
|
||
|
||
if(!c.VAO){ glGenVertexArrays(1,&c.VAO); glGenBuffers(1,&c.VBO); glGenBuffers(1,&c.EBO); }
|
||
glBindVertexArray(c.VAO);
|
||
glBindBuffer(GL_ARRAY_BUFFER,c.VBO);
|
||
glBufferData(GL_ARRAY_BUFFER,(GLsizeiptr)(verts.size()*sizeof(Vertex)),verts.data(),GL_DYNAMIC_DRAW);
|
||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,c.EBO);
|
||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,(GLsizeiptr)(idx.size()*sizeof(GLuint)),idx.data(),GL_DYNAMIC_DRAW);
|
||
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0); glEnableVertexAttribArray(0);
|
||
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1);
|
||
glBindVertexArray(0);
|
||
|
||
c.indexCount = (int)idx.size();
|
||
c.meshDirty = false;
|
||
c.meshReady = true;
|
||
}
|
||
|
||
// Drain finished-generation queue, install chunks, schedule their meshes + neighbours
|
||
static void drainFinishedChunks(){
|
||
while(true){
|
||
FinishedChunk fc;
|
||
{ std::lock_guard<std::mutex> lk(FINISHED_MTX);
|
||
if(FINISHED_Q.empty()) break;
|
||
fc=std::move(FINISHED_Q.front()); FINISHED_Q.pop(); }
|
||
|
||
ChunkKey k{fc.cx,fc.cz};
|
||
CHUNKS[k]=std::move(fc.chunk);
|
||
|
||
// Schedule this chunk AND its four face-neighbours for meshing.
|
||
// The neighbours need to be rebuilt so their border faces toward this
|
||
// newly arrived chunk get properly culled.
|
||
DIRTY_SET.insert(k);
|
||
DIRTY_SET.insert({fc.cx-1,fc.cz});
|
||
DIRTY_SET.insert({fc.cx+1,fc.cz});
|
||
DIRTY_SET.insert({fc.cx,fc.cz-1});
|
||
DIRTY_SET.insert({fc.cx,fc.cz+1});
|
||
}
|
||
}
|
||
|
||
// Process up to N chunks from the dirty set each frame.
|
||
// Only build a chunk if it AND all four face-neighbours are generated —
|
||
// this ensures the border-face culling is correct on the first build.
|
||
static void processDirtyMeshes(int maxPerFrame, int pcx, int pcz){
|
||
if(DIRTY_SET.empty()) return;
|
||
|
||
// Sort dirty chunks by distance to player for closest-first pop-in
|
||
std::vector<ChunkKey> sorted(DIRTY_SET.begin(),DIRTY_SET.end());
|
||
std::sort(sorted.begin(),sorted.end(),[&](const ChunkKey& a,const ChunkKey& b){
|
||
int da=(a.x-pcx)*(a.x-pcx)+(a.z-pcz)*(a.z-pcz);
|
||
int db=(b.x-pcx)*(b.x-pcx)+(b.z-pcz)*(b.z-pcz);
|
||
return da<db;
|
||
});
|
||
|
||
int built=0;
|
||
for(auto& k : sorted){
|
||
if(built>=maxPerFrame) break;
|
||
|
||
auto it=CHUNKS.find(k);
|
||
if(it==CHUNKS.end()){ DIRTY_SET.erase(k); continue; } // evicted
|
||
|
||
Chunk& c=*it->second;
|
||
if(!c.generated){ continue; } // still generating
|
||
|
||
// Check all four face-neighbours exist and are generated.
|
||
// If a neighbour doesn't exist yet, keep this chunk dirty and skip —
|
||
// it will be rebuilt properly when the neighbour arrives.
|
||
bool allNeighbours=true;
|
||
const int dx[]={-1,1,0,0}, dz[]={0,0,-1,1};
|
||
for(int i=0;i<4;i++){
|
||
auto nit=CHUNKS.find({k.x+dx[i],k.z+dz[i]});
|
||
if(nit==CHUNKS.end()||!nit->second->generated){
|
||
allNeighbours=false; break;
|
||
}
|
||
}
|
||
if(!allNeighbours) continue; // wait for neighbours
|
||
|
||
buildChunkMesh(c);
|
||
DIRTY_SET.erase(k);
|
||
++built;
|
||
}
|
||
}
|
||
|
||
// ─── Shaders ─────────────────────────────────────────────────────────────────
|
||
static const char* VS_SRC = R"(
|
||
#version 330 core
|
||
layout(location=0) in vec3 aPos;
|
||
layout(location=1) in vec3 aColor;
|
||
out vec3 vColor;
|
||
out float vFog;
|
||
uniform mat4 uMVP;
|
||
uniform vec3 uCamPos;
|
||
void main(){
|
||
gl_Position = uMVP * vec4(aPos,1.0);
|
||
vColor = aColor;
|
||
float dist = length(aPos - uCamPos);
|
||
vFog = clamp((dist - 80.0) / 60.0, 0.0, 1.0);
|
||
})";
|
||
|
||
static const char* FS_SRC = R"(
|
||
#version 330 core
|
||
in vec3 vColor;
|
||
in float vFog;
|
||
out vec4 fragColor;
|
||
void main(){
|
||
vec3 fogColor = vec3(0.53, 0.81, 0.98);
|
||
fragColor = vec4(mix(vColor, fogColor, vFog), 1.0);
|
||
})";
|
||
|
||
static const char* VS_UI = R"(
|
||
#version 330 core
|
||
layout(location=0) in vec2 aPos;
|
||
layout(location=1) in vec3 aColor;
|
||
out vec3 vColor;
|
||
uniform mat4 uProj;
|
||
void main(){ gl_Position = uProj*vec4(aPos,0.0,1.0); vColor = aColor; })";
|
||
|
||
static const char* FS_UI = R"(
|
||
#version 330 core
|
||
in vec3 vColor;
|
||
out vec4 fragColor;
|
||
uniform float uAlpha;
|
||
void main(){ fragColor = vec4(vColor, uAlpha); })";
|
||
|
||
static GLuint compileShader(GLenum type, const char* src){
|
||
GLuint s=glCreateShader(type); glShaderSource(s,1,&src,nullptr); glCompileShader(s);
|
||
GLint ok; glGetShaderiv(s,GL_COMPILE_STATUS,&ok);
|
||
if(!ok){ char buf[512]; glGetShaderInfoLog(s,512,nullptr,buf); std::cerr<<"Shader err: "<<buf<<"\n"; }
|
||
return s;
|
||
}
|
||
static GLuint buildProg(const char* vs, const char* fs){
|
||
GLuint v=compileShader(GL_VERTEX_SHADER,vs), f=compileShader(GL_FRAGMENT_SHADER,fs);
|
||
GLuint p=glCreateProgram(); glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p);
|
||
glDeleteShader(v); glDeleteShader(f); return p;
|
||
}
|
||
|
||
// ─── 2D UI ───────────────────────────────────────────────────────────────────
|
||
struct UIRenderer {
|
||
GLuint prog=0,VAO=0,VBO=0;
|
||
GLint projLoc=-1,alphaLoc=-1;
|
||
glm::mat4 proj{1.0f};
|
||
|
||
void init(){
|
||
prog=buildProg(VS_UI,FS_UI);
|
||
projLoc =glGetUniformLocation(prog,"uProj");
|
||
alphaLoc=glGetUniformLocation(prog,"uAlpha");
|
||
proj=glm::ortho(0.0f,(float)SCREEN_W,(float)SCREEN_H,0.0f);
|
||
glGenVertexArrays(1,&VAO); glGenBuffers(1,&VBO);
|
||
glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO);
|
||
glBufferData(GL_ARRAY_BUFFER,1024*sizeof(float),nullptr,GL_DYNAMIC_DRAW);
|
||
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)0); glEnableVertexAttribArray(0);
|
||
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float))); glEnableVertexAttribArray(1);
|
||
glBindVertexArray(0);
|
||
}
|
||
void begin(){
|
||
glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE);
|
||
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
|
||
glUseProgram(prog);
|
||
glUniformMatrix4fv(projLoc,1,GL_FALSE,glm::value_ptr(proj));
|
||
glBindVertexArray(VAO);
|
||
}
|
||
void end(){
|
||
glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE);
|
||
glBindVertexArray(0);
|
||
}
|
||
void rect(float x,float y,float w,float h,float r,float g,float b,float a=1.0f){
|
||
float v[]={x,y,r,g,b, x+w,y,r,g,b, x+w,y+h,r,g,b, x,y,r,g,b, x+w,y+h,r,g,b, x,y+h,r,g,b};
|
||
glUniform1f(alphaLoc,a);
|
||
glBindBuffer(GL_ARRAY_BUFFER,VBO);
|
||
glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(v),v);
|
||
glDrawArrays(GL_TRIANGLES,0,6);
|
||
}
|
||
void outline(float x,float y,float w,float h,float r,float g,float b,float t=2.0f){
|
||
rect(x,y,w,t,r,g,b); rect(x,y+h-t,w,t,r,g,b);
|
||
rect(x,y,t,h,r,g,b); rect(x+w-t,y,t,h,r,g,b);
|
||
}
|
||
} UI;
|
||
|
||
// ─── Bitmap font 5×7 (ASCII 32–126) ──────────────────────────────────────────
|
||
static const uint8_t FONT5x7[][5]={
|
||
{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},
|
||
{0x14,0x7F,0x14,0x7F,0x14},{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},
|
||
{0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},{0x00,0x1C,0x22,0x41,0x00},
|
||
{0x00,0x41,0x22,0x1C,0x00},{0x14,0x08,0x3E,0x08,0x14},{0x08,0x08,0x3E,0x08,0x08},
|
||
{0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},
|
||
{0x20,0x10,0x08,0x04,0x02},{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},
|
||
{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},{0x18,0x14,0x12,0x7F,0x10},
|
||
{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},
|
||
{0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00},
|
||
{0x00,0x56,0x36,0x00,0x00},{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},
|
||
{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},{0x32,0x49,0x79,0x41,0x3E},
|
||
{0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
|
||
{0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},
|
||
{0x3E,0x41,0x49,0x49,0x7A},{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},
|
||
{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},{0x7F,0x40,0x40,0x40,0x40},
|
||
{0x7F,0x02,0x04,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
|
||
{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},
|
||
{0x46,0x49,0x49,0x49,0x31},{0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},
|
||
{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},{0x63,0x14,0x08,0x14,0x63},
|
||
{0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43},{0x00,0x7F,0x41,0x41,0x00},
|
||
{0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04},
|
||
{0x40,0x40,0x40,0x40,0x40},{0x00,0x01,0x02,0x04,0x00},{0x20,0x54,0x54,0x54,0x78},
|
||
{0x7F,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},{0x38,0x44,0x44,0x48,0x7F},
|
||
{0x38,0x54,0x54,0x54,0x18},{0x08,0x7E,0x09,0x01,0x02},{0x0C,0x52,0x52,0x52,0x3E},
|
||
{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x44,0x3D,0x00},
|
||
{0x7F,0x10,0x28,0x44,0x00},{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x18,0x04,0x78},
|
||
{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},{0x7C,0x14,0x14,0x14,0x08},
|
||
{0x08,0x14,0x14,0x18,0x7C},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20},
|
||
{0x04,0x3F,0x44,0x40,0x20},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},
|
||
{0x3C,0x40,0x30,0x40,0x3C},{0x44,0x28,0x10,0x28,0x44},{0x0C,0x50,0x50,0x50,0x3C},
|
||
{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},{0x00,0x00,0x7F,0x00,0x00},
|
||
{0x00,0x41,0x36,0x08,0x00},{0x10,0x08,0x08,0x10,0x08},
|
||
};
|
||
static void drawText(const char* text,float x,float y,float scale,
|
||
float r,float g,float b,float a=1.0f){
|
||
for(int i=0;text[i];i++){
|
||
int c=text[i]-32;
|
||
if(c<0||c>=(int)(sizeof(FONT5x7)/sizeof(FONT5x7[0]))){x+=6*scale;continue;}
|
||
for(int col=0;col<5;col++){
|
||
uint8_t bits=FONT5x7[c][col];
|
||
for(int row=0;row<7;row++)
|
||
if(bits&(1<<row)) UI.rect(x+col*scale,y+row*scale,scale,scale,r,g,b,a);
|
||
}
|
||
x+=6*scale;
|
||
}
|
||
}
|
||
|
||
// ─── Loading screen ───────────────────────────────────────────────────────────
|
||
static void drawLoadingScreen(SDL_Window* win, int ready, int total){
|
||
glClearColor(0.08f,0.08f,0.12f,1.0f);
|
||
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||
UI.begin();
|
||
|
||
const char* title="MiniCraft";
|
||
drawText(title,(SCREEN_W-strlen(title)*6*4.f)*0.5f,SCREEN_H*0.35f,4.f,0.4f,0.8f,0.4f);
|
||
const char* lbl="Generating world...";
|
||
drawText(lbl,(SCREEN_W-strlen(lbl)*6*2.f)*0.5f,SCREEN_H*0.52f,2.f,0.8f,0.8f,0.8f);
|
||
|
||
float bw=500,bh=22,bx=(SCREEN_W-bw)*0.5f,by=SCREEN_H*0.58f;
|
||
float pct=total>0?(float)ready/total:0;
|
||
UI.rect(bx,by,bw,bh,0.2f,0.2f,0.2f);
|
||
UI.rect(bx,by,bw*pct,bh,0.3f,0.7f,0.3f);
|
||
UI.outline(bx,by,bw,bh,0.5f,0.5f,0.5f,2.f);
|
||
|
||
char buf[48]; snprintf(buf,48,"%d / %d chunks",ready,total);
|
||
drawText(buf,(SCREEN_W-strlen(buf)*6*1.5f)*0.5f,by+bh+8,1.5f,0.7f,0.7f,0.7f);
|
||
|
||
UI.end();
|
||
SDL_GL_SwapWindow(win);
|
||
}
|
||
|
||
// ─── Inventory ───────────────────────────────────────────────────────────────
|
||
struct Inventory {
|
||
uint8_t hotbar[HOTBAR_SIZE]={GRASS,DIRT,STONE,WOOD,LEAVES,SAND,SNOW,WATER};
|
||
int selected=0; bool open=false;
|
||
uint8_t selectedBlock()const{return hotbar[selected];}
|
||
};
|
||
static void drawInventory(Inventory& inv){
|
||
const float S=52,G=6,B=4;
|
||
float tw=HOTBAR_SIZE*(S+G)-G, hx=(SCREEN_W-tw)*0.5f, hy=SCREEN_H-S-16;
|
||
UI.rect(hx-8,hy-8,tw+16,S+16,0.15f,0.15f,0.15f,0.75f);
|
||
UI.outline(hx-8,hy-8,tw+16,S+16,0,0,0);
|
||
for(int i=0;i<HOTBAR_SIZE;i++){
|
||
float sx=hx+i*(S+G),sy=hy; bool sel=(i==inv.selected);
|
||
UI.rect(sx,sy,S,S,sel?.3f:.2f,sel?.3f:.2f,sel?.3f:.2f,.9f);
|
||
uint8_t bt=inv.hotbar[i];
|
||
if(bt!=AIR){
|
||
auto& bc=BLOCK_COLORS[bt];
|
||
UI.rect(sx+B,sy+B,S-2*B,S-2*B-10,bc.r,bc.g,bc.b);
|
||
UI.rect(sx+B,sy+S-B-14,S-2*B,4,bc.r*.65f,bc.g*.65f,bc.b*.65f);
|
||
}
|
||
if(sel) UI.outline(sx,sy,S,S,1,1,1,B*.5f);
|
||
else UI.outline(sx,sy,S,S,0,0,0,2);
|
||
char num[3]; snprintf(num,3,"%d",i+1);
|
||
drawText(num,sx+4,sy+S-12,1.5f,.8f,.8f,.8f);
|
||
}
|
||
{ const char* n=BLOCK_NAMES[inv.selectedBlock()];
|
||
drawText(n,(SCREEN_W-strlen(n)*12.f)*0.5f,hy-26,2,.9f,.9f,.9f,.9f); }
|
||
if(!inv.open) return;
|
||
|
||
const int COLS=4,ROWS=(NUM_BLOCK_TYPES-1+COLS-1)/COLS;
|
||
float iw=COLS*(S+G)-G+32,ih=ROWS*(S+G)-G+60;
|
||
float ix=(SCREEN_W-iw)*.5f,iy=(SCREEN_H-ih)*.5f;
|
||
UI.rect(0,0,(float)SCREEN_W,(float)SCREEN_H,0,0,0,.45f);
|
||
UI.rect(ix,iy,iw,ih,.18f,.18f,.18f,.95f);
|
||
UI.outline(ix,iy,iw,ih,.6f,.6f,.6f,2);
|
||
drawText("INVENTORY",ix+10,iy+10,2,1,1,1);
|
||
for(int bi=1;bi<NUM_BLOCK_TYPES;bi++){
|
||
int row=(bi-1)/COLS,col=(bi-1)%COLS;
|
||
float sx=ix+16+col*(S+G),sy=iy+40+row*(S+G);
|
||
bool inH=false; for(int h=0;h<HOTBAR_SIZE;h++) if(inv.hotbar[h]==bi) inH=true;
|
||
auto& bc=BLOCK_COLORS[bi];
|
||
UI.rect(sx,sy,S,S,.25f,.25f,.25f,.9f);
|
||
UI.rect(sx+B,sy+B,S-2*B,S-2*B-8,bc.r,bc.g,bc.b);
|
||
UI.rect(sx+B,sy+S-B-12,S-2*B,4,bc.r*.65f,bc.g*.65f,bc.b*.65f);
|
||
UI.outline(sx,sy,S,S,inH?1.f:.4f,inH?1.f:.4f,inH?0.f:.4f,2);
|
||
float ntw=strlen(BLOCK_NAMES[bi])*9.f;
|
||
drawText(BLOCK_NAMES[bi],sx+(S-ntw)*.5f,sy+S-10,1.5f,.9f,.9f,.9f);
|
||
}
|
||
drawText("Click slot (1-8). E=close.",ix+10,iy+ih-18,1.5f,.7f,.7f,.7f);
|
||
}
|
||
|
||
// ─── HUD ─────────────────────────────────────────────────────────────────────
|
||
static void drawHUD(const glm::vec3& pos, bool thirdPerson,
|
||
int bgPending, int fps, int visChunks){
|
||
// Crosshair
|
||
float cx=SCREEN_W*.5f,cy=SCREEN_H*.5f;
|
||
UI.rect(cx-1,cy-10,2,20,1,1,1,.85f);
|
||
UI.rect(cx-10,cy-1,20,2,1,1,1,.85f);
|
||
UI.rect(cx-1,cy-1,2,2,0,0,0,.85f);
|
||
|
||
// Stats panel (top-left)
|
||
char buf[128];
|
||
snprintf(buf,128,"XYZ: %.0f / %.0f / %.0f",pos.x,pos.y-PH,pos.z);
|
||
float pw=strlen(buf)*6*1.5f+10;
|
||
UI.rect(5,5,pw,60,0,0,0,.55f);
|
||
drawText(buf,9,9,1.5f,1,1,1);
|
||
|
||
char fbuf[32]; snprintf(fbuf,32,"FPS: %d",fps);
|
||
drawText(fbuf,9,22,1.5f, fps>=55?0.3f:fps>=30?1.f:1.f,
|
||
fps>=55?1.0f:fps>=30?0.8f:0.3f,
|
||
fps>=55?0.3f:fps>=30?0.1f:0.1f);
|
||
|
||
char cbuf[48]; snprintf(cbuf,48,"Chunks: %d vis / %d load",visChunks,bgPending);
|
||
drawText(cbuf,9,35,1.5f,.7f,.7f,.7f);
|
||
|
||
if(thirdPerson) drawText("[F5] 3rd person",9,49,1.5f,.8f,.8f,.5f);
|
||
|
||
// Loading badge (top-right)
|
||
if(bgPending>0){
|
||
char lb[40]; snprintf(lb,40,"Loading %d...",bgPending);
|
||
float lw=strlen(lb)*9+8;
|
||
UI.rect(SCREEN_W-lw-4,5,lw,16,0,0,0,.6f);
|
||
drawText(lb,SCREEN_W-lw,8,1.5f,.4f,1,.4f);
|
||
}
|
||
}
|
||
|
||
// ─── Camera ──────────────────────────────────────────────────────────────────
|
||
struct Camera {
|
||
glm::vec3 pos{0,40,0}; float yaw=0,pitch=0;
|
||
glm::vec3 vel{0,0,0}; bool onGround=false;
|
||
glm::vec3 forward()const{
|
||
float y=glm::radians(yaw),p=glm::radians(pitch);
|
||
return glm::normalize(glm::vec3(cosf(p)*cosf(y),sinf(p),cosf(p)*sinf(y)));
|
||
}
|
||
glm::vec3 right()const{return glm::normalize(glm::cross(forward(),{0,1,0}));}
|
||
glm::mat4 firstPersonView()const{return glm::lookAt(pos,pos+forward(),{0,1,0});}
|
||
glm::mat4 thirdPersonView()const{
|
||
return glm::lookAt(pos-forward()*6.f+glm::vec3(0,2,0),pos,{0,1,0});
|
||
}
|
||
};
|
||
static bool solidAt(float fx,float fy,float fz){
|
||
if((int)floorf(fy)<0) return true;
|
||
if((int)floorf(fy)>=WORLD_H) return false;
|
||
uint8_t b=getBlock((int)floorf(fx),(int)floorf(fy),(int)floorf(fz));
|
||
return b!=AIR&&b!=WATER&&b!=LEAVES;
|
||
}
|
||
static bool aabbSolid(float px,float py,float pz){
|
||
int x0=(int)floorf(px-HW),x1=(int)floorf(px+HW);
|
||
int y0=(int)floorf(py-PH),y1=(int)floorf(py+0.05f);
|
||
int z0=(int)floorf(pz-HW),z1=(int)floorf(pz+HW);
|
||
for(int x=x0;x<=x1;x++) for(int y=y0;y<=y1;y++) for(int z=z0;z<=z1;z++)
|
||
if(solidAt((float)x,(float)y,(float)z)) return true;
|
||
return false;
|
||
}
|
||
static void moveCamera(Camera& cam,const glm::vec3& move,float dt){
|
||
cam.pos.x+=move.x*dt; if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)) cam.pos.x-=move.x*dt;
|
||
cam.pos.z+=move.z*dt; if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)) cam.pos.z-=move.z*dt;
|
||
cam.vel.y+=GRAVITY*dt; cam.pos.y+=cam.vel.y*dt;
|
||
if(cam.vel.y<0){
|
||
if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){
|
||
cam.pos.y=floorf(cam.pos.y-PH)+1.f+PH+.001f; cam.vel.y=0; cam.onGround=true;
|
||
} else cam.onGround=false;
|
||
} else {
|
||
if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){
|
||
cam.pos.y=floorf(cam.pos.y+.05f)-.05f-.001f; cam.vel.y=0;
|
||
}
|
||
cam.onGround=false;
|
||
}
|
||
}
|
||
|
||
// ─── Player model ─────────────────────────────────────────────────────────────
|
||
struct PlayerModel {
|
||
GLuint VAO=0,VBO=0,EBO=0;
|
||
std::vector<Vertex> verts; std::vector<GLuint> idx;
|
||
int indexCount=0;
|
||
void addBox(float x0,float y0,float z0,float x1,float y1,float z1,float r,float g,float b){
|
||
auto face=[&](glm::vec3 a,glm::vec3 b_,glm::vec3 c,glm::vec3 d,float s){
|
||
GLuint base=(GLuint)verts.size();
|
||
verts.push_back({a.x,a.y,a.z,r*s,g*s,b*s}); verts.push_back({b_.x,b_.y,b_.z,r*s,g*s,b*s});
|
||
verts.push_back({c.x,c.y,c.z,r*s,g*s,b*s}); verts.push_back({d.x,d.y,d.z,r*s,g*s,b*s});
|
||
idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3});
|
||
};
|
||
face({x0,y1,z0},{x1,y1,z0},{x1,y1,z1},{x0,y1,z1},1.f);
|
||
face({x0,y0,z1},{x1,y0,z1},{x1,y0,z0},{x0,y0,z0},.5f);
|
||
face({x0,y0,z1},{x1,y0,z1},{x1,y1,z1},{x0,y1,z1},.8f);
|
||
face({x1,y0,z0},{x0,y0,z0},{x0,y1,z0},{x1,y1,z0},.7f);
|
||
face({x0,y0,z0},{x0,y0,z1},{x0,y1,z1},{x0,y1,z0},.65f);
|
||
face({x1,y0,z1},{x1,y0,z0},{x1,y1,z0},{x1,y1,z1},.65f);
|
||
}
|
||
void build(){
|
||
verts.clear(); idx.clear();
|
||
addBox(-.3f,1.55f,-.3f,.3f,2.15f,.3f,.9f,.72f,.55f);
|
||
addBox(-.32f,1.9f,-.32f,.32f,2.17f,.32f,.3f,.18f,.08f);
|
||
addBox(-.3f,.7f,-.2f,.3f,1.5f,.2f,.25f,.45f,.75f);
|
||
addBox(-.55f,.7f,-.15f,-.32f,1.45f,.15f,.9f,.72f,.55f);
|
||
addBox(.32f,.7f,-.15f,.55f,1.45f,.15f,.9f,.72f,.55f);
|
||
addBox(-.28f,0,-.15f,-.05f,.7f,.15f,.2f,.2f,.6f);
|
||
addBox(.05f,0,-.15f,.28f,.7f,.15f,.2f,.2f,.6f);
|
||
indexCount=(int)idx.size();
|
||
if(!VAO){glGenVertexArrays(1,&VAO);glGenBuffers(1,&VBO);glGenBuffers(1,&EBO);}
|
||
glBindVertexArray(VAO);
|
||
glBindBuffer(GL_ARRAY_BUFFER,VBO);
|
||
glBufferData(GL_ARRAY_BUFFER,(GLsizeiptr)(verts.size()*sizeof(Vertex)),verts.data(),GL_STATIC_DRAW);
|
||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
|
||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,(GLsizeiptr)(idx.size()*sizeof(GLuint)),idx.data(),GL_STATIC_DRAW);
|
||
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0); glEnableVertexAttribArray(0);
|
||
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1);
|
||
glBindVertexArray(0);
|
||
}
|
||
void draw(GLuint prog,GLint mvpLoc,GLint camLoc,
|
||
const glm::mat4& proj,const glm::mat4& view,const Camera& cam)const{
|
||
glm::vec3 feet=cam.pos-glm::vec3(0,PH,0);
|
||
glm::mat4 model=glm::rotate(glm::translate(glm::mat4(1),feet),
|
||
glm::radians(cam.yaw+180.f),glm::vec3(0,1,0));
|
||
glm::mat4 mvp=proj*view*model;
|
||
glUseProgram(prog);
|
||
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
|
||
glUniform3fv(camLoc,1,glm::value_ptr(cam.pos));
|
||
glBindVertexArray(VAO);
|
||
glDrawElements(GL_TRIANGLES,(GLsizei)indexCount,GL_UNSIGNED_INT,0);
|
||
glBindVertexArray(0);
|
||
}
|
||
} PLAYER_MODEL;
|
||
|
||
// ─── Raycast ─────────────────────────────────────────────────────────────────
|
||
static bool raycast(const Camera& cam,glm::ivec3& hit,glm::ivec3& prev){
|
||
glm::vec3 dir=cam.forward(),p=cam.pos;
|
||
glm::ivec3 last{(int)floorf(p.x),(int)floorf(p.y),(int)floorf(p.z)};
|
||
for(float t=0;t<8.f;t+=0.05f){
|
||
glm::vec3 rp=p+dir*t;
|
||
glm::ivec3 bp{(int)floorf(rp.x),(int)floorf(rp.y),(int)floorf(rp.z)};
|
||
if(bp.y<0||bp.y>=WORLD_H){last=bp;continue;}
|
||
uint8_t b=getBlock(bp.x,bp.y,bp.z);
|
||
if(b!=AIR&&b!=WATER){hit=bp;prev=last;return true;}
|
||
last=bp;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ─── Chunk range management ───────────────────────────────────────────────────
|
||
static void requestChunks(int pcx,int pcz){
|
||
struct E{int dx,dz,d2;};
|
||
std::vector<E> needed;
|
||
for(int dx=-RENDER_DIST;dx<=RENDER_DIST;dx++)
|
||
for(int dz=-RENDER_DIST;dz<=RENDER_DIST;dz++){
|
||
ChunkKey k{pcx+dx,pcz+dz};
|
||
if(CHUNKS.count(k)) continue;
|
||
needed.push_back({dx,dz,dx*dx+dz*dz});
|
||
}
|
||
std::sort(needed.begin(),needed.end(),[](auto& a,auto& b){return a.d2<b.d2;});
|
||
for(auto& e:needed) submitChunkGen(pcx+e.dx,pcz+e.dz);
|
||
}
|
||
|
||
static void evictChunks(int pcx,int pcz){
|
||
std::vector<ChunkKey> rem;
|
||
for(auto& [k,c]:CHUNKS)
|
||
if(abs(k.x-pcx)>RENDER_DIST+2||abs(k.z-pcz)>RENDER_DIST+2) rem.push_back(k);
|
||
for(auto& k:rem){ CHUNKS.erase(k); DIRTY_SET.erase(k); }
|
||
}
|
||
|
||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||
int main(){
|
||
SDL_Init(SDL_INIT_VIDEO);
|
||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
|
||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,3);
|
||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,SDL_GL_CONTEXT_PROFILE_CORE);
|
||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
|
||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,24);
|
||
|
||
SDL_Window* win=SDL_CreateWindow("MiniCraft",
|
||
SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,SCREEN_W,SCREEN_H,
|
||
SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN);
|
||
SDL_GLContext ctx=SDL_GL_CreateContext(win);
|
||
SDL_GL_SetSwapInterval(1);
|
||
glewExperimental=GL_TRUE; glewInit();
|
||
|
||
glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK);
|
||
|
||
GLuint prog3d=buildProg(VS_SRC,FS_SRC);
|
||
GLint mvpLoc=glGetUniformLocation(prog3d,"uMVP");
|
||
GLint camLoc=glGetUniformLocation(prog3d,"uCamPos");
|
||
|
||
UI.init();
|
||
initNoise(12345);
|
||
|
||
std::cout<<"Using "<<GEN_THREADS<<" generation threads, RENDER_DIST="<<RENDER_DIST<<"\n";
|
||
POOL.start(GEN_THREADS);
|
||
|
||
// Pre-generate 5×5 spawn area synchronously so player has solid ground
|
||
for(int dx=-2;dx<=2;dx++)
|
||
for(int dz=-2;dz<=2;dz++){
|
||
auto c=std::make_unique<Chunk>(dx,dz);
|
||
generateChunkData(*c);
|
||
CHUNKS[{dx,dz}]=std::move(c);
|
||
}
|
||
// Build their meshes (all neighbours present so culling is correct)
|
||
for(auto& [k,c]:CHUNKS) buildChunkMesh(*c);
|
||
|
||
// Submit the rest async
|
||
requestChunks(0,0);
|
||
|
||
int totalChunks=(2*RENDER_DIST+1)*(2*RENDER_DIST+1);
|
||
int minToPlay=9; // 3×3 inner zone
|
||
|
||
Camera cam;
|
||
for(int y=WORLD_H-1;y>=0;y--){
|
||
if(getBlock(0,y,0)!=AIR&&getBlock(0,y,0)!=WATER){
|
||
cam.pos.y=y+1+PH+0.1f; break;
|
||
}
|
||
}
|
||
cam.pos.x=0.5f; cam.pos.z=0.5f;
|
||
PLAYER_MODEL.build();
|
||
|
||
glm::mat4 proj=glm::perspective(glm::radians(70.f),(float)SCREEN_W/SCREEN_H,0.05f,1000.f);
|
||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||
|
||
Inventory inv;
|
||
bool thirdPerson=false, running=true, loading=true;
|
||
Uint64 last=SDL_GetPerformanceCounter();
|
||
int lastPcx=INT_MAX, lastPcz=INT_MAX;
|
||
|
||
// FPS tracking
|
||
int fpsCounter=0;
|
||
float fpsTimer=0.f;
|
||
int fpsDisplay=0;
|
||
|
||
std::cout<<"WASD=move Space=jump LMB=break RMB=place\n"
|
||
"1-8=hotbar E=inv F5=3rd-person ESC=quit\n";
|
||
|
||
while(running){
|
||
// ── Delta time ──
|
||
Uint64 now=SDL_GetPerformanceCounter();
|
||
float dt=(float)(now-last)/SDL_GetPerformanceFrequency();
|
||
if(dt>0.1f) dt=0.1f;
|
||
last=now;
|
||
|
||
// ── FPS ──
|
||
fpsCounter++;
|
||
fpsTimer+=dt;
|
||
if(fpsTimer>=1.0f){ fpsDisplay=fpsCounter; fpsCounter=0; fpsTimer-=1.0f; }
|
||
|
||
// ── Drain finished chunks ──
|
||
drainFinishedChunks();
|
||
int readyChunks=(int)CHUNKS.size();
|
||
if(loading&&readyChunks>=minToPlay) loading=false;
|
||
|
||
// ── Events ──
|
||
SDL_Event e;
|
||
while(SDL_PollEvent(&e)){
|
||
if(e.type==SDL_QUIT) running=false;
|
||
if(e.type==SDL_KEYDOWN&&e.key.keysym.sym==SDLK_ESCAPE) running=false;
|
||
if(loading) continue;
|
||
|
||
if(e.type==SDL_KEYDOWN){
|
||
switch(e.key.keysym.sym){
|
||
case SDLK_e: inv.open=!inv.open;
|
||
SDL_SetRelativeMouseMode(inv.open?SDL_FALSE:SDL_TRUE); break;
|
||
case SDLK_F5: thirdPerson=!thirdPerson; break;
|
||
case SDLK_1: inv.selected=0; break; case SDLK_2: inv.selected=1; break;
|
||
case SDLK_3: inv.selected=2; break; case SDLK_4: inv.selected=3; break;
|
||
case SDLK_5: inv.selected=4; break; case SDLK_6: inv.selected=5; break;
|
||
case SDLK_7: inv.selected=6; break; case SDLK_8: inv.selected=7; break;
|
||
}
|
||
}
|
||
if(e.type==SDL_MOUSEWHEEL)
|
||
inv.selected=(inv.selected-e.wheel.y+HOTBAR_SIZE)%HOTBAR_SIZE;
|
||
if(!inv.open){
|
||
if(e.type==SDL_MOUSEMOTION){
|
||
cam.yaw+=e.motion.xrel*MOUSE_SENS;
|
||
cam.pitch=std::max(-89.f,std::min(89.f,cam.pitch-e.motion.yrel*MOUSE_SENS));
|
||
}
|
||
if(e.type==SDL_MOUSEBUTTONDOWN){
|
||
glm::ivec3 hit,prev;
|
||
if(raycast(cam,hit,prev)){
|
||
if(e.button.button==SDL_BUTTON_LEFT) setBlock(hit.x,hit.y,hit.z,AIR);
|
||
else if(e.button.button==SDL_BUTTON_RIGHT)
|
||
setBlock(prev.x,prev.y,prev.z,inv.selectedBlock());
|
||
}
|
||
}
|
||
} else if(e.type==SDL_MOUSEBUTTONDOWN&&e.button.button==SDL_BUTTON_LEFT){
|
||
const int COLS=4; const float SL=52,GL=6;
|
||
float iw=COLS*(SL+GL)-GL+32,ih=((NUM_BLOCK_TYPES-1+COLS-1)/COLS)*(SL+GL)-GL+60;
|
||
float ix=(SCREEN_W-iw)*.5f,iy=(SCREEN_H-ih)*.5f;
|
||
for(int bi=1;bi<NUM_BLOCK_TYPES;bi++){
|
||
int row=(bi-1)/COLS,col=(bi-1)%COLS;
|
||
float sx=ix+16+col*(SL+GL),sy=iy+40+row*(SL+GL);
|
||
if(e.button.x>=sx&&e.button.x<=sx+SL&&e.button.y>=sy&&e.button.y<=sy+SL)
|
||
inv.hotbar[inv.selected]=(uint8_t)bi;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Loading screen ──
|
||
if(loading){
|
||
drawLoadingScreen(win,readyChunks,totalChunks);
|
||
SDL_Delay(16);
|
||
continue;
|
||
}
|
||
|
||
// ── Player movement ──
|
||
if(!inv.open){
|
||
const Uint8* keys=SDL_GetKeyboardState(nullptr);
|
||
glm::vec3 fwd=cam.forward(); fwd.y=0; if(glm::length(fwd)>0) fwd=glm::normalize(fwd);
|
||
glm::vec3 rgt=cam.right(); rgt.y=0; if(glm::length(rgt)>0) rgt=glm::normalize(rgt);
|
||
glm::vec3 mv{0,0,0};
|
||
if(keys[SDL_SCANCODE_W]) mv+=fwd*MOVE_SPEED;
|
||
if(keys[SDL_SCANCODE_S]) mv-=fwd*MOVE_SPEED;
|
||
if(keys[SDL_SCANCODE_D]) mv+=rgt*MOVE_SPEED;
|
||
if(keys[SDL_SCANCODE_A]) mv-=rgt*MOVE_SPEED;
|
||
if(keys[SDL_SCANCODE_SPACE]&&cam.onGround) cam.vel.y=JUMP_VEL;
|
||
moveCamera(cam,mv,dt);
|
||
}
|
||
|
||
// ── Chunk streaming ──
|
||
int pcx=(int)floorf(cam.pos.x/CHUNK_SIZE);
|
||
int pcz=(int)floorf(cam.pos.z/CHUNK_SIZE);
|
||
if(pcx!=lastPcx||pcz!=lastPcz){
|
||
requestChunks(pcx,pcz);
|
||
evictChunks(pcx,pcz);
|
||
lastPcx=pcx; lastPcz=pcz;
|
||
}
|
||
|
||
// ── Build dirty meshes ──
|
||
processDirtyMeshes(MESH_BUILDS_PER_FRAME, pcx, pcz);
|
||
|
||
// ── 3D render ──
|
||
glClearColor(0.53f,0.81f,0.98f,1.0f);
|
||
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||
glm::mat4 view=thirdPerson?cam.thirdPersonView():cam.firstPersonView();
|
||
glm::mat4 mvp=proj*view;
|
||
glUseProgram(prog3d);
|
||
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
|
||
glUniform3fv(camLoc,1,glm::value_ptr(cam.pos));
|
||
|
||
int visChunks=0;
|
||
for(auto& [k,c]:CHUNKS)
|
||
if(c->meshReady&&c->indexCount>0){
|
||
glBindVertexArray(c->VAO);
|
||
glDrawElements(GL_TRIANGLES,c->indexCount,GL_UNSIGNED_INT,0);
|
||
++visChunks;
|
||
}
|
||
|
||
if(thirdPerson) PLAYER_MODEL.draw(prog3d,mvpLoc,camLoc,proj,view,cam);
|
||
|
||
// ── 2D UI ──
|
||
UI.begin();
|
||
drawHUD(cam.pos,thirdPerson,POOL.pending.load(),fpsDisplay,visChunks);
|
||
drawInventory(inv);
|
||
UI.end();
|
||
|
||
SDL_GL_SwapWindow(win);
|
||
}
|
||
|
||
POOL.shutdown();
|
||
CHUNKS.clear();
|
||
glDeleteProgram(prog3d);
|
||
glDeleteProgram(UI.prog);
|
||
glDeleteVertexArrays(1,&UI.VAO); glDeleteBuffers(1,&UI.VBO);
|
||
glDeleteVertexArrays(1,&PLAYER_MODEL.VAO);
|
||
glDeleteBuffers(1,&PLAYER_MODEL.VBO); glDeleteBuffers(1,&PLAYER_MODEL.EBO);
|
||
SDL_GL_DeleteContext(ctx);
|
||
SDL_DestroyWindow(win);
|
||
SDL_Quit();
|
||
return 0;
|
||
} |