Working -- prior to mushroom cloud addition.

This commit is contained in:
The Dust Council 2026-06-01 16:10:04 -07:00
commit 5defe65535
5 changed files with 1147 additions and 0 deletions

View file

@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(make)",
"Bash(make clean *)",
"Bash(./vectordesert --help)",
"Bash(echo \"exit: $?\")",
"Bash(echo \"exit $?\")"
]
}
}

24
Makefile Normal file
View file

@ -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

126
README.md Normal file
View file

@ -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.

986
main.c Normal file
View file

@ -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 <GLFW/glfw3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <ctype.h>
#include <math.h>
#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 v<lo?lo:(v>hi?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<oct;i++){
sum += amp * valnoise(x, y);
norm += amp;
x *= 2.0f; y *= 2.0f; amp *= 0.5f;
}
return sum / norm;
}
/* ------------------------------------------------------------------ */
/* user settings */
/* ------------------------------------------------------------------ */
typedef struct {
float terrainHue; /* 0..1 */
float mountainFreq; /* how often ranges appear (scale) */
float mountainRough; /* 0 = smooth domes .. 1 = jagged peaks */
float mountainMinH; /* height of the low parts of a range */
float mountainMaxH; /* height of the tallest peaks */
float cactusFreq; /* 0..1 probability a cell holds a cactus */
float cactusSizeVar; /* 0..1 random size spread */
float cactusMinSize; /* smallest allowed cactus height */
float cactusMaxSize; /* largest allowed cactus height */
float cactusHue; /* 0..1 */
int maxArms; /* arms on the largest cacti */
} Settings;
static Settings S = {
.terrainHue = 0.075f, /* desert orange/tan */
.mountainFreq = 1.0f,
.mountainRough= 0.5f,
.mountainMinH = 6.0f,
.mountainMaxH = 26.0f,
.cactusFreq = 0.35f,
.cactusSizeVar= 0.55f,
.cactusMinSize= 4.0f,
.cactusMaxSize= 12.0f,
.cactusHue = 0.33f, /* green */
.maxArms = 5,
};
#define ARM_LIMIT 12
/* ------------------------------------------------------------------ */
/* terrain */
/* ------------------------------------------------------------------ */
static float terrainHeight(float x, float z){
/* The default state of the world is a near-flat desert plain with only
* very gentle undulation. */
float plains = (fbm(x*0.02f, z*0.02f, 3) - 0.5f) * 2.0f;
/* Mountain ranges are coherent low-frequency blobs at a *fixed* spatial
* scale (so a range stays a sensibly sized range). The user's
* mountainFreq controls how much of the plain those ranges cover, i.e.
* how frequently the plain is interrupted: higher freq -> lower
* threshold -> ranges appear more often. */
float mask = valnoise(x*0.006f, z*0.006f);
float thr = clampf(0.80f - 0.11f*S.mountainFreq, 0.35f, 0.86f);
float mountainMask = smoothstepf(thr, thr+0.10f, mask);
if(mountainMask <= 0.0f) return plains;
/* Base frequency scaled by range height keeps slopes proportional for
* short and tall ranges alike (no-op at the default max height). */
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<n;i++){
glVertex2f(pen + g[i*4+0]*sc, y + (6.0f - g[i*4+1])*sc);
glVertex2f(pen + g[i*4+2]*sc, y + (6.0f - g[i*4+3])*sc);
}
glEnd();
}
pen += 5.0f*sc;
}
}
/* ------------------------------------------------------------------ */
/* heads-up display */
/* ------------------------------------------------------------------ */
static void drawHUD(int fbw, int fbh, float alpha){
float sc = fbh / 240.0f;
if(sc < 2.0f) sc = 2.0f;
float lineH = 9.0f * sc;
float x = 4.0f * sc;
float y = 4.0f * sc;
/* 2D overlay, no depth */
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[64];
/* title */
glColor4f(0.95f, 0.85f, 0.35f, alpha);
drawText("VECTOR DESERT", x, y, sc*1.1f);
y += lineH*1.6f;
glColor4f(0.55f, 1.0f, 0.75f, alpha);
#define LINE(...) do{ snprintf(buf,sizeof(buf),__VA_ARGS__); \
drawText(buf, x, y, sc); y += lineH; }while(0)
LINE("1/2 TERRAIN HUE %.2f", S.terrainHue);
LINE("3/4 MTN FREQ %.2f", S.mountainFreq);
LINE(",/. MTN ROUGH %.2f", S.mountainRough);
LINE("T/Y MTN MIN H %.0f", S.mountainMinH);
LINE("U/I MTN MAX H %.0f", S.mountainMaxH);
LINE("5/6 CACTUS FREQ %.2f", S.cactusFreq);
LINE("7/8 SIZE VAR %.2f", S.cactusSizeVar);
LINE("J/K MIN SIZE %.1f", S.cactusMinSize);
LINE("N/M MAX SIZE %.1f", S.cactusMaxSize);
LINE("9/0 CACTUS HUE %.2f", S.cactusHue);
LINE("-/= MAX ARMS %d", S.maxArms);
LINE("[/] RENDER DIST %.0f", g_viewRadius);
#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);
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;i<n;i++){
/* tangent at this point */
V3 d;
if(i==0) d = norm3(sub(pts[1], pts[0]));
else if(i==n-1) d = norm3(sub(pts[i], pts[i-1]));
else d = norm3(sub(pts[i+1], pts[i-1]));
/* parallel-transport the frame from prevDir to d */
if(i>0){
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;k<sides;k++){
float a = 2.0f*(float)M_PI * k / sides;
V3 off = add(scl(u, cosf(a)*rad[i]), scl(v, sinf(a)*rad[i]));
ring[k] = add(pts[i], off);
}
batchEnsure(b, sides*4);
for(int k=0;k<sides;k++) /* ring loop */
batchSeg(b, ring[k], ring[(k+1)%sides], col);
if(i>0) /* longitudinal ribs */
for(int k=0;k<sides;k++)
batchSeg(b, prevRing[k], ring[k], col);
for(int k=0;k<sides;k++) prevRing[k] = ring[k];
}
}
/* ------------------------------------------------------------------ */
/* cactus */
/* ------------------------------------------------------------------ */
/* one saguaro arm: starts on the trunk surface, curves outward through a
* quarter circle, then rises straight up to a rounded tip. */
static void appendArm(Batch *b, V3 attach, V3 dir, V3 up, float bendR,
float upLen, float ra, const V3 *col, int sides){
V3 pts[MAX_PTS];
float rad[MAX_PTS];
int n = 0;
const int nb = 6; /* points along the bend */
for(int j=0;j<=nb;j++){
float a = (float)j/nb * (float)M_PI*0.5f;
V3 p = add(attach, add(scl(dir, sinf(a)*bendR),
scl(up, (1.0f-cosf(a))*bendR)));
pts[n] = p; rad[n] = ra; n++;
}
/* straight vertical section */
V3 bendTop = pts[n-1];
const int nu = 5;
for(int j=1;j<=nu;j++){
float t = (float)j/nu;
pts[n] = add(bendTop, scl(up, upLen*t));
/* taper to a rounded tip */
rad[n] = ra * (t < 0.8f ? 1.0f : lerpf(1.0f, 0.18f, (t-0.8f)/0.2f));
n++;
}
appendTube(b, pts, rad, n, sides, col);
}
/* build one saguaro into the batch. `dist` is its distance from the camera,
* used to drop ring facets (level of detail) on far-off cacti. */
static void buildCactus(Batch *b, float wx, float wz, float dist){
/* deterministic per-cell randomness */
int cx = (int)floorf(wx);
int cz = (int)floorf(wz);
uint32_t hh = hashi(cx*3, cz*7);
float r0 = ((hh ) & 0xffff) / (float)0xffff;
/* size: variation spreads around the midpoint of the user's min/max
* bounds, so the result is always constrained to [minSize, maxSize]. */
float mid = (S.cactusMinSize + S.cactusMaxSize) * 0.5f;
float spread = (S.cactusMaxSize - S.cactusMinSize) * 0.5f * S.cactusSizeVar;
float minH = mid - spread;
float maxH = mid + spread;
float H = lerpf(minH, maxH, r0);
/* normalized size -> 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<nT;i++){
float t = (float)i/(nT-1);
tpts[i] = add(base, scl(up, t*H));
float r = rt * (1.0f - 0.20f*t); /* gentle taper */
if(t > 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<arms;i++){
uint32_t ah = hashi(cx*13 + i*101, cz*17 - i*53);
float a0 = ((ah ) & 0xffff)/(float)0xffff;
float a1 = ((ah >> 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<nx;j++){
float z = oz + j*cell;
for(int i=0;i<nx;i++){
float x = ox + i*cell;
float h = terrainHeight(x, z);
int id = j*nx + i;
hgt[id] = h;
float bright = 0.30f + clampf(h/fmaxf(1.0f,S.mountainMaxH), 0.0f, 1.0f)*0.55f;
V3 col = hsv2rgb(S.terrainHue, 0.55f, bright);
cr[id]=col.x; cg[id]=col.y; cb[id]=col.z;
}
}
/* emit shared grid edges (east + north neighbour of each vertex) */
for(int j=0;j<nx;j++){
float z = oz + j*cell;
for(int i=0;i<nx;i++){
float x = ox + i*cell;
float dx = x-cx, dz = z-cz;
if(dx*dx + dz*dz > 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<ARM_LIMIT) S.maxArms++; printSettings(); break;
case GLFW_KEY_LEFT_BRACKET: g_viewRadius = clampf(g_viewRadius-16.0f, 40.0f, 960.0f); printSettings(); break;
case GLFW_KEY_RIGHT_BRACKET: g_viewRadius = clampf(g_viewRadius+16.0f, 40.0f, 960.0f); printSettings(); break;
case GLFW_KEY_COMMA: S.mountainRough = clampf(S.mountainRough-0.05f,0.0f,1.0f); printSettings(); break;
case GLFW_KEY_PERIOD: S.mountainRough = clampf(S.mountainRough+0.05f,0.0f,1.0f); printSettings(); break;
case GLFW_KEY_T: S.mountainMinH = clampf(S.mountainMinH-1.0f, 0.0f, S.mountainMaxH); printSettings(); break;
case GLFW_KEY_Y: S.mountainMinH = clampf(S.mountainMinH+1.0f, 0.0f, S.mountainMaxH); printSettings(); break;
case GLFW_KEY_U: S.mountainMaxH = clampf(S.mountainMaxH-1.0f, S.mountainMinH, 80.0f); printSettings(); break;
case GLFW_KEY_I: S.mountainMaxH = clampf(S.mountainMaxH+1.0f, S.mountainMinH, 80.0f); printSettings(); break;
case GLFW_KEY_J: S.cactusMinSize = clampf(S.cactusMinSize-0.5f, 1.0f, S.cactusMaxSize); printSettings(); break;
case GLFW_KEY_K: S.cactusMinSize = clampf(S.cactusMinSize+0.5f, 1.0f, S.cactusMaxSize); printSettings(); break;
case GLFW_KEY_N: S.cactusMaxSize = clampf(S.cactusMaxSize-0.5f, S.cactusMinSize, 30.0f); printSettings(); break;
case GLFW_KEY_M: S.cactusMaxSize = clampf(S.cactusMaxSize+0.5f, S.cactusMinSize, 30.0f); printSettings(); break;
default: break;
}
}
static void fbSizeCB(GLFWwindow *w, int width, int height){
(void)w;
glViewport(0, 0, width, height);
}
/* ------------------------------------------------------------------ */
/* command line */
/* ------------------------------------------------------------------ */
static bool argval(const char *arg, const char *key, float *out){
size_t n = strlen(key);
if(strncmp(arg, key, n)==0 && arg[n]=='='){
*out = (float)atof(arg+n+1);
return true;
}
return false;
}
static void usage(const char *prog){
printf("usage: %s [--fullscreen] [options]\n", prog);
printf(" --terrain-hue=0..1 --mountain-freq=0.2..4\n");
printf(" --mountain-rough=0..1\n");
printf(" --mountain-min-height=0..80 --mountain-max-height=0..80\n");
printf(" --cactus-freq=0..1\n");
printf(" --cactus-size-var=0..0.95 --cactus-hue=0..1\n");
printf(" --cactus-min-size=1..30 --cactus-max-size=1..30\n");
printf(" --max-arms=0..%d\n", ARM_LIMIT);
}
int main(int argc, char **argv){
bool startFull = false;
float fv;
for(int i=1;i<argc;i++){
if(strcmp(argv[i],"--fullscreen")==0) startFull = true;
else if(strcmp(argv[i],"--help")==0 || strcmp(argv[i],"-h")==0){ usage(argv[0]); return 0; }
else if(argval(argv[i],"--terrain-hue",&fv)) S.terrainHue = clampf(fv,0,1);
else if(argval(argv[i],"--mountain-freq",&fv)) S.mountainFreq = clampf(fv,0.2f,4.0f);
else if(argval(argv[i],"--mountain-rough",&fv)) S.mountainRough= clampf(fv,0.0f,1.0f);
else if(argval(argv[i],"--mountain-min-height",&fv)) S.mountainMinH= clampf(fv,0.0f,80.0f);
else if(argval(argv[i],"--mountain-max-height",&fv)) S.mountainMaxH= clampf(fv,0.0f,80.0f);
else if(argval(argv[i],"--cactus-freq",&fv)) S.cactusFreq = clampf(fv,0,1);
else if(argval(argv[i],"--cactus-size-var",&fv)) S.cactusSizeVar= clampf(fv,0,0.95f);
else if(argval(argv[i],"--cactus-min-size",&fv)) S.cactusMinSize= clampf(fv,1.0f,30.0f);
else if(argval(argv[i],"--cactus-max-size",&fv)) S.cactusMaxSize= clampf(fv,1.0f,30.0f);
else if(argval(argv[i],"--cactus-hue",&fv)) S.cactusHue = clampf(fv,0,1);
else if(argval(argv[i],"--max-arms",&fv)) S.maxArms = (int)clampf(fv,0,ARM_LIMIT);
else { fprintf(stderr,"unknown arg: %s\n", argv[i]); usage(argv[0]); return 1; }
}
if(S.cactusMinSize > 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;
}

BIN
vectordesert Executable file

Binary file not shown.