Initial public commit.

This commit is contained in:
The Dust Council 2026-06-12 12:07:06 -07:00
parent 5defe65535
commit ad48ebabe5
3 changed files with 524 additions and 32 deletions

521
main.c
View file

@ -125,54 +125,107 @@ static Settings S = {
#define ARM_LIMIT 12
/* ------------------------------------------------------------------ */
/* blast craters: bombs flatten the terrain (and clear cacti) inside a */
/* radius. Stored here so terrainHeight() can carve them out. */
/* ------------------------------------------------------------------ */
typedef struct { float x, z, r, bowlR, depth; } Crater;
static Crater *g_craters = NULL; /* append-only: every bomb keeps a crater */
static int g_nCraters = 0;
static int g_craterCap = 0;
static void addCrater(float x, float z, float size){
if(g_nCraters == g_craterCap){
g_craterCap = g_craterCap ? g_craterCap*2 : 32;
g_craters = (Crater*)realloc(g_craters, (size_t)g_craterCap*sizeof(Crater));
}
Crater *c = &g_craters[g_nCraters++];
c->x = x; c->z = z;
c->r = 0.5f * size; /* blast / clear radius */
c->bowlR = 0.16f * size; /* crater radius */
c->depth = clampf(size*0.12f, 20.0f, 130.0f); /* deep, well-defined */
}
static bool inCrater(float x, float z){
for(int i=0;i<g_nCraters;i++){
float dx=x-g_craters[i].x, dz=z-g_craters[i].z;
if(dx*dx + dz*dz < g_craters[i].r*g_craters[i].r) return true;
}
return false;
}
/* ------------------------------------------------------------------ */
/* terrain */
/* ------------------------------------------------------------------ */
static float terrainHeight(float x, float z){
/* The default state of the world is a near-flat desert plain with only
* very gentle undulation. */
float plains = (fbm(x*0.02f, z*0.02f, 3) - 0.5f) * 2.0f;
/* Mountain ranges are coherent low-frequency blobs at a *fixed* spatial
* scale (so a range stays a sensibly sized range). The user's
* mountainFreq controls how much of the plain those ranges cover, i.e.
* how frequently the plain is interrupted: higher freq -> lower
* threshold -> ranges appear more often. */
float mask = valnoise(x*0.006f, z*0.006f);
float thr = clampf(0.80f - 0.11f*S.mountainFreq, 0.35f, 0.86f);
float mountainMask = smoothstepf(thr, thr+0.10f, mask);
if(mountainMask <= 0.0f) return plains;
/* Base frequency scaled by range height keeps slopes proportional for
* short and tall ranges alike (no-op at the default max height). */
/* The shape of a range/basin in 0..1: a ridged multifractal that roughness
* morphs from a flat-topped mesa (0) to jagged crests (1). Used for both the
* mountains (added) and the valleys (subtracted). */
static float rangeRidge(float x, float z){
float bf = 0.05f * (26.0f / fmaxf(S.mountainMaxH, 1.0f));
/* Accumulate a ridged multifractal. The first octave is the broad mass;
* the remaining octaves add higher-frequency jagged detail riding on the
* existing crests. */
float freq=bf, amp=0.5f, sum=0.0f, norm=0.0f, weight=1.0f, mass=0.0f;
for(int i=0;i<6;i++){
float n = 1.0f - fabsf(2.0f*valnoise(x*freq, z*freq) - 1.0f);
n *= n;
if(i==0) mass = n; /* broad base shape */
n *= weight; /* detail only rides on existing ridge*/
if(i==0) mass = n;
n *= weight;
weight = clampf(n*2.5f, 0.0f, 1.0f);
sum += n*amp;
norm += amp;
freq *= 2.0f; amp *= 0.5f;
}
float jagged = clampf(sum / norm, 0.0f, 1.0f);
float mesa = clampf((mass - 0.25f) * 3.2f, 0.0f, 1.0f);
return lerpf(mesa, jagged, S.mountainRough);
}
/* A mesa: saturate the broad mass so its top clamps to a flat plateau
* with steeper sides. */
float mesa = clampf((mass - 0.25f) * 3.2f, 0.0f, 1.0f);
static float terrainHeight(float x, float z){
/* The default state of the world is a near-flat desert plain with only
* very gentle undulation. */
float plains = (fbm(x*0.02f, z*0.02f, 3) - 0.5f) * 2.0f;
float h = plains;
/* mountainRough morphs flat-topped mesas (0) into jagged peaks (1) */
float ridge = lerpf(mesa, jagged, S.mountainRough);
/* Coherent low-frequency blobs decide where ranges/basins occur; the
* user's mountainFreq sets how much of the plain they cover. */
float thr = clampf(0.80f - 0.11f*S.mountainFreq, 0.35f, 0.86f);
/* ridge (0..1) maps the range between the user's min and max heights */
float body = lerpf(S.mountainMinH, S.mountainMaxH, ridge);
return plains + mountainMask * body;
/* mountain ranges rise above the plain */
float mMask = smoothstepf(thr, thr+0.10f, valnoise(x*0.006f, z*0.006f));
if(mMask > 0.0f)
h += mMask * lerpf(S.mountainMinH, S.mountainMaxH, rangeRidge(x, z));
/* valleys / basins carve below the plain (a differently-phased mask so
* they fall in different places than the mountains). Roughness shapes
* them too: flat-bottomed basins -> rugged canyons. */
float vMask = smoothstepf(thr, thr+0.10f, valnoise(x*0.006f+19.3f, z*0.006f-23.1f));
if(vMask > 0.0f)
h -= vMask * lerpf(S.mountainMinH, S.mountainMaxH, rangeRidge(x+57.0f, z-91.0f)) * 0.85f;
/* blast craters. Two separable effects so that overlapping blasts
* accumulate rather than wipe each other out:
* - flatten: erase the procedural mountains/valleys back to the plain
* across the blast radius (combined as a max, applied once);
* - carve: a deep bowl + raised rim, summed over every crater so that
* repeated blasts in one place deepen and reshape the crater. */
float flat = 0.0f, carve = 0.0f;
for(int i=0;i<g_nCraters;i++){
Crater *c = &g_craters[i];
float dx=x-c->x, dz=z-c->z;
float d2 = dx*dx + dz*dz;
if(d2 >= c->r*c->r) continue;
float d = sqrtf(d2);
float f = smoothstepf(c->r, c->r*0.65f, d); /* 1 inside, 0 at rim */
if(f > flat) flat = f;
/* bowl: flat floor inside, steep wall up to the lip */
float wall = smoothstepf(c->bowlR, c->bowlR*0.55f, d);
carve -= c->depth * wall;
/* raised rim: a bump straddling the lip, rising above the surface */
float rim = (d - c->bowlR)/(c->bowlR*0.25f);
carve += c->depth*0.35f*expf(-rim*rim);
}
if(flat > 0.0f) h = lerpf(h, plains, flat);
h += carve;
return h;
}
/* ------------------------------------------------------------------ */
@ -223,6 +276,7 @@ static void lookAtGL(V3 eye, V3 center, V3 up){
/* ------------------------------------------------------------------ */
static V3 camPos;
static float g_viewRadius = 128.0f; /* rendering distance (runtime) */
static bool g_showAlt = false; /* altitude indicator toggle */
#define FOG_NEAR 18.0f
/* fog reaches full opacity a little inside the draw radius so the mesh
@ -232,6 +286,57 @@ static float fogFarDist(void){
return f < FOG_NEAR + 5.0f ? FOG_NEAR + 5.0f : f;
}
/* ------------------------------------------------------------------ */
/* settings persistence: every user setting is saved on exit and */
/* reloaded at startup, so the program resumes with the last values. */
/* ------------------------------------------------------------------ */
static const char *configPath(void){
static char path[512];
const char *home = getenv("HOME");
if(home && *home) snprintf(path, sizeof path, "%s/.vectordesert.cfg", home);
else snprintf(path, sizeof path, "vectordesert.cfg");
return path;
}
static void saveSettings(void){
FILE *f = fopen(configPath(), "w");
if(!f) return;
fprintf(f, "terrainHue %.5f\n", S.terrainHue);
fprintf(f, "mountainFreq %.5f\n", S.mountainFreq);
fprintf(f, "mountainRough %.5f\n", S.mountainRough);
fprintf(f, "mountainMinH %.5f\n", S.mountainMinH);
fprintf(f, "mountainMaxH %.5f\n", S.mountainMaxH);
fprintf(f, "cactusFreq %.5f\n", S.cactusFreq);
fprintf(f, "cactusSizeVar %.5f\n", S.cactusSizeVar);
fprintf(f, "cactusMinSize %.5f\n", S.cactusMinSize);
fprintf(f, "cactusMaxSize %.5f\n", S.cactusMaxSize);
fprintf(f, "cactusHue %.5f\n", S.cactusHue);
fprintf(f, "maxArms %d\n", S.maxArms);
fprintf(f, "viewRadius %.3f\n", g_viewRadius);
fclose(f);
}
static void loadSettings(void){
FILE *f = fopen(configPath(), "r");
if(!f) return;
char key[64]; float v;
while(fscanf(f, "%63s %f", key, &v) == 2){
if (!strcmp(key,"terrainHue")) S.terrainHue = v;
else if (!strcmp(key,"mountainFreq")) S.mountainFreq = v;
else if (!strcmp(key,"mountainRough")) S.mountainRough = v;
else if (!strcmp(key,"mountainMinH")) S.mountainMinH = v;
else if (!strcmp(key,"mountainMaxH")) S.mountainMaxH = v;
else if (!strcmp(key,"cactusFreq")) S.cactusFreq = v;
else if (!strcmp(key,"cactusSizeVar")) S.cactusSizeVar = v;
else if (!strcmp(key,"cactusMinSize")) S.cactusMinSize = v;
else if (!strcmp(key,"cactusMaxSize")) S.cactusMaxSize = v;
else if (!strcmp(key,"cactusHue")) S.cactusHue = v;
else if (!strcmp(key,"maxArms")) S.maxArms = (int)v;
else if (!strcmp(key,"viewRadius")) g_viewRadius = v;
}
fclose(f);
}
/* ------------------------------------------------------------------ */
/* line batch: collect every wireframe segment into one vertex+colour */
/* array so a whole frame's geometry ships in a single glDrawArrays. */
@ -399,12 +504,66 @@ static void drawHUD(int fbw, int fbh, float alpha){
LINE("9/0 CACTUS HUE %.2f", S.cactusHue);
LINE("-/= MAX ARMS %d", S.maxArms);
LINE("[/] RENDER DIST %.0f", g_viewRadius);
LINE("X FULL STOP");
LINE("G FLOOR ALT %s", g_showAlt ? "ON" : "OFF");
#undef LINE
/* camera + window controls footer */
y += lineH*0.5f;
glColor4f(0.45f, 0.70f, 0.85f, alpha);
drawText("WASD MOVE ARROWS PAN PGUP/DN ALT F FULL", x, y, sc*0.85f);
drawText("WASD MOVE ARROWS PAN PGUP/DN ALT SPACE BOMB F FULL", x, y, sc*0.85f);
glEnable(GL_FOG);
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
}
/* on-screen altitude indicator: the terrain floor directly beneath the
* camera, the camera's own altitude, and the clearance between them, plus a
* vertical clearance gauge. Drawn top-right when toggled on. */
static void drawAltIndicator(int fbw, int fbh){
float floor = terrainHeight(camPos.x, camPos.z);
float camAlt = camPos.y;
float clear = camAlt - floor;
float sc = fbh/240.0f; if(sc < 2.0f) sc = 2.0f;
float lineH = 9.0f*sc, pad = 5.0f*sc;
float gaugeW = 6.0f*sc, gaugeH = 56.0f*sc;
float gx1 = fbw - pad, gx0 = gx1 - gaugeW;
float textW = 12.0f*5.0f*sc;
float tx = gx0 - 6.0f*sc - textW;
float ty = pad;
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
glOrtho(0.0, fbw, fbh, 0.0, -1.0, 1.0);
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity();
glDisable(GL_DEPTH_TEST);
glDisable(GL_FOG);
char buf[48];
glColor4f(0.95f, 0.85f, 0.35f, 1.0f);
drawText("ALTITUDE", tx, ty, sc); ty += lineH*1.4f;
glColor4f(0.55f, 1.0f, 0.75f, 1.0f);
snprintf(buf,sizeof buf,"FLOOR %.1f", floor); drawText(buf,tx,ty,sc); ty+=lineH;
snprintf(buf,sizeof buf,"CAM %.1f", camAlt); drawText(buf,tx,ty,sc); ty+=lineH;
snprintf(buf,sizeof buf,"CLEAR %.1f", clear); drawText(buf,tx,ty,sc); ty+=lineH;
/* vertical clearance gauge (0..120) */
float gTop = pad, gBot = pad + gaugeH;
glColor4f(0.45f, 0.70f, 0.85f, 1.0f);
glBegin(GL_LINE_LOOP);
glVertex2f(gx0, gTop); glVertex2f(gx1, gTop);
glVertex2f(gx1, gBot); glVertex2f(gx0, gBot);
glEnd();
float cl = clampf(clear/120.0f, 0.0f, 1.0f);
float fillY = gBot - cl*(gBot-gTop);
glColor4f(0.55f, 1.0f, 0.75f, 1.0f);
glBegin(GL_LINES);
for(float yy=gBot-1.5f*sc; yy>fillY; yy-=2.5f*sc){
glVertex2f(gx0+1.0f, yy); glVertex2f(gx1-1.0f, yy);
}
glEnd();
glEnable(GL_FOG);
glEnable(GL_DEPTH_TEST);
@ -693,6 +852,7 @@ static void buildCacti(float cx, float cz, float margin){
float dx = wx-cx, dz = wz-cz;
float d2 = dx*dx + dz*dz;
if(d2 > reach2) continue;
if(inCrater(wx, wz)) continue; /* obliterated by a blast */
buildCactus(&gCacti, wx, wz, sqrtf(d2));
}
}
@ -751,6 +911,294 @@ static float camPitch = -0.18f; /* slightly downward */
static V3 camVel; /* persistent world-space velocity (W/S) */
static double g_lastInput = 0.0; /* time of last keypress (for HUD fade) */
/* ================================================================== */
/* bombs + animated vector mushroom clouds */
/* ================================================================== */
#define MAX_BOMBS 48
#define BOMB_GRAV 120.0f /* fall acceleration */
#define EXPLO_DUR 12.0f /* explosion lifetime (seconds) */
typedef struct {
bool active;
bool exploding; /* false = falling, true = mushroom */
float gx, gz; /* ground impact x,z */
float y; /* current altitude while falling */
float vy; /* fall speed */
float groundY; /* terrain height at impact */
float t; /* time since detonation */
float size; /* cloud size (20 * max mountain height)*/
V3 col; /* cloud hue */
} Bomb;
static Bomb bombs[MAX_BOMBS];
static uint32_t g_rng = 0x2545F491u;
static float frand(void){
g_rng = g_rng*1664525u + 1013904223u;
return ((g_rng >> 8) & 0xffffff) / (float)0xffffff;
}
static V3 vlerp(V3 a, V3 b, float t){
return v3(lerpf(a.x,b.x,t), lerpf(a.y,b.y,t), lerpf(a.z,b.z,t));
}
/* a wobbly horizontal ring -- the basic cloud-billow primitive */
static void drawWobRing(float cx,float cy,float cz,float r,int nA,
float wob,int lobes,float phase,V3 col,float a){
if(r <= 0.0f || a <= 0.0f) return;
glColor4f(col.x,col.y,col.z,a);
glBegin(GL_LINE_LOOP);
for(int k=0;k<nA;k++){
float ang = 2.0f*(float)M_PI*k/nA;
float rr = r*(1.0f + wob*sinf(lobes*ang + phase));
glVertex3f(cx+cosf(ang)*rr, cy, cz+sinf(ang)*rr);
}
glEnd();
}
/* a wireframe surface of revolution (stem / cap): stacked rings plus
* longitudinal strands, with an animated billow wobble */
static void drawLathe(float cx,float cz,const float*ys,const float*rs,
const V3*cols,int nP,float wob,int lobes,
float phase,float alpha){
const int nA=48, stride=4; /* 12 longitudinal strands */
for(int i=0;i<nP;i++){
glColor4f(cols[i].x,cols[i].y,cols[i].z,alpha);
glBegin(GL_LINE_LOOP);
for(int k=0;k<nA;k++){
float ang = 2.0f*(float)M_PI*k/nA;
float rr = rs[i]*(1.0f + wob*sinf(lobes*ang + phase + i*0.7f));
glVertex3f(cx+cosf(ang)*rr, ys[i], cz+sinf(ang)*rr);
}
glEnd();
}
for(int k=0;k<nA;k+=stride){
float ang = 2.0f*(float)M_PI*k/nA;
glBegin(GL_LINE_STRIP);
for(int i=0;i<nP;i++){
glColor4f(cols[i].x,cols[i].y,cols[i].z,alpha);
float rr = rs[i]*(1.0f + wob*sinf(lobes*ang + phase + i*0.7f));
glVertex3f(cx+cosf(ang)*rr, ys[i], cz+sinf(ang)*rr);
}
glEnd();
}
}
/* wireframe sphere for the initial fireball */
static void drawSphereWire(float cx,float cy,float cz,float r,
V3 col,float alpha,float phase){
for(int j=1;j<6;j++){
float th = (float)M_PI*j/6;
drawWobRing(cx, cy+cosf(th)*r, cz, sinf(th)*r, 32,
0.08f, 6, phase+j, col, alpha);
}
for(int m=0;m<6;m++){
float a = (float)M_PI*m/6;
glColor4f(col.x,col.y,col.z,alpha);
glBegin(GL_LINE_STRIP);
for(int j=0;j<=16;j++){
float th = (float)M_PI*j/16;
float rr = sinf(th)*r;
glVertex3f(cx+cosf(a)*rr, cy+cosf(th)*r, cz+sinf(a)*rr);
}
glEnd();
}
}
/* a vortex torus -- the rolling cap whose outer edge curls up and over.
* Rmaj is the ring radius, rmin the tube radius; the tube cross-section is
* drawn as latitude rings plus meridional strands so the roll reads clearly */
static void drawTorusCloud(float cx,float cz,float cy,float Rmaj,float rmin,
V3 col,V3 hot,float alpha,float roil){
if(alpha <= 0.01f) return;
const int nPhi=10, nTh=40, nStrand=14;
/* latitude rings around the tube */
for(int p=0;p<nPhi;p++){
float phi = 2.0f*(float)M_PI*p/nPhi;
float Rr = Rmaj + rmin*cosf(phi);
float yy = cy + rmin*sinf(phi);
float ct = 0.5f + 0.5f*cosf(phi); /* 1 outer .. 0 inner */
V3 c = vlerp(hot, col, ct);
float wob = 0.12f + 0.05f*sinf(roil + phi);
drawWobRing(cx, yy, cz, Rr, nTh, wob, 7, roil*1.1f + phi*2.0f, c, alpha);
}
/* meridional strands showing the rolling tube cross-section */
for(int m=0;m<nStrand;m++){
float th = 2.0f*(float)M_PI*m/nStrand;
glBegin(GL_LINE_STRIP);
for(int p=0;p<=nPhi;p++){
float phi = 2.0f*(float)M_PI*p/nPhi;
float Rr = Rmaj + rmin*cosf(phi);
float yy = cy + rmin*sinf(phi);
float ct = 0.5f + 0.5f*cosf(phi);
V3 c = vlerp(hot, col, ct);
glColor4f(c.x,c.y,c.z, alpha);
glVertex3f(cx+cosf(th)*Rr, yy, cz+sinf(th)*Rr);
}
glEnd();
}
}
/* the full animated mushroom cloud */
static void drawMushroom(const Bomb *b){
float t=b->t, H=b->size, gY=b->groundY, cx=b->gx, cz=b->gz;
float eg = smoothstepf(0.0f, 3.0f, t); /* main growth */
float late = clampf((t-3.0f)/6.0f, 0.0f, 1.0f); /* slow billow */
float fade = (t < EXPLO_DUR-2.5f) ? 1.0f
: clampf((EXPLO_DUR-t)/2.5f,0.0f,1.0f);
float roil = t*1.6f; /* roll phase */
V3 hue = b->col;
V3 hot = vlerp(hue, v3(1.0f,0.95f,0.7f), 0.75f); /* incandescent*/
float alpha = fade;
float top = H*(0.22f + 0.78f*eg);
float stemTopY = gY + top*0.50f;
float capR = H*0.30f*(0.35f + 0.65f*eg)*(1.0f + 0.4f*late);
float stemR = H*0.055f*(0.5f + 0.5f*eg);
/* the head always sits on top of the stem -- it never detaches. It just
* keeps billowing and rolling (the roil phase advances with time) as the
* whole cloud fades away. */
float capCenterY = stemTopY + capR*0.45f;
float capH = top*0.50f;
/* initial blinding fireball, fading into the rising column */
if(t < 1.0f){
float ff = clampf(t/0.6f, 0.0f, 1.0f);
float fr = H*0.14f*ff + H*0.02f;
float fy = gY + fr*0.9f + (stemTopY-gY)*0.2f*ff;
float fa = alpha*clampf(1.0f-(t-0.4f)/0.6f, 0.0f, 1.0f);
if(fa > 0.01f) drawSphereWire(cx,fy,cz,fr, hot, fa, roil*2.0f);
}
/* ground shock + dust skirt */
float shockR = H*0.5f*smoothstepf(0.0f,2.0f,t);
float shockA = alpha*clampf(1.0f - t/4.0f, 0.0f,1.0f)*0.7f;
if(shockA > 0.01f){
drawWobRing(cx, gY+H*0.005f, cz, shockR, 48, 0.05f, 8, roil, hue, shockA);
drawWobRing(cx, gY+H*0.02f, cz, shockR*0.7f, 44, 0.10f, 6, roil*1.3f, hue, shockA*0.8f);
}
/* rising vortex rings climbing the stem */
for(int k=0;k<3;k++){
float rp = fmodf(t*0.5f + k*0.34f, 1.0f);
float ry = gY + rp*(stemTopY-gY);
float rr = stemR*(1.0f + rp*2.2f);
drawWobRing(cx, ry, cz, rr, 32, 0.14f, 5, roil+k, vlerp(hot,hue,rp), alpha*(1.0f-rp)*0.7f);
}
/* stem -- always joined to the cap */
{
const int nP=8; float ys[8], rs[8]; V3 cs[8];
for(int i=0;i<nP;i++){
float f=(float)i/(nP-1);
ys[i]=gY + f*(stemTopY-gY);
rs[i]=stemR*(0.8f + 0.35f*sinf(f*(float)M_PI));
cs[i]=vlerp(hot,hue, clampf(f*1.2f,0.0f,1.0f));
}
drawLathe(cx,cz, ys,rs,cs, nP, 0.08f, 5, roil, alpha*0.9f);
}
/* domed top of the cap */
{
const int nP=8; float ys[8], rs[8]; V3 cs[8];
for(int i=0;i<nP;i++){
float f=(float)i/(nP-1);
float dome=sqrtf(clampf(1.0f-f*f,0.0f,1.0f));
ys[i]=capCenterY + f*(capH*0.5f);
rs[i]=capR*dome;
cs[i]=vlerp(hot,hue, clampf(0.3f+f,0.0f,1.0f));
}
float wob=0.12f + 0.05f*sinf(roil);
drawLathe(cx,cz, ys,rs,cs, nP, wob, 7, roil*1.2f, alpha);
}
/* the billowing cap as a rolling vortex torus whose outer edge curls
* over -- its underside meets the top of the stem */
drawTorusCloud(cx,cz,capCenterY, capR*0.78f, capR*0.5f, hue, hot, alpha*0.85f, roil);
}
/* the falling bomb: a small finned body trailing a bright tracer */
static void drawBomb(const Bomb *b){
float x=b->gx, y=b->y, z=b->gz, s=1.8f;
glColor4f(1.0f,0.75f,0.35f,0.85f);
glBegin(GL_LINES);
glVertex3f(x,y,z); glVertex3f(x,y+10.0f,z);
glEnd();
glColor4f(0.9f,0.9f,0.95f,1.0f);
glBegin(GL_LINE_LOOP);
glVertex3f(x,y+s*2,z); glVertex3f(x+s,y,z);
glVertex3f(x,y-s*2,z); glVertex3f(x-s,y,z);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex3f(x,y+s*2,z); glVertex3f(x,y,z+s);
glVertex3f(x,y-s*2,z); glVertex3f(x,y,z-s);
glEnd();
}
/* drop a new bomb into a random spot in the field of view, between half
* the rendering distance and the full rendering distance */
static void spawnBomb(void){
int slot=-1;
for(int i=0;i<MAX_BOMBS;i++) if(!bombs[i].active){ slot=i; break; }
if(slot<0){ /* all busy: reuse the oldest */
float best=-1.0f;
for(int i=0;i<MAX_BOMBS;i++) if(bombs[i].t>best){ best=bombs[i].t; slot=i; }
}
Bomb *b=&bombs[slot];
float R = g_viewRadius;
float a = camYaw + (frand()-0.5f)*1.1f; /* within the FOV */
float dist = lerpf(0.5f*R, R, frand()); /* half..full distance*/
b->gx = camPos.x + sinf(a)*dist;
b->gz = camPos.z - cosf(a)*dist;
b->groundY = terrainHeight(b->gx, b->gz);
b->size = 20.0f * S.mountainMaxH;
b->y = b->groundY + fmaxf(160.0f, b->size*0.55f);
b->vy = 0.0f;
b->t = 0.0f;
b->exploding = false;
b->active = true;
b->col = hsv2rgb(frand(), 0.85f, 1.0f); /* vivid, extremely bright */
}
static void updateBombs(float dt){
for(int i=0;i<MAX_BOMBS;i++){
Bomb *b=&bombs[i];
if(!b->active) continue;
if(!b->exploding){
b->vy += BOMB_GRAV*dt;
b->y -= b->vy*dt;
if(b->y <= b->groundY){ /* impact -> detonate */
b->y = b->groundY;
b->exploding = true;
b->t = 0.0f;
/* obliterate mountains + cacti within the cloud's total
* radius, and gouge a real crater bowl at the impact point */
addCrater(b->gx, b->gz, b->size);
tk.ok = false; ck.ok = false; /* force terrain+cacti rebuild */
}
} else {
b->t += dt;
if(b->t >= EXPLO_DUR) b->active = false;
}
}
}
static void drawBombs(void){
bool any=false;
for(int i=0;i<MAX_BOMBS;i++) if(bombs[i].active){ any=true; break; }
if(!any) return;
/* additive blending + no fog makes the clouds glow at full, extreme
* brightness regardless of distance */
glDisable(GL_FOG);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
for(int i=0;i<MAX_BOMBS;i++){
if(!bombs[i].active) continue;
if(bombs[i].exploding) drawMushroom(&bombs[i]);
else drawBomb(&bombs[i]);
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_FOG);
}
static void toggleFullscreen(GLFWwindow *w){
g_fullscreen = !g_fullscreen;
if(g_fullscreen){
@ -771,6 +1219,9 @@ static void keyCB(GLFWwindow *w, int key, int sc, int action, int mods){
switch(key){
case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(w, GLFW_TRUE); break;
case GLFW_KEY_F: if(action==GLFW_PRESS) toggleFullscreen(w); break;
case GLFW_KEY_SPACE: if(action==GLFW_PRESS) spawnBomb(); break;
case GLFW_KEY_X: if(action==GLFW_PRESS) camVel = v3(0,0,0); break; /* full stop */
case GLFW_KEY_G: if(action==GLFW_PRESS) g_showAlt = !g_showAlt; break;
case GLFW_KEY_1: S.terrainHue = fmodf(S.terrainHue-0.01f+1.0f,1.0f); printSettings(); break;
case GLFW_KEY_2: S.terrainHue = fmodf(S.terrainHue+0.01f,1.0f); printSettings(); break;
@ -831,6 +1282,7 @@ static void usage(const char *prog){
int main(int argc, char **argv){
bool startFull = false;
float fv;
loadSettings(); /* resume last session's settings; CLI args below still override */
for(int i=1;i<argc;i++){
if(strcmp(argv[i],"--fullscreen")==0) startFull = true;
else if(strcmp(argv[i],"--help")==0 || strcmp(argv[i],"-h")==0){ usage(argv[0]); return 0; }
@ -874,6 +1326,7 @@ int main(int argc, char **argv){
printf("vectordesert -- wire-mesh vector desert flythrough\n");
printf("move: W/S throttle A/D strafe PageUp/Dn altitude arrows pan\n");
printf(" X full stop SPACE drops a bomb G floor-altitude indicator\n");
printf("keys: 1/2 terrainHue 3/4 mtnFreq ,/. mtnRough T/Y U/I mtnH\n");
printf(" 5/6 cactusFreq 7/8 sizeVar J/K minSize N/M maxSize\n");
printf(" 9/0 cactusHue -/= maxArms [ ] renderDist F full ESC quit\n\n");
@ -898,6 +1351,7 @@ int main(int argc, char **argv){
double t0 = glfwGetTime();
g_lastInput = t0; /* HUD starts visible, then fades */
g_rng ^= (uint32_t)(t0*1000.0) * 2654435761u; /* seed the bomb RNG */
camPos = v3(0.0f, 14.0f, 0.0f);
const float moveSpeed = 28.0f; /* WASD / strafe units per second */
const float climbSpeed= 22.0f; /* PageUp / PageDown */
@ -943,6 +1397,8 @@ int main(int argc, char **argv){
float floorY = terrainHeight(camPos.x, camPos.z) + 2.0f;
if(camPos.y < floorY) camPos.y = floorY;
updateBombs(dt);
V3 target = add(camPos, fwd);
int fbw, fbh;
@ -967,6 +1423,7 @@ int main(int argc, char **argv){
ensureCaches();
batchDraw(&gTerr);
batchDraw(&gCacti);
drawBombs();
/* HUD: fully visible, then fades after 10s without a keypress */
double idle = glfwGetTime() - g_lastInput;
@ -974,12 +1431,14 @@ int main(int argc, char **argv){
? 1.0f
: clampf(1.0f - (float)(idle-10.0)/1.5f, 0.0f, 1.0f);
if(hudAlpha > 0.001f) drawHUD(fbw, fbh, hudAlpha);
if(g_showAlt) drawAltIndicator(fbw, fbh);
glfwSwapBuffers(win);
glfwPollEvents();
}
printf("\n");
saveSettings(); /* persist settings for next launch */
glfwDestroyWindow(win);
glfwTerminate();
return 0;