/* crumbler, Copyright (c) 2018 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" \
"*showFPS: False \n" \
"*wireframe: False \n" \
"*suppressRotationAnimation: True\n" \
# define release_crumbler 0
#include "xlockmore.h"
#include "colors.h"
#include "rotator.h"
#include "quickhull.h"
#include "gltrackball.h"
#include <ctype.h>
#ifdef USE_GL /* whole file */
#define DEF_SPIN "True"
#define DEF_WANDER "True"
#define DEF_SPEED "1.0"
#define DEF_DENSITY "1.0"
#define DEF_FRACTURE "0"
#undef RANDSIGN
#define RANDSIGN() ((random() & 1) ? 1 : -1)
typedef struct {
qh_vertex_t *verts; /* interior point cloud */
int nverts, onverts;
qh_vertex_t min, max; /* enclosing box */
qh_vertex_t mid, vec;
int polygon_count;
GLuint dlist;
int color;
int color_shift;
} chunk;
typedef struct {
GLXContext *glx_context;
rotator *rot;
trackball_state *trackball;
enum { IDLE, SPLIT, PAUSE, FLEE, ZOOM } state;
GLfloat tick;
Bool button_down_p;
int nchunks;
chunk **chunks;
chunk *ghost;
int ncolors;
XColor *colors;
} crumbler_configuration;
static crumbler_configuration *bps = NULL;
static Bool do_spin;
static GLfloat speed;
static GLfloat density;
static int fracture;
static Bool do_wander;
static XrmOptionDescRec opts[] = {
{ "-spin", ".spin", XrmoptionNoArg, "True" },
{ "+spin", ".spin", XrmoptionNoArg, "False" },
{ "-speed", ".speed", XrmoptionSepArg, 0 },
{ "-density", ".density", XrmoptionSepArg, 0 },
{ "-fracture",".fracture",XrmoptionSepArg, 0 },
{ "-wander", ".wander", XrmoptionNoArg, "True" },
{ "+wander", ".wander", XrmoptionNoArg, "False" }
};
static argtype vars[] = {
{&do_spin, "spin", "Spin", DEF_SPIN, t_Bool},
{&do_wander, "wander", "Wander", DEF_WANDER, t_Bool},
{&speed, "speed", "Speed", DEF_SPEED, t_Float},
{&density, "density", "Density", DEF_DENSITY, t_Float},
{&fracture, "fracture","Fracture",DEF_FRACTURE,t_Int},
};
ENTRYPOINT ModeSpecOpt crumbler_opts = {countof(opts), opts, countof(vars), vars, NULL};
/* Create a roughly spherical cloud of N random points.
*/
static void
make_point_cloud (qh_vertex_t *verts, int nverts)
{
int i = 0;
while (i < nverts)
{
verts[i].x = (0.5 - frand(1.0));
verts[i].y = (0.5 - frand(1.0));
verts[i].z = (0.5 - frand(1.0));
if ((verts[i].x * verts[i].x +
verts[i].y * verts[i].y +
verts[i].z * verts[i].z)
< 0.25)
i++;
}
}
static void crumbler_oom (void)
{
# ifdef HAVE_JWXYZ
jwxyz_abort ("%s: out of memory, try reducing 'density'", progname);
# else
fprintf (stderr, "%s: out of memory, try reducing 'density'\n", progname);
exit (1);
# endif
}
static chunk *
make_chunk (void)
{
chunk *c = (chunk *) calloc (1, sizeof(*c));
if (!c) crumbler_oom();
c->dlist = glGenLists (1);
c->color_shift = 1 + (random() % 3) * RANDSIGN();
return c;
}
/* Return False if out of memory */
static Bool
render_chunk (ModeInfo *mi, chunk *c)
{
int wire = MI_IS_WIREFRAME(mi);
int i, j;
qh_mesh_t m;
GLfloat d;
if (c->nverts <= 3)
{
fprintf (stderr, "%s: nverts %d\n", progname, c->nverts);
abort();
}
c->polygon_count = 0;
c->min.x = c->min.y = c->min.z = 999999;
c->max.x = c->max.y = c->max.z = -999999;
for (i = 0; i < c->nverts; i++)
{
if (c->verts[i].x < c->min.x) c->min.x = c->verts[i].x;
if (c->verts[i].y < c->min.y) c->min.y = c->verts[i].y;
if (c->verts[i].z < c->min.z) c->min.z = c->verts[i].z;
if (c->verts[i].x > c->max.x) c->max.x = c->verts[i].x;
if (c->verts[i].y > c->max.y) c->max.y = c->verts[i].y;
if (c->verts[i].z > c->max.z) c->max.z = c->verts[i].z;
}
c->mid.x = (c->max.x + c->min.x) / 2;
c->mid.y = (c->max.y + c->min.y) / 2;
c->mid.z = (c->max.z + c->min.z) / 2;
/* midpoint as normalized vector from origin */
d = sqrt (c->mid.x * c->mid.x +
c->mid.y * c->mid.y +
c->mid.z * c->mid.z);
c->vec.x = c->mid.x / d;
c->vec.y = c->mid.y / d;
c->vec.z = c->mid.z / d;
if (c->nverts <= 3)
{
fprintf (stderr, "%s: nverts %d\n", progname, c->nverts);
abort();
}
m = qh_quickhull3d (c->verts, c->nverts);
if (!m.vertices) /* out of memory */
{
qh_free_mesh (m);
return False;
}
glNewList (c->dlist, GL_COMPILE);
if (! wire) glBegin (GL_TRIANGLES);
for (i = 0, j = 0; i < m.nindices; i += 3, j++)
{
qh_vertex_t *v0 = &m.vertices[m.indices[i+0]];
qh_vertex_t *v1 = &m.vertices[m.indices[i+1]];
qh_vertex_t *v2 = &m.vertices[m.indices[i+2]];
qh_vec3_t *n = &m.normals[m.normalindices[j]];
if (i+2 >= m.nindices) abort();
if (j >= m.nnormals) abort();
glNormal3f (n->x, n->y, n->z);
if (wire) glBegin(GL_LINE_LOOP);
glVertex3f (v0->x, v0->y, v0->z);
glVertex3f (v1->x, v1->y, v1->z);
glVertex3f (v2->x, v2->y, v2->z);
if (wire) glEnd();
c->polygon_count++;
}
if (! wire) glEnd();
if (wire)
{
glPointSize (1);
glColor3f (0, 1, 0);
glBegin (GL_POINTS);
for (i = 0; i < c->nverts; i++)
{
if (i > 0 && i == c->onverts)
{
glEnd();
glColor3f (1, 0, 0);
glBegin (GL_POINTS);
}
glVertex3f (c->verts[i].x, c->verts[i].y, c->verts[i].z);
}
glEnd();
}
glEndList();
qh_free_mesh (m);
return True;
}
static void
free_chunk (chunk *c)
{
if (c->dlist)
glDeleteLists (c->dlist, 1);
free (c->verts);
free (c);
}
/* Make sure the chunk contains at least N points.
As we subdivide, the number of points is reduced.
This adds new points to the interior that do not
affect the shape of the outer hull.
*/
static void
pad_chunk (chunk *c, int min)
{
/* Allocate a new array of size N
Copy the old points into it
while size < N
pick two random points
add a point that is somewhere along the line between them
(that point will still be inside the old hull)
*/
qh_vertex_t *verts;
int i;
if (c->nverts >= min) return;
if (c->nverts <= 3) abort();
verts = (qh_vertex_t *) calloc (min, sizeof(*verts));
if (!verts) crumbler_oom();
memcpy (verts, c->verts, c->nverts * sizeof(*verts));
i = c->nverts;
while (i < min)
{
qh_vertex_t v;
int j0, j1;
GLfloat r;
j0 = random() % c->nverts;
do {
j1 = random() % c->nverts;
} while (j0 == j1);
r = 0.2 + frand(0.6);
# undef R
# define R(F) v.F = c->verts[j0].F + \
r * (fabs (c->verts[j1].F - c->verts[j0].F)) \
* (c->verts[j0].F > c->verts[j1].F ? -1 : 1)
R(x);
R(y);
R(z);
# undef R
/* Sometimes quickhull.c is giving us concave and un-closed polygons.
Maybe it gets confused if there are duplicate points? So reject
this point if it is within epsilon of any earlier point.
*/
# if 0 /* Nope, that's not it. */
{
Bool ok = True;
int j;
for (j = 0; j < i; j++)
{
double X = c->verts[j].x - v.x;
double Y = c->verts[j].y - v.y;
double Z = c->verts[j].z - v.z;
double d2 = X*X + Y*Y + Z*Z;
if (d2 < 0.0001)
{
/* fprintf (stderr, "## REJ %f\n",d2); */
ok = False;
break;
}
}
if (! ok) continue;
}
# endif
verts[i++] = v;
}
#if 0
fprintf (stdout, " int n = %d;\n", min);
fprintf (stdout, " qh_vertex_t v[] = {");
for (i = 0; i < min; i++)
fprintf(stdout,"{%f,%f,%f},", verts[i].x, verts[i].y, verts[i].z);
fprintf (stdout, "};\n\n");
#endif
free (c->verts);
c->verts = verts;
c->onverts = c->nverts;
c->nverts = min;
#if 0
qh_vertex_t *verts2 = (qh_vertex_t *) calloc (n, sizeof(*verts2));
if (!verts2) crumbler_oom();
memcpy (verts2, v, n * sizeof(*verts2));
free (c->verts);
c->verts = verts2;
c->onverts = 0;
c->nverts = n;
#endif
}
/* Returns a list of N new chunks.
*/
static chunk **
split_chunk (ModeInfo *mi, chunk *c, int nchunks)
{
/* Pick N key-points from the cloud.
Create N new chunks.
For each old point:
It goes in chunk N if it is closest to key-point N.
Free old chunk.
for each new chunk
render_chunk
*/
crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
chunk **chunks;
int *keys;
int i, j;
int retries = 0;
RETRY:
chunks = (chunk **) calloc (nchunks, sizeof(*chunks));
if (!chunks) crumbler_oom();
keys = (int *) calloc (nchunks, sizeof(*keys));
if (!keys) crumbler_oom();
for (i = 0; i < nchunks; i++)
{
/* Fill keys with random numbers that are not duplicates. */
Bool ok = True;
chunk *c2 = 0;
if (nchunks >= c->nverts)
{
fprintf (stderr, "%s: nverts %d nchunks %d\n", progname,
c->nverts, nchunks);
abort();
}
do {
keys[i] = random() % c->nverts;
ok = True;
for (j = 0; j < i; j++)
if (keys[i] == keys[j])
{
ok = False;
break;
}
} while (!ok);
c2 = make_chunk();
chunks[i] = c2;
chunks[i]->nverts = 0;
c2->verts = (qh_vertex_t *) calloc (c->nverts, sizeof(*c2->verts));
if (!c2->verts) crumbler_oom();
c2->color = (c->color + (random() % (1 + (bp->ncolors / 3))))
% bp->ncolors;
}
/* Add the verts to the approprate chunks
*/
for (i = 0; i < c->nverts; i++)
{
qh_vertex_t *v0 = &c->verts[i];
int target_chunk = -1;
double target_d2 = 9999999;
chunk *c2 = 0;
for (j = 0; j < nchunks; j++)
{
qh_vertex_t *v1 = &c->verts[keys[j]];
double X = v1->x - v0->x;
double Y = v1->y - v0->y;
double Z = v1->z - v0->z;
double d2 = X*X + Y*Y + Z*Z;
if (d2 < target_d2)
{
target_d2 = d2;
target_chunk = j;
}
}
if (target_chunk == -1) abort();
c2 = chunks[target_chunk];
c2->verts[c2->nverts++] = *v0;
if (c2->nverts > c->nverts) abort();
}
free (keys);
keys = 0;
for (i = 0; i < nchunks; i++)
{
chunk *c2 = chunks[i];
/* It is possible that the keys we have chosen have resulted in one or
more cells that have 3 or fewer points in them. If that's the case,
re-randomize.
*/
if (c2->nverts <= 3)
{
for (j = 0; j < nchunks; j++)
free_chunk (chunks[j]);
free (chunks);
chunks = 0;
if (retries++ > 100)
{
fprintf(stderr, "%s: unsplittable\n", progname);
abort();
}
goto RETRY;
}
if (i == 0) /* The one we're gonna keep */
pad_chunk (c2, c->nverts);
if (! render_chunk (mi, c2))
crumbler_oom(); /* We are too far in to recover from this */
}
return chunks;
}
static void
tick_crumbler (ModeInfo *mi)
{
crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
GLfloat ts;
if (bp->button_down_p) return;
switch (bp->state) {
case IDLE: ts = 0.02; break;
case SPLIT: ts = 0.01; break;
case PAUSE: ts = 0.008; break;
case FLEE: ts = 0.005; break;
case ZOOM: ts = 0.03; break;
default: abort(); break;
}
bp->tick += ts * speed;
if (bp->tick < 1) return;
bp->tick = 0;
bp->state = (bp->state + 1) % (ZOOM + 1);
switch (bp->state) {
case IDLE:
{
chunk *c = bp->chunks[0];
int i;
/* We already animated it zooming to full size. Now make it real. */
GLfloat X = (c->max.x - c->min.x);
GLfloat Y = (c->max.y - c->min.y);
GLfloat Z = (c->max.z - c->min.z);
GLfloat s = 1 / MAX(X, MAX(Y, Z));
for (i = 0; i < c->nverts; i++)
{
c->verts[i].x *= s;
c->verts[i].y *= s;
c->verts[i].z *= s;
}
/* Re-render it to move the verts in the display list too.
This also recomputes min, max and mid.
*/
if (! render_chunk (mi, c))
crumbler_oom(); /* We are too far in to recover from this */
break;
}
case SPLIT:
{
chunk *c = bp->chunks[0];
int frac = (fracture >= 2 ? fracture : 2 + (2 * (random() % 5)));
chunk **chunks = split_chunk (mi, c, frac);
if (bp->nchunks != 1) abort();
if (bp->ghost) abort();
bp->ghost = c;
free (bp->chunks);
bp->chunks = chunks;
bp->nchunks = frac;
break;
}
case PAUSE:
break;
case FLEE:
if (bp->ghost) free_chunk (bp->ghost);
bp->ghost = 0;
break;
case ZOOM:
{
chunk *c = bp->chunks[0];
int i;
for (i = 1; i < bp->nchunks; i++)
free_chunk (bp->chunks[i]);
bp->nchunks = 1;
/* We already animated the remaining chunk moving toward the origin.
Make it real.
*/
for (i = 0; i < c->nverts; i++)
{
c->verts[i].x -= c->mid.x;
c->verts[i].y -= c->mid.y;
c->verts[i].z -= c->mid.z;
}
/* Re-render it to move the verts in the display list too.
This also recomputes min, max and mid (now 0).
*/
if (! render_chunk (mi, c))
crumbler_oom(); /* We are too far in to recover from this */
break;
}
default: abort(); break;
}
}
static GLfloat
ease_fn (GLfloat r)
{
return cos ((r/2 + 1) * M_PI) + 1; /* Smooth curve up, end at slope 1. */
}
static GLfloat
ease_ratio (GLfloat r)
{
GLfloat ease = 0.35;
if (r <= 0) return 0;
else if (r >= 1) return 1;
else if (r <= ease) return ease * ease_fn (r / ease);
else if (r > 1-ease) return 1 - ease * ease_fn ((1 - r) / ease);
else return r;
}
/* Window management, etc
*/
ENTRYPOINT void
reshape_crumbler (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, (GLint) width, (GLint) height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective (30.0, 1/h, 1.0, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt( 0.0, 0.0, 30.0,
0.0, 0.0, 0.0,
0.0, 1.0, 0.0);
{
GLfloat s = (MI_WIDTH(mi) < MI_HEIGHT(mi)
? (MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi))
: 1);
glScalef (s, s, s);
}
glClear(GL_COLOR_BUFFER_BIT);
}
ENTRYPOINT Bool
crumbler_handle_event (ModeInfo *mi, XEvent *event)
{
crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
if (gltrackball_event_handler (event, bp->trackball,
MI_WIDTH (mi), MI_HEIGHT (mi),
&bp->button_down_p))
return True;
return False;
}
ENTRYPOINT void
init_crumbler (ModeInfo *mi)
{
crumbler_configuration *bp;
int wire = MI_IS_WIREFRAME(mi);
int i;
MI_INIT (mi, bps);
bp = &bps[MI_SCREEN(mi)];
bp->glx_context = init_GL(mi);
reshape_crumbler (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
if (!wire)
{
GLfloat pos[4] = {1.0, 1.0, 1.0, 0.0};
GLfloat amb[4] = {0.0, 0.0, 0.0, 1.0};
GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
GLfloat spc[4] = {0.0, 1.0, 1.0, 1.0};
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glLightfv(GL_LIGHT0, GL_POSITION, pos);
glLightfv(GL_LIGHT0, GL_AMBIENT, amb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
glEnable (GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
{
double spin_speed = 0.5 * speed;
double spin_accel = 0.3;
double wander_speed = 0.01 * speed;
bp->rot = make_rotator (do_spin ? spin_speed : 0,
do_spin ? spin_speed : 0,
do_spin ? spin_speed : 0,
spin_accel,
do_wander ? wander_speed : 0,
True);
bp->trackball = gltrackball_init (True);
}
bp->ncolors = 1024;
bp->colors = (XColor *) calloc(bp->ncolors, sizeof(XColor));
if (!bp->colors) crumbler_oom();
make_smooth_colormap (0, 0, 0,
bp->colors, &bp->ncolors,
False, 0, False);
/* brighter colors, please... */
for (i = 0; i < bp->ncolors; i++)
{
# undef R
# define R(F) F = 65535 * (0.3 + 0.7 * ((F) / 65535.0))
R (bp->colors[i].red);
R (bp->colors[i].green);
R (bp->colors[i].blue);
# undef R
}
{
double d2 = density;
chunk *c;
bp->nchunks = 1;
bp->chunks = (chunk **) calloc (bp->nchunks, sizeof(*bp->chunks));
if (! bp->chunks) crumbler_oom();
c = make_chunk();
if (! c) crumbler_oom();
bp->chunks[0] = c;
while (1)
{
c->nverts = 4500 * d2;
c->verts = (qh_vertex_t *) calloc (c->nverts, sizeof(*c->verts));
if (c->verts)
{
make_point_cloud (c->verts, c->nverts);
/* Let's shrink it to a point then zoom in. */
bp->state = ZOOM;
bp->tick = 0;
for (i = 0; i < c->nverts; i++)
{
c->verts[i].x /= 500;
c->verts[i].y /= 500;
c->verts[i].z /= 500;
}
if (! render_chunk (mi, c))
{
free (c->verts);
c->verts = 0;
}
}
if (c->verts)
break;
if (d2 < 0.1)
crumbler_oom();
d2 *= 0.9;
}
if (density != d2)
fprintf (stderr,
"%s: out of memory: reduced density from %.01f to %0.1f\n",
progname, density, d2);
}
}
static void
draw_chunk (ModeInfo *mi, chunk *c, GLfloat alpha)
{
crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
GLfloat color[4];
color[0] = bp->colors[c->color].red / 65536.0;
color[1] = bp->colors[c->color].green / 65536.0;
color[2] = bp->colors[c->color].blue / 65536.0;
color[3] = alpha;
glColor4fv (color);
glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);
c->color += c->color_shift;
while (c->color < 0) c->color += bp->ncolors;
while (c->color >= bp->ncolors) c->color -= bp->ncolors;
glCallList (c->dlist);
mi->polygon_count += c->polygon_count;
}
ENTRYPOINT void
draw_crumbler (ModeInfo *mi)
{
int wire = MI_IS_WIREFRAME(mi);
crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
Display *dpy = MI_DISPLAY(mi);
Window window = MI_WINDOW(mi);
GLfloat alpha = 1;
int i;
static const GLfloat bspec[4] = {1.0, 1.0, 1.0, 1.0};
static const GLfloat bshiny = 128.0;
if (!bp->glx_context)
return;
glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
tick_crumbler (mi);
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);
glEnable(GL_NORMALIZE);
glEnable(GL_CULL_FACE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix ();
{
double x, y, z;
get_position (bp->rot, &x, &y, &z, !bp->button_down_p);
glTranslatef((x - 0.5) * 8,
(y - 0.5) * 8,
(z - 0.5) * 15);
gltrackball_rotate (bp->trackball);
get_rotation (bp->rot, &x, &y, &z, !bp->button_down_p);
glRotatef (x * 360, 1.0, 0.0, 0.0);
glRotatef (y * 360, 0.0, 1.0, 0.0);
glRotatef (z * 360, 0.0, 0.0, 1.0);
}
mi->polygon_count = 0;
glMaterialfv (GL_FRONT, GL_SPECULAR, bspec);
glMateriali (GL_FRONT, GL_SHININESS, bshiny);
if (do_wander)
glScalef (10, 10, 10);
else
glScalef (13, 13, 13);
alpha = 1;
for (i = 0; i < bp->nchunks; i++)
{
chunk *c = bp->chunks[i];
glPushMatrix();
switch (bp->state) {
case FLEE:
{
GLfloat r = ease_ratio (bp->tick);
/* Move everybody toward the origin, so that chunk #0 ends up
centered there. */
glTranslatef (-r * c->mid.x,
-r * c->mid.y,
-r * c->mid.z);
if (i != 0)
{
/* Move this chunk away from the center, along a vector from
the origin to its midpoint. */
GLfloat d2 = r * 6;
glTranslatef (c->vec.x * d2, c->vec.y * d2, c->vec.z * d2);
alpha = 1 - r;
}
}
break;
case ZOOM:
{
chunk *c = bp->chunks[0];
GLfloat X = (c->max.x - c->min.x);
GLfloat Y = (c->max.y - c->min.y);
GLfloat Z = (c->max.z - c->min.z);
GLfloat size0 = MAX(X, MAX(Y, Z));
GLfloat size1 = 1.0;
GLfloat r = 1 - ease_ratio (bp->tick);
GLfloat s = 1 / (size0 + r * (size1 - size0));
glScalef (s, s, s);
}
break;
default:
break;
}
draw_chunk (mi, c, alpha);
glPopMatrix();
}
/* Draw the old one, fading out. */
if (!wire && bp->state == SPLIT && bp->ghost)
{
GLfloat s;
/* alpha = 1 - bp->tick; */
alpha = 1;
/* s = 0.7 + (0.3 * ease_ratio (1-bp->tick)); */
s = 2 * ease_ratio ((1-bp->tick) / 2);
s *= 1.01;
glScalef (s, s, s);
draw_chunk (mi, bp->ghost, alpha);
}
glPopMatrix ();
if (mi->fps_p) do_fps (mi);
glFinish();
glXSwapBuffers(dpy, window);
}
ENTRYPOINT void
free_crumbler (ModeInfo *mi)
{
crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
int i;
if (!bp->glx_context) return;
glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
if (bp->trackball) gltrackball_free (bp->trackball);
if (bp->rot) free_rotator (bp->rot);
if (bp->colors) free (bp->colors);
for (i = 0; i < bp->nchunks; i++)
free_chunk (bp->chunks[i]);
if (bp->chunks) free (bp->chunks);
}
XSCREENSAVER_MODULE ("Crumbler", crumbler)
#endif /* USE_GL */