diff --git a/README.md b/README.md index 8aa15f8..5003eea 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,14 @@ Example: | keys | does | |------|------| | `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 | | `Page Up` / `Page Down` | raise / lower camera altitude | | `←` / `→` | pan view left / right | | `↑` / `↓` | pan view up / down | | `[` / `]` | 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 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 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 - **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 to a bare trunk) and the largest always reach `max-arms`. - 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. ## Performance diff --git a/main.c b/main.c index 3442bde..207668c 100644 --- a/main.c +++ b/main.c @@ -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 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;ix, 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;kt, 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;igx, 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;ibest){ 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;iactive) 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 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; diff --git a/vectordesert b/vectordesert index 68e1f54..2c795fc 100755 Binary files a/vectordesert and b/vectordesert differ