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

View file

@ -61,11 +61,14 @@ Example:
| keys | does | | keys | does |
|------|------| |------|------|
| `W` / `S` | throttle: accelerate / decelerate forward speed (holds when released; `S` past zero goes into reverse) | | `W` / `S` | throttle: accelerate / decelerate forward speed (holds when released; `S` past zero goes into reverse) |
| `X` | full stop (zero the camera's velocity) |
| `A` / `D` | strafe left / right | | `A` / `D` | strafe left / right |
| `Page Up` / `Page Down` | raise / lower camera altitude | | `Page Up` / `Page Down` | raise / lower camera altitude |
| `←` / `→` | pan view left / right | | `←` / `→` | pan view left / right |
| `↑` / `↓` | pan view up / down | | `↑` / `↓` | pan view up / down |
| `[` / `]` | decrease / increase rendering distance | | `[` / `]` | decrease / increase rendering distance |
| `G` | toggle the floor-altitude indicator |
| `Space` | drop a bomb |
Velocity is held in world space, so panning the view with the arrow keys Velocity is held in world space, so panning the view with the arrow keys
only changes where you look — it does not change the direction you are only changes where you look — it does not change the direction you are
@ -97,13 +100,43 @@ on-screen heads-up display (drawn in the same vector style) lists every
setting next to the key(s) that change it. The HUD fades out after 10 setting next to the key(s) that change it. The HUD fades out after 10
seconds with no keypresses and snaps back as soon as you press a key. seconds with no keypresses and snaps back as soon as you press a key.
All settings (and the rendering distance) are saved to `~/.vectordesert.cfg`
on exit and reloaded at startup, so the program resumes with the values from
your last session. Command-line options still override the saved values for
that run.
## Bombs
Press `Space` to drop a bomb. It falls from the sky onto a random spot in
your field of view, between half the rendering distance and the full
rendering distance away. When it strikes the ground it detonates into an
animated, all-vector mushroom cloud twenty times the height of the tallest
mountains, in an extremely bright random hue (additively blended so it
glows):
- an initial blinding fireball,
- a rising stem with climbing vortex rings,
- a billowing cap that stays joined to the stem — a rolling vortex torus
whose outer edge curls up and over — and keeps animating as the whole
cloud fades away.
The blast **obliterates every mountain and cactus within the cloud's total
radius** and **gouges a deep crater** — a steep-walled bowl with a raised
rim thrown up above the surrounding surface — at the impact point. Every
bomb leaves its own permanent crater, and craters never erase one another:
repeated blasts in the same place **accumulate**, deepening and reshaping
the crater. Drop as many as you like; the clouds animate and dissipate
independently.
## Notes ## Notes
- **Arms scale with size:** the number of arms is derived from a cactus's - **Arms scale with size:** the number of arms is derived from a cactus's
normalized size, so the smallest cacti always have the fewest arms (down normalized size, so the smallest cacti always have the fewest arms (down
to a bare trunk) and the largest always reach `max-arms`. to a bare trunk) and the largest always reach `max-arms`.
- The world is generated procedurally from a hash-based value-noise height - The world is generated procedurally from a hash-based value-noise height
field, so it is effectively infinite — the camera flies forever. field, so it is effectively infinite — the camera flies forever. As well
as mountain ranges rising above the plain, the terrain carves valleys and
basins below it (and bomb craters), so it has real depth, not just relief.
- Distance fog fades distant geometry into the dusk sky for depth. - Distance fog fades distant geometry into the dusk sky for depth.
## Performance ## Performance

521
main.c
View file

@ -125,54 +125,107 @@ static Settings S = {
#define ARM_LIMIT 12 #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 */ /* 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 /* The shape of a range/basin in 0..1: a ridged multifractal that roughness
* scale (so a range stays a sensibly sized range). The user's * morphs from a flat-topped mesa (0) to jagged crests (1). Used for both the
* mountainFreq controls how much of the plain those ranges cover, i.e. * mountains (added) and the valleys (subtracted). */
* how frequently the plain is interrupted: higher freq -> lower static float rangeRidge(float x, float z){
* 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). */
float bf = 0.05f * (26.0f / fmaxf(S.mountainMaxH, 1.0f)); 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; 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++){ for(int i=0;i<6;i++){
float n = 1.0f - fabsf(2.0f*valnoise(x*freq, z*freq) - 1.0f); float n = 1.0f - fabsf(2.0f*valnoise(x*freq, z*freq) - 1.0f);
n *= n; n *= n;
if(i==0) mass = n; /* broad base shape */ if(i==0) mass = n;
n *= weight; /* detail only rides on existing ridge*/ n *= weight;
weight = clampf(n*2.5f, 0.0f, 1.0f); weight = clampf(n*2.5f, 0.0f, 1.0f);
sum += n*amp; sum += n*amp;
norm += amp; norm += amp;
freq *= 2.0f; amp *= 0.5f; freq *= 2.0f; amp *= 0.5f;
} }
float jagged = clampf(sum / norm, 0.0f, 1.0f); float jagged = clampf(sum / norm, 0.0f, 1.0f);
/* 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); float mesa = clampf((mass - 0.25f) * 3.2f, 0.0f, 1.0f);
return lerpf(mesa, jagged, S.mountainRough);
}
/* mountainRough morphs flat-topped mesas (0) into jagged peaks (1) */ static float terrainHeight(float x, float z){
float ridge = lerpf(mesa, jagged, S.mountainRough); /* 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;
/* ridge (0..1) maps the range between the user's min and max heights */ /* Coherent low-frequency blobs decide where ranges/basins occur; the
float body = lerpf(S.mountainMinH, S.mountainMaxH, ridge); * user's mountainFreq sets how much of the plain they cover. */
return plains + mountainMask * body; float thr = clampf(0.80f - 0.11f*S.mountainFreq, 0.35f, 0.86f);
/* 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 V3 camPos;
static float g_viewRadius = 128.0f; /* rendering distance (runtime) */ static float g_viewRadius = 128.0f; /* rendering distance (runtime) */
static bool g_showAlt = false; /* altitude indicator toggle */
#define FOG_NEAR 18.0f #define FOG_NEAR 18.0f
/* fog reaches full opacity a little inside the draw radius so the mesh /* 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; 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 */ /* line batch: collect every wireframe segment into one vertex+colour */
/* array so a whole frame's geometry ships in a single glDrawArrays. */ /* 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("9/0 CACTUS HUE %.2f", S.cactusHue);
LINE("-/= MAX ARMS %d", S.maxArms); LINE("-/= MAX ARMS %d", S.maxArms);
LINE("[/] RENDER DIST %.0f", g_viewRadius); LINE("[/] RENDER DIST %.0f", g_viewRadius);
LINE("X FULL STOP");
LINE("G FLOOR ALT %s", g_showAlt ? "ON" : "OFF");
#undef LINE #undef LINE
/* camera + window controls footer */ /* camera + window controls footer */
y += lineH*0.5f; y += lineH*0.5f;
glColor4f(0.45f, 0.70f, 0.85f, alpha); 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_FOG);
glEnable(GL_DEPTH_TEST); 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 dx = wx-cx, dz = wz-cz;
float d2 = dx*dx + dz*dz; float d2 = dx*dx + dz*dz;
if(d2 > reach2) continue; if(d2 > reach2) continue;
if(inCrater(wx, wz)) continue; /* obliterated by a blast */
buildCactus(&gCacti, wx, wz, sqrtf(d2)); 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 V3 camVel; /* persistent world-space velocity (W/S) */
static double g_lastInput = 0.0; /* time of last keypress (for HUD fade) */ 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){ static void toggleFullscreen(GLFWwindow *w){
g_fullscreen = !g_fullscreen; g_fullscreen = !g_fullscreen;
if(g_fullscreen){ if(g_fullscreen){
@ -771,6 +1219,9 @@ static void keyCB(GLFWwindow *w, int key, int sc, int action, int mods){
switch(key){ switch(key){
case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(w, GLFW_TRUE); break; case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(w, GLFW_TRUE); break;
case GLFW_KEY_F: if(action==GLFW_PRESS) toggleFullscreen(w); 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_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; 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){ int main(int argc, char **argv){
bool startFull = false; bool startFull = false;
float fv; float fv;
loadSettings(); /* resume last session's settings; CLI args below still override */
for(int i=1;i<argc;i++){ for(int i=1;i<argc;i++){
if(strcmp(argv[i],"--fullscreen")==0) startFull = true; 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; } 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("vectordesert -- wire-mesh vector desert flythrough\n");
printf("move: W/S throttle A/D strafe PageUp/Dn altitude arrows pan\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("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(" 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"); 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(); double t0 = glfwGetTime();
g_lastInput = t0; /* HUD starts visible, then fades */ 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); camPos = v3(0.0f, 14.0f, 0.0f);
const float moveSpeed = 28.0f; /* WASD / strafe units per second */ const float moveSpeed = 28.0f; /* WASD / strafe units per second */
const float climbSpeed= 22.0f; /* PageUp / PageDown */ 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; float floorY = terrainHeight(camPos.x, camPos.z) + 2.0f;
if(camPos.y < floorY) camPos.y = floorY; if(camPos.y < floorY) camPos.y = floorY;
updateBombs(dt);
V3 target = add(camPos, fwd); V3 target = add(camPos, fwd);
int fbw, fbh; int fbw, fbh;
@ -967,6 +1423,7 @@ int main(int argc, char **argv){
ensureCaches(); ensureCaches();
batchDraw(&gTerr); batchDraw(&gTerr);
batchDraw(&gCacti); batchDraw(&gCacti);
drawBombs();
/* HUD: fully visible, then fades after 10s without a keypress */ /* HUD: fully visible, then fades after 10s without a keypress */
double idle = glfwGetTime() - g_lastInput; double idle = glfwGetTime() - g_lastInput;
@ -974,12 +1431,14 @@ int main(int argc, char **argv){
? 1.0f ? 1.0f
: clampf(1.0f - (float)(idle-10.0)/1.5f, 0.0f, 1.0f); : clampf(1.0f - (float)(idle-10.0)/1.5f, 0.0f, 1.0f);
if(hudAlpha > 0.001f) drawHUD(fbw, fbh, hudAlpha); if(hudAlpha > 0.001f) drawHUD(fbw, fbh, hudAlpha);
if(g_showAlt) drawAltIndicator(fbw, fbh);
glfwSwapBuffers(win); glfwSwapBuffers(win);
glfwPollEvents(); glfwPollEvents();
} }
printf("\n"); printf("\n");
saveSettings(); /* persist settings for next launch */
glfwDestroyWindow(win); glfwDestroyWindow(win);
glfwTerminate(); glfwTerminate();
return 0; return 0;

Binary file not shown.