/* gravitywell, Copyright (c) 2019 Jamie Zawinski <jwz@jwz.org>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
#define DEFAULTS "*delay: 30000 \n" \
"*count: 15 \n" \
"*gridColor: #00FF00\n" \
"*gridColor2: #FF0000\n" \
"*showFPS: False \n" \
"*wireframe: False \n"
# define release_gw 0
#undef countof
#define countof(x) (sizeof((x))/sizeof((*x)))
#define DEF_SPEED "1.0"
#define DEF_RESOLUTION "1.0"
#define DEF_GRID_SIZE "1.0"
#include "xlockmore.h"
#include "gltrackball.h"
#include "colors.h"
#include "hsv.h"
#include <ctype.h>
#define ASSERT(x)
#ifdef USE_GL /* whole file */
typedef struct {
GLfloat mass;
GLfloat ro2, rm2, ri2; /* outer/middle/inner */
GLfloat ro, radius;
GLfloat x, y, dx, dy;
GLfloat surface_gravity, depth;
} star;
typedef struct {
GLXContext *glx_context;
trackball_state *user_trackball;
Bool button_down_p;
int nstars;
star *stars;
int grid_w, grid_h;
GLfloat *grid;
char *segs;
GLfloat *vtx, *col;
GLfloat color[4];
int ncolors;
XColor *colors;
} gw_configuration;
static gw_configuration *bps = NULL;
static GLfloat speed, resolution, grid_size;
#define RESOLUTION_BASE 512
#define GRID_SIZE_BASE 7
#define SPEED_BASE 2.5
#define MASS_EPSILON 0.03
#define SLOPE_EPSILON 0.06
#define GRID_SEG 16u /* Power-of-two here is faster. */
#define MAX_MASS_COLOR 120
static XrmOptionDescRec opts[] = {
{ "-speed", ".speed", XrmoptionSepArg, 0 },
{ "-resolution", ".resolution", XrmoptionSepArg, 0 },
{ "-grid-size", ".gridSize", XrmoptionSepArg, 0 },
};
static argtype vars[] = {
{&speed, "speed", "Speed", DEF_SPEED, t_Float},
{&resolution, "resolution", "Resolution", DEF_RESOLUTION, t_Float},
{&grid_size, "gridSize", "GridSize", DEF_GRID_SIZE, t_Float},
};
ENTRYPOINT ModeSpecOpt gw_opts = {
countof(opts), opts, countof(vars), vars, NULL};
#define MAX(a,b) ((a)>(b)?(a):(b))
#define MIN(a,b) ((a)<(b)?(a):(b))
#define WCLIP(x,hi) MIN(MAX((int)(x),0),(hi))
/* Window management, etc
*/
ENTRYPOINT void
reshape_gw (ModeInfo *mi, int width, int height)
{
GLfloat h = (GLfloat) height / (GLfloat) width;
int y = 0;
if (width > height * 5) { /* tiny window: show middle */
height = width * 9/16;
y = -height/2;
h = height / (GLfloat) width;
}
glViewport (0, y, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective (40, 1/h, 10, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt( 0, 0, 30,
0, 0, 0,
0, 1, 0);
glClear(GL_COLOR_BUFFER_BIT);
}
ENTRYPOINT Bool
gw_handle_event (ModeInfo *mi, XEvent *event)
{
gw_configuration *bp = &bps[MI_SCREEN(mi)];
if (gltrackball_event_handler (event, bp->user_trackball,
MI_WIDTH (mi), MI_HEIGHT (mi),
&bp->button_down_p))
return True;
return False;
}
static void
parse_color (ModeInfo *mi, char *key, GLfloat color[4])
{
XColor xcolor;
char *string = get_string_resource (mi->dpy, key, "Color");
if (!XParseColor (mi->dpy, mi->xgwa.colormap, string, &xcolor))
{
fprintf (stderr, "%s: unparsable color in %s: %s\n", progname,
key, string);
exit (1);
}
free (string);
color[0] = xcolor.red / 65536.0;
color[1] = xcolor.green / 65536.0;
color[2] = xcolor.blue / 65536.0;
color[3] = 1;
}
static void
new_star (const gw_configuration *bp, star *s)
{
int w = bp->grid_w * GRID_SEG;
s->radius = 2 * (2 + frand(3) + frand(3) + frand(3));
s->mass = s->radius * 150 * (2 + frand(3) + frand(3) + frand(3));
s->ro2 = s->mass / MASS_EPSILON;
s->ro = sqrt (s->ro2);
s->rm2 = pow (s->mass * (2.0f / SLOPE_EPSILON), 2.0f / 3.0f);
s->ri2 = s->radius * s->radius;
if (s->rm2 < s->ri2)
s->rm2 = s->ri2;
if (s->ro2 < s->rm2)
s->ro2 = s->rm2;
s->x = w * (s == bp->stars ? 0.5 : (0.35 + frand(0.3)));
s->dx = ((frand(1.0) - 0.5) * 0.1) / resolution;
s->dy = (0.1 + frand(0.6)) / resolution;
/* What the experienced gravitation would be at the surface of the
star, were the mass actually held in a singularity at its center.
*/
s->surface_gravity = s->mass / s->ri2;
s->depth = s->surface_gravity;
}
static void
move_stars (ModeInfo *mi)
{
gw_configuration *bp = &bps[MI_SCREEN(mi)];
int w = bp->grid_w * GRID_SEG;
int h = bp->grid_h * GRID_SEG;
int i;
for (i = 0; i < bp->nstars; i++)
{
star *s = &bp->stars[i];
/* Move stars off screen until most of their influence fades */
GLfloat off = speed * SPEED_BASE * resolution;
s->x += s->dx * off;
s->y += s->dy * off;
if (s->x < -s->ro ||
s->y < -s->ro ||
s->x >= w + s->ro ||
s->y >= h + s->ro)
{
new_star (bp, s);
s->y = -s->ro;
}
}
}
static void
calc_o (gw_configuration *bp, GLfloat mass, GLfloat cx, GLfloat y02,
unsigned from, unsigned to)
{
GLfloat x0 = cx - from * GRID_SEG;
GLfloat g0 = mass / (x0*x0 + y02);
unsigned x;
ASSERT (to <= bp->grid_w || to <= bp->grid_h);
for (x = from; x < to; x++)
{
GLfloat *g = &bp->grid[x * GRID_SEG];
GLfloat g1;
x0 = cx - (x + 1) * GRID_SEG;
g1 = mass / (x0*x0 + y02);
g[0] += g0;
if (bp->segs[x])
{
GLfloat d = (g1 - g0) / GRID_SEG;
unsigned i;
for(i = 1; i != GRID_SEG; i++)
{
g0 += d;
g[i] += g0;
}
}
g0 = g1;
}
}
static void
make_hires (gw_configuration *bp, unsigned from, unsigned to, unsigned w)
{
unsigned x;
/* One bigger than from/to so that there's a good angle between the middle
and inner zones.
Don't make the last GRID_SEG high-res. This keeps the length consistent.
*/
if (from)
from--;
from = MIN(from / GRID_SEG, w - 1);
to = MIN(to / GRID_SEG + 1, w - 1);
ASSERT (to <= bp->grid_w - 1 || to <= bp->grid_h - 1);
for (x = from; x < to; x++)
{
if (! bp->segs[x])
{
GLfloat *g = &bp->grid[x * GRID_SEG];
GLfloat g0 = g[0], g1 = g[GRID_SEG];
GLfloat d = (g1 - g0) / GRID_SEG;
unsigned i;
for (i = 1; i != GRID_SEG; i++)
{
g0 += d;
g[i] = g0;
}
bp->segs[x] = True;
}
}
}
static void
calc_m (gw_configuration *bp, GLfloat mass, GLfloat cx, GLfloat y02,
unsigned from, unsigned to)
{
GLfloat *gridp = bp->grid;
unsigned x;
ASSERT (to <= bp->grid_w * GRID_SEG + 1 || to <= bp->grid_h * GRID_SEG + 1);
for (x = from; x < to; x++)
{
/* Inverse square of distance from mass as a point source */
GLfloat x0 = cx - x;
gridp[x] += mass / (x0*x0 + y02);
}
}
#define EASE(r) (sin ((r) * M_PI_2))
static void
draw_row (ModeInfo *mi, int w, int y, Bool swap)
{
gw_configuration *bp = &bps[MI_SCREEN(mi)];
int i;
int x;
int polys;
int w2 = w * GRID_SEG;
GLfloat *vtx_x;
GLfloat *vtx_y;
GLfloat *gridp = bp->grid;
memset (gridp, 0, w2 * sizeof(*gridp));
memset (bp->segs, 0, w);
for (i = 0; i < bp->nstars; i++)
{
star *s = &bp->stars[i];
GLfloat cx, cy;
unsigned olo, ohi, mlo, mhi, ilo, ihi;
GLfloat mass, max;
/* Move stars off screen until most of their influence fades */
GLfloat ro, rm, ri;
GLfloat y0;
GLfloat y02;
if (swap)
{
cy = s->x;
cx = s->y;
}
else
{
cx = s->x;
cy = s->y;
}
mass = s->mass;
max = s->surface_gravity;
y0 = cy - y;
y02 = y0 * y0;
if (y02 > s->ro2) continue;
ro = sqrtf (s->ro2 - y02);
olo = WCLIP((cx - ro) / GRID_SEG + 1, w); /* GLfloat -> int */
ohi = WCLIP((cx + ro) / GRID_SEG + 1, w);
rm = s->rm2 > y02 ? sqrtf (s->rm2 - y02) : 0;
mlo = WCLIP((cx - rm) + 1, w2);
mhi = WCLIP((cx + rm) + 1, w2);
ASSERT (mlo <= mhi);
if (mlo != mhi)
{
ri = s->ri2 > y02 ? sqrtf (s->ri2 - y02) : 0;
ilo = WCLIP(cx - ri + 1, w2);
ihi = WCLIP(cx + ri + 1, w2);
mlo -= mlo % GRID_SEG;
mhi += GRID_SEG - 1;
mhi -= mhi % GRID_SEG;
/* These go first. */
make_hires (bp, mlo, ilo, w);
make_hires (bp, ihi, mhi, w);
calc_m (bp, mass, cx, y02, mlo, ilo);
calc_m (bp, mass, cx, y02, ihi, mhi);
/* This does a bit more work than it needs to. */
for (x = ilo; x < ihi; x++)
gridp[x] += max;
}
calc_o (bp, mass, cx, y02, olo, mlo / GRID_SEG);
calc_o (bp, mass, cx, y02, mhi / GRID_SEG, ohi);
}
if (swap)
{
vtx_y = bp->vtx;
vtx_x = bp->vtx + 1;
}
else
{
vtx_x = bp->vtx;
vtx_y = bp->vtx + 1;
}
# define COLOR_CODE 0
# if COLOR_CODE
{
unsigned grid_max = bp->grid_w > bp->grid_h ? bp->grid_w : bp->grid_h;
GLfloat *color = malloc(sizeof(GLfloat) * 4 * (grid_max * GRID_SEG + 1));
glEnableClientState (GL_COLOR_ARRAY);
glColorPointer (4, GL_FLOAT, 0, color);
# endif
ASSERT (! bp->segs[w - 1]);
polys = 0;
for (x = 0; x != w; x++)
{
if (! bp->segs[x])
{
int ci;
size_t vp = polys * 3;
size_t cp = polys * 4;
# if COLOR_CODE
GLfloat slope = 0;
if (x != 0)
slope += fabs(gridp[x * GRID_SEG] - gridp[(x - 1) * GRID_SEG]);
if (x != w - 1)
slope += fabs(gridp[(x + 1) * GRID_SEG] - gridp[x * GRID_SEG]);
slope = 1 - (slope / (SLOPE_EPSILON * 2));
color[cp] = slope;
color[cp + 1] = slope;
color[cp + 2] = 1;
color[cp + 3] = 1;
# endif
vtx_x[vp] = x * GRID_SEG;
bp->vtx[vp + 2] = gridp[x * GRID_SEG];
polys += 1;
ci = EASE (bp->vtx[vp + 2] / MAX_MASS_COLOR) * bp->ncolors;
bp->col[cp] = bp->colors[ci].red / 65536.0;
bp->col[cp+1] = bp->colors[ci].green / 65536.0;
bp->col[cp+2] = bp->colors[ci].blue / 65536.0;
bp->col[cp+3] = 1;
}
else
{
for(i = 0; i != GRID_SEG; i++)
{
int ci;
size_t vp = (polys + i) * 3;
size_t cp = (polys + i) * 4;
# if COLOR_CODE
color[cp] = 1;
color[cp + 1] = 0.75;
color[cp + 2] = 0;
color[cp + 3] = 1;
# endif
vtx_x[vp] = x * GRID_SEG + i;
bp->vtx[vp + 2] = gridp[x * GRID_SEG + i];
ci = EASE (bp->vtx[vp + 2] / MAX_MASS_COLOR) * bp->ncolors;
bp->col[cp] = bp->colors[ci].red / 65536.0;
bp->col[cp+1] = bp->colors[ci].green / 65536.0;
bp->col[cp+2] = bp->colors[ci].blue / 65536.0;
bp->col[cp+3] = 1;
}
polys += GRID_SEG;
}
}
for (i = 0; i < polys; i++)
vtx_y[i * 3] = y; /* + random() * (MASS_EPSILON / (MAXRAND)); */
mi->polygon_count += polys;
glDrawArrays (GL_LINE_STRIP, 0, polys);
# if COLOR_CODE
glDisableClientState (GL_COLOR_ARRAY);
free (color);
}
# endif
}
ENTRYPOINT void
init_gw (ModeInfo *mi)
{
gw_configuration *bp;
unsigned grid_max, vtx_max;
int i;
MI_INIT (mi, bps);
bp = &bps[MI_SCREEN(mi)];
bp->glx_context = init_GL(mi);
reshape_gw (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);
glEnable(GL_NORMALIZE);
glEnable(GL_CULL_FACE);
{
int h1, h2;
double s1, v1, s2, v2;
GLfloat color2[4];
parse_color (mi, "gridColor", bp->color);
parse_color (mi, "gridColor2", color2);
rgb_to_hsv (bp->color[0] * 65536,
bp->color[1] * 65536,
bp->color[2] * 65536,
&h1, &s1, &v1);
rgb_to_hsv (color2[0] * 65536,
color2[1] * 65536,
color2[2] * 65536,
&h2, &s2, &v2);
bp->ncolors = 128;
bp->colors = (XColor *) calloc(bp->ncolors, sizeof(XColor));
make_color_ramp (0, 0, 0,
h1, s1, v1, h2, s2, v2,
bp->colors, &bp->ncolors,
False, 0, False);
}
bp->user_trackball = gltrackball_init (False);
bp->grid_w = (RESOLUTION_BASE * resolution) / GRID_SEG;
if (bp->grid_w < 2) bp->grid_w = 2;
bp->grid_h = bp->grid_w;
grid_max = bp->grid_w > bp->grid_h ? bp->grid_w : bp->grid_h;
vtx_max = grid_max * GRID_SEG;
bp->grid = (GLfloat *) calloc (vtx_max, sizeof(*bp->grid));
bp->vtx = (GLfloat *) calloc (vtx_max * 3, sizeof(*bp->vtx));
bp->col = (GLfloat *) calloc (vtx_max * 4, sizeof(*bp->col));
bp->segs = (char *) calloc (grid_max, sizeof(*bp->segs));
if (! bp->grid || ! bp->vtx || ! bp->col || ! bp->segs) abort();
bp->nstars = MI_COUNT(mi);
bp->stars = (star *) calloc (bp->nstars, sizeof (star));
for (i = 0; i < bp->nstars; i++)
{
star *s = &bp->stars[i];
new_star (bp, s);
s->y = frand(s->ro * 2 + bp->grid_h * GRID_SEG) - s->ro;
}
/* Let's tilt the floor a little. */
gltrackball_reset (bp->user_trackball,
-0.4 + frand(0.8),
-0.3 + frand(0.2));
}
ENTRYPOINT void
draw_gw (ModeInfo *mi)
{
gw_configuration *bp = &bps[MI_SCREEN(mi)];
int wire = MI_IS_WIREFRAME(mi);
Display *dpy = MI_DISPLAY(mi);
Window window = MI_WINDOW(mi);
int gridmod = grid_size * GRID_SIZE_BASE;
int x, y, i;
int sample_x, sample_y;
GLfloat sample_z = -1;
if (!bp->glx_context)
return;
glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix ();
# ifdef HAVE_MOBILE
glRotatef (current_device_rotation(), 0, 0, 1); /* right side up */
# endif
gltrackball_rotate (bp->user_trackball);
#if 0
glScalef(0.05/resolution, 0.05/resolution, 0.05/resolution);
#endif
glRotatef (90, 1, 0, 0);
glTranslatef (-bp->grid_w * (GRID_SEG / 2.0f),
-bp->grid_h * (GRID_SEG * 0.75f),
3);
#if 0
glColor3f(1,0,0);
glPushMatrix();
glTranslatef(0,0,0);
glScalef (bp->grid_w * GRID_SEG,
bp->grid_w * GRID_SEG,
bp->grid_w * GRID_SEG);
glDisable (GL_FOG);
glBegin(GL_LINE_LOOP);
glVertex3f(0, 0, 0);
glVertex3f(1, 0, 0);
glVertex3f(1, 1, 0);
glVertex3f(.4, 1, 0);
glVertex3f(.5, .5, 0);
glVertex3f(.6, 1, 0);
glVertex3f(0, 1, 0);
glEnd();
glPopMatrix();
glColor3f(0,1,0);
if (!wire) glEnable (GL_FOG);
#endif
if (!wire)
{
GLfloat fog_color[4] = { 0, 0, 0, 1 };
glLineWidth (2);
glEnable (GL_LINE_SMOOTH);
glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable (GL_BLEND);
glFogi (GL_FOG_MODE, GL_EXP2);
glFogfv (GL_FOG_COLOR, fog_color);
glFogf (GL_FOG_DENSITY, 0.005);
glEnable (GL_FOG);
}
glEnableClientState (GL_COLOR_ARRAY);
glEnableClientState (GL_VERTEX_ARRAY);
glColorPointer (4, GL_FLOAT, 0, bp->col);
glVertexPointer (3, GL_FLOAT, 0, bp->vtx);
/* Somewhere near the midpoint of the view */
sample_x = ((int) (bp->grid_w * GRID_SEG * 0.5) / gridmod) * gridmod;
sample_y = ((int) (bp->grid_h * GRID_SEG * 0.75) / GRID_SEG) * GRID_SEG;
/* Find the cumulative gravitational effect at the midpoint of each star,
for the depth of the foot-circle. This duplicates some of the draw_row()
logic. */
for (i = 0; i < bp->nstars; i++)
{
star *s0 = &bp->stars[i];
GLfloat x0 = s0->x;
GLfloat y0 = s0->y;
int j;
s0->depth = s0->surface_gravity;
for (j = 0; j < bp->nstars; j++)
{
star *s1;
GLfloat x1, y1, d2;
if (i == j) continue;
s1 = &bp->stars[j];
x1 = s1->x;
y1 = s1->y;
d2 = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0);
s0->depth += s1->mass / d2;
}
}
mi->polygon_count = 0;
for (y = 0; y < (bp->grid_h - 1) * GRID_SEG; y += gridmod)
draw_row (mi, bp->grid_w, y, False);
for (x = 0; x < (bp->grid_w - 1) * GRID_SEG; x += gridmod)
{
draw_row (mi, bp->grid_h, x, True);
if (x == sample_x)
sample_z = bp->grid[sample_y];
}
if (mi->fps_p)
{
/* Mass of Sol is 2x10^30kg, or 332 kilo-Earths.
But I'm not sure what the funniest number to put here is. */
/* mi->recursion_depth = (int) sample_z/4; */
mi->recursion_depth = (int) (sample_z * 30000);
glColor4fv (bp->color);
glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, bp->color);
glBegin(GL_LINES);
glVertex3f (sample_x-0.15, sample_y-0.15, sample_z);
glVertex3f (sample_x+0.15, sample_y+0.15, sample_z);
glVertex3f (sample_x-0.15, sample_y+0.15, sample_z);
glVertex3f (sample_x+0.15, sample_y-0.15, sample_z);
glEnd();
}
/* Draw a circle around the "footprint" at the bottom of the gravity well.
*/
for (i = 0; i < bp->nstars; i++)
{
int steps = 16;
star *s = &bp->stars[i];
GLfloat th, color[4];
int ci;
ci = EASE (s->depth / MAX_MASS_COLOR) * bp->ncolors;
color[0] = bp->colors[ci].red / 65536.0;
color[1] = bp->colors[ci].green / 65536.0;
color[2] = bp->colors[ci].blue / 65536.0;
color[3] = 1;
glColor4fv (color);
glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, color);
glPushMatrix();
glTranslatef (s->x, s->y, 0);
glBegin (GL_LINE_LOOP);
for (th = 0; th < M_PI * 2; th += M_PI/steps)
glVertex3f (s->radius * cos(th), s->radius * sin(th), s->depth);
glEnd();
glPopMatrix();
mi->polygon_count += steps;
}
#if 0
{
for (i = 0; i < bp->nstars; i++)
{
star *s = &bp->stars[i];
GLfloat maxr = sqrt (s->mass / MASS_EPSILON);
GLfloat th;
glPushMatrix();
glTranslatef (s->x, s->y, 0);
glColor3f(0, 0, 1);
glBegin (GL_LINE_LOOP);
for (th = 0; th < M_PI * 2; th += M_PI/32)
glVertex3f (s->radius * cos(th), s->radius * sin(th), 0);
glEnd();
glColor3f(0, 0, 0.5);
glBegin (GL_LINE_LOOP);
for (th = 0; th < M_PI * 2; th += M_PI/32)
glVertex3f (maxr * cos(th), maxr * sin(th), 0);
glEnd();
glBegin (GL_LINES);
glVertex3f ( 3000 * s->dx, 3000 * s->dy, 0);
glVertex3f (-3000 * s->dx, -3000 * s->dy, 0);
glEnd();
glPopMatrix();
}
}
#endif
glPopMatrix ();
if (! bp->button_down_p)
move_stars (mi);
if (mi->fps_p) do_fps (mi);
glFinish();
glXSwapBuffers(dpy, window);
}
ENTRYPOINT void
free_gw (ModeInfo *mi)
{
gw_configuration *bp = &bps[MI_SCREEN(mi)];
if (!bp->glx_context) return;
glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
if (bp->user_trackball) gltrackball_free (bp->user_trackball);
if (bp->stars) free (bp->stars);
if (bp->grid) free (bp->grid);
if (bp->vtx) free (bp->vtx);
if (bp->col) free (bp->col);
if (bp->segs) free (bp->segs);
if (bp->colors) free (bp->colors);
}
XSCREENSAVER_MODULE_2 ("GravityWell", gravitywell, gw)
#endif /* USE_GL */