Web, Commodore, Windows version, Linux standalone, Windows Screensaver complete.

This commit is contained in:
The Dust Council 2026-06-05 00:39:31 -07:00
parent 47730936de
commit 9257f21598
19 changed files with 794 additions and 18 deletions

View file

@ -67,8 +67,18 @@
#define NOMINMAX
#include <windows.h> /* provides APIENTRY / CALLBACK that the GL headers need */
#endif
#ifdef __EMSCRIPTEN__
/* Browser build: WebGL via Emscripten's legacy-GL emulation. GLU is not
* provided, so the three GLU helpers used here are shimmed below. */
#include <emscripten.h>
#include <emscripten/html5.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <GL/gl.h>
#else
#include <GLFW/glfw3.h>
#include <GL/glu.h>
#endif
/* Legacy GL constants that the Windows (MinGW) OpenGL 1.1 headers may omit.
* The values are standard; the GPU driver provides the functionality at run
@ -88,6 +98,50 @@
#include <time.h>
#include <stddef.h>
#ifdef __EMSCRIPTEN__
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* Minimal GLU replacements (Emscripten has no GLU). gluOrtho2D/gluPerspective
* post-multiply onto the current fixed-function matrix; gluProject is pure math. */
static void gluOrtho2D(double l, double r, double b, double t) {
float m[16] = { 2.0f/(float)(r-l),0,0,0, 0,2.0f/(float)(t-b),0,0, 0,0,-1,0,
-(float)((r+l)/(r-l)), -(float)((t+b)/(t-b)), 0, 1 };
glMultMatrixf(m);
}
static void gluPerspective(double fovy, double aspect, double zn, double zf) {
double f = 1.0 / tan(fovy * (M_PI / 360.0));
float m[16] = { (float)(f/aspect),0,0,0, 0,(float)f,0,0,
0,0,(float)((zf+zn)/(zn-zf)),-1, 0,0,(float)((2*zf*zn)/(zn-zf)),0 };
glMultMatrixf(m);
}
static int gluProject(double ox, double oy, double oz, const double m[16],
const double p[16], const int vp[4],
double *wx, double *wy, double *wz) {
double a0=m[0]*ox+m[4]*oy+m[8]*oz+m[12], a1=m[1]*ox+m[5]*oy+m[9]*oz+m[13];
double a2=m[2]*ox+m[6]*oy+m[10]*oz+m[14], a3=m[3]*ox+m[7]*oy+m[11]*oz+m[15];
double c0=p[0]*a0+p[4]*a1+p[8]*a2+p[12]*a3, c1=p[1]*a0+p[5]*a1+p[9]*a2+p[13]*a3;
double c2=p[2]*a0+p[6]*a1+p[10]*a2+p[14]*a3, c3=p[3]*a0+p[7]*a1+p[11]*a2+p[15]*a3;
if (c3 == 0.0) return 0;
c0/=c3; c1/=c3; c2/=c3;
*wx = vp[0] + (c0*0.5+0.5)*vp[2];
*wy = vp[1] + (c1*0.5+0.5)*vp[3];
*wz = c2*0.5 + 0.5;
return 1;
}
/* The legacy-GL emulation exposes only the float entry points; map the handful
* of double-precision calls used here onto them (all glGetDoublev calls query a
* 4x4 matrix). */
static void vg_get_doublev(GLenum pname, double *out) {
float f[16];
glGetFloatv(pname, f);
for (int i = 0; i < 16; i++) out[i] = f[i];
}
#define glGetDoublev(pname, out) vg_get_doublev((pname), (out))
#define glVertex2d(x, y) glVertex2f((float)(x), (float)(y))
#define glVertex3d(x, y, z) glVertex3f((float)(x), (float)(y), (float)(z))
#endif /* __EMSCRIPTEN__ */
/* --- OpenGL 1.5 buffer objects (Step 1: retained-mode geometry) --------------
* The hot wireframe geometry is uploaded to VBOs and drawn with glDrawElements
* instead of per-vertex glBegin/glVertex3fv, which removes the millions of
@ -2873,6 +2927,7 @@ static unsigned g_drawn = 0; /* bodies that passed frustum cull
static double g_frustum[6][4]; /* world-space frustum planes (a,b,c,d) */
static int g_vsync = 1; /* F2 toggles glfwSwapInterval(0/1) */
static int g_show_hud = 1; /* F1 toggles the perf HUD */
static GLFWwindow *g_win = NULL; /* the window/canvas, shared with the frame fn */
/* --- retained-mode geometry buffers (one per shape) --------------------------
* g_shape_vbomode[si]: 0 = immediate mode (clocks/dynamics whose topology is
@ -3207,6 +3262,9 @@ static void settings_path(char *buf, size_t n) {
}
static void save_settings(void) {
#ifdef __EMSCRIPTEN__
return; /* no persistent filesystem in the browser */
#else
char path[1024];
settings_path(path, sizeof path);
FILE *f = fopen(path, "w");
@ -3230,9 +3288,13 @@ static void save_settings(void) {
fprintf(f, "mirror_count=%d\n", cfg.mirror_count);
fprintf(f, "fullscreen=%d\n", cfg.fullscreen);
fclose(f);
#endif
}
static void load_settings(void) {
#ifdef __EMSCRIPTEN__
return; /* no persistent filesystem in the browser */
#else
char path[1024];
settings_path(path, sizeof path);
FILE *f = fopen(path, "r");
@ -3282,11 +3344,33 @@ static void load_settings(void) {
cfg.multicolor = cfg.multicolor ? 1 : 0;
cfg.cycle_shapes = cfg.cycle_shapes ? 1 : 0;
cfg.fullscreen = cfg.fullscreen ? 1 : 0;
#endif
}
#ifdef __EMSCRIPTEN__
/* Keep cfg.fullscreen in sync with the browser for *any* fullscreen change
* (e.g. the user pressing Esc, which doesn't go through toggle_fullscreen). The
* actual canvas/viewport sizing is handled per-frame in render_frame(), which
* tracks the canvas's real displayed size. */
static EM_BOOL on_fullscreen_change(int type,
const EmscriptenFullscreenChangeEvent *e,
void *user) {
(void)type; (void)user;
cfg.fullscreen = e->isFullscreen ? 1 : 0;
return EM_FALSE;
}
#endif
static void toggle_fullscreen(GLFWwindow *win) {
static int wx = 100, wy = 100, ww = 1100, wh = 760;
cfg.fullscreen = !cfg.fullscreen;
#ifdef __EMSCRIPTEN__
(void)win; /* browser: request canvas fullscreen */
/* Just enter/exit fullscreen here; on_fullscreen_change() does the canvas
* resize so the render fills the screen at the correct (screen) aspect. */
if (cfg.fullscreen) emscripten_request_fullscreen("#canvas", 1);
else emscripten_exit_fullscreen();
#else
static int wx = 100, wy = 100, ww = 1100, wh = 760;
if (cfg.fullscreen) {
glfwGetWindowPos(win, &wx, &wy);
glfwGetWindowSize(win, &ww, &wh);
@ -3296,6 +3380,7 @@ static void toggle_fullscreen(GLFWwindow *win) {
} else {
glfwSetWindowMonitor(win, NULL, wx, wy, ww, wh, 0);
}
#endif
}
static void key_cb(GLFWwindow *win, int key, int sc, int action, int mods) {
@ -3503,12 +3588,24 @@ static void draw_edges(const Solid *s, float p[][3]) {
glDrawElements(GL_LINES, s->ne * 2, GL_UNSIGNED_SHORT, (const void *)0);
return;
}
/* Immediate-mode path (clocks / dynamics, whose topology is rebuilt each
* frame). Detach the vertex buffer / client array first: WebGL's legacy-GL
* immediate-mode emulation otherwise folds the stale array state into the
* glVertex stream and draws garbage long lines. (No-op on desktop, where
* immediate mode already ignores client arrays.) glVertex3f is also better
* supported by the emulation than the 3fv vector form. */
if (g_have_vbo) {
glDisableClientState(GL_VERTEX_ARRAY);
vgBindBuffer(GL_ARRAY_BUFFER, 0);
}
glBegin(GL_LINES);
for (int e = 0; e < s->ne; e++) {
glVertex3fv(p[s->e[e][0]]);
glVertex3fv(p[s->e[e][1]]);
const float *a = p[s->e[e][0]], *b = p[s->e[e][1]];
glVertex3f(a[0], a[1], a[2]);
glVertex3f(b[0], b[1], b[2]);
}
glEnd();
if (g_have_vbo) glEnableClientState(GL_VERTEX_ARRAY);
}
/* Build the 6 world-space view-frustum planes from the current modelview *
@ -3607,7 +3704,9 @@ static void ensure_scene_tex(int w, int h) {
static void grab_scene(int fbw, int fbh) {
ensure_scene_tex(fbw, fbh);
glBindTexture(GL_TEXTURE_2D, g_scene_tex);
glReadBuffer(GL_BACK);
#ifndef __EMSCRIPTEN__
glReadBuffer(GL_BACK); /* WebGL1 has no glReadBuffer (back is default) */
#endif
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0,0, 0,0, fbw, fbh);
}
@ -3625,7 +3724,7 @@ static int ensure_bloom_targets(int fbw, int fbh) {
h = h > 1 ? h / 2 : 1;
g_bl_w[i] = w; g_bl_h[i] = h;
glBindTexture(GL_TEXTURE_2D, g_bl_tex[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
@ -3940,6 +4039,8 @@ static void draw_mirrorballs(const int *list, int n, int fbw, int fbh) {
glMatrixMode(GL_MODELVIEW); glPopMatrix();
}
static void render_frame(void); /* one frame; defined after main() */
int main(int argc, char **argv) {
#ifdef SCREENSAVER
{
@ -3965,6 +4066,18 @@ int main(int argc, char **argv) {
srand((unsigned)time(NULL));
init_solids();
load_settings(); /* user's last-used settings become this run's defaults */
#ifdef __EMSCRIPTEN__
/* The browser build has no persisted settings, so start from a chosen look.
* density 205 * (1520/140) ~= 2226 on-screen bodies. */
cfg.speed = 100; cfg.tumble = 65; cfg.tumble_var = 100;
cfg.render_dist = 1520; cfg.density = 205;
cfg.size_min = 1.4f; cfg.size_max = 50.2f;
cfg.hue_cycle = 60; cfg.multicolor = 1;
cfg.glow = 75; cfg.flicker = 28;
cfg.mag_count = 25; cfg.glass_count = 25; cfg.mirror_count = 25;
cfg.cycle_shapes = 0; /* random shapes */
g_bloom = 0; /* legacy glow mode */
#endif
if (!g_screensaver)
printf("Vectorgons: %d shape types loaded.\n", num_shapes);
@ -4004,14 +4117,36 @@ int main(int argc, char **argv) {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE); /* additive glow */
glEnable(GL_LINE_SMOOTH);
#ifndef __EMSCRIPTEN__
glEnable(GL_LINE_SMOOTH); /* not available in WebGL */
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glEnable(GL_MULTISAMPLE);
#endif
last_input_time = glfwGetTime();
double last = glfwGetTime();
g_win = win;
#ifdef __EMSCRIPTEN__
/* Resize the canvas to fill the screen (at screen aspect) on fullscreen. */
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT,
NULL, EM_TRUE, on_fullscreen_change);
/* The browser drives the frame loop via requestAnimationFrame; main() returns. */
emscripten_set_main_loop(render_frame, 0, 1);
#else
while (!glfwWindowShouldClose(win) && !(g_screensaver && g_quit)) render_frame();
if (!g_screensaver) save_settings(); /* persist settings (not from the saver) */
glfwDestroyWindow(win);
glfwTerminate();
#endif
return 0;
}
while (!glfwWindowShouldClose(win) && !(g_screensaver && g_quit)) {
/* One simulation + render frame. Driven by the native while-loop on desktop, or
* by emscripten_set_main_loop() in the browser. */
static void render_frame(void) {
GLFWwindow *win = g_win;
static double last = 0.0;
if (last == 0.0) last = glfwGetTime();
{
double now = glfwGetTime();
float raw = (float)(now - last); /* true frame time (for FPS) */
float dt = raw > 0.05f ? 0.05f : raw; /* clamped for the simulation */
@ -4029,7 +4164,28 @@ int main(int argc, char **argv) {
g_batches = 0; g_verts = 0; g_drawn = 0;
int fbw, fbh;
#ifdef __EMSCRIPTEN__
/* Drive the viewport from the canvas's *actual displayed* size rather
* than GLFW's cached window size (which doesn't follow CSS/fullscreen
* reliably). Size the drawing buffer to match the displayed CSS pixels,
* so fullscreen (CSS 100vw x 100vh => the whole screen) renders at the
* screen's aspect ratio and fills it edge-to-edge no black bars, no
* stretching. Windowed, the CSS size is 1100x760, unchanged. */
{
double cssw = 0, cssh = 0;
emscripten_get_element_css_size("#canvas", &cssw, &cssh);
fbw = (int)(cssw + 0.5);
fbh = (int)(cssh + 0.5);
if (fbw < 1) fbw = 1;
if (fbh < 1) fbh = 1;
int bw = 0, bh = 0;
emscripten_get_canvas_element_size("#canvas", &bw, &bh);
if (bw != fbw || bh != fbh)
emscripten_set_canvas_element_size("#canvas", fbw, fbh);
}
#else
glfwGetFramebufferSize(win, &fbw, &fbh);
#endif
if (fbh < 1) fbh = 1;
glViewport(0, 0, fbw, fbh);
@ -4065,7 +4221,11 @@ int main(int argc, char **argv) {
glTranslatef(-cam_x, -cam_y, 0.0f); /* apply camera pan (WASD) */
build_frustum(); /* for CPU culling of off-screen bodies */
glClearColor(0.006f, 0.010f, 0.035f, 1.0f);
#ifdef __EMSCRIPTEN__
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); /* pure black in the browser */
#else
glClearColor(0.006f, 0.010f, 0.035f, 1.0f); /* faint blue-black ambiance */
#endif
glClear(GL_COLOR_BUFFER_BIT);
/* continuous hue cycling (advances even while paused) */
@ -4259,9 +4419,4 @@ int main(int argc, char **argv) {
glfwSwapBuffers(win);
glfwPollEvents();
}
if (!g_screensaver) save_settings(); /* persist settings (not from the saver) */
glfwDestroyWindow(win);
glfwTerminate();
return 0;
}