1 /* crumbler, Copyright (c) 2018 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 #define DEFAULTS "*delay: 30000 \n" \
13 "*showFPS: False \n" \
14 "*wireframe: False \n" \
15 "*suppressRotationAnimation: True\n" \
17 # define release_crumbler 0
19 #define countof(x) (sizeof((x))/sizeof((*x)))
21 #include "xlockmore.h"
24 #include "quickhull.h"
25 #include "gltrackball.h"
28 #ifdef USE_GL /* whole file */
31 #define DEF_SPIN "True"
32 #define DEF_WANDER "True"
33 #define DEF_SPEED "1.0"
34 #define DEF_DENSITY "1.0"
35 #define DEF_FRACTURE "0"
38 #define RANDSIGN() ((random() & 1) ? 1 : -1)
41 qh_vertex_t *verts; /* interior point cloud */
43 qh_vertex_t min, max; /* enclosing box */
52 GLXContext *glx_context;
54 trackball_state *trackball;
55 enum { IDLE, SPLIT, PAUSE, FLEE, ZOOM } state;
64 } crumbler_configuration;
66 static crumbler_configuration *bps = NULL;
70 static GLfloat density;
72 static Bool do_wander;
74 static XrmOptionDescRec opts[] = {
75 { "-spin", ".spin", XrmoptionNoArg, "True" },
76 { "+spin", ".spin", XrmoptionNoArg, "False" },
77 { "-speed", ".speed", XrmoptionSepArg, 0 },
78 { "-density", ".density", XrmoptionSepArg, 0 },
79 { "-fracture",".fracture",XrmoptionSepArg, 0 },
80 { "-wander", ".wander", XrmoptionNoArg, "True" },
81 { "+wander", ".wander", XrmoptionNoArg, "False" }
84 static argtype vars[] = {
85 {&do_spin, "spin", "Spin", DEF_SPIN, t_Bool},
86 {&do_wander, "wander", "Wander", DEF_WANDER, t_Bool},
87 {&speed, "speed", "Speed", DEF_SPEED, t_Float},
88 {&density, "density", "Density", DEF_DENSITY, t_Float},
89 {&fracture, "fracture","Fracture",DEF_FRACTURE,t_Int},
92 ENTRYPOINT ModeSpecOpt crumbler_opts = {countof(opts), opts, countof(vars), vars, NULL};
95 /* Create a roughly spherical cloud of N random points.
98 make_point_cloud (qh_vertex_t *verts, int nverts)
103 verts[i].x = (0.5 - frand(1.0));
104 verts[i].y = (0.5 - frand(1.0));
105 verts[i].z = (0.5 - frand(1.0));
106 if ((verts[i].x * verts[i].x +
107 verts[i].y * verts[i].y +
108 verts[i].z * verts[i].z)
118 chunk *c = (chunk *) calloc (1, sizeof(*c));
119 c->dlist = glGenLists (1);
120 c->color_shift = 1 + (random() % 3) * RANDSIGN();
125 render_chunk (ModeInfo *mi, chunk *c)
127 int wire = MI_IS_WIREFRAME(mi);
134 fprintf (stderr, "%s: nverts %d\n", progname, c->nverts);
138 c->polygon_count = 0;
139 c->min.x = c->min.y = c->min.z = 999999;
140 c->max.x = c->max.y = c->max.z = -999999;
142 for (i = 0; i < c->nverts; i++)
144 if (c->verts[i].x < c->min.x) c->min.x = c->verts[i].x;
145 if (c->verts[i].y < c->min.y) c->min.y = c->verts[i].y;
146 if (c->verts[i].z < c->min.z) c->min.z = c->verts[i].z;
147 if (c->verts[i].x > c->max.x) c->max.x = c->verts[i].x;
148 if (c->verts[i].y > c->max.y) c->max.y = c->verts[i].y;
149 if (c->verts[i].z > c->max.z) c->max.z = c->verts[i].z;
152 c->mid.x = (c->max.x + c->min.x) / 2;
153 c->mid.y = (c->max.y + c->min.y) / 2;
154 c->mid.z = (c->max.z + c->min.z) / 2;
156 /* midpoint as normalized vector from origin */
157 d = sqrt (c->mid.x * c->mid.x +
158 c->mid.y * c->mid.y +
159 c->mid.z * c->mid.z);
160 c->vec.x = c->mid.x / d;
161 c->vec.y = c->mid.y / d;
162 c->vec.z = c->mid.z / d;
166 fprintf (stderr, "%s: nverts %d\n", progname, c->nverts);
170 m = qh_quickhull3d (c->verts, c->nverts);
172 glNewList (c->dlist, GL_COMPILE);
173 if (! wire) glBegin (GL_TRIANGLES);
174 for (i = 0, j = 0; i < m.nindices; i += 3, j++)
176 qh_vertex_t *v0 = &m.vertices[m.indices[i+0]];
177 qh_vertex_t *v1 = &m.vertices[m.indices[i+1]];
178 qh_vertex_t *v2 = &m.vertices[m.indices[i+2]];
179 qh_vec3_t *n = &m.normals[m.normalindices[j]];
181 if (i+2 >= m.nindices) abort();
182 if (j >= m.nnormals) abort();
184 glNormal3f (n->x, n->y, n->z);
185 if (wire) glBegin(GL_LINE_LOOP);
186 glVertex3f (v0->x, v0->y, v0->z);
187 glVertex3f (v1->x, v1->y, v1->z);
188 glVertex3f (v2->x, v2->y, v2->z);
199 for (i = 0; i < c->nverts; i++)
201 if (i > 0 && i == c->onverts)
207 glVertex3f (c->verts[i].x, c->verts[i].y, c->verts[i].z);
219 free_chunk (chunk *c)
222 glDeleteLists (c->dlist, 1);
228 /* Make sure the chunk contains at least N points.
229 As we subdivide, the number of points is reduced.
230 This adds new points to the interior that do not
231 affect the shape of the outer hull.
234 pad_chunk (chunk *c, int min)
236 /* Allocate a new array of size N
237 Copy the old points into it
239 pick two random points
240 add a point that is somewhere along the line between them
241 (that point will still be inside the old hull)
245 if (c->nverts >= min) return;
246 if (c->nverts <= 3) abort();
247 verts = (qh_vertex_t *) calloc (min, sizeof(*verts));
249 memcpy (verts, c->verts, c->nverts * sizeof(*verts));
256 j0 = random() % c->nverts;
258 j1 = random() % c->nverts;
261 r = 0.2 + frand(0.6);
263 # define R(F) v.F = c->verts[j0].F + \
264 r * (fabs (c->verts[j1].F - c->verts[j0].F)) \
265 * (c->verts[j0].F > c->verts[j1].F ? -1 : 1)
271 /* Sometimes quickhull.c is giving us concave and un-closed polygons.
272 Maybe it gets confused if there are duplicate points? So reject
273 this point if it is within epsilon of any earlier point.
275 # if 0 /* Nope, that's not it. */
279 for (j = 0; j < i; j++)
282 double X = c->verts[j].x - v.x;
283 double Y = c->verts[j].y - v.y;
284 double Z = c->verts[j].z - v.z;
285 double d2 = X*X + Y*Y + Z*Z;
288 /* fprintf (stderr, "## REJ %f\n",d2); */
301 fprintf (stdout, " int n = %d;\n", min);
302 fprintf (stdout, " qh_vertex_t v[] = {");
303 for (i = 0; i < min; i++)
304 fprintf(stdout,"{%f,%f,%f},", verts[i].x, verts[i].y, verts[i].z);
305 fprintf (stdout, "};\n\n");
310 c->onverts = c->nverts;
314 qh_vertex_t *verts2 = (qh_vertex_t *) calloc (n, sizeof(*verts2));
315 memcpy (verts2, v, n * sizeof(*verts2));
324 /* Returns a list of N new chunks.
327 split_chunk (ModeInfo *mi, chunk *c, int nchunks)
329 /* Pick N key-points from the cloud.
332 It goes in chunk N if it is closest to key-point N.
337 crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
344 chunks = (chunk **) calloc (nchunks, sizeof(*chunks));
345 keys = (int *) calloc (nchunks, sizeof(*keys));
347 for (i = 0; i < nchunks; i++)
349 /* Fill keys with random numbers that are not duplicates. */
352 if (nchunks >= c->nverts)
354 fprintf (stderr, "%s: nverts %d nchunks %d\n", progname,
359 keys[i] = random() % c->nverts;
361 for (j = 0; j < i; j++)
362 if (keys[i] == keys[j])
371 chunks[i]->nverts = 0;
372 c2->verts = (qh_vertex_t *) calloc (c->nverts, sizeof(*c2->verts));
373 c2->color = (c->color + (random() % (1 + (bp->ncolors / 3))))
377 /* Add the verts to the approprate chunks
379 for (i = 0; i < c->nverts; i++)
381 qh_vertex_t *v0 = &c->verts[i];
382 int target_chunk = -1;
383 double target_d2 = 9999999;
386 for (j = 0; j < nchunks; j++)
388 qh_vertex_t *v1 = &c->verts[keys[j]];
389 double X = v1->x - v0->x;
390 double Y = v1->y - v0->y;
391 double Z = v1->z - v0->z;
392 double d2 = X*X + Y*Y + Z*Z;
399 if (target_chunk == -1) abort();
401 c2 = chunks[target_chunk];
402 c2->verts[c2->nverts++] = *v0;
403 if (c2->nverts > c->nverts) abort();
409 for (i = 0; i < nchunks; i++)
411 chunk *c2 = chunks[i];
413 /* It is possible that the keys we have chosen have resulted in one or
414 more cells that have 3 or fewer points in them. If that's the case,
419 for (j = 0; j < nchunks; j++)
420 free_chunk (chunks[j]);
425 fprintf(stderr, "%s: unsplittable\n", progname);
431 if (i == 0) /* The one we're gonna keep */
432 pad_chunk (c2, c->nverts);
433 render_chunk (mi, c2);
441 tick_crumbler (ModeInfo *mi)
443 crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
446 if (bp->button_down_p) return;
449 case IDLE: ts = 0.02; break;
450 case SPLIT: ts = 0.01; break;
451 case PAUSE: ts = 0.008; break;
452 case FLEE: ts = 0.005; break;
453 case ZOOM: ts = 0.03; break;
454 default: abort(); break;
457 bp->tick += ts * speed;
459 if (bp->tick < 1) return;
462 bp->state = (bp->state + 1) % (ZOOM + 1);
467 chunk *c = bp->chunks[0];
470 /* We already animated it zooming to full size. Now make it real. */
471 GLfloat X = (c->max.x - c->min.x);
472 GLfloat Y = (c->max.y - c->min.y);
473 GLfloat Z = (c->max.z - c->min.z);
474 GLfloat s = 1 / MAX(X, MAX(Y, Z));
476 for (i = 0; i < c->nverts; i++)
483 /* Re-render it to move the verts in the display list too.
484 This also recomputes min, max and mid.
486 render_chunk (mi, c);
492 chunk *c = bp->chunks[0];
493 int frac = (fracture >= 2 ? fracture : 2 + (2 * (random() % 5)));
494 chunk **chunks = split_chunk (mi, c, frac);
495 if (bp->nchunks != 1) abort();
496 if (bp->ghost) abort();
508 if (bp->ghost) free_chunk (bp->ghost);
514 chunk *c = bp->chunks[0];
516 for (i = 1; i < bp->nchunks; i++)
517 free_chunk (bp->chunks[i]);
520 /* We already animated the remaining chunk moving toward the origin.
523 for (i = 0; i < c->nverts; i++)
525 c->verts[i].x -= c->mid.x;
526 c->verts[i].y -= c->mid.y;
527 c->verts[i].z -= c->mid.z;
530 /* Re-render it to move the verts in the display list too.
531 This also recomputes min, max and mid (now 0).
533 render_chunk (mi, c);
537 default: abort(); break;
545 return cos ((r/2 + 1) * M_PI) + 1; /* Smooth curve up, end at slope 1. */
550 ease_ratio (GLfloat r)
553 if (r <= 0) return 0;
554 else if (r >= 1) return 1;
555 else if (r <= ease) return ease * ease_fn (r / ease);
556 else if (r > 1-ease) return 1 - ease * ease_fn ((1 - r) / ease);
561 /* Window management, etc
564 reshape_crumbler (ModeInfo *mi, int width, int height)
566 GLfloat h = (GLfloat) height / (GLfloat) width;
569 if (width > height * 5) { /* tiny window: show middle */
570 height = width * 9/16;
572 h = height / (GLfloat) width;
575 glViewport (0, y, (GLint) width, (GLint) height);
577 glMatrixMode(GL_PROJECTION);
579 gluPerspective (30.0, 1/h, 1.0, 100.0);
581 glMatrixMode(GL_MODELVIEW);
583 gluLookAt( 0.0, 0.0, 30.0,
587 # ifdef HAVE_MOBILE /* Keep it the same relative size when rotated. */
589 int o = (int) current_device_rotation();
590 if (o != 0 && o != 180 && o != -180)
591 glScalef (1/h, 1/h, 1/h);
595 glClear(GL_COLOR_BUFFER_BIT);
600 crumbler_handle_event (ModeInfo *mi, XEvent *event)
602 crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
604 if (gltrackball_event_handler (event, bp->trackball,
605 MI_WIDTH (mi), MI_HEIGHT (mi),
614 init_crumbler (ModeInfo *mi)
616 crumbler_configuration *bp;
617 int wire = MI_IS_WIREFRAME(mi);
621 bp = &bps[MI_SCREEN(mi)];
623 bp->glx_context = init_GL(mi);
625 reshape_crumbler (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
629 GLfloat pos[4] = {1.0, 1.0, 1.0, 0.0};
630 GLfloat amb[4] = {0.0, 0.0, 0.0, 1.0};
631 GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
632 GLfloat spc[4] = {0.0, 1.0, 1.0, 1.0};
634 glEnable(GL_LIGHTING);
636 glEnable(GL_DEPTH_TEST);
637 glEnable(GL_CULL_FACE);
639 glLightfv(GL_LIGHT0, GL_POSITION, pos);
640 glLightfv(GL_LIGHT0, GL_AMBIENT, amb);
641 glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
642 glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
645 glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
649 double spin_speed = 0.5 * speed;
650 double spin_accel = 0.3;
651 double wander_speed = 0.01 * speed;
653 bp->rot = make_rotator (do_spin ? spin_speed : 0,
654 do_spin ? spin_speed : 0,
655 do_spin ? spin_speed : 0,
657 do_wander ? wander_speed : 0,
659 bp->trackball = gltrackball_init (True);
663 bp->colors = (XColor *) calloc(bp->ncolors, sizeof(XColor));
664 make_smooth_colormap (0, 0, 0,
665 bp->colors, &bp->ncolors,
668 /* brighter colors, please... */
669 for (i = 0; i < bp->ncolors; i++)
672 # define R(F) F = 65535 * (0.3 + 0.7 * ((F) / 65535.0))
673 R (bp->colors[i].red);
674 R (bp->colors[i].green);
675 R (bp->colors[i].blue);
681 density *= 0.5; /* iPhone 6s runs out of memory at 4500 nverts. */
683 density *= 0.3; /* Android Nexus_5_8.1 emulator runs out earlier. */
690 bp->chunks = (chunk **) calloc (bp->nchunks, sizeof(*bp->chunks));
693 c->nverts = 4500 * density;
694 c->verts = (qh_vertex_t *) calloc (c->nverts, sizeof(*c->verts));
695 make_point_cloud (c->verts, c->nverts);
697 /* Let's shrink it to a point then zoom in. */
700 for (i = 0; i < c->nverts; i++)
702 c->verts[i].x /= 500;
703 c->verts[i].y /= 500;
704 c->verts[i].z /= 500;
707 render_chunk (mi, c);
713 draw_chunk (ModeInfo *mi, chunk *c, GLfloat alpha)
715 crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
718 color[0] = bp->colors[c->color].red / 65536.0;
719 color[1] = bp->colors[c->color].green / 65536.0;
720 color[2] = bp->colors[c->color].blue / 65536.0;
723 glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);
725 c->color += c->color_shift;
726 while (c->color < 0) c->color += bp->ncolors;
727 while (c->color >= bp->ncolors) c->color -= bp->ncolors;
729 glCallList (c->dlist);
730 mi->polygon_count += c->polygon_count;
735 draw_crumbler (ModeInfo *mi)
737 int wire = MI_IS_WIREFRAME(mi);
738 crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
739 Display *dpy = MI_DISPLAY(mi);
740 Window window = MI_WINDOW(mi);
744 static const GLfloat bspec[4] = {1.0, 1.0, 1.0, 1.0};
745 static const GLfloat bshiny = 128.0;
747 if (!bp->glx_context)
750 glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(bp->glx_context));
754 glShadeModel(GL_SMOOTH);
755 glEnable(GL_DEPTH_TEST);
756 glEnable(GL_NORMALIZE);
757 glEnable(GL_CULL_FACE);
759 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
765 get_position (bp->rot, &x, &y, &z, !bp->button_down_p);
766 glTranslatef((x - 0.5) * 8,
770 gltrackball_rotate (bp->trackball);
772 get_rotation (bp->rot, &x, &y, &z, !bp->button_down_p);
773 glRotatef (x * 360, 1.0, 0.0, 0.0);
774 glRotatef (y * 360, 0.0, 1.0, 0.0);
775 glRotatef (z * 360, 0.0, 0.0, 1.0);
778 mi->polygon_count = 0;
780 glMaterialfv (GL_FRONT, GL_SPECULAR, bspec);
781 glMateriali (GL_FRONT, GL_SHININESS, bshiny);
784 glScalef (10, 10, 10);
786 glScalef (13, 13, 13);
789 for (i = 0; i < bp->nchunks; i++)
791 chunk *c = bp->chunks[i];
798 GLfloat r = ease_ratio (bp->tick);
799 /* Move everybody toward the origin, so that chunk #0 ends up
801 glTranslatef (-r * c->mid.x,
806 /* Move this chunk away from the center, along a vector from
807 the origin to its midpoint. */
809 glTranslatef (c->vec.x * d2, c->vec.y * d2, c->vec.z * d2);
817 chunk *c = bp->chunks[0];
818 GLfloat X = (c->max.x - c->min.x);
819 GLfloat Y = (c->max.y - c->min.y);
820 GLfloat Z = (c->max.z - c->min.z);
821 GLfloat size0 = MAX(X, MAX(Y, Z));
823 GLfloat r = 1 - ease_ratio (bp->tick);
824 GLfloat s = 1 / (size0 + r * (size1 - size0));
833 draw_chunk (mi, c, alpha);
837 /* Draw the old one, fading out. */
838 if (!wire && bp->state == SPLIT && bp->ghost)
841 /* alpha = 1 - bp->tick; */
843 /* s = 0.7 + (0.3 * ease_ratio (1-bp->tick)); */
844 s = 2 * ease_ratio ((1-bp->tick) / 2);
847 draw_chunk (mi, bp->ghost, alpha);
852 if (mi->fps_p) do_fps (mi);
855 glXSwapBuffers(dpy, window);
860 free_crumbler (ModeInfo *mi)
862 crumbler_configuration *bp = &bps[MI_SCREEN(mi)];
864 free (bp->trackball);
867 for (i = 0; i < bp->nchunks; i++)
868 free_chunk (bp->chunks[i]);
873 XSCREENSAVER_MODULE ("Crumbler", crumbler)