From 5defe6553595881da6feadc7bbe114259473f59e Mon Sep 17 00:00:00 2001 From: The Dust Council Date: Mon, 1 Jun 2026 16:10:04 -0700 Subject: [PATCH] Working -- prior to mushroom cloud addition. --- .claude/settings.local.json | 11 + Makefile | 24 + README.md | 126 +++++ main.c | 986 ++++++++++++++++++++++++++++++++++++ vectordesert | Bin 0 -> 50384 bytes 5 files changed, 1147 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.c create mode 100755 vectordesert diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..82b0a5b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(make)", + "Bash(make clean *)", + "Bash(./vectordesert --help)", + "Bash(echo \"exit: $?\")", + "Bash(echo \"exit $?\")" + ] + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d01756e --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra +PKGCFG := $(shell pkg-config --cflags --libs glfw3 2>/dev/null) + +# Fallbacks if pkg-config has no glfw3 entry +ifeq ($(strip $(PKGCFG)),) +PKGCFG := -lglfw +endif + +# OpenGL: Linux uses -lGL, macOS uses the framework +UNAME := $(shell uname -s) +ifeq ($(UNAME),Darwin) +GL := -framework OpenGL +else +GL := -lGL +endif + +vectordesert: main.c + $(CC) $(CFLAGS) -o $@ main.c $(PKGCFG) $(GL) -lm + +clean: + rm -f vectordesert + +.PHONY: clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..8aa15f8 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# vectordesert + +An infinite wire-mesh vector desert flythrough, written in C with OpenGL +(GLFW). A vast, mostly-flat wireframe plain rolls past, interrupted now and +then by vector mountain ranges, with saguaro cacti scattered across both. +The terrain is built from **square wire-mesh frames**; the saguaros are +ribbed wire-mesh tubes shaped to resemble real cacti (tapered, dome-tipped +trunks with arms that curve out and sweep upward). + +## Build + +Requires a C compiler, GLFW 3, and OpenGL. + +Debian/Ubuntu: +``` +sudo apt install build-essential libglfw3-dev +``` +Fedora: +``` +sudo dnf install glfw-devel +``` +macOS (Homebrew): +``` +brew install glfw +``` + +Then: +``` +make +``` + +## Run + +``` +./vectordesert # windowed +./vectordesert --fullscreen # full screen +``` + +### Command-line settings +``` +--terrain-hue=0..1 hue of the terrain mesh (default 0.075) +--mountain-freq=0.2..4 how often the plain is interrupted by a range + (low = rare; high = frequent) (default 1.0) +--mountain-rough=0..1 flat-topped mesas .. jagged peaks (default 0.5) +--mountain-min-height=0..80 height of a range's low parts (default 6) +--mountain-max-height=0..80 height of the tallest peaks (default 26) +--cactus-freq=0..1 chance of a cactus per cell (default 0.35) +--cactus-size-var=0..0.95 random cactus size spread (default 0.55) +--cactus-min-size=1..30 smallest cactus height (default 4) +--cactus-max-size=1..30 largest cactus height (default 12) +--cactus-hue=0..1 hue of the cacti (default 0.33) +--max-arms=0..12 arms on the largest cacti (default 5) +``` +Example: +``` +./vectordesert --fullscreen --terrain-hue=0.55 --mountain-freq=1.8 \ + --cactus-hue=0.30 --cactus-freq=0.5 --max-arms=8 +``` + +### Camera +| keys | does | +|------|------| +| `W` / `S` | throttle: accelerate / decelerate forward speed (holds when released; `S` past zero goes into reverse) | +| `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 | + +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 +travelling. To steer, turn and then apply throttle. + +The terrain is generated around the camera and cached — rebuilt only as you +travel out of the cached region — so the plain extends infinitely in every +direction. Rendering distance (`[` / `]`) ranges from 40 up to 960 units. + +### Live setting controls +| keys | adjusts | +|------|---------| +| `1` / `2` | terrain hue | +| `3` / `4` | mountain range frequency | +| `,` / `.` | mountain roughness (flat mesas ↔ jagged peaks) | +| `T` / `Y` | minimum mountain range height | +| `U` / `I` | maximum mountain range height | +| `5` / `6` | cactus frequency | +| `7` / `8` | cactus size variation | +| `J` / `K` | minimum cactus size | +| `N` / `M` | maximum cactus size | +| `9` / `0` | cactus hue | +| `-` / `=` | maximum cactus arms | +| `F` | toggle fullscreen | +| `ESC` | quit | + +Current settings are printed to the terminal as you change them, and an +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. + +## 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. +- Distance fog fades distant geometry into the dusk sky for depth. + +## Performance + +The renderer is built for throughput at large draw distances: + +- All wireframe segments for a frame are collected into one vertex/colour + array and drawn with a single `glDrawArrays(GL_LINES)` per mesh, instead + of per-object immediate-mode `glBegin`/`glEnd`. +- Terrain and cactus meshes are **cached** and only rebuilt when a relevant + setting changes or the camera leaves the cached region; most frames do no + geometry work at all. +- Terrain uses a **cell budget** (level of detail): the grid spacing grows + with the rendering distance, so the cell count stays bounded as the radius + scales up. +- Each terrain vertex's height is computed once and shared by its edges (no + redundant noise evaluations). +- Cacti render out to the full rendering distance (like the terrain), but + drop ring facets and trunk segments with distance so far-off ones stay + cheap. The distance fade is done by **hardware fog** rather than the CPU. diff --git a/main.c b/main.c new file mode 100644 index 0000000..3442bde --- /dev/null +++ b/main.c @@ -0,0 +1,986 @@ +/* + * 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;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). */ + 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*/ + 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); + + /* 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); + + /* mountainRough morphs flat-topped mesas (0) into jagged peaks (1) */ + float ridge = lerpf(mesa, jagged, S.mountainRough); + + /* 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; +} + +/* ------------------------------------------------------------------ */ +/* 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) */ +#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; +} + +/* ------------------------------------------------------------------ */ +/* 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;i 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; + 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) */ + +static void toggleFullscreen(GLFWwindow *w){ + g_fullscreen = !g_fullscreen; + if(g_fullscreen){ + glfwGetWindowPos(w, &g_winX, &g_winY); + glfwGetWindowSize(w, &g_winW, &g_winH); + GLFWmonitor *m = glfwGetPrimaryMonitor(); + const GLFWvidmode *vm = glfwGetVideoMode(m); + glfwSetWindowMonitor(w, m, 0, 0, vm->width, 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_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("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 */ + 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; + + 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); + + /* 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); + + glfwSwapBuffers(win); + glfwPollEvents(); + } + + printf("\n"); + glfwDestroyWindow(win); + glfwTerminate(); + return 0; +} diff --git a/vectordesert b/vectordesert new file mode 100755 index 0000000000000000000000000000000000000000..68e1f541d5b0592b86c5a03e84d6686ce66f2176 GIT binary patch literal 50384 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

J--->2~kYFsTe>Me87kM(I~Gl0fLm`o&C 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