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

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;
}