/* * vectordesert - an infinite wire-mesh vector desert flythrough * * A retro "vector graphics" landscape: vast wireframe plains, occasional * vector mountain ranges, and saguaro cacti built entirely out of square * wire-mesh frames. Everything (terrain included) is drawn as square * wireframe loops. * * Controls and settings are documented in README.md and printed at startup. */ #include #include #include #include #include #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif /* ------------------------------------------------------------------ */ /* tiny vector math */ /* ------------------------------------------------------------------ */ typedef struct { float x, y, z; } V3; static V3 v3(float x, float y, float z){ V3 r={x,y,z}; return r; } static V3 add(V3 a, V3 b){ return v3(a.x+b.x, a.y+b.y, a.z+b.z); } static V3 sub(V3 a, V3 b){ return v3(a.x-b.x, a.y-b.y, a.z-b.z); } static V3 scl(V3 a, float s){ return v3(a.x*s, a.y*s, a.z*s); } static V3 cross3(V3 a, V3 b){ return v3(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x); } static float dot3(V3 a, V3 b){ return a.x*b.x + a.y*b.y + a.z*b.z; } static float len3(V3 a){ return sqrtf(a.x*a.x + a.y*a.y + a.z*a.z); } static V3 norm3(V3 a){ float l=len3(a); if(l<1e-6f) return v3(0,1,0); return scl(a,1.0f/l); } /* rotate v around unit axis by angle (Rodrigues) */ static V3 rotateAround(V3 v, V3 axis, float ang){ float c = cosf(ang), s = sinf(ang); V3 t1 = scl(v, c); V3 t2 = scl(cross3(axis, v), s); V3 t3 = scl(axis, dot3(axis, v) * (1.0f - c)); return add(add(t1, t2), t3); } static float clampf(float v, float lo, float hi){ return vhi?hi:v); } static float lerpf(float a, float b, float t){ return a + (b-a)*t; } static float smoothstepf(float e0, float e1, float x){ float t = clampf((x-e0)/(e1-e0), 0.0f, 1.0f); return t*t*(3.0f-2.0f*t); } /* ------------------------------------------------------------------ */ /* hashing + value noise */ /* ------------------------------------------------------------------ */ static uint32_t hashi(int x, int y){ uint32_t h = (uint32_t)(x*374761393) + (uint32_t)(y*668265263); h = (h ^ (h >> 13)) * 1274126177u; h ^= h >> 16; return h; } static float hashf(int x, int y){ return (hashi(x,y) & 0xffffffu) / (float)0xffffff; } static float valnoise(float x, float y){ int xi = (int)floorf(x), yi = (int)floorf(y); float xf = x - xi, yf = y - yi; float v00 = hashf(xi, yi); float v10 = hashf(xi+1, yi); float v01 = hashf(xi, yi+1); float v11 = hashf(xi+1, yi+1); float u = xf*xf*(3.0f-2.0f*xf); float v = yf*yf*(3.0f-2.0f*yf); return lerpf(lerpf(v00, v10, u), lerpf(v01, v11, u), v); } /* fractal brownian motion in [0,1] */ static float fbm(float x, float y, int oct){ float sum = 0.0f, amp = 0.5f, norm = 0.0f; for(int i=0;ix = 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 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; } /* ------------------------------------------------------------------ */ /* color: HSV -> RGB */ /* ------------------------------------------------------------------ */ static V3 hsv2rgb(float h, float s, float v){ h = h - floorf(h); float i = floorf(h*6.0f); float f = h*6.0f - i; float p = v*(1.0f-s); float q = v*(1.0f-s*f); float t = v*(1.0f-s*(1.0f-f)); switch(((int)i)%6){ case 0: return v3(v,t,p); case 1: return v3(q,v,p); case 2: return v3(p,v,t); case 3: return v3(p,q,v); case 4: return v3(t,p,v); default:return v3(v,p,q); } } /* ------------------------------------------------------------------ */ /* camera matrices (no GLU dependency) */ /* ------------------------------------------------------------------ */ static void perspectiveGL(float fovyDeg, float aspect, float znear, float zfar){ float fH = tanf(fovyDeg * (float)M_PI / 360.0f) * znear; float fW = fH * aspect; glFrustum(-fW, fW, -fH, fH, znear, zfar); } static void lookAtGL(V3 eye, V3 center, V3 up){ V3 f = norm3(sub(center, eye)); V3 s = norm3(cross3(f, up)); V3 u = cross3(s, f); float m[16] = { s.x, u.x, -f.x, 0.0f, s.y, u.y, -f.y, 0.0f, s.z, u.z, -f.z, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; glMultMatrixf(m); glTranslatef(-eye.x, -eye.y, -eye.z); } /* ------------------------------------------------------------------ */ /* fog (distance fade) */ /* ------------------------------------------------------------------ */ 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 * edge fades into the sky instead of popping */ static float fogFarDist(void){ float f = g_viewRadius - 8.0f; 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. */ /* Distance fade is done by hardware fog, not per-vertex on the CPU. */ /* ------------------------------------------------------------------ */ typedef struct { float *v; float *c; int n, cap; } Batch; static void batchEnsure(Batch *b, int extra){ if(b->n + extra <= b->cap) return; int nc = b->cap ? b->cap : 8192; while(nc < b->n + extra) nc *= 2; b->v = (float*)realloc(b->v, (size_t)nc*3*sizeof(float)); b->c = (float*)realloc(b->c, (size_t)nc*3*sizeof(float)); b->cap = nc; } static inline void batchVert(Batch *b, float x, float y, float z, const V3 *col){ int i = b->n++; b->v[i*3]=x; b->v[i*3+1]=y; b->v[i*3+2]=z; b->c[i*3]=col->x; b->c[i*3+1]=col->y; b->c[i*3+2]=col->z; } static inline void batchSeg(Batch *b, V3 a, V3 q, const V3 *col){ batchEnsure(b, 2); batchVert(b, a.x,a.y,a.z, col); batchVert(b, q.x,q.y,q.z, col); } static void batchDraw(const Batch *b){ if(b->n <= 0) return; glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); glVertexPointer(3, GL_FLOAT, 0, b->v); glColorPointer(3, GL_FLOAT, 0, b->c); glDrawArrays(GL_LINES, 0, b->n); glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_VERTEX_ARRAY); } /* ------------------------------------------------------------------ */ /* vector stroke font (for the HUD) */ /* Each glyph is a set of line segments on a 0..4 (x) by 0..6 (y) grid */ /* ------------------------------------------------------------------ */ #define GLY(name) static const float name[] = GLY(gA){0,0,2,6, 2,6,4,0, 1,2,3,2}; GLY(gB){0,0,0,6, 0,6,3,6, 0,3,3,3, 0,0,3,0, 3,6,3,3, 3,3,3,0}; GLY(gC){4,6,1,6, 1,6,0,5, 0,5,0,1, 0,1,1,0, 1,0,4,0}; GLY(gD){0,0,0,6, 0,6,2,6, 2,6,4,4, 4,4,4,2, 4,2,2,0, 2,0,0,0}; GLY(gE){4,6,0,6, 0,6,0,0, 0,0,4,0, 0,3,3,3}; GLY(gF){4,6,0,6, 0,6,0,0, 0,3,3,3}; GLY(gG){4,6,1,6, 1,6,0,5, 0,5,0,1, 0,1,1,0, 1,0,4,0, 4,0,4,3, 4,3,2,3}; GLY(gH){0,0,0,6, 4,0,4,6, 0,3,4,3}; GLY(gI){1,6,3,6, 2,6,2,0, 1,0,3,0}; GLY(gJ){4,6,4,1, 4,1,3,0, 3,0,1,0, 1,0,0,1}; GLY(gK){0,0,0,6, 4,6,0,3, 0,3,4,0}; GLY(gL){0,6,0,0, 0,0,4,0}; GLY(gM){0,0,0,6, 0,6,2,3, 2,3,4,6, 4,6,4,0}; GLY(gN){0,0,0,6, 0,6,4,0, 4,0,4,6}; GLY(gO){1,0,3,0, 3,0,4,2, 4,2,4,4, 4,4,3,6, 3,6,1,6, 1,6,0,4, 0,4,0,2, 0,2,1,0}; GLY(gP){0,0,0,6, 0,6,3,6, 3,6,3,3, 3,3,0,3}; GLY(gQ){1,0,3,0, 3,0,4,2, 4,2,4,4, 4,4,3,6, 3,6,1,6, 1,6,0,4, 0,4,0,2, 0,2,1,0, 2,2,4,0}; GLY(gR){0,0,0,6, 0,6,3,6, 3,6,3,3, 3,3,0,3, 0,3,4,0}; GLY(gS){4,5,3,6, 3,6,1,6, 1,6,0,5, 0,5,1,3, 1,3,3,3, 3,3,4,1, 4,1,3,0, 3,0,1,0, 1,0,0,1}; GLY(gT){0,6,4,6, 2,6,2,0}; GLY(gU){0,6,0,1, 0,1,1,0, 1,0,3,0, 3,0,4,1, 4,1,4,6}; GLY(gV){0,6,2,0, 2,0,4,6}; GLY(gW){0,6,1,0, 1,0,2,4, 2,4,3,0, 3,0,4,6}; GLY(gX){0,0,4,6, 0,6,4,0}; GLY(gY){0,6,2,3, 4,6,2,3, 2,3,2,0}; GLY(gZ){0,6,4,6, 4,6,0,0, 0,0,4,0}; GLY(g0){1,0,3,0, 3,0,4,2, 4,2,4,4, 4,4,3,6, 3,6,1,6, 1,6,0,4, 0,4,0,2, 0,2,1,0, 0,1,4,5}; GLY(g1){1,4,2,6, 2,6,2,0, 1,0,3,0}; GLY(g2){0,5,1,6, 1,6,3,6, 3,6,4,5, 4,5,4,4, 4,4,0,0, 0,0,4,0}; GLY(g3){0,6,3,6, 3,6,4,5, 4,5,3,3, 2,3,3,3, 3,3,4,2, 4,2,3,0, 3,0,1,0, 1,0,0,1}; GLY(g4){3,0,3,6, 3,6,0,2, 0,2,4,2}; GLY(g5){4,6,0,6, 0,6,0,3, 0,3,3,3, 3,3,4,2, 4,2,4,1, 4,1,3,0, 3,0,1,0, 1,0,0,1}; GLY(g6){4,5,3,6, 3,6,1,6, 1,6,0,4, 0,4,0,1, 0,1,1,0, 1,0,3,0, 3,0,4,1, 4,1,4,2, 4,2,3,3, 3,3,0,3}; GLY(g7){0,6,4,6, 4,6,1,0}; GLY(g8){1,3,3,3, 1,3,0,4, 0,4,0,5, 0,5,1,6, 1,6,3,6, 3,6,4,5, 4,5,4,4, 4,4,3,3, 3,3,4,2, 4,2,4,1, 4,1,3,0, 3,0,1,0, 1,0,0,1, 0,1,0,2, 0,2,1,3}; GLY(g9){0,1,1,0, 1,0,3,0, 3,0,4,2, 4,2,4,5, 4,5,3,6, 3,6,1,6, 1,6,0,5, 0,5,0,4, 0,4,1,3, 1,3,4,3}; GLY(gDot){2,0,2,1}; GLY(gDash){1,3,3,3}; GLY(gColon){2,1,2,2, 2,3,2,4}; GLY(gPct){0,0,4,6, 0,5,1,4, 3,2,4,1}; GLY(gSlash){0,0,4,6}; GLY(gLBrk){3,6,1,6, 1,6,1,0, 1,0,3,0}; GLY(gRBrk){1,6,3,6, 3,6,3,0, 3,0,1,0}; GLY(gEq){0,2,4,2, 0,4,4,4}; GLY(gComma){2,1,1,-1}; #undef GLY /* returns number of segments and sets *out to the segment array */ static int glyphFor(char c, const float **out){ #define R(arr) do{ *out = arr; return (int)(sizeof(arr)/(4*sizeof(float))); }while(0) switch(c){ case 'A': R(gA); case 'B': R(gB); case 'C': R(gC); case 'D': R(gD); case 'E': R(gE); case 'F': R(gF); case 'G': R(gG); case 'H': R(gH); case 'I': R(gI); case 'J': R(gJ); case 'K': R(gK); case 'L': R(gL); case 'M': R(gM); case 'N': R(gN); case 'O': R(gO); case 'P': R(gP); case 'Q': R(gQ); case 'R': R(gR); case 'S': R(gS); case 'T': R(gT); case 'U': R(gU); case 'V': R(gV); case 'W': R(gW); case 'X': R(gX); case 'Y': R(gY); case 'Z': R(gZ); case '0': R(g0); case '1': R(g1); case '2': R(g2); case '3': R(g3); case '4': R(g4); case '5': R(g5); case '6': R(g6); case '7': R(g7); case '8': R(g8); case '9': R(g9); case '.': R(gDot); case '-': R(gDash); case ':': R(gColon); case '%': R(gPct); case '/': R(gSlash); case '[': R(gLBrk); case ']': R(gRBrk); case '=': R(gEq); case ',': R(gComma); default: *out = NULL; return 0; /* space / unknown */ } #undef R } /* draw a string; (x,y) is the top-left of the text cell, in pixels */ static void drawText(const char *s, float x, float y, float sc){ float pen = x; for(const char *p=s; *p; ++p){ char c = (char)toupper((unsigned char)*p); const float *g; int n = glyphFor(c, &g); if(n){ glBegin(GL_LINES); for(int i=0;ifillY; yy-=2.5f*sc){ glVertex2f(gx0+1.0f, yy); glVertex2f(gx1-1.0f, yy); } glEnd(); glEnable(GL_FOG); glEnable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } /* ------------------------------------------------------------------ */ /* drawing: square wire-mesh tube */ /* ------------------------------------------------------------------ */ static void perpBasis(V3 n, V3 *u, V3 *v){ V3 a = (fabsf(n.y) < 0.99f) ? v3(0,1,0) : v3(1,0,0); *u = norm3(cross3(a, n)); *v = cross3(n, *u); } /* Draw a wire-mesh tube with a (ribbed) circular cross-section that follows * an arbitrary centerline. The cross-section frame is parallel-transported * along the path so it never twists, which lets arms curve smoothly. The * polygonal rings read as the vertical ribs/flutes of a saguaro. */ #define MAX_PTS 64 #define MAX_SIDES 16 static void appendTube(Batch *b, const V3 *pts, const float *rad, int n, int sides, const V3 *col){ if(n < 2) return; if(sides > MAX_SIDES) sides = MAX_SIDES; if(sides < 3) sides = 3; V3 prevDir = norm3(sub(pts[1], pts[0])); V3 u, v; perpBasis(prevDir, &u, &v); V3 prevRing[MAX_SIDES]; for(int i=0;i0){ V3 ax = cross3(prevDir, d); float s = len3(ax); if(s > 1e-5f){ ax = scl(ax, 1.0f/s); float ang = atan2f(s, dot3(prevDir, d)); u = rotateAround(u, ax, ang); v = rotateAround(v, ax, ang); } } prevDir = d; /* build the ring */ V3 ring[MAX_SIDES]; for(int k=0;k0) /* longitudinal ribs */ for(int k=0;k arm count. smaller = fewer, larger = more. */ float tsize = (maxH - minH > 1e-3f) ? (H - minH)/(maxH - minH) : 0.5f; int arms = (int)lroundf(tsize * S.maxArms); if(arms < 0) arms = 0; if(arms > S.maxArms) arms = S.maxArms; if(arms > ARM_LIMIT) arms = ARM_LIMIT; float ground = terrainHeight(wx, wz); V3 base = v3(wx, ground, wz); V3 up = v3(0,1,0); float rt = 0.35f + 0.03f*H; /* slender trunk */ V3 col = hsv2rgb(S.cactusHue, 0.7f, 0.9f); /* level of detail by distance */ int tSides = (dist < 90.0f) ? 10 : (dist < 160.0f ? 7 : 5); int aSides = (dist < 90.0f) ? 8 : 5; float rscale = (dist < 90.0f) ? 1.0f : (dist < 160.0f ? 0.6f : 0.4f); /* trunk: tapered fluted column with a rounded dome at the top */ V3 tpts[MAX_PTS]; float trad[MAX_PTS]; int nT = (int)(H*rscale) + (rscale < 1.0f ? 3 : 6); if(nT < 3) nT = 3; if(nT > MAX_PTS) nT = MAX_PTS; for(int i=0;i 0.92f) /* dome the very top */ r *= sqrtf(clampf(1.0f - powf((t-0.92f)/0.08f, 2.0f), 0.0f, 1.0f)); trad[i] = fmaxf(r, 0.02f); } appendTube(b, tpts, trad, nT, tSides, &col); /* arms: spread around the trunk, attaching in the mid/upper region */ for(int i=0;i> 10 ) & 0xffff)/(float)0xffff; float angle = (i * (2.0f*(float)M_PI / (arms>0?arms:1))) + a0*0.8f; V3 dir = v3(cosf(angle), 0.0f, sinf(angle)); float hf = lerpf(0.35f, 0.62f, a1); /* attach height frac */ float yh = H*hf; float rAtt = rt * (1.0f - 0.20f*hf); /* trunk radius there */ V3 attach = add(base, add(scl(up, yh), scl(dir, rAtt*0.7f))); float ra = rt * 0.62f; float bendR = H * 0.10f; float upLen = H * (0.28f + 0.18f*a0) * (1.0f - hf*0.4f); appendArm(b, attach, dir, up, bendR, upLen, ra, &col, aSides); } } /* ------------------------------------------------------------------ */ /* terrain + scene rendering */ /* ------------------------------------------------------------------ */ #define CELL 2.5f /* finest terrain quad size */ #define CACTUS_CELL 3.0f #define TERRAIN_CELLS 360 /* budget: max cells across the view diameter*/ /* Cached scene geometry. The terrain and cactus meshes are rebuilt only * when a relevant setting changes or the camera leaves the region the * cache covers; every other frame is just two glDrawArrays calls. */ static Batch gTerr, gCacti; static struct { bool ok; float cx, cz, radius, hueT, mFreq, mRough, mMinH, mMaxH, margin; } tk; /* terrain cache key */ static struct { bool ok; float cx, cz, radius, freq, sizeVar, minS, maxS, hue, margin; int arms; } ck; /* cactus cache key */ /* terrain LOD: cell size grows with the rendering distance so the cell * count (and therefore the work) stays bounded no matter how far we draw */ static float terrainCell(void){ return fmaxf(CELL, (2.0f*g_viewRadius)/(float)TERRAIN_CELLS); } static void buildTerrain(float cx, float cz, float margin){ gTerr.n = 0; float cell = terrainCell(); float reach = fogFarDist() + margin + 2.0f*cell; /* world half-extent */ int nx = (int)ceilf((2.0f*reach)/cell) + 1; if(nx > 700) nx = 700; /* hard safety cap */ float ox = floorf((cx - reach)/cell)*cell; float oz = floorf((cz - reach)/cell)*cell; float cull = fogFarDist() + margin + cell; /* emit within here */ float cull2 = cull*cull; /* one height + colour per grid vertex, computed once and shared by the * (up to) four edges that touch it -- no redundant terrainHeight calls */ static float *hgt=NULL,*cr=NULL,*cg=NULL,*cb=NULL; static int hcap=0; int nv = nx*nx; if(nv > hcap){ hcap = nv; hgt = (float*)realloc(hgt, (size_t)nv*sizeof(float)); cr = (float*)realloc(cr, (size_t)nv*sizeof(float)); cg = (float*)realloc(cg, (size_t)nv*sizeof(float)); cb = (float*)realloc(cb, (size_t)nv*sizeof(float)); } for(int j=0;j cull2) continue; int id = j*nx + i; V3 c = v3(cr[id],cg[id],cb[id]); if(i < nx-1){ int e = id+1; batchEnsure(&gTerr, 2); batchVert(&gTerr, x, hgt[id], z, &c); V3 ce = v3(cr[e],cg[e],cb[e]); batchVert(&gTerr, x+cell, hgt[e], z, &ce); } if(j < nx-1){ int nn = id+nx; batchEnsure(&gTerr, 2); batchVert(&gTerr, x, hgt[id], z, &c); V3 cn = v3(cr[nn],cg[nn],cb[nn]); batchVert(&gTerr, x, hgt[nn], z+cell, &cn); } } } } static void buildCacti(float cx, float cz, float margin){ gCacti.n = 0; float reach = g_viewRadius + margin; /* cacti reach as far as terrain */ float reach2 = reach*reach; float ox = floorf((cx - reach)/CACTUS_CELL)*CACTUS_CELL; float oz = floorf((cz - reach)/CACTUS_CELL)*CACTUS_CELL; float xe = cx + reach, ze = cz + reach; for(float z=oz; z<=ze; z+=CACTUS_CELL){ for(float x=ox; x<=xe; x+=CACTUS_CELL){ int gx=(int)floorf(x/CACTUS_CELL), gz=(int)floorf(z/CACTUS_CELL); float place = hashf(gx*991+7, gz*787+13); if(place >= S.cactusFreq) continue; float jx = (hashf(gx*5+1, gz*9+2)-0.5f) * CACTUS_CELL*0.7f; float jz = (hashf(gx*11+3, gz*3+4)-0.5f) * CACTUS_CELL*0.7f; float wx = (gx+0.5f)*CACTUS_CELL + jx; float wz = (gz+0.5f)*CACTUS_CELL + jz; 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)); } } } /* rebuild whichever caches are stale, then leave them ready to draw */ static void ensureCaches(void){ float tmargin = fmaxf(24.0f, g_viewRadius*0.18f); float dxt = camPos.x - tk.cx, dzt = camPos.z - tk.cz; if(!tk.ok || dxt*dxt+dzt*dzt > (tmargin*0.9f)*(tmargin*0.9f) || tk.radius!=g_viewRadius || tk.hueT!=S.terrainHue || tk.mFreq!=S.mountainFreq || tk.mRough!=S.mountainRough || tk.mMinH!=S.mountainMinH || tk.mMaxH!=S.mountainMaxH){ buildTerrain(camPos.x, camPos.z, tmargin); tk.ok=true; tk.cx=camPos.x; tk.cz=camPos.z; tk.radius=g_viewRadius; tk.hueT=S.terrainHue; tk.mFreq=S.mountainFreq; tk.mRough=S.mountainRough; tk.mMinH=S.mountainMinH; tk.mMaxH=S.mountainMaxH; tk.margin=tmargin; } float cmargin = fmaxf(16.0f, g_viewRadius*0.12f); float dxc = camPos.x - ck.cx, dzc = camPos.z - ck.cz; if(!ck.ok || dxc*dxc+dzc*dzc > (cmargin*0.9f)*(cmargin*0.9f) || ck.radius!=g_viewRadius || ck.freq!=S.cactusFreq || ck.sizeVar!=S.cactusSizeVar || ck.minS!=S.cactusMinSize || ck.maxS!=S.cactusMaxSize || ck.hue!=S.cactusHue || ck.arms!=S.maxArms){ buildCacti(camPos.x, camPos.z, cmargin); ck.ok=true; ck.cx=camPos.x; ck.cz=camPos.z; ck.radius=g_viewRadius; ck.freq=S.cactusFreq; ck.sizeVar=S.cactusSizeVar; ck.minS=S.cactusMinSize; ck.maxS=S.cactusMaxSize; ck.hue=S.cactusHue; ck.arms=S.maxArms; } } /* ------------------------------------------------------------------ */ /* settings printout + key handling */ /* ------------------------------------------------------------------ */ static void printSettings(void){ printf("\rterrainHue=%.3f mtnFreq=%.2f mtnRough=%.2f mtnH=%.0f-%.0f " "cactusFreq=%.2f sizeVar=%.2f size=%.1f-%.1f cactusHue=%.3f " "maxArms=%d rDist=%.0f ", S.terrainHue, S.mountainFreq, S.mountainRough, S.mountainMinH, S.mountainMaxH, S.cactusFreq, S.cactusSizeVar, S.cactusMinSize, S.cactusMaxSize, S.cactusHue, S.maxArms, g_viewRadius); fflush(stdout); } static bool g_fullscreen = false; static int g_winX=80, g_winY=80, g_winW=1280, g_winH=720; /* free-fly camera state */ static float camYaw = 0.0f; /* 0 looks toward -z */ 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;iwidth, vm->height, vm->refreshRate); } else { glfwSetWindowMonitor(w, NULL, g_winX, g_winY, g_winW, g_winH, 0); } } static void keyCB(GLFWwindow *w, int key, int sc, int action, int mods){ (void)sc; (void)mods; if(action != GLFW_PRESS && action != GLFW_REPEAT) return; g_lastInput = glfwGetTime(); 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; case GLFW_KEY_3: S.mountainFreq = clampf(S.mountainFreq-0.05f,0.2f,4.0f);printSettings(); break; case GLFW_KEY_4: S.mountainFreq = clampf(S.mountainFreq+0.05f,0.2f,4.0f);printSettings(); break; case GLFW_KEY_5: S.cactusFreq = clampf(S.cactusFreq-0.02f,0.0f,1.0f); printSettings(); break; case GLFW_KEY_6: S.cactusFreq = clampf(S.cactusFreq+0.02f,0.0f,1.0f); printSettings(); break; case GLFW_KEY_7: S.cactusSizeVar= clampf(S.cactusSizeVar-0.02f,0.0f,0.95f);printSettings(); break; case GLFW_KEY_8: S.cactusSizeVar= clampf(S.cactusSizeVar+0.02f,0.0f,0.95f);printSettings(); break; case GLFW_KEY_9: S.cactusHue = fmodf(S.cactusHue-0.01f+1.0f,1.0f); printSettings(); break; case GLFW_KEY_0: S.cactusHue = fmodf(S.cactusHue+0.01f,1.0f); printSettings(); break; case GLFW_KEY_MINUS: if(S.maxArms>0) S.maxArms--; printSettings(); break; case GLFW_KEY_EQUAL: if(S.maxArms S.cactusMaxSize){ float t = S.cactusMinSize; S.cactusMinSize = S.cactusMaxSize; S.cactusMaxSize = t; } if(S.mountainMinH > S.mountainMaxH){ float t = S.mountainMinH; S.mountainMinH = S.mountainMaxH; S.mountainMaxH = t; } if(!glfwInit()){ fprintf(stderr,"glfwInit failed\n"); return 1; } GLFWmonitor *mon = NULL; int cw = g_winW, ch = g_winH; if(startFull){ mon = glfwGetPrimaryMonitor(); const GLFWvidmode *vm = glfwGetVideoMode(mon); cw = vm->width; ch = vm->height; g_fullscreen = true; } GLFWwindow *win = glfwCreateWindow(cw, ch, "vectordesert", mon, NULL); if(!win){ fprintf(stderr,"window creation failed\n"); glfwTerminate(); return 1; } glfwMakeContextCurrent(win); glfwSwapInterval(1); glfwSetKeyCallback(win, keyCB); glfwSetFramebufferSizeCallback(win, fbSizeCB); 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"); printSettings(); printf("\n"); glEnable(GL_DEPTH_TEST); glEnable(GL_LINE_SMOOTH); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glLineWidth(1.0f); /* hardware linear fog fades distant geometry into the sky -- far cheaper * than computing a per-vertex fade on the CPU, and it lets the scene * geometry stay in static cached vertex arrays */ GLfloat fogcol[4] = { 0.03f, 0.02f, 0.06f, 1.0f }; glEnable(GL_FOG); glFogi(GL_FOG_MODE, GL_LINEAR); glFogfv(GL_FOG_COLOR, fogcol); glHint(GL_FOG_HINT, GL_NICEST); 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 */ const float panSpeed = 1.4f; /* arrow-key turn rate (rad/s) */ while(!glfwWindowShouldClose(win)){ double t1 = glfwGetTime(); float dt = (float)(t1 - t0); t0 = t1; /* arrow keys pan the view */ if(glfwGetKey(win, GLFW_KEY_LEFT) == GLFW_PRESS) camYaw -= panSpeed*dt; if(glfwGetKey(win, GLFW_KEY_RIGHT) == GLFW_PRESS) camYaw += panSpeed*dt; if(glfwGetKey(win, GLFW_KEY_UP) == GLFW_PRESS) camPitch += panSpeed*dt; if(glfwGetKey(win, GLFW_KEY_DOWN) == GLFW_PRESS) camPitch -= panSpeed*dt; camPitch = clampf(camPitch, -1.45f, 1.45f); /* view direction from yaw/pitch (yaw 0 -> -z) */ V3 fwd = v3(sinf(camYaw)*cosf(camPitch), sinf(camPitch), -cosf(camYaw)*cosf(camPitch)); V3 hfwd = norm3(v3(fwd.x, 0.0f, fwd.z)); /* ground-plane forward */ V3 right = norm3(cross3(fwd, v3(0,1,0))); /* W/S are a throttle: they add thrust along the current heading to a * persistent world-space velocity that holds when released. Because * the velocity lives in world space, panning the view with the arrow * keys does not change the direction of travel. */ const float accel = 22.0f, maxSpeed = 90.0f; if(glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) camVel = add(camVel, scl(hfwd, accel*dt)); if(glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) camVel = add(camVel, scl(hfwd, -accel*dt)); float sp = len3(camVel); if(sp > maxSpeed) camVel = scl(camVel, maxSpeed/sp); camPos = add(camPos, scl(camVel, dt)); /* A/D strafe; PageUp/Down change altitude */ if(glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) camPos = add(camPos, scl(right, moveSpeed*dt)); if(glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) camPos = add(camPos, scl(right,-moveSpeed*dt)); if(glfwGetKey(win, GLFW_KEY_PAGE_UP) == GLFW_PRESS) camPos.y += climbSpeed*dt; if(glfwGetKey(win, GLFW_KEY_PAGE_DOWN) == GLFW_PRESS) camPos.y -= climbSpeed*dt; /* never sink below the terrain */ 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; glfwGetFramebufferSize(win, &fbw, &fbh); float aspect = (fbh>0) ? (float)fbw/(float)fbh : 1.0f; glClearColor(0.03f, 0.02f, 0.06f, 1.0f); /* dusk desert sky */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_PROJECTION); glLoadIdentity(); perspectiveGL(60.0f, aspect, 0.5f, g_viewRadius + 50.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); lookAtGL(camPos, target, v3(0,1,0)); /* distance fade tracks the rendering distance */ glFogf(GL_FOG_START, FOG_NEAR); glFogf(GL_FOG_END, fogFarDist()); ensureCaches(); batchDraw(&gTerr); batchDraw(&gCacti); drawBombs(); /* HUD: fully visible, then fades after 10s without a keypress */ double idle = glfwGetTime() - g_lastInput; float hudAlpha = (idle < 10.0) ? 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; }