1607 lines
69 KiB
C
1607 lines
69 KiB
C
/*
|
|
* Vectorgons — a starfield simulator of colorful vector-drawn platonic
|
|
* solids tumbling through space toward the camera.
|
|
*
|
|
* C + OpenGL (legacy immediate mode for true wireframe "vector" rendering)
|
|
* Windowing & input via GLFW3, perspective via GLU.
|
|
*
|
|
* Features:
|
|
* - 100+ shape types streaming at the camera: the 5 platonic solids,
|
|
* archimedeans, prisms, antiprisms, bipyramids, trapezohedra, the
|
|
* many-faced solids (tri/tetra/.../hecato-hedron), star polygons and the
|
|
* unicursal hexagram, googie / atomic-age starbursts, 2-D and 3-D symbols
|
|
* (smiley, biohazard, peace, cross, ? ! # $ £), 4/5/6-D polytopes
|
|
* (tesseract, penteract, 24-cell, ...) that morph as they tumble, plus
|
|
* randomly generated 3-D convex-hull asteroids
|
|
* - WASD pans the camera and the arrow keys rotate it
|
|
* - User-settable render distance (how far away solids spawn)
|
|
* - Random per-body sizes (user-settable min / max)
|
|
* - Guaranteed no overlap between solids (bounding-sphere rejection)
|
|
* - Adjustable tumble rate AND tumble-speed variance
|
|
* - Adjustable CRT-style glow / light bleed
|
|
* - Adjustable vector flicker
|
|
* - On-screen display (self-contained vector font) that fades after idle
|
|
* - Single-hue and multicolor modes; adjustable hue + continuous hue cycling
|
|
* - Fullscreen toggle
|
|
* - Settings auto-saved to ~/.vectorgons on exit, reloaded as defaults
|
|
*
|
|
* All controls are listed in the on-screen display (and printed at startup).
|
|
*/
|
|
|
|
#define GL_SILENCE_DEPRECATION
|
|
#include <GLFW/glfw3.h>
|
|
#include <GL/glu.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
|
|
/* ================================================================== */
|
|
/* Geometry: an N-dimensional polytope engine (3..6 dimensions). */
|
|
/* */
|
|
/* Every shape stores vertices in up to MAXD dimensions plus an edge */
|
|
/* list. 3-D shapes (platonic solids, archimedeans, prisms, stars) */
|
|
/* are drawn directly; 4/5/6-D polytopes (tesseract, 24-cell, ...) */
|
|
/* are rotated in their own dimension and perspective-projected down */
|
|
/* to 3-D every frame, so they morph like true hyperobjects while */
|
|
/* also tumbling in 3-D. */
|
|
/* */
|
|
/* For regular figures, edges are derived automatically: connect */
|
|
/* every vertex pair at the shared minimum distance. Families with */
|
|
/* non-uniform edges (prisms, stars, ...) set edges explicitly. */
|
|
/* ================================================================== */
|
|
|
|
#define MAXD 6
|
|
#define MAX_VERTS 128
|
|
#define MAX_EDGES 512
|
|
#define MAX_SHAPES 160
|
|
#define TAU 6.283185307179586
|
|
|
|
typedef struct {
|
|
int dim; /* 3..6 */
|
|
int nv;
|
|
float v[MAX_VERTS][MAXD];
|
|
int ne;
|
|
int e[MAX_EDGES][2];
|
|
} Solid;
|
|
|
|
static Solid solids[MAX_SHAPES];
|
|
static int num_shapes = 0;
|
|
|
|
static float frand(void); /* uniform [0,1); defined below */
|
|
|
|
static Solid *new_solid(int dim) {
|
|
Solid *s = &solids[num_shapes++];
|
|
s->dim = dim; s->nv = 0; s->ne = 0;
|
|
return s;
|
|
}
|
|
static void PV(Solid *s, double a,double b,double c,double d,double e,double f) {
|
|
if (s->nv >= MAX_VERTS) return;
|
|
float *p = s->v[s->nv++];
|
|
p[0]=(float)a; p[1]=(float)b; p[2]=(float)c; p[3]=(float)d; p[4]=(float)e; p[5]=(float)f;
|
|
}
|
|
static void AE(Solid *s, int i, int j) {
|
|
if (s->ne >= MAX_EDGES) return;
|
|
s->e[s->ne][0]=i; s->e[s->ne][1]=j; s->ne++;
|
|
}
|
|
static void AE_unique(Solid *s, int i, int j) {
|
|
if (i > j) { int t = i; i = j; j = t; }
|
|
for (int k = 0; k < s->ne; k++)
|
|
if (s->e[k][0] == i && s->e[k][1] == j) return;
|
|
AE(s, i, j);
|
|
}
|
|
|
|
/* Center on the centroid and scale so the farthest vertex sits on the
|
|
* unit sphere (radius == 1, in `dim` dimensions). */
|
|
static void center_normalize(Solid *s) {
|
|
double cen[MAXD] = {0,0,0,0,0,0};
|
|
for (int i = 0; i < s->nv; i++)
|
|
for (int k = 0; k < s->dim; k++) cen[k] += s->v[i][k];
|
|
for (int k = 0; k < s->dim; k++) cen[k] /= (s->nv > 0 ? s->nv : 1);
|
|
|
|
double maxr2 = 0;
|
|
for (int i = 0; i < s->nv; i++) {
|
|
double r2 = 0;
|
|
for (int k = 0; k < s->dim; k++) { s->v[i][k] -= cen[k]; r2 += (double)s->v[i][k]*s->v[i][k]; }
|
|
if (r2 > maxr2) maxr2 = r2;
|
|
}
|
|
double inv = maxr2 > 1e-12 ? 1.0/sqrt(maxr2) : 1.0;
|
|
for (int i = 0; i < s->nv; i++)
|
|
for (int k = 0; k < s->dim; k++) s->v[i][k] *= inv;
|
|
}
|
|
|
|
/* Derive edges from the minimum pairwise distance (regular figures). */
|
|
static void derive_edges(Solid *s) {
|
|
s->ne = 0;
|
|
double mind2 = 1e18;
|
|
for (int i = 0; i < s->nv; i++)
|
|
for (int j = i+1; j < s->nv; j++) {
|
|
double d2 = 0;
|
|
for (int k = 0; k < s->dim; k++) { double dd = s->v[i][k]-s->v[j][k]; d2 += dd*dd; }
|
|
if (d2 < mind2) mind2 = d2;
|
|
}
|
|
double tol2 = mind2 * 1.04; /* ~2% slack on length */
|
|
for (int i = 0; i < s->nv; i++)
|
|
for (int j = i+1; j < s->nv; j++) {
|
|
double d2 = 0;
|
|
for (int k = 0; k < s->dim; k++) { double dd = s->v[i][k]-s->v[j][k]; d2 += dd*dd; }
|
|
if (d2 <= tol2) AE(s, i, j);
|
|
}
|
|
}
|
|
|
|
/* ---- generic N-dimensional regular polytopes ---------------------- */
|
|
|
|
static void gen_hypercube(int d) { /* {4,3..} measure polytope */
|
|
Solid *s = new_solid(d);
|
|
int N = 1 << d;
|
|
for (int m = 0; m < N; m++) {
|
|
double c[MAXD] = {0,0,0,0,0,0};
|
|
for (int k = 0; k < d; k++) c[k] = (m >> k & 1) ? 1 : -1;
|
|
PV(s, c[0],c[1],c[2],c[3],c[4],c[5]);
|
|
}
|
|
center_normalize(s); derive_edges(s);
|
|
}
|
|
static void gen_orthoplex(int d) { /* cross-polytope */
|
|
Solid *s = new_solid(d);
|
|
for (int k = 0; k < d; k++) {
|
|
double c[MAXD] = {0,0,0,0,0,0};
|
|
c[k] = 1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]);
|
|
c[k] = -1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]);
|
|
}
|
|
center_normalize(s); derive_edges(s);
|
|
}
|
|
static void gen_simplex(int nv) { /* regular (nv-1)-simplex */
|
|
Solid *s = new_solid(nv); /* basis e_i in R^nv */
|
|
for (int i = 0; i < nv; i++) {
|
|
double c[MAXD] = {0,0,0,0,0,0};
|
|
c[i] = 1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]);
|
|
}
|
|
center_normalize(s); derive_edges(s); /* -> complete graph */
|
|
}
|
|
static void gen_24cell(void) { /* perms of (+-1,+-1,0,0) */
|
|
Solid *s = new_solid(4);
|
|
for (int a = 0; a < 4; a++)
|
|
for (int b = a+1; b < 4; b++)
|
|
for (int sa = -1; sa <= 1; sa += 2)
|
|
for (int sb = -1; sb <= 1; sb += 2) {
|
|
double c[MAXD] = {0,0,0,0,0,0};
|
|
c[a]=sa; c[b]=sb; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]);
|
|
}
|
|
center_normalize(s); derive_edges(s);
|
|
}
|
|
|
|
/* ---- curated 3-D solids ------------------------------------------- */
|
|
|
|
static void gen_cuboctahedron(void) { /* perms of (+-1,+-1,0) */
|
|
Solid *s = new_solid(3);
|
|
for (int a = 0; a < 3; a++)
|
|
for (int b = a+1; b < 3; b++)
|
|
for (int sa = -1; sa <= 1; sa += 2)
|
|
for (int sb = -1; sb <= 1; sb += 2) {
|
|
double c[3] = {0,0,0};
|
|
c[a]=sa; c[b]=sb; PV(s,c[0],c[1],c[2],0,0,0);
|
|
}
|
|
center_normalize(s); derive_edges(s);
|
|
}
|
|
static void gen_truncated_octahedron(void) { /* perms of (0,+-1,+-2) */
|
|
Solid *s = new_solid(3);
|
|
int perm[6][3] = {{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}};
|
|
for (int p = 0; p < 6; p++)
|
|
for (int s1 = -1; s1 <= 1; s1 += 2)
|
|
for (int s2 = -1; s2 <= 1; s2 += 2) {
|
|
double c[3] = {0,0,0};
|
|
c[perm[p][1]] = s1 * 1.0; /* axis carrying value 1 */
|
|
c[perm[p][2]] = s2 * 2.0; /* axis carrying value 2 */
|
|
PV(s, c[0],c[1],c[2],0,0,0);
|
|
}
|
|
center_normalize(s); derive_edges(s);
|
|
}
|
|
static void gen_stella_octangula(void) { /* two interlocked tetrahedra */
|
|
Solid *s = new_solid(3);
|
|
for (int m = 0; m < 8; m++)
|
|
PV(s, (m&1)?1:-1, (m&2)?1:-1, (m&4)?1:-1, 0,0,0);
|
|
center_normalize(s);
|
|
/* connect vertices whose minus-bit parity matches (each tetra) */
|
|
for (int i = 0; i < 8; i++)
|
|
for (int j = i+1; j < 8; j++) {
|
|
int pi = __builtin_popcount(i & 7) & 1;
|
|
int pj = __builtin_popcount(j & 7) & 1;
|
|
if (pi == pj) AE(s, i, j);
|
|
}
|
|
}
|
|
|
|
/* ---- parametric 3-D families -------------------------------------- */
|
|
|
|
static void gen_prism(int n) {
|
|
Solid *s = new_solid(3);
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 1, 0,0,0); }
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), -1, 0,0,0); }
|
|
for (int i = 0; i < n; i++) {
|
|
AE(s, i, (i+1)%n);
|
|
AE(s, n+i, n+(i+1)%n);
|
|
AE(s, i, n+i);
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
static void gen_antiprism(int n) {
|
|
Solid *s = new_solid(3);
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 1, 0,0,0); }
|
|
for (int i = 0; i < n; i++) { double a = TAU*(i+0.5)/n; PV(s, cos(a), sin(a), -1, 0,0,0); }
|
|
for (int i = 0; i < n; i++) {
|
|
AE(s, i, (i+1)%n);
|
|
AE(s, n+i, n+(i+1)%n);
|
|
AE(s, i, n+i);
|
|
AE(s, i, n+(i+n-1)%n);
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
static void gen_bipyramid(int n) {
|
|
Solid *s = new_solid(3);
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0, 0,0,0); }
|
|
PV(s, 0,0, 1.25, 0,0,0); /* apex top = index n */
|
|
PV(s, 0,0,-1.25, 0,0,0); /* apex bottom = index n+1 */
|
|
for (int i = 0; i < n; i++) {
|
|
AE(s, i, (i+1)%n);
|
|
AE(s, n, i);
|
|
AE(s, n+1, i);
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
static void gen_star(int n, int k) { /* star polygon {n/k} */
|
|
Solid *s = new_solid(3);
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0, 0,0,0); }
|
|
for (int i = 0; i < n; i++) AE(s, i, (i+k)%n);
|
|
center_normalize(s);
|
|
}
|
|
/* n-gonal trapezohedron (dual of an antiprism): two staggered rings wired
|
|
* in a zig-zag equator and capped by two apexes -> 2n kite faces. Used for
|
|
* the larger many-faced solids (24-, 30-, 60-hedra). */
|
|
static void gen_trapezohedron(int n) {
|
|
Solid *s = new_solid(3);
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0.45, 0,0,0); }
|
|
for (int i = 0; i < n; i++) { double a = TAU*(i+0.5)/n; PV(s, cos(a), sin(a), -0.45, 0,0,0); }
|
|
int top = 2*n, bot = 2*n + 1;
|
|
PV(s, 0,0, 1.5, 0,0,0); /* top apex = 2n */
|
|
PV(s, 0,0, -1.5, 0,0,0); /* bottom apex = 2n+1 */
|
|
for (int i = 0; i < n; i++) {
|
|
AE(s, i, n + i); /* upper ring -> staggered lower ring */
|
|
AE(s, n + i, (i + 1) % n); /* lower ring -> next upper-ring vertex */
|
|
AE(s, top, i); /* top apex -> upper ring */
|
|
AE(s, bot, n + i); /* bottom apex-> lower ring */
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
/* Unicursal hexagram: the six-pointed star drawn as a single closed stroke,
|
|
* the way the occult symbol actually is. Unlike the Star of David ({6/2}, two
|
|
* separate triangles), a regular hexagram cannot be traced in one pen-stroke,
|
|
* so this visits the six tips in the order 90 -> 210 -> 330 -> 270 -> 30 -> 150
|
|
* degrees: the long 120-degree chords cross through the interior and weave the
|
|
* star, leaving the figure 2-fold symmetric like the genuine emblem. */
|
|
static void gen_unicursal_hexagram(void) {
|
|
Solid *s = new_solid(3);
|
|
const int order[6] = { 90, 210, 330, 270, 30, 150 }; /* tip angles, path order */
|
|
for (int i = 0; i < 6; i++) {
|
|
double a = order[i] * TAU / 360.0;
|
|
PV(s, cos(a), sin(a), 0, 0,0,0);
|
|
}
|
|
for (int i = 0; i < 6; i++) AE(s, i, (i + 1) % 6); /* one closed loop */
|
|
center_normalize(s);
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Symbols & signs: a small 2-D stroke builder that finalizes either */
|
|
/* flat (depth 0) or extruded into a 3-D "wireframe prism" (depth > 0): */
|
|
/* a front layer at +z, a back layer at -z, and rungs joining them. */
|
|
/* ================================================================== */
|
|
|
|
typedef struct {
|
|
float px[MAX_VERTS], py[MAX_VERTS];
|
|
int ea[MAX_EDGES], eb[MAX_EDGES];
|
|
int np, ne;
|
|
} SymB;
|
|
|
|
static void sym_init(SymB *b) { b->np = 0; b->ne = 0; }
|
|
static int sym_pt(SymB *b, double x, double y) {
|
|
if (b->np >= MAX_VERTS) return b->np - 1;
|
|
b->px[b->np] = (float)x; b->py[b->np] = (float)y; return b->np++;
|
|
}
|
|
static void sym_edge(SymB *b, int i, int j) {
|
|
if (b->ne >= MAX_EDGES || i < 0 || j < 0 || i == j) return;
|
|
b->ea[b->ne] = i; b->eb[b->ne] = j; b->ne++;
|
|
}
|
|
static void sym_line(SymB *b, double x0,double y0,double x1,double y1) {
|
|
sym_edge(b, sym_pt(b,x0,y0), sym_pt(b,x1,y1));
|
|
}
|
|
static void sym_ring(SymB *b, double cx,double cy,double r,int seg) { /* closed circle */
|
|
int first = -1, prev = -1;
|
|
for (int i = 0; i < seg; i++) {
|
|
double a = TAU * i / seg;
|
|
int idx = sym_pt(b, cx + r*cos(a), cy + r*sin(a));
|
|
if (first < 0) first = idx;
|
|
sym_edge(b, prev, idx);
|
|
prev = idx;
|
|
}
|
|
sym_edge(b, prev, first);
|
|
}
|
|
static void sym_arc(SymB *b, double cx,double cy,double r,double a0,double a1,int seg) {
|
|
int prev = -1; /* open arc */
|
|
for (int i = 0; i <= seg; i++) {
|
|
double a = a0 + (a1 - a0) * i / seg;
|
|
int idx = sym_pt(b, cx + r*cos(a), cy + r*sin(a));
|
|
sym_edge(b, prev, idx);
|
|
prev = idx;
|
|
}
|
|
}
|
|
static void sym_poly(SymB *b, const double *xy, int n, int closed) {
|
|
int first = -1, prev = -1;
|
|
for (int i = 0; i < n; i++) {
|
|
int idx = sym_pt(b, xy[2*i], xy[2*i+1]);
|
|
if (first < 0) first = idx;
|
|
sym_edge(b, prev, idx);
|
|
prev = idx;
|
|
}
|
|
if (closed) sym_edge(b, prev, first);
|
|
}
|
|
static double sym_deg(double d) { return d * TAU / 360.0; }
|
|
|
|
/* Turn the accumulated strokes into a Solid: flat if depth<=0, else extruded. */
|
|
static void sym_finish(SymB *b, float depth) {
|
|
Solid *s = new_solid(3);
|
|
if (depth <= 0.0f) {
|
|
for (int k = 0; k < b->np; k++) PV(s, b->px[k], b->py[k], 0, 0,0,0);
|
|
for (int k = 0; k < b->ne; k++) AE(s, b->ea[k], b->eb[k]);
|
|
} else {
|
|
int n = b->np;
|
|
for (int k = 0; k < n; k++) PV(s, b->px[k], b->py[k], depth, 0,0,0); /* front */
|
|
for (int k = 0; k < n; k++) PV(s, b->px[k], b->py[k], -depth, 0,0,0); /* back */
|
|
for (int k = 0; k < b->ne; k++) {
|
|
AE(s, b->ea[k], b->eb[k]);
|
|
AE(s, n + b->ea[k], n + b->eb[k]);
|
|
}
|
|
for (int k = 0; k < n; k++) AE(s, k, n + k); /* rungs */
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
|
|
/* ---- individual symbol strokes (drawn roughly within [-1,1]) ------- */
|
|
|
|
static void build_smiley(SymB *b) {
|
|
sym_ring(b, 0.0, 0.0, 1.0, 18); /* face */
|
|
sym_ring(b, -0.35, 0.32, 0.13, 6); /* l eye */
|
|
sym_ring(b, 0.35, 0.32, 0.13, 6); /* r eye */
|
|
sym_arc (b, 0.0, 0.10, 0.55, sym_deg(200), sym_deg(340), 8); /* smile */
|
|
}
|
|
static void build_biohazard(SymB *b) {
|
|
sym_ring(b, 0.0, 0.0, 0.24, 8); /* central circle */
|
|
for (int k = 0; k < 3; k++) { /* three broken rings, gaps facing center */
|
|
double th = sym_deg(90 + 120*k);
|
|
double cx = 0.58*cos(th), cy = 0.58*sin(th);
|
|
double g = sym_deg(90 + 120*k + 180); /* gap direction (toward center) */
|
|
sym_arc(b, cx, cy, 0.44, g + sym_deg(38), g + sym_deg(322), 9);
|
|
}
|
|
}
|
|
static void build_peace(SymB *b) {
|
|
sym_ring(b, 0.0, 0.0, 1.0, 18);
|
|
int c = sym_pt(b, 0.0, 0.0);
|
|
const double ang[4] = {90, 270, 225, 315}; /* up, down, lower-left, lower-right */
|
|
for (int i = 0; i < 4; i++) {
|
|
double a = sym_deg(ang[i]);
|
|
sym_edge(b, c, sym_pt(b, cos(a), sin(a)));
|
|
}
|
|
}
|
|
static void build_cross(SymB *b) { /* Latin cross outline */
|
|
const double p[] = {
|
|
-0.22, 1.00, 0.22, 1.00, 0.22, 0.45, 0.60, 0.45,
|
|
0.60, 0.00, 0.22, 0.00, 0.22,-1.00, -0.22,-1.00,
|
|
-0.22, 0.00, -0.60, 0.00, -0.60, 0.45, -0.22, 0.45 };
|
|
sym_poly(b, p, 12, 1);
|
|
}
|
|
static void build_question(SymB *b) {
|
|
sym_arc(b, 0.0, 0.42, 0.42, sym_deg(200), sym_deg(-30), 10); /* top hook */
|
|
int last = b->np - 1;
|
|
int t1 = sym_pt(b, 0.0, -0.05); sym_edge(b, last, t1); /* tail curls in */
|
|
int t2 = sym_pt(b, 0.0, -0.35); sym_edge(b, t1, t2);
|
|
sym_ring(b, 0.0, -0.72, 0.12, 6); /* dot */
|
|
}
|
|
static void build_exclaim(SymB *b) {
|
|
const double bar[] = { -0.12, 1.0, 0.12, 1.0, 0.06, -0.20, -0.06, -0.20 };
|
|
sym_poly(b, bar, 4, 1); /* tapered stroke */
|
|
sym_ring(b, 0.0, -0.62, 0.13, 6); /* dot */
|
|
}
|
|
static void build_hash(SymB *b) { /* # — two verticals, two horizontals */
|
|
sym_line(b, -0.32, 0.85, -0.20, -0.85);
|
|
sym_line(b, 0.20, 0.85, 0.32, -0.85);
|
|
sym_line(b, -0.85, 0.32, 0.85, 0.32);
|
|
sym_line(b, -0.85, -0.32, 0.85, -0.32);
|
|
}
|
|
static void build_dollar(SymB *b) { /* $ — S-curve with a vertical bar through it */
|
|
const double S[] = {
|
|
0.80, 0.64, 0.40, 0.96, -0.40, 0.96, -0.80, 0.64,
|
|
-0.40, 0.00, 0.40, 0.00, 0.80,-0.32, 0.40,-0.96,
|
|
-0.40,-0.96, -0.80,-0.64 };
|
|
sym_poly(b, S, 10, 0);
|
|
sym_line(b, 0.0, 1.18, 0.0, -1.18);
|
|
}
|
|
static void build_sterling(SymB *b) { /* £ — hooked top, stem, crossbar, base */
|
|
sym_arc(b, 0.05, 0.45, 0.42, sym_deg(8), sym_deg(200), 9);
|
|
int last = b->np - 1;
|
|
int s1 = sym_pt(b, -0.33, -0.58); sym_edge(b, last, s1); /* stem down */
|
|
int base0 = sym_pt(b, -0.52, -0.62);
|
|
int base1 = sym_pt(b, 0.52, -0.62);
|
|
sym_edge(b, s1, base0); sym_edge(b, base0, base1); /* base */
|
|
sym_line(b, -0.33, -0.05, 0.42, -0.05); /* crossbar */
|
|
}
|
|
|
|
/* ---- googie / atomic-age starbursts -------------------------------- */
|
|
|
|
/* Flat multi-point sparkle (the bowling-alley twinkle): sharp tips at radius
|
|
* r1 (or alternating r1/r2) joined through deep notches at radius `inner`. */
|
|
static void build_sparkle(SymB *b, int tips, double r1, double r2, double inner) {
|
|
int first = -1, prev = -1;
|
|
for (int i = 0; i < tips; i++) {
|
|
double ta = TAU*0.25 + TAU*i/tips; /* first tip points up */
|
|
double na = ta + TAU/(2*tips);
|
|
double tr = (r2 > 0 && (i & 1)) ? r2 : r1;
|
|
int t = sym_pt(b, tr*cos(ta), tr*sin(ta));
|
|
if (first < 0) first = t;
|
|
sym_edge(b, prev, t); prev = t;
|
|
int n = sym_pt(b, inner*cos(na), inner*sin(na));
|
|
sym_edge(b, prev, n); prev = n;
|
|
}
|
|
sym_edge(b, prev, first);
|
|
}
|
|
/* Flat atomic burst: spokes of alternating length from a hub, with little
|
|
* "electron" rings capping the long ones. */
|
|
static void build_atomic(SymB *b) {
|
|
int c = sym_pt(b, 0.0, 0.0);
|
|
int n = 12;
|
|
for (int i = 0; i < n; i++) {
|
|
double a = TAU*i/n;
|
|
double r = (i & 1) ? 0.55 : 1.0;
|
|
sym_edge(b, c, sym_pt(b, r*cos(a), r*sin(a)));
|
|
if (!(i & 1)) sym_ring(b, cos(a), sin(a), 0.08, 5);
|
|
}
|
|
}
|
|
/* Flat sunburst: a dense fan of alternating-length rays from a hub. */
|
|
static void build_sunburst(SymB *b) {
|
|
int c = sym_pt(b, 0.0, 0.0);
|
|
int n = 20;
|
|
for (int i = 0; i < n; i++) {
|
|
double a = TAU*i/n;
|
|
double r = (i & 1) ? 0.78 : 1.0;
|
|
sym_edge(b, c, sym_pt(b, r*cos(a), r*sin(a)));
|
|
}
|
|
}
|
|
/* True 3-D Sputnik: rods to the 12 icosahedral directions, each tipped with a
|
|
* small ring "ball" lying perpendicular to the rod. */
|
|
static void gen_sputnik(void) {
|
|
Solid *s = new_solid(3);
|
|
const double P = 1.6180339887;
|
|
const double dir[12][3] = {
|
|
{0,1,P},{0,1,-P},{0,-1,P},{0,-1,-P},{1,P,0},{1,-P,0},
|
|
{-1,P,0},{-1,-P,0},{P,0,1},{P,0,-1},{-P,0,1},{-P,0,-1} };
|
|
int c = s->nv; PV(s, 0,0,0, 0,0,0);
|
|
for (int i = 0; i < 12; i++) {
|
|
double L = sqrt(dir[i][0]*dir[i][0] + dir[i][1]*dir[i][1] + dir[i][2]*dir[i][2]);
|
|
double ux = dir[i][0]/L, uy = dir[i][1]/L, uz = dir[i][2]/L;
|
|
int tip = s->nv; PV(s, ux, uy, uz, 0,0,0);
|
|
AE(s, c, tip);
|
|
double rx = uy, ry = -ux, rz = 0, rl = sqrt(rx*rx + ry*ry + rz*rz);
|
|
if (rl < 1e-6) { rx = 1; ry = 0; rz = 0; rl = 1; }
|
|
rx /= rl; ry /= rl; rz /= rl;
|
|
double sx = uy*rz - uz*ry, sy = uz*rx - ux*rz, sz = ux*ry - uy*rx; /* u x r */
|
|
int ring0 = -1, prev = -1; double rr = 0.14;
|
|
for (int k = 0; k < 5; k++) {
|
|
double a = TAU*k/5;
|
|
double bx = ux + rr*(cos(a)*rx + sin(a)*sx);
|
|
double by = uy + rr*(cos(a)*ry + sin(a)*sy);
|
|
double bz = uz + rr*(cos(a)*rz + sin(a)*sz);
|
|
int idx = s->nv; PV(s, bx, by, bz, 0,0,0);
|
|
if (ring0 < 0) ring0 = idx;
|
|
if (prev >= 0) AE(s, prev, idx);
|
|
prev = idx;
|
|
}
|
|
AE(s, prev, ring0);
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
|
|
/* ---- more googie shapes: flat builders + naturally-3-D generators --- */
|
|
|
|
static void build_sparkle4(SymB *b) { build_sparkle(b, 4, 1.0, 0.0, 0.13); }
|
|
static void build_sparkle8(SymB *b) { build_sparkle(b, 8, 1.0, 0.62, 0.12); }
|
|
|
|
static void sym_ellipse(SymB *b, double cx,double cy,double ra,double rb,double rot,int seg) {
|
|
int first = -1, prev = -1;
|
|
for (int i = 0; i < seg; i++) {
|
|
double a = TAU*i/seg, ex = ra*cos(a), ey = rb*sin(a);
|
|
int idx = sym_pt(b, cx + ex*cos(rot) - ey*sin(rot),
|
|
cy + ex*sin(rot) + ey*cos(rot));
|
|
if (first < 0) first = idx;
|
|
sym_edge(b, prev, idx); prev = idx;
|
|
}
|
|
sym_edge(b, prev, first);
|
|
}
|
|
static void build_atom2d(SymB *b) { /* flat atom: 3 ellipses + nucleus */
|
|
sym_ring(b, 0, 0, 0.16, 7);
|
|
sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(0), 16);
|
|
sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(60), 16);
|
|
sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(120), 16);
|
|
}
|
|
static void build_boomerang(SymB *b) { /* crescent / boomerang band */
|
|
double Ro = 1.0, Ri = 0.6, a0 = sym_deg(30), a1 = sym_deg(150); int seg = 8;
|
|
int first = -1, prev = -1;
|
|
for (int i = 0; i <= seg; i++) { double a = a0 + (a1-a0)*i/seg;
|
|
int idx = sym_pt(b, Ro*cos(a), Ro*sin(a)); if (first<0) first=idx;
|
|
sym_edge(b, prev, idx); prev = idx; }
|
|
for (int i = 0; i <= seg; i++) { double a = a1 + (a0-a1)*i/seg;
|
|
int idx = sym_pt(b, Ri*cos(a), Ri*sin(a)); sym_edge(b, prev, idx); prev = idx; }
|
|
sym_edge(b, prev, first);
|
|
}
|
|
static void build_amoeba(SymB *b) { /* googie kidney / amoeba blob */
|
|
int seg = 18, first = -1, prev = -1;
|
|
for (int i = 0; i < seg; i++) {
|
|
double a = TAU*i/seg;
|
|
double r = 0.72 + 0.26*sin(3*a + 0.6) + 0.12*sin(2*a - 0.4);
|
|
int idx = sym_pt(b, r*cos(a), 0.82*r*sin(a));
|
|
if (first < 0) first = idx;
|
|
sym_edge(b, prev, idx); prev = idx;
|
|
}
|
|
sym_edge(b, prev, first);
|
|
}
|
|
static void build_doublestar(SymB *b) { /* big 4-point twinkle + inner one at 45 */
|
|
build_sparkle(b, 4, 1.0, 0.0, 0.12);
|
|
int first = -1, prev = -1, tips = 4; double R = 0.5, inner = 0.07, off = TAU/8;
|
|
for (int i = 0; i < tips; i++) {
|
|
double ta = TAU*0.25 + off + TAU*i/tips, na = ta + TAU/(2*tips);
|
|
int t = sym_pt(b, R*cos(ta), R*sin(ta)); if (first<0) first=t;
|
|
sym_edge(b, prev, t); prev = t;
|
|
int n = sym_pt(b, inner*cos(na), inner*sin(na)); sym_edge(b, prev, n); prev = n;
|
|
}
|
|
sym_edge(b, prev, first);
|
|
}
|
|
static void build_orbit2d(SymB *b) { /* concentric orbit rings + ticks */
|
|
sym_ring(b, 0, 0, 1.00, 18);
|
|
sym_ring(b, 0, 0, 0.64, 14);
|
|
sym_ring(b, 0, 0, 0.30, 9);
|
|
for (int i = 0; i < 4; i++) { double a = sym_deg(45 + 90*i);
|
|
sym_line(b, 0.30*cos(a), 0.30*sin(a), cos(a), sin(a)); }
|
|
}
|
|
static void build_starorb2d(SymB *b) { /* dense ray star with a core ring */
|
|
int c = sym_pt(b, 0, 0), n = 24;
|
|
for (int i = 0; i < n; i++) { double a = TAU*i/n;
|
|
sym_edge(b, c, sym_pt(b, cos(a), sin(a))); }
|
|
sym_ring(b, 0, 0, 0.30, 10);
|
|
}
|
|
|
|
/* 3-D atom: three circular orbits in tilted planes around an octahedral core. */
|
|
static void gen_atom3d(void) {
|
|
Solid *s = new_solid(3); int seg = 20;
|
|
const double tilt[3] = {0, 60, 120};
|
|
for (int o = 0; o < 3; o++) {
|
|
double t = sym_deg(tilt[o]); int first = -1, prev = -1;
|
|
for (int i = 0; i < seg; i++) {
|
|
double a = TAU*i/seg, x = cos(a), y = sin(a), z = 0;
|
|
double y2 = y*cos(t) - z*sin(t), z2 = y*sin(t) + z*cos(t);
|
|
int idx = s->nv; PV(s, x, y2, z2, 0,0,0);
|
|
if (first < 0) first = idx;
|
|
if (prev >= 0) AE(s, prev, idx);
|
|
prev = idx;
|
|
}
|
|
AE(s, prev, first);
|
|
}
|
|
int b0 = s->nv; double r = 0.13;
|
|
PV(s, r,0,0, 0,0,0); PV(s,-r,0,0, 0,0,0); PV(s, 0,r,0, 0,0,0);
|
|
PV(s, 0,-r,0, 0,0,0); PV(s, 0,0,r, 0,0,0); PV(s, 0,0,-r, 0,0,0);
|
|
const int oe[12][2] = {{0,2},{0,3},{0,4},{0,5},{1,2},{1,3},{1,4},{1,5},{2,4},{2,5},{3,4},{3,5}};
|
|
for (int e = 0; e < 12; e++) AE(s, b0+oe[e][0], b0+oe[e][1]);
|
|
center_normalize(s);
|
|
}
|
|
/* 3-D gyroscope: three great circles in the coordinate planes. */
|
|
static void gen_gyroscope3d(void) {
|
|
Solid *s = new_solid(3); int seg = 22;
|
|
for (int p = 0; p < 3; p++) {
|
|
int first = -1, prev = -1;
|
|
for (int i = 0; i < seg; i++) {
|
|
double a = TAU*i/seg, c = cos(a), sn = sin(a), x, y, z;
|
|
if (p == 0) { x = c; y = sn; z = 0; }
|
|
else if (p == 1) { x = c; y = 0; z = sn; }
|
|
else { x = 0; y = c; z = sn; }
|
|
int idx = s->nv; PV(s, x, y, z, 0,0,0);
|
|
if (first < 0) first = idx;
|
|
if (prev >= 0) AE(s, prev, idx);
|
|
prev = idx;
|
|
}
|
|
AE(s, prev, first);
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
/* 3-D spike orb / sea-urchin: rods to 20 symmetric directions from a hub. */
|
|
static void gen_spikeorb3d(void) {
|
|
Solid *s = new_solid(3);
|
|
const double P = 1.6180339887;
|
|
const double d[20][3] = {
|
|
{0,1,P},{0,1,-P},{0,-1,P},{0,-1,-P},{1,P,0},{1,-P,0},{-1,P,0},{-1,-P,0},
|
|
{P,0,1},{P,0,-1},{-P,0,1},{-P,0,-1},
|
|
{1,1,1},{1,1,-1},{1,-1,1},{1,-1,-1},{-1,1,1},{-1,1,-1},{-1,-1,1},{-1,-1,-1} };
|
|
int c = s->nv; PV(s, 0,0,0, 0,0,0);
|
|
for (int i = 0; i < 20; i++) {
|
|
double L = sqrt(d[i][0]*d[i][0] + d[i][1]*d[i][1] + d[i][2]*d[i][2]);
|
|
int tip = s->nv; PV(s, d[i][0]/L, d[i][1]/L, d[i][2]/L, 0,0,0);
|
|
AE(s, c, tip);
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
|
|
/* Build a symbol in both its flat and extruded-3-D forms. */
|
|
static void add_symbol(void (*build)(SymB *)) {
|
|
SymB b;
|
|
sym_init(&b); build(&b); sym_finish(&b, 0.0f); /* 2-D */
|
|
sym_init(&b); build(&b); sym_finish(&b, 0.5f); /* 3-D */
|
|
}
|
|
|
|
/* Random lumpy 3-D asteroid: n points scattered over a sphere at jittered
|
|
* radii, wired up as the convex hull (brute-force face enumeration). The
|
|
* vertex count `n` sets the number of sides / overall complexity, and the
|
|
* radial jitter gives each rock its irregular, faceted silhouette. */
|
|
static void gen_asteroid(int n) {
|
|
if (n > MAX_VERTS) n = MAX_VERTS;
|
|
if (n < 6) n = 6;
|
|
Solid *s = new_solid(3);
|
|
for (int i = 0; i < n; i++) {
|
|
double z = 2.0*frand() - 1.0; /* uniform on the sphere */
|
|
double t = TAU*frand();
|
|
double rr = sqrt(1.0 - z*z);
|
|
double rad = 0.74 + 0.26*frand(); /* lumpy radius */
|
|
PV(s, rr*cos(t)*rad, rr*sin(t)*rad, z*rad, 0,0,0);
|
|
}
|
|
/* a triangle (a,b,c) is a hull face iff every other vertex lies on one
|
|
* side of its plane; collect the edges of all such faces. */
|
|
for (int a = 0; a < n; a++)
|
|
for (int b = a+1; b < n; b++)
|
|
for (int c = b+1; c < n; c++) {
|
|
double ux = s->v[b][0]-s->v[a][0], uy = s->v[b][1]-s->v[a][1], uz = s->v[b][2]-s->v[a][2];
|
|
double vx = s->v[c][0]-s->v[a][0], vy = s->v[c][1]-s->v[a][1], vz = s->v[c][2]-s->v[a][2];
|
|
double nx = uy*vz - uz*vy, ny = uz*vx - ux*vz, nz = ux*vy - uy*vx;
|
|
double nl = sqrt(nx*nx + ny*ny + nz*nz);
|
|
if (nl < 1e-9) continue; /* degenerate triple */
|
|
int pos = 0, neg = 0;
|
|
for (int p = 0; p < n; p++) {
|
|
if (p == a || p == b || p == c) continue;
|
|
double dx = s->v[p][0]-s->v[a][0], dy = s->v[p][1]-s->v[a][1], dz = s->v[p][2]-s->v[a][2];
|
|
double d = (nx*dx + ny*dy + nz*dz) / nl;
|
|
if (d > 1e-5) pos++; else if (d < -1e-5) neg++;
|
|
}
|
|
if (pos == 0 || neg == 0) {
|
|
AE_unique(s, a, b); AE_unique(s, b, c); AE_unique(s, c, a);
|
|
}
|
|
}
|
|
center_normalize(s);
|
|
}
|
|
|
|
static void init_solids(void) {
|
|
const double P = 1.6180339887, I = 1.0/1.6180339887;
|
|
Solid *s;
|
|
|
|
/* --- the five Platonic solids --- */
|
|
s = new_solid(3);
|
|
PV(s,1,1,1,0,0,0); PV(s,1,-1,-1,0,0,0); PV(s,-1,1,-1,0,0,0); PV(s,-1,-1,1,0,0,0);
|
|
center_normalize(s); derive_edges(s); /* tetrahedron */
|
|
gen_hypercube(3); /* cube */
|
|
gen_orthoplex(3); /* octahedron */
|
|
s = new_solid(3); /* icosahedron */
|
|
PV(s,0,1,P,0,0,0); PV(s,0,1,-P,0,0,0); PV(s,0,-1,P,0,0,0); PV(s,0,-1,-P,0,0,0);
|
|
PV(s,1,P,0,0,0,0); PV(s,1,-P,0,0,0,0); PV(s,-1,P,0,0,0,0); PV(s,-1,-P,0,0,0,0);
|
|
PV(s,P,0,1,0,0,0); PV(s,P,0,-1,0,0,0); PV(s,-P,0,1,0,0,0); PV(s,-P,0,-1,0,0,0);
|
|
center_normalize(s); derive_edges(s);
|
|
s = new_solid(3); /* dodecahedron */
|
|
PV(s,1,1,1,0,0,0); PV(s,1,1,-1,0,0,0); PV(s,1,-1,1,0,0,0); PV(s,1,-1,-1,0,0,0);
|
|
PV(s,-1,1,1,0,0,0); PV(s,-1,1,-1,0,0,0); PV(s,-1,-1,1,0,0,0); PV(s,-1,-1,-1,0,0,0);
|
|
PV(s,0,I,P,0,0,0); PV(s,0,I,-P,0,0,0); PV(s,0,-I,P,0,0,0); PV(s,0,-I,-P,0,0,0);
|
|
PV(s,I,P,0,0,0,0); PV(s,I,-P,0,0,0,0); PV(s,-I,P,0,0,0,0); PV(s,-I,-P,0,0,0,0);
|
|
PV(s,P,0,I,0,0,0); PV(s,P,0,-I,0,0,0); PV(s,-P,0,I,0,0,0); PV(s,-P,0,-I,0,0,0);
|
|
center_normalize(s); derive_edges(s);
|
|
|
|
/* --- other 3-D solids --- */
|
|
gen_cuboctahedron();
|
|
gen_truncated_octahedron();
|
|
gen_stella_octangula();
|
|
|
|
/* --- prisms, antiprisms, bipyramids --- */
|
|
gen_prism(3); gen_prism(5); gen_prism(6); gen_prism(7); gen_prism(8);
|
|
gen_antiprism(3); gen_antiprism(4); gen_antiprism(5); gen_antiprism(6); gen_antiprism(8);
|
|
gen_bipyramid(3); gen_bipyramid(4); gen_bipyramid(5); gen_bipyramid(6); gen_bipyramid(8);
|
|
|
|
/* --- star polygons --- */
|
|
gen_star(5,2); gen_star(6,2); gen_star(7,2); gen_star(7,3); gen_star(8,3);
|
|
gen_star(9,2); gen_star(9,4); gen_star(12,5);
|
|
|
|
/* --- many-faced solids (named for their face count) + a hexagram ---
|
|
* Built from prism / bipyramid / trapezohedron families so each lands on
|
|
* exactly the requested number of faces. (The 20-face icosahedron here is
|
|
* a decagonal bipyramid, distinct from the platonic icosahedron above.) */
|
|
gen_prism(11); /* tridecahedron (13 faces) */
|
|
gen_bipyramid(7); /* tetradecahedron (14 faces) */
|
|
gen_prism(13); /* pentadecahedron (15 faces) */
|
|
gen_prism(15); /* heptadecahedron (17 faces) */
|
|
gen_bipyramid(9); /* octadecahedron (18 faces) */
|
|
gen_prism(17); /* enneadecahedron (19 faces) */
|
|
gen_bipyramid(10); /* icosahedron (20 faces) */
|
|
gen_trapezohedron(12); /* icositetrahedron (24 faces) */
|
|
gen_trapezohedron(15); /* triacontahedron (30 faces) */
|
|
gen_trapezohedron(30); /* hexacontahedron (60 faces) */
|
|
gen_bipyramid(50); /* hecatohedron (100 faces) */
|
|
gen_unicursal_hexagram(); /* unicursal hexagram */
|
|
|
|
/* --- googie / atomic-age starbursts, each in 2-D and extruded 3-D --- */
|
|
add_symbol(build_sparkle4); /* 4-point bowling-alley twinkle */
|
|
add_symbol(build_sparkle8); /* 8-point twinkle */
|
|
add_symbol(build_atomic); /* atomic burst with electrons */
|
|
add_symbol(build_sunburst); /* ray sunburst */
|
|
add_symbol(build_doublestar); /* layered double starburst */
|
|
add_symbol(build_boomerang); /* boomerang / crescent */
|
|
add_symbol(build_amoeba); /* kidney / amoeba blob */
|
|
/* flat motifs paired with a naturally-3-D counterpart below */
|
|
{ SymB b; sym_init(&b); build_atom2d(&b); sym_finish(&b, 0.0f); } /* flat atom */
|
|
{ SymB b; sym_init(&b); build_orbit2d(&b); sym_finish(&b, 0.0f); } /* orbit rings */
|
|
{ SymB b; sym_init(&b); build_starorb2d(&b); sym_finish(&b, 0.0f); } /* dense star */
|
|
gen_sputnik(); /* 3-D sputnik satellite */
|
|
gen_atom3d(); /* 3-D orbital atom */
|
|
gen_gyroscope3d(); /* 3-D gyroscope / orbit cage */
|
|
gen_spikeorb3d(); /* 3-D spike orb / sea-urchin */
|
|
|
|
/* --- symbols & signs, each in a flat (2-D) and an extruded (3-D) form --- */
|
|
add_symbol(build_smiley); /* smiley face */
|
|
add_symbol(build_biohazard); /* biohazard */
|
|
add_symbol(build_peace); /* peace sign */
|
|
add_symbol(build_cross); /* cross */
|
|
add_symbol(build_question); /* question mark */
|
|
add_symbol(build_exclaim); /* exclamation point */
|
|
add_symbol(build_hash); /* hash / pound (#) */
|
|
add_symbol(build_dollar); /* dollar sign */
|
|
add_symbol(build_sterling); /* pound sterling (£) */
|
|
|
|
/* --- 4/5/6-dimensional polytopes (projected & morphing) --- */
|
|
gen_simplex(5); /* 5-cell (4-simplex) */
|
|
gen_hypercube(4); /* tesseract / 8-cell */
|
|
gen_hypercube(5); /* penteract / 5-cube */
|
|
gen_hypercube(6); /* 6-cube */
|
|
gen_orthoplex(4); /* 16-cell (4-orthoplex) */
|
|
gen_orthoplex(5); /* 5-orthoplex */
|
|
gen_orthoplex(6); /* 6-orthoplex */
|
|
gen_24cell(); /* 24-cell */
|
|
gen_simplex(6); /* 5-simplex */
|
|
|
|
/* --- random vector asteroids (each session generates a fresh set) --- */
|
|
for (int k = 0; k < 18; k++) gen_asteroid(7 + (rand() % 20)); /* 7..26 verts */
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* The field of tumbling solids. */
|
|
/* ================================================================== */
|
|
|
|
#define MAX_BODIES 7200
|
|
#define RENDER_REF 140.0f /* render distance at which density == body count */
|
|
/* The field is a sphere around the camera whose radius is the user-settable
|
|
* render distance (cfg.render_dist). Bodies stream along -Z (forward) or +Z
|
|
* (when speed is negative) and, once they leave the sphere, are recycled back
|
|
* to the render-distance shell on the incoming side -- so objects always appear
|
|
* far away and approach, never popping in close. The active body count scales
|
|
* with render distance, so a deeper field simply holds proportionally more
|
|
* shapes. The camera can rotate a full 360 degrees around it. */
|
|
|
|
typedef struct {
|
|
int shape;
|
|
float x, y, z; /* world position; camera at origin, looking -Z */
|
|
float size; /* == bounding-sphere radius (unit solid scaled) */
|
|
float axis[3]; /* tumble axis (normalized) */
|
|
float spin_seed; /* in [-1,1]; tumble spread applied live */
|
|
float angle; /* current tumble angle (radians) */
|
|
float hue_offset; /* used in multicolor mode */
|
|
float spawn_fade; /* 0->1 ease-in after (re)spawn, kills pop-in */
|
|
} Body;
|
|
|
|
static Body bodies[MAX_BODIES];
|
|
|
|
/* ================================================================== */
|
|
/* Settings (driven by keyboard). */
|
|
/* ================================================================== */
|
|
|
|
typedef struct {
|
|
float speed; /* approach speed 0..100 */
|
|
float tumble; /* tumble rate 0..100 */
|
|
float tumble_var; /* tumble variance 0..100 */
|
|
float render_dist; /* field sphere radius 40..1520 */
|
|
int density; /* active body count */
|
|
float size_min; /* min solid size */
|
|
float size_max; /* max solid size */
|
|
float hue; /* base hue 0..360 */
|
|
float hue_cycle; /* auto hue-cycle rate 0..100 (0 = off) */
|
|
int multicolor; /* 0 = single, 1 = multi */
|
|
int cycle_shapes; /* 0 = random, 1 = cycle */
|
|
float glow; /* CRT glow / bleed 0..100 */
|
|
float flicker; /* vector flicker 0..100 */
|
|
int fullscreen;
|
|
int paused;
|
|
} Settings;
|
|
|
|
static Settings cfg = {
|
|
.speed = 35, .tumble = 40, .tumble_var = 50, .render_dist = 140, .density = 50,
|
|
.size_min = 1.4f, .size_max = 4.6f, .hue = 200, .hue_cycle = 0,
|
|
.multicolor = 0, .cycle_shapes = 0,
|
|
.glow = 45, .flicker = 18, .fullscreen = 0, .paused = 0
|
|
};
|
|
|
|
static int spawn_counter = 0;
|
|
static float max_line_width = 10.0f; /* queried at runtime */
|
|
static float cam_x = 0.0f, cam_y = 0.0f; /* camera pan (WASD) */
|
|
static float cam_yaw = 0.0f, cam_pitch = 0.0f; /* camera rotate (arrow keys) */
|
|
|
|
static float frand(void) { return (float)rand() / (float)RAND_MAX; }
|
|
|
|
/* Bounding-sphere overlap test against the first `check_count` bodies. */
|
|
static int collides(const Body *c, int check_count, int self) {
|
|
for (int i = 0; i < check_count; i++) {
|
|
if (i == self) continue;
|
|
const Body *o = &bodies[i];
|
|
float dx = c->x - o->x, dy = c->y - o->y, dz = c->z - o->z;
|
|
float rr = c->size + o->size + 0.6f; /* +margin so they never touch */
|
|
if (dx*dx + dy*dy + dz*dz < rr*rr) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* (Re)initialize one body. reset=0 (recycle) re-enters it ONLY at the render-
|
|
* distance shell, on the incoming side (which flips with the travel direction),
|
|
* so objects always appear far away and approach -- never popping in close.
|
|
* reset=1 (initial fill) spreads bodies through the volume instead, as if they
|
|
* had each spawned at the shell at a different past time, so the field starts
|
|
* populated rather than blank. Position is rejection-sampled to avoid overlap. */
|
|
static void spawn_body(Body *b, int reset, int check_count, int self) {
|
|
b->shape = cfg.cycle_shapes ? (spawn_counter++ % num_shapes)
|
|
: (rand() % num_shapes);
|
|
|
|
float smin = cfg.size_min, smax = cfg.size_max;
|
|
if (smax < smin) smax = smin;
|
|
b->size = smin + frand() * (smax - smin);
|
|
|
|
float ax = frand()*2-1, ay = frand()*2-1, az = frand()*2-1;
|
|
float len = sqrtf(ax*ax + ay*ay + az*az);
|
|
if (len < 1e-4f) { ax = 1; ay = 0; az = 0; len = 1; }
|
|
b->axis[0] = ax/len; b->axis[1] = ay/len; b->axis[2] = az/len;
|
|
|
|
b->spin_seed = frand()*2 - 1;
|
|
b->angle = frand() * 6.2831853f;
|
|
b->hue_offset = frand() * 360.0f;
|
|
b->spawn_fade = reset ? 1.0f : 0.0f;
|
|
|
|
float R = cfg.render_dist;
|
|
float side = (cfg.speed >= 0.0f) ? 1.0f : -1.0f; /* +Z if flying forward */
|
|
for (int t = 0; t < 80; t++) {
|
|
if (reset) {
|
|
/* initial fill: uniform through the volume of the sphere */
|
|
float zc = 2.0f*frand() - 1.0f;
|
|
float th = 6.2831853f*frand();
|
|
float rr = sqrtf(1.0f - zc*zc);
|
|
float rad = R * cbrtf(frand());
|
|
b->x = rr*cosf(th)*rad;
|
|
b->y = rr*sinf(th)*rad;
|
|
b->z = zc*rad;
|
|
} else {
|
|
/* recycle: enter at the shell, on the incoming-side cap */
|
|
float rho = 0.9f * R * sqrtf(frand());
|
|
float phi = 6.2831853f*frand();
|
|
b->x = rho*cosf(phi);
|
|
b->y = rho*sinf(phi);
|
|
b->z = side * sqrtf(R*R - rho*rho) * 0.999f;
|
|
}
|
|
if (!collides(b, check_count, self)) break;
|
|
}
|
|
}
|
|
|
|
/* How many bodies are active right now: the density knob, scaled up with render
|
|
* distance so a larger sphere holds proportionally more shapes (constant near-
|
|
* field density), clamped to the hard array limit. */
|
|
static int active_count(void) {
|
|
int n = (int)(cfg.density * (cfg.render_dist / RENDER_REF) + 0.5f);
|
|
if (n < 1) n = 1;
|
|
if (n > MAX_BODIES) n = MAX_BODIES;
|
|
return n;
|
|
}
|
|
|
|
/* Make sure bodies[0..n) are initialized (spawns any not yet filled). */
|
|
static int g_filled = 0;
|
|
static void ensure_filled(int n) {
|
|
if (n > MAX_BODIES) n = MAX_BODIES;
|
|
for (int i = g_filled; i < n; i++)
|
|
spawn_body(&bodies[i], 1, i, i);
|
|
if (n > g_filled) g_filled = n;
|
|
}
|
|
|
|
static void rebuild_field(void) {
|
|
if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES;
|
|
if (cfg.density < 1) cfg.density = 1;
|
|
ensure_filled(active_count());
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Color. */
|
|
/* ================================================================== */
|
|
|
|
static void hsv_to_rgb(float h, float s, float v, float *r, float *g, float *b) {
|
|
h = fmodf(h, 360.0f); if (h < 0) h += 360.0f;
|
|
float c = v * s;
|
|
float x = c * (1 - fabsf(fmodf(h / 60.0f, 2) - 1));
|
|
float m = v - c, rr, gg, bb;
|
|
if (h < 60) { rr=c; gg=x; bb=0; }
|
|
else if (h < 120) { rr=x; gg=c; bb=0; }
|
|
else if (h < 180) { rr=0; gg=c; bb=x; }
|
|
else if (h < 240) { rr=0; gg=x; bb=c; }
|
|
else if (h < 300) { rr=x; gg=0; bb=c; }
|
|
else { rr=c; gg=0; bb=x; }
|
|
*r = rr + m; *g = gg + m; *b = bb + m;
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Self-contained vector stroke font (for the on-screen display). */
|
|
/* Each glyph is a polyline list on a 0..4 (x) by 0..6 (y) grid. */
|
|
/* PU lifts the pen (start a new stroke); EN ends the glyph. */
|
|
/* ================================================================== */
|
|
|
|
#define PU 127
|
|
#define EN (-128)
|
|
#define G_ static const signed char
|
|
|
|
G_ g_A[] = {0,0, 2,6, 4,0, PU, 1,2, 3,2, EN};
|
|
G_ g_B[] = {0,0, 0,6, 3,6, 4,5, 3,3, 0,3, PU, 3,3, 4,1, 3,0, 0,0, EN};
|
|
G_ g_C[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, EN};
|
|
G_ g_D[] = {0,0, 0,6, 2,6, 4,4, 4,2, 2,0, 0,0, EN};
|
|
G_ g_E[] = {4,6, 0,6, 0,0, 4,0, PU, 0,3, 3,3, EN};
|
|
G_ g_F[] = {4,6, 0,6, 0,0, PU, 0,3, 3,3, EN};
|
|
G_ g_G[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, 4,3, 2,3, EN};
|
|
G_ g_H[] = {0,0, 0,6, PU, 4,0, 4,6, PU, 0,3, 4,3, EN};
|
|
G_ g_I[] = {1,0, 3,0, PU, 2,0, 2,6, PU, 1,6, 3,6, EN};
|
|
G_ g_J[] = {3,6, 3,1, 2,0, 1,0, 0,1, EN};
|
|
G_ g_K[] = {0,0, 0,6, PU, 4,6, 0,3, 4,0, EN};
|
|
G_ g_L[] = {0,6, 0,0, 4,0, EN};
|
|
G_ g_M[] = {0,0, 0,6, 2,3, 4,6, 4,0, EN};
|
|
G_ g_N[] = {0,0, 0,6, 4,0, 4,6, EN};
|
|
G_ g_O[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, EN};
|
|
G_ g_P[] = {0,0, 0,6, 3,6, 4,5, 4,4, 3,3, 0,3, EN};
|
|
G_ g_Q[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, PU, 2,2, 4,0, EN};
|
|
G_ g_R[] = {0,0, 0,6, 3,6, 4,5, 4,4, 3,3, 0,3, PU, 2,3, 4,0, EN};
|
|
G_ g_S[] = {4,5, 3,6, 1,6, 0,5, 1,3, 3,3, 4,2, 3,0, 1,0, 0,1, EN};
|
|
G_ g_T[] = {0,6, 4,6, PU, 2,6, 2,0, EN};
|
|
G_ g_U[] = {0,6, 0,1, 1,0, 3,0, 4,1, 4,6, EN};
|
|
G_ g_V[] = {0,6, 2,0, 4,6, EN};
|
|
G_ g_W[] = {0,6, 1,0, 2,3, 3,0, 4,6, EN};
|
|
G_ g_X[] = {0,0, 4,6, PU, 0,6, 4,0, EN};
|
|
G_ g_Y[] = {0,6, 2,3, 4,6, PU, 2,3, 2,0, EN};
|
|
G_ g_Z[] = {0,6, 4,6, 0,0, 4,0, EN};
|
|
|
|
G_ g_0[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, PU, 1,1, 3,5, EN};
|
|
G_ g_1[] = {1,5, 2,6, 2,0, PU, 1,0, 3,0, EN};
|
|
G_ g_2[] = {0,5, 1,6, 3,6, 4,5, 4,4, 0,0, 4,0, EN};
|
|
G_ g_3[] = {0,6, 4,6, 2,3, PU, 2,3, 4,2, 4,1, 3,0, 1,0, 0,1, EN};
|
|
G_ g_4[] = {3,0, 3,6, 0,2, 4,2, EN};
|
|
G_ g_5[] = {4,6, 0,6, 0,3, 3,3, 4,2, 4,1, 3,0, 1,0, 0,1, EN};
|
|
G_ g_6[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, 4,2, 3,3, 0,3, EN};
|
|
G_ g_7[] = {0,6, 4,6, 2,0, EN};
|
|
G_ g_8[] = {1,3, 0,4, 0,5, 1,6, 3,6, 4,5, 4,4, 3,3, 1,3, 0,2, 0,1, 1,0, 3,0, 4,1, 4,2, 3,3, EN};
|
|
G_ g_9[] = {0,1, 1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,4, 1,3, 4,3, EN};
|
|
|
|
G_ g_pct[] = {0,0, 4,6, PU, 0,5,1,5,1,6,0,6,0,5, PU, 3,0,4,0,4,1,3,1,3,0, EN};
|
|
G_ g_slash[] = {0,0, 4,6, EN};
|
|
G_ g_plus[] = {2,1, 2,5, PU, 0,3, 4,3, EN};
|
|
G_ g_minus[] = {0,3, 4,3, EN};
|
|
G_ g_dot[] = {2,0, 2,1, EN};
|
|
G_ g_lbr[] = {3,6, 1,6, 1,0, 3,0, EN};
|
|
G_ g_rbr[] = {1,6, 3,6, 3,0, 1,0, EN};
|
|
G_ g_lpar[] = {3,6, 1,4, 1,2, 3,0, EN};
|
|
G_ g_rpar[] = {1,6, 3,4, 3,2, 1,0, EN};
|
|
G_ g_dash[] = {0,3, 4,3, EN}; /* same as minus, used by '/'-fallback */
|
|
|
|
static const signed char *glyph(char c) {
|
|
switch (toupper((unsigned char)c)) {
|
|
case 'A': return g_A; case 'B': return g_B; case 'C': return g_C;
|
|
case 'D': return g_D; case 'E': return g_E; case 'F': return g_F;
|
|
case 'G': return g_G; case 'H': return g_H; case 'I': return g_I;
|
|
case 'J': return g_J; case 'K': return g_K; case 'L': return g_L;
|
|
case 'M': return g_M; case 'N': return g_N; case 'O': return g_O;
|
|
case 'P': return g_P; case 'Q': return g_Q; case 'R': return g_R;
|
|
case 'S': return g_S; case 'T': return g_T; case 'U': return g_U;
|
|
case 'V': return g_V; case 'W': return g_W; case 'X': return g_X;
|
|
case 'Y': return g_Y; case 'Z': return g_Z;
|
|
case '0': return g_0; case '1': return g_1; case '2': return g_2;
|
|
case '3': return g_3; case '4': return g_4; case '5': return g_5;
|
|
case '6': return g_6; case '7': return g_7; case '8': return g_8;
|
|
case '9': return g_9;
|
|
case '%': return g_pct; case '/': return g_slash;
|
|
case '+': return g_plus; case '-': return g_minus;
|
|
case '.': return g_dot; case '[': return g_lbr;
|
|
case ']': return g_rbr; case '(': return g_lpar;
|
|
case ')': return g_rpar; case '_': return g_dash;
|
|
default: return NULL; /* space and unknowns advance only */
|
|
}
|
|
}
|
|
|
|
/* Draw a string with its bottom-left at (x,y); glyph height = h px. */
|
|
static float stroke_text(const char *str, float x, float y, float h) {
|
|
float sx = h / 6.0f, sy = h / 6.0f;
|
|
float advance = sx * 6.0f; /* 4 wide + 2 spacing */
|
|
float cx = x;
|
|
for (const char *p = str; *p; p++) {
|
|
const signed char *g = glyph(*p);
|
|
if (g) {
|
|
glBegin(GL_LINE_STRIP);
|
|
for (int i = 0;;) {
|
|
signed char a = g[i++];
|
|
if (a == EN) break;
|
|
if (a == PU) { glEnd(); glBegin(GL_LINE_STRIP); continue; }
|
|
signed char b = g[i++];
|
|
glVertex2f(cx + a * sx, y + b * sy);
|
|
}
|
|
glEnd();
|
|
}
|
|
cx += advance;
|
|
}
|
|
return cx;
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* On-screen display. */
|
|
/* ================================================================== */
|
|
|
|
static void render_osd(int fbw, int fbh, float alpha) {
|
|
if (alpha <= 0.001f) return;
|
|
|
|
glMatrixMode(GL_PROJECTION);
|
|
glPushMatrix();
|
|
glLoadIdentity();
|
|
gluOrtho2D(0, fbw, 0, fbh);
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glPushMatrix();
|
|
glLoadIdentity();
|
|
|
|
const float h = 13.0f;
|
|
const float left = 22.0f;
|
|
const float lineStep = h + 9.0f;
|
|
const int nlines = 19;
|
|
const float panelW = 384.0f;
|
|
float panelTop = fbh - 18.0f;
|
|
float panelH = nlines * lineStep + 16.0f;
|
|
|
|
/* dim backdrop for readability (normal alpha blend) */
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
glColor4f(0.02f, 0.03f, 0.06f, 0.55f * alpha);
|
|
glBegin(GL_QUADS);
|
|
glVertex2f(8, panelTop + 10);
|
|
glVertex2f(panelW, panelTop + 10);
|
|
glVertex2f(panelW, panelTop + 10 - panelH);
|
|
glVertex2f(8, panelTop + 10 - panelH);
|
|
glEnd();
|
|
/* subtle border */
|
|
glColor4f(0.3f, 0.6f, 0.9f, 0.35f * alpha);
|
|
glBegin(GL_LINE_LOOP);
|
|
glVertex2f(8, panelTop + 10);
|
|
glVertex2f(panelW, panelTop + 10);
|
|
glVertex2f(panelW, panelTop + 10 - panelH);
|
|
glVertex2f(8, panelTop + 10 - panelH);
|
|
glEnd();
|
|
|
|
/* additive, theme-tinted text with a faint glow pass */
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
|
float tr, tg, tb;
|
|
hsv_to_rgb(cfg.hue, 0.35f, 1.0f, &tr, &tg, &tb);
|
|
|
|
char buf[80];
|
|
float y = panelTop - h;
|
|
|
|
/* title */
|
|
glColor4f(tr*alpha, tg*alpha, tb*alpha, alpha);
|
|
glLineWidth(2.4f); stroke_text("- VECTORGONS -", left, y, h + 3);
|
|
glLineWidth(1.6f);
|
|
y -= lineStep + 6;
|
|
|
|
#define LINE(...) do { snprintf(buf,sizeof buf,__VA_ARGS__); \
|
|
/* glow pass */ \
|
|
glColor4f(tr*alpha*0.6f,tg*alpha*0.6f,tb*alpha*0.6f, alpha*0.5f); \
|
|
glLineWidth(3.0f); stroke_text(buf,left,y,h); \
|
|
/* crisp pass */ \
|
|
glColor4f(tr*alpha,tg*alpha,tb*alpha,alpha); \
|
|
glLineWidth(1.5f); stroke_text(buf,left,y,h); \
|
|
y -= lineStep; } while(0)
|
|
|
|
LINE("SPEED PGUP/DN %.0f%%%s", cfg.speed, cfg.speed < 0 ? " REV" : "");
|
|
LINE("TUMBLE Q/E %.0f%%", cfg.tumble);
|
|
LINE("TUMBLE VAR T/Y %.0f%%", cfg.tumble_var);
|
|
LINE("RENDER DST Z/X %.0f", cfg.render_dist);
|
|
LINE("DENSITY +/- %d", active_count());
|
|
LINE("SIZE MIN U/J %.1f", cfg.size_min);
|
|
LINE("SIZE MAX I/K %.1f", cfg.size_max);
|
|
LINE("HUE [ / ] %.0f", cfg.hue);
|
|
if (cfg.hue_cycle > 0.001f) LINE("HUE CYCLE C/V %.0f%%", cfg.hue_cycle);
|
|
else LINE("%s", "HUE CYCLE C/V OFF");
|
|
LINE("COLOR M %s", cfg.multicolor ? "MULTICOLOR" : "SINGLE");
|
|
LINE("GLOW O/L %.0f%%", cfg.glow);
|
|
LINE("FLICKER G/H %.0f%%", cfg.flicker);
|
|
LINE("SHAPES N %s", cfg.cycle_shapes ? "CYCLE" : "RANDOM");
|
|
LINE("MOVE CAM WASD %+.0f %+.0f", cam_x, cam_y);
|
|
LINE("ROTATE CAM ARROWS %+.0f %+.0f", cam_yaw, cam_pitch);
|
|
LINE("FULLSCREEN F %s", cfg.fullscreen ? "ON" : "OFF");
|
|
LINE("PAUSE SPACE %s", cfg.paused ? "PAUSED" : "RUNNING");
|
|
LINE("%s", "QUIT ESC");
|
|
#undef LINE
|
|
|
|
glMatrixMode(GL_PROJECTION);
|
|
glPopMatrix();
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glPopMatrix();
|
|
/* leave additive blending on for the next 3-D frame */
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Input. */
|
|
/* ================================================================== */
|
|
|
|
static double last_input_time = 0.0;
|
|
|
|
static void clampf(float *v, float lo, float hi) {
|
|
if (*v < lo) *v = lo;
|
|
if (*v > hi) *v = hi;
|
|
}
|
|
|
|
static void normalize_sizes(void) {
|
|
if (cfg.size_min < 0.4f) cfg.size_min = 0.4f;
|
|
if (cfg.size_max < cfg.size_min) cfg.size_max = cfg.size_min;
|
|
if (cfg.size_max > 54.0f) cfg.size_max = 54.0f;
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Settings persistence: a plain key=value file under $HOME so the */
|
|
/* user's last-used settings become the defaults next launch. */
|
|
/* ================================================================== */
|
|
|
|
static void settings_path(char *buf, size_t n) {
|
|
const char *home = getenv("HOME");
|
|
if (home && *home) snprintf(buf, n, "%s/.vectorgons", home);
|
|
else snprintf(buf, n, ".vectorgons");
|
|
}
|
|
|
|
static void save_settings(void) {
|
|
char path[1024];
|
|
settings_path(path, sizeof path);
|
|
FILE *f = fopen(path, "w");
|
|
if (!f) return;
|
|
fprintf(f, "# Vectorgons settings (auto-saved on exit)\n");
|
|
fprintf(f, "speed=%g\n", cfg.speed);
|
|
fprintf(f, "tumble=%g\n", cfg.tumble);
|
|
fprintf(f, "tumble_var=%g\n", cfg.tumble_var);
|
|
fprintf(f, "render_dist=%g\n", cfg.render_dist);
|
|
fprintf(f, "density=%d\n", cfg.density);
|
|
fprintf(f, "size_min=%g\n", cfg.size_min);
|
|
fprintf(f, "size_max=%g\n", cfg.size_max);
|
|
fprintf(f, "hue=%g\n", cfg.hue);
|
|
fprintf(f, "hue_cycle=%g\n", cfg.hue_cycle);
|
|
fprintf(f, "multicolor=%d\n", cfg.multicolor);
|
|
fprintf(f, "cycle_shapes=%d\n", cfg.cycle_shapes);
|
|
fprintf(f, "glow=%g\n", cfg.glow);
|
|
fprintf(f, "flicker=%g\n", cfg.flicker);
|
|
fprintf(f, "fullscreen=%d\n", cfg.fullscreen);
|
|
fclose(f);
|
|
}
|
|
|
|
static void load_settings(void) {
|
|
char path[1024];
|
|
settings_path(path, sizeof path);
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return; /* first run: keep built-in defaults */
|
|
char line[256];
|
|
while (fgets(line, sizeof line, f)) {
|
|
if (line[0] == '#' || line[0] == '\n') continue;
|
|
char *eq = strchr(line, '=');
|
|
if (!eq) continue;
|
|
*eq = '\0';
|
|
const char *key = line, *val = eq + 1;
|
|
double d = atof(val);
|
|
if (!strcmp(key, "speed")) cfg.speed = (float)d;
|
|
else if (!strcmp(key, "tumble")) cfg.tumble = (float)d;
|
|
else if (!strcmp(key, "tumble_var")) cfg.tumble_var = (float)d;
|
|
else if (!strcmp(key, "render_dist")) cfg.render_dist = (float)d;
|
|
else if (!strcmp(key, "density")) cfg.density = (int)d;
|
|
else if (!strcmp(key, "size_min")) cfg.size_min = (float)d;
|
|
else if (!strcmp(key, "size_max")) cfg.size_max = (float)d;
|
|
else if (!strcmp(key, "hue")) cfg.hue = (float)d;
|
|
else if (!strcmp(key, "hue_cycle")) cfg.hue_cycle = (float)d;
|
|
else if (!strcmp(key, "multicolor")) cfg.multicolor = (int)d;
|
|
else if (!strcmp(key, "cycle_shapes")) cfg.cycle_shapes = (int)d;
|
|
else if (!strcmp(key, "glow")) cfg.glow = (float)d;
|
|
else if (!strcmp(key, "flicker")) cfg.flicker = (float)d;
|
|
else if (!strcmp(key, "fullscreen")) cfg.fullscreen = (int)d;
|
|
}
|
|
fclose(f);
|
|
/* defend against a hand-edited or stale file by clamping into range */
|
|
clampf(&cfg.speed, -100, 100);
|
|
clampf(&cfg.tumble, 0, 100);
|
|
clampf(&cfg.tumble_var, 0, 100);
|
|
clampf(&cfg.render_dist, 40, 1520);
|
|
clampf(&cfg.hue_cycle, 0, 100);
|
|
clampf(&cfg.glow, 0, 100);
|
|
clampf(&cfg.flicker, 0, 100);
|
|
cfg.hue = fmodf(cfg.hue, 360.0f); if (cfg.hue < 0) cfg.hue += 360.0f;
|
|
if (cfg.density < 1) cfg.density = 1;
|
|
if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES;
|
|
normalize_sizes();
|
|
cfg.multicolor = cfg.multicolor ? 1 : 0;
|
|
cfg.cycle_shapes = cfg.cycle_shapes ? 1 : 0;
|
|
cfg.fullscreen = cfg.fullscreen ? 1 : 0;
|
|
}
|
|
|
|
static void toggle_fullscreen(GLFWwindow *win) {
|
|
static int wx = 100, wy = 100, ww = 1100, wh = 760;
|
|
cfg.fullscreen = !cfg.fullscreen;
|
|
if (cfg.fullscreen) {
|
|
glfwGetWindowPos(win, &wx, &wy);
|
|
glfwGetWindowSize(win, &ww, &wh);
|
|
GLFWmonitor *mon = glfwGetPrimaryMonitor();
|
|
const GLFWvidmode *m = glfwGetVideoMode(mon);
|
|
glfwSetWindowMonitor(win, mon, 0, 0, m->width, m->height, m->refreshRate);
|
|
} else {
|
|
glfwSetWindowMonitor(win, NULL, wx, wy, ww, wh, 0);
|
|
}
|
|
}
|
|
|
|
static void key_cb(GLFWwindow *win, int key, int sc, int action, int mods) {
|
|
(void)sc; (void)mods;
|
|
if (action != GLFW_PRESS && action != GLFW_REPEAT) return;
|
|
last_input_time = glfwGetTime(); /* keeps the OSD awake */
|
|
|
|
switch (key) {
|
|
case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(win, 1); break;
|
|
|
|
/* WASD pans the camera and the arrow keys rotate it; both are polled
|
|
* per-frame in the main loop for smooth, frame-rate-independent motion. */
|
|
case GLFW_KEY_PAGE_UP: cfg.speed += 2.5f; clampf(&cfg.speed, -100,100); break;
|
|
case GLFW_KEY_PAGE_DOWN: cfg.speed -= 2.5f; clampf(&cfg.speed, -100,100); break;
|
|
|
|
case GLFW_KEY_E: cfg.tumble += 2.5f; clampf(&cfg.tumble,0,100); break;
|
|
case GLFW_KEY_Q: cfg.tumble -= 2.5f; clampf(&cfg.tumble,0,100); break;
|
|
|
|
case GLFW_KEY_T: cfg.tumble_var += 5; clampf(&cfg.tumble_var,0,100); break;
|
|
case GLFW_KEY_Y: cfg.tumble_var -= 5; clampf(&cfg.tumble_var,0,100); break;
|
|
|
|
case GLFW_KEY_X: cfg.render_dist += 10; clampf(&cfg.render_dist,40,1520); break;
|
|
case GLFW_KEY_Z: cfg.render_dist -= 10; clampf(&cfg.render_dist,40,1520); break;
|
|
|
|
case GLFW_KEY_U: cfg.size_min += 0.2f; normalize_sizes(); break;
|
|
case GLFW_KEY_J: cfg.size_min -= 0.2f; normalize_sizes(); break;
|
|
case GLFW_KEY_I: cfg.size_max += 0.2f; normalize_sizes(); break;
|
|
case GLFW_KEY_K: cfg.size_max -= 0.2f; normalize_sizes(); break;
|
|
|
|
case GLFW_KEY_LEFT_BRACKET: cfg.hue -= 6; if (cfg.hue < 0) cfg.hue += 360; break;
|
|
case GLFW_KEY_RIGHT_BRACKET: cfg.hue += 6; if (cfg.hue >= 360) cfg.hue -= 360; break;
|
|
|
|
case GLFW_KEY_C: cfg.hue_cycle += 5; clampf(&cfg.hue_cycle,0,100); break;
|
|
case GLFW_KEY_V: cfg.hue_cycle -= 5; clampf(&cfg.hue_cycle,0,100); break;
|
|
|
|
case GLFW_KEY_M: cfg.multicolor = !cfg.multicolor; break;
|
|
case GLFW_KEY_N: cfg.cycle_shapes = !cfg.cycle_shapes; spawn_counter = 0; break;
|
|
|
|
case GLFW_KEY_O: cfg.glow += 5; clampf(&cfg.glow,0,100); break;
|
|
case GLFW_KEY_L: cfg.glow -= 5; clampf(&cfg.glow,0,100); break;
|
|
case GLFW_KEY_G: cfg.flicker += 5; clampf(&cfg.flicker,0,100); break;
|
|
case GLFW_KEY_H: cfg.flicker -= 5; clampf(&cfg.flicker,0,100); break;
|
|
|
|
/* +/- adjust the live on-screen count and back-solve the baseline
|
|
* density, so a press always moves the count even when render-distance
|
|
* scaling has the active total pinned at MAX_BODIES. */
|
|
case GLFW_KEY_EQUAL: {
|
|
int a = active_count() + 10; if (a > MAX_BODIES) a = MAX_BODIES;
|
|
cfg.density = (int)(a * (RENDER_REF / cfg.render_dist) + 0.5f);
|
|
if (cfg.density < 1) cfg.density = 1;
|
|
if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES;
|
|
rebuild_field(); break;
|
|
}
|
|
case GLFW_KEY_MINUS: {
|
|
int a = active_count() - 10; if (a < 1) a = 1;
|
|
cfg.density = (int)(a * (RENDER_REF / cfg.render_dist) + 0.5f);
|
|
if (cfg.density < 1) cfg.density = 1;
|
|
if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES;
|
|
break;
|
|
}
|
|
|
|
case GLFW_KEY_F:
|
|
case GLFW_KEY_F11: toggle_fullscreen(win); break;
|
|
|
|
case GLFW_KEY_SPACE: cfg.paused = !cfg.paused; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Projection + body wireframe (modelview transform must be set). */
|
|
/* ================================================================== */
|
|
|
|
static void rot2(double *c, int a, int b, double th) {
|
|
double ca = cos(th), sa = sin(th), x = c[a], y = c[b];
|
|
c[a] = x*ca - y*sa;
|
|
c[b] = x*sa + y*ca;
|
|
}
|
|
|
|
/* Produce 3-D vertices for a body. 3-D shapes are copied through;
|
|
* higher-D shapes are rotated in their own dimension (driven by the
|
|
* tumble angle + a per-body phase) and perspective-projected down to
|
|
* 3-D, then renormalized to the unit sphere so the bounding radius
|
|
* stays <= 1 (which keeps the no-overlap guarantee intact). */
|
|
static void project_body(const Solid *s, float angf, float phasef, float out[][3]) {
|
|
int d = s->dim;
|
|
if (d == 3) {
|
|
for (int i = 0; i < s->nv; i++) {
|
|
out[i][0] = s->v[i][0]; out[i][1] = s->v[i][1]; out[i][2] = s->v[i][2];
|
|
}
|
|
return;
|
|
}
|
|
double ang = angf, ph = phasef;
|
|
for (int i = 0; i < s->nv; i++) {
|
|
double c[MAXD];
|
|
for (int k = 0; k < MAXD; k++) c[k] = (k < d) ? s->v[i][k] : 0.0;
|
|
|
|
rot2(c, 0, 1, ang*0.5);
|
|
if (d >= 4) { rot2(c, 2, 3, ang*0.9 + ph); rot2(c, 0, 3, ang*0.6); }
|
|
if (d >= 5) { rot2(c, 1, 4, ang*0.7 + ph*0.5); rot2(c, 3, 4, ang*0.45); }
|
|
if (d >= 6) { rot2(c, 2, 5, ang*0.8); rot2(c, 4, 5, ang*0.4 + ph*0.3); }
|
|
|
|
for (int k = d-1; k >= 3; k--) { /* project k-D -> (k-1)-D */
|
|
double den = 4.0 - c[k];
|
|
if (den < 0.5) den = 0.5;
|
|
double f = 4.0 / den;
|
|
for (int j = 0; j < k; j++) c[j] *= f;
|
|
}
|
|
out[i][0] = (float)c[0]; out[i][1] = (float)c[1]; out[i][2] = (float)c[2];
|
|
}
|
|
double mr2 = 0;
|
|
for (int i = 0; i < s->nv; i++) {
|
|
double r2 = (double)out[i][0]*out[i][0] + (double)out[i][1]*out[i][1] + (double)out[i][2]*out[i][2];
|
|
if (r2 > mr2) mr2 = r2;
|
|
}
|
|
if (mr2 > 1e-9) {
|
|
float inv = (float)(1.0/sqrt(mr2));
|
|
for (int i = 0; i < s->nv; i++) { out[i][0]*=inv; out[i][1]*=inv; out[i][2]*=inv; }
|
|
}
|
|
}
|
|
|
|
static void draw_edges(const Solid *s, float p[][3]) {
|
|
glBegin(GL_LINES);
|
|
for (int e = 0; e < s->ne; e++) {
|
|
glVertex3fv(p[s->e[e][0]]);
|
|
glVertex3fv(p[s->e[e][1]]);
|
|
}
|
|
glEnd();
|
|
}
|
|
|
|
/* ================================================================== */
|
|
/* Help / main. */
|
|
/* ================================================================== */
|
|
|
|
static void print_help(void) {
|
|
printf(
|
|
"\nVECTORGONS — vector solids & asteroids tumbling through space\n"
|
|
"------------------------------------------------------------\n"
|
|
" W/A/S/D move (pan) camera Arrows rotate camera\n"
|
|
" PgUp/PgDn approach speed (PgDn past 0 = reverse)\n"
|
|
" Q / E tumble rate\n"
|
|
" T / Y tumble variance Z / X render distance\n"
|
|
" U / J size min I / K size max\n"
|
|
" [ / ] hue C / V hue-cycle rate\n"
|
|
" O / L glow G / H flicker\n"
|
|
" + / - density M color N shapes\n"
|
|
" F / F11 fullscreen Space pause Esc quit\n\n"
|
|
" (all settings are also shown in the on-screen display)\n\n");
|
|
fflush(stdout);
|
|
}
|
|
|
|
int main(void) {
|
|
srand((unsigned)time(NULL));
|
|
init_solids();
|
|
load_settings(); /* user's last-used settings become this run's defaults */
|
|
printf("Vectorgons: %d shape types loaded.\n", num_shapes);
|
|
|
|
if (!glfwInit()) { fprintf(stderr, "Failed to init GLFW\n"); return 1; }
|
|
glfwWindowHint(GLFW_SAMPLES, 4);
|
|
|
|
GLFWwindow *win = glfwCreateWindow(1100, 760, "Vectorgons", NULL, NULL);
|
|
if (!win) { fprintf(stderr, "Failed to create window\n"); glfwTerminate(); return 1; }
|
|
glfwMakeContextCurrent(win);
|
|
glfwSwapInterval(1);
|
|
glfwSetKeyCallback(win, key_cb);
|
|
|
|
GLfloat lwr[2] = {1, 10};
|
|
glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, lwr);
|
|
max_line_width = lwr[1] > 1 ? lwr[1] : 10.0f;
|
|
|
|
if (cfg.fullscreen) { cfg.fullscreen = 0; toggle_fullscreen(win); } /* restore saved */
|
|
|
|
rebuild_field();
|
|
print_help();
|
|
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE); /* additive glow */
|
|
glEnable(GL_LINE_SMOOTH);
|
|
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
|
|
glEnable(GL_MULTISAMPLE);
|
|
|
|
last_input_time = glfwGetTime();
|
|
double last = glfwGetTime();
|
|
|
|
while (!glfwWindowShouldClose(win)) {
|
|
double now = glfwGetTime();
|
|
float dt = (float)(now - last);
|
|
last = now;
|
|
if (dt > 0.05f) dt = 0.05f;
|
|
|
|
int fbw, fbh;
|
|
glfwGetFramebufferSize(win, &fbw, &fbh);
|
|
if (fbh < 1) fbh = 1;
|
|
glViewport(0, 0, fbw, fbh);
|
|
|
|
/* --- WASD pan + arrow-key rotate (held keys, frame-rate independent) --- */
|
|
{
|
|
float pan = 42.0f * dt;
|
|
float rot = 60.0f * dt; /* degrees / second */
|
|
int moved = 0;
|
|
if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) { cam_y += pan; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) { cam_y -= pan; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) { cam_x += pan; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) { cam_x -= pan; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_LEFT) == GLFW_PRESS) { cam_yaw -= rot; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_RIGHT) == GLFW_PRESS) { cam_yaw += rot; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_UP) == GLFW_PRESS) { cam_pitch += rot; moved = 1; }
|
|
if (glfwGetKey(win, GLFW_KEY_DOWN) == GLFW_PRESS) { cam_pitch -= rot; moved = 1; }
|
|
if (cam_x < -180) cam_x = -180;
|
|
if (cam_x > 180) cam_x = 180;
|
|
if (cam_y < -180) cam_y = -180;
|
|
if (cam_y > 180) cam_y = 180;
|
|
cam_yaw = fmodf(cam_yaw, 360.0f); /* free 360 rotation, both axes */
|
|
cam_pitch = fmodf(cam_pitch, 360.0f);
|
|
if (moved) last_input_time = now; /* keep the OSD awake */
|
|
}
|
|
|
|
glMatrixMode(GL_PROJECTION);
|
|
glLoadIdentity();
|
|
gluPerspective(60.0, (double)fbw / (double)fbh, 0.5, cfg.render_dist + 80.0);
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glLoadIdentity();
|
|
glRotatef(cam_pitch, 1.0f, 0.0f, 0.0f); /* apply camera rotation (arrows) */
|
|
glRotatef(cam_yaw, 0.0f, 1.0f, 0.0f);
|
|
glTranslatef(-cam_x, -cam_y, 0.0f); /* apply camera pan (WASD) */
|
|
|
|
glClearColor(0.006f, 0.010f, 0.035f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
/* continuous hue cycling (advances even while paused) */
|
|
if (cfg.hue_cycle > 0.001f) {
|
|
cfg.hue += (cfg.hue_cycle / 100.0f) * 120.0f * dt; /* up to 120 deg/s */
|
|
cfg.hue = fmodf(cfg.hue, 360.0f);
|
|
if (cfg.hue < 0) cfg.hue += 360.0f;
|
|
}
|
|
|
|
/* signed approach: negative speed flies the field backward (Page Down) */
|
|
float sp = cfg.speed / 100.0f;
|
|
float approach = copysignf(powf(fabsf(sp), 1.6f), sp) * 120.0f;
|
|
float tumble_rate = powf(cfg.tumble / 100.0f, 1.4f) * 3.2f;
|
|
float var = cfg.tumble_var / 100.0f;
|
|
float glow = cfg.glow / 100.0f;
|
|
float fl = cfg.flicker / 100.0f;
|
|
|
|
int active = active_count();
|
|
ensure_filled(active);
|
|
|
|
for (int i = 0; i < active; i++) {
|
|
Body *b = &bodies[i];
|
|
|
|
if (!cfg.paused) {
|
|
float spin = powf(4.0f, b->spin_seed * var); /* live variance */
|
|
b->z -= approach * dt;
|
|
b->angle += tumble_rate * spin * dt;
|
|
}
|
|
if (b->spawn_fade < 1.0f) { /* ease in after spawn */
|
|
b->spawn_fade += dt * 2.2f;
|
|
if (b->spawn_fade > 1.0f) b->spawn_fade = 1.0f;
|
|
}
|
|
/* recycle once the body leaves the render-distance sphere (in any
|
|
* direction), so culling is purely radial and rotation-agnostic */
|
|
float dist2 = b->x*b->x + b->y*b->y + b->z*b->z;
|
|
float R = cfg.render_dist;
|
|
if (dist2 > R*R) { spawn_body(b, 0, active, i); continue; }
|
|
|
|
float depth = sqrtf(dist2) / R; /* 0 at camera .. 1 at far sphere */
|
|
if (depth < 0) depth = 0;
|
|
if (depth > 1) depth = 1;
|
|
|
|
float bright = 0.25f + (1.0f - depth) * 0.75f;
|
|
if (fl > 0.001f) bright *= 1.0f - fl * 0.6f * frand(); /* flicker */
|
|
|
|
float alpha = b->spawn_fade;
|
|
if (depth > 0.85f) alpha *= (1.0f - depth) / 0.15f; /* fade in far */
|
|
if (alpha < 0) alpha = 0;
|
|
|
|
float hue = cfg.multicolor ? (cfg.hue + b->hue_offset) : cfg.hue;
|
|
float r, g, bl;
|
|
hsv_to_rgb(hue, 0.9f, 1.0f, &r, &g, &bl);
|
|
|
|
float lw = 1.0f + (1.0f - depth) * 2.0f;
|
|
const Solid *s = &solids[b->shape];
|
|
|
|
/* project once (4/5/6-D shapes morph; 3-D shapes pass through) */
|
|
static float p3[MAX_VERTS][3];
|
|
project_body(s, b->angle, b->hue_offset * 0.01745329f, p3);
|
|
|
|
glPushMatrix();
|
|
glTranslatef(b->x, b->y, -b->z);
|
|
glRotatef(b->angle * 57.2957795f, b->axis[0], b->axis[1], b->axis[2]);
|
|
glScalef(b->size, b->size, b->size);
|
|
|
|
/* CRT phosphor glow: a sharp core wrapped in a soft, dim mist.
|
|
* Hardware caps line width (~10px), so the near halo is built from
|
|
* many faint antialiased width layers fading outward (a smooth
|
|
* gradient, not one fat blurry line), and a few faint scaled-up
|
|
* ghost copies bloom the glow into a larger volume than the line
|
|
* width alone could ever reach. */
|
|
if (glow > 0.001f) {
|
|
float maxw = max_line_width;
|
|
|
|
/* (a) soft halo hugging each vector, fading out into mist */
|
|
for (int p = 1; p <= 5; p++) {
|
|
float t = p / 5.0f; /* 0..1 outward */
|
|
float w = lw + (maxw - lw) * t;
|
|
if (w < 1.0f) w = 1.0f;
|
|
float fade = (1.0f - t) * (1.0f - t); /* dim, fades out */
|
|
glColor4f(r*bright, g*bright, bl*bright, alpha * glow * 0.16f * fade);
|
|
glLineWidth(w);
|
|
draw_edges(s, p3);
|
|
}
|
|
|
|
/* (b) volumetric bloom: a few faint, widely-spaced enlarged
|
|
* ghosts drawn at the maximum (blurriest) width, so they smear
|
|
* into an outer haze that fills a larger volume than the line-
|
|
* width-capped halo can reach -- without reading as crisp rings */
|
|
glLineWidth(maxw);
|
|
for (int p = 1; p <= 3; p++) {
|
|
float t = p / 3.0f;
|
|
float sc = 1.0f + glow * (0.22f + 0.55f * t);
|
|
glColor4f(r*bright, g*bright, bl*bright,
|
|
alpha * glow * 0.035f * (1.0f - 0.6f * t));
|
|
glPushMatrix();
|
|
glScalef(sc, sc, sc);
|
|
draw_edges(s, p3);
|
|
glPopMatrix();
|
|
}
|
|
}
|
|
|
|
/* crisp core, drawn on top so the vector stays sharp */
|
|
glColor4f(r*bright, g*bright, bl*bright, alpha);
|
|
glLineWidth(lw);
|
|
draw_edges(s, p3);
|
|
|
|
glPopMatrix();
|
|
}
|
|
|
|
/* OSD: full for 10 s after last keypress, then fades over 4 s */
|
|
float idle = (float)(now - last_input_time);
|
|
float osd_alpha = 1.0f;
|
|
if (idle > 10.0f) osd_alpha = 1.0f - (idle - 10.0f) / 4.0f;
|
|
if (osd_alpha < 0) osd_alpha = 0;
|
|
render_osd(fbw, fbh, osd_alpha);
|
|
|
|
glfwSwapBuffers(win);
|
|
glfwPollEvents();
|
|
}
|
|
|
|
save_settings(); /* persist current settings as next run's defaults */
|
|
glfwDestroyWindow(win);
|
|
glfwTerminate();
|
|
return 0;
|
|
}
|