/ Pi_Matrix_Cube / globe.cc
globe.cc
  1  // SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  /*
  6  IF GLOBES WERE SQUARE: a revolving "globe" for 6X square RGB LED matrices on
  7  Raspberry Pi w/Adafruit Matrix Bonnet or HAT.
  8  
  9  usage: sudo ./globe [options]
 10  
 11  (You may or may not need the 'sudo' depending how the rpi-rgb-matrix
 12  library is configured)
 13  
 14  Options include all of the rpi-rgb-matrix flags, such as --led-pwm-bits=N
 15  or --led-gpio-slowdown=N, and then the following:
 16  
 17    -i <filename> : Image filename for texture map. MUST be JPEG image format.
 18                    Default is maps/earth.jpg
 19    -v            : Orient cube with vertices at top & bottom, rather than
 20                    flat faces on top & bottom. No accompanying value.
 21    -s <float>    : Spin time in seconds (per revolution). Positive values
 22                    will revolve in the correct direction for the Earth map.
 23                    Negative values spin the opposite direction (magnitude
 24                    specifies seconds), maybe useful for text, logos or Uranus.
 25    -a <int>      : Antialiasing samples, per-axis. Range 1-8. Default is 2,
 26                    for 2x2 supersampling. Fast hardware can go higher, slow
 27                    devices should use 1.
 28    -t <float>    : Run time in seconds. Program will exit after this.
 29                    Default is to run indefinitely, until crtl+C received.
 30    -f <float>    : Fade in/out time in seconds. Used in combination with the
 31                    -t option, this provides a nice fade-in, revolve for a
 32                    while, fade-out and exit. Combined with a simple shell
 33                    script, it provides a classy way to cycle among different
 34                    planetoids/scenes/etc. without having to explicitly
 35                    implement such a feature here.
 36    -e <float>    : Edge-to-edge physical measure of LED matrix. Combined
 37                    with -E below, provides spatial compensation for edge
 38                    bezels when matrices are arranged in a cube (i.e. pixels
 39                    don't "jump" across the seam -- has a bit of dead space).
 40    -E <float>    : Edge-to-edge measure of opposite faces of assembled cube,
 41                    used in combination with -e above. This will be a little
 42                    larger than the -e value (lower/upper case is to emphasize
 43                    this relationship). Units for both are arbitrary; use
 44                    millimeters, inches, whatever, it's the ratio that's
 45                    important.
 46  
 47  rpi-rgb-matrix has the following single-character abbreviations for
 48  some configurables: -b (--led-brightness), -c (--led-chain),
 49  -m (--led-gpio-mapping), -p (--led-pwm-bits), -P (--led-parallel),
 50  -r (--led-rows). AVOID THESE in any future configurables added to this
 51  program, as some users may have "muscle memory" for those options.
 52  
 53  This code depends on libjpeg and rpi-rgb-matrix libraries. While this
 54  .cc file has a permissive MIT licence, those libraries may (or not) have
 55  restrictions on commercial use, distributing precompiled binaries, etc.
 56  Check their license terms if this is relevant to your situation.
 57  */
 58  
 59  #include <math.h>
 60  #include <signal.h>
 61  #include <stdio.h>
 62  #include <string.h>
 63  #include <sys/time.h>
 64  #include <getopt.h>
 65  #include <jpeglib.h>
 66  #include <led-matrix.h>
 67  
 68  using namespace rgb_matrix;
 69  
 70  // GLOBAL VARIABLES --------------------------------------------------------
 71  
 72  // Some constants first...
 73  const char default_filename[] = "maps/earth.jpg";
 74  
 75  /* Cube coordinates use a right-handed coordinate system;
 76     +X is to right, +Y is up, +Z is toward your face
 77  
 78     Default is a flat/square/axis-aligned cube.
 79     Poles are at center of top & bottom faces.
 80     Numbers here are vertex indices (0-7):
 81  
 82          1-------------2
 83         /|   +Y       /|
 84        / |    ^      / |
 85       /  |    |     /  |
 86      0-------------3   |
 87      |   |    |    |   |
 88      |   |    0----|->+X
 89      |   |   /     |   |
 90      |   5--/------|---6
 91      |  /  L       |  /
 92      | /  +Z       | /
 93      |/            |/
 94      4-------------7
 95  */
 96  const float square_coords[8][3] = {{-1, 1, 1},  {-1, 1, -1}, {1, 1, -1},
 97                                     {1, 1, 1},   {-1, -1, 1}, {-1, -1, -1},
 98                                     {1, -1, -1}, {1, -1, 1}};
 99  
100  const uint8_t verts[6][3] = {
101      {0, 1, 3}, // Vertex indices for UL, UR, LL of top face matrix
102      {0, 4, 1}, // " left
103      {0, 3, 4}, // " front face
104      {7, 3, 6}, // " right
105      {2, 1, 6}, // " back
106      {5, 4, 6}, // " bottom matrix
107  };
108  
109  // Alternate coordinates for a rotated cube with points at poles.
110  // Vertex indices are the same (does not need a new verts[] array),
111  // relationships are the same, the whole thing is just pivoted to
112  // "hang" from vertex 3 at top. I will NOT attempt ASCII art of this.
113  const float xx = sqrt(26.0 / 9.0);
114  const float yy = sqrt(3.0) / 3.0;
115  const float cc = -0.5;       // cos(120.0 * M_PI / 180.0);
116  const float ss = sqrt(0.75); // sin(120.0 * M_PI / 180.0);
117  const float pointy_coords[8][3] = {
118      {-xx, yy, 0.0},          // Vertex 0 = leftmost point
119      {xx * cc, -yy, -xx *ss}, // 1
120      {-xx * cc, yy, -xx *ss}, // 2
121      {0.0, sqrt(3.0), 0.0},   // 3 = top
122      {xx * cc, -yy, xx *ss},  // 4
123      {0.0, -sqrt(3.0), 0.0},  // 5 = bottom
124      {xx, -yy, 0.0},          // 6 = rightmost point
125      {-xx * cc, yy, xx *ss}   // 7
126  };
127  
128  // These globals have defaults but are runtime configurable:
129  uint8_t aa = 2;             // Antialiasing samples, per axis
130  uint16_t matrix_size = 64;  // Matrix X&Y pixel count (must be square)
131  bool pointy = false;        // Cube orientation has a vertex at top
132  float matrix_measure = 1.0; // Edge-to-edge dimension of LED matrix
133  float cube_measure = 1.0;   // Face-to-face dimension of assembled cube
134  float spin_time = 10.0;     // Seconds per rotation
135  char *map_filename = (char *)default_filename; // Planet image file
136  float run_time = -1.0;        // Time before exit (negative = run forever)
137  float fade_time = 0.0;        // Fade in/out time (if run_time is set)
138  float max_brightness = 255.0; // Fade up to, down from this value
139  
140  // These globals are computed or allocated at runtime after taking input:
141  uint16_t samples_per_pixel; // Total antialiasing samples per pixel
142  int map_width;              // Map image width in pixels
143  int map_height;             // Map image height in pixels
144  uint8_t *map_data;          // Map image pixel data in RAM
145  float *longitude;           // Table of longitude values
146  uint16_t *latitude;         // Table of latitude values
147  
148  // INTERRUPT HANDLER (to allow clearing matrix on exit) --------------------
149  
150  volatile bool interrupt_received = false;
151  static void InterruptHandler(int signo) { interrupt_received = true; }
152  
153  // IMAGE LOADING -----------------------------------------------------------
154  
155  // Barebones JPEG reader; no verbose error handling, etc.
156  int read_jpeg(const char *filename) {
157    FILE *file;
158    if ((file = fopen(filename, "rb"))) {
159      struct jpeg_decompress_struct info;
160      struct jpeg_error_mgr err;
161      info.err = jpeg_std_error(&err);
162      jpeg_create_decompress(&info);
163      jpeg_stdio_src(&info, file);
164      jpeg_read_header(&info, TRUE);
165      jpeg_start_decompress(&info);
166      map_width = info.image_width;
167      map_height = info.image_height;
168      if ((map_data = (uint8_t *)malloc(map_width * map_height * 3))) {
169        unsigned char *rowptr[1] = {map_data};
170        while (info.output_scanline < info.output_height) {
171          (void)jpeg_read_scanlines(&info, rowptr, 1);
172          if (info.output_components == 1) { // Convert gray to RGB if needed
173            for (int x = map_width - 1; x >= 0; x--) {
174              rowptr[0][x * 3] = rowptr[0][x * 3 + 1] = rowptr[0][x * 3 + 2] =
175                  rowptr[0][x];
176            }
177          }
178          rowptr[0] += map_width * 3;
179        }
180      }
181      jpeg_finish_decompress(&info);
182      jpeg_destroy_decompress(&info);
183      fclose(file);
184      return (map_data != NULL);
185    }
186    return 0;
187  }
188  
189  // COMMAND-LINE HELP -------------------------------------------------------
190  
191  static int usage(const char *progname) {
192    fprintf(stderr, "usage: %s [options]\n", progname);
193    fprintf(stderr, "Options:\n");
194    rgb_matrix::PrintMatrixFlags(stderr);
195    fprintf(stderr, "\t-i <filename> : Image filename for texture map\n");
196    fprintf(stderr, "\t-v            : Orient cube with vertex at top\n");
197    fprintf(stderr, "\t-s <float>    : Spin time in seconds (per revolution)\n");
198    fprintf(stderr, "\t-a <int>      : Antialiasing samples, per-axis\n");
199    fprintf(stderr, "\t-t <float>    : Run time in seconds\n");
200    fprintf(stderr, "\t-f <float>    : Fade in/out time in seconds\n");
201    fprintf(stderr, "\t-e <float>    : Edge-to-edge measure of LED matrix\n");
202    fprintf(stderr, "\t-E <float>    : Edge-to-edge measure of assembled cube\n");
203    return 1;
204  }
205  
206  // MAIN CODE ---------------------------------------------------------------
207  
208  int main(int argc, char *argv[]) {
209    RGBMatrix *matrix;
210    FrameCanvas *canvas;
211  
212    // INITIALIZE DEFAULTS and PROCESS COMMAND-LINE INPUT --------------------
213  
214    RGBMatrix::Options matrix_options;
215    rgb_matrix::RuntimeOptions runtime_opt;
216  
217    matrix_options.cols = matrix_options.rows = matrix_size;
218    matrix_options.chain_length = 6;
219    runtime_opt.gpio_slowdown = 4; // For Pi 4 w/6 matrices
220  
221    // Parse matrix-related command line options first
222    if (!ParseOptionsFromFlags(&argc, &argv, &matrix_options, &runtime_opt)) {
223      return usage(argv[0]);
224    }
225  
226    // Validate inputs for cube-like behavior
227    if (matrix_options.cols != matrix_options.rows) {
228      fprintf(stderr, "%s: matrix columns, rows must be equal (square matrix)\n",
229              argv[0]);
230      return 1;
231    }
232    if (matrix_options.chain_length * matrix_options.parallel != 6) {
233      fprintf(stderr, "%s: total chained/parallel matrices must equal 6\n",
234              argv[0]);
235      return 1;
236    }
237  
238    max_brightness = (float)matrix_options.brightness * 2.55; // 0-100 -> 0-255
239  
240    // Then parse any lingering program options (filename, etc.)
241    int opt;
242    while ((opt = getopt(argc, argv, "i:vs:a:t:f:e:E:")) != -1) {
243      switch (opt) {
244      case 'i':
245        map_filename = strdup(optarg);
246        break;
247      case 'v':
248        pointy = true;
249        break;
250      case 's':
251        spin_time = strtof(optarg, NULL);
252        break;
253      case 'a':
254        aa = abs(atoi(optarg));
255        if (aa < 1)
256          aa = 1;
257        else if (aa > 8)
258          aa = 8;
259        break;
260      case 't':
261        run_time = fabs(strtof(optarg, NULL));
262        break;
263      case 'f':
264        fade_time = fabs(strtof(optarg, NULL));
265        break;
266      case 'e':
267        matrix_measure = strtof(optarg, NULL);
268        break;
269      case 'E':
270        cube_measure = strtof(optarg, NULL);
271        break;
272      default: /* '?' */
273        return usage(argv[0]);
274      }
275    }
276  
277    // LOAD and ALLOCATE DATA STRUCTURES -------------------------------------
278  
279    // Load map image; initializes globals map_width, map_height, map_data:
280    if (!read_jpeg(map_filename)) {
281      fprintf(stderr, "%s: error loading image '%s'\n", argv[0], map_filename);
282      return 1;
283    }
284  
285    // Allocate huge arrays for longitude & latitude values
286    matrix_size = matrix_options.rows;
287    samples_per_pixel = aa * aa;
288    int num_elements = 6 * matrix_size * matrix_size * samples_per_pixel;
289    if (!(longitude = (float *)malloc(num_elements * sizeof(float))) ||
290        !(latitude = (uint16_t *)malloc(num_elements * sizeof(uint16_t)))) {
291      fprintf(stderr, "%s: can't allocate space for lat/long tables\n", argv[0]);
292      return 1;
293    }
294  
295    // PRECOMPUTE LONGITUDE, LATITUDE TABLES ---------------------------------
296  
297    float *coords = (float *)(pointy ? pointy_coords : square_coords);
298  
299    // Longitude & latitude tables have one entry for each pixel (or subpixel,
300    // if supersampling) on each face of the cube. e.g. 64x64x2x2x6 pairs of
301    // values when using 64x64 matrices and 2x2 supersampling. Although some
302    // of the interim values through here could be computed and stored (e.g.
303    // corner-to-corner distances, matrix_size * aa, etc.), it's the 21st
304    // century and optimizing compilers are really dang good at this now, so
305    // let it do its job and keep the code relatively short.
306    int i = 0; // Index into longitude[] and latitude[] arrays
307    float mr = matrix_measure / cube_measure;                // Scale ratio
308    float mo = ((1.0 - mr) + mr / (matrix_size * aa)) * 0.5; // Axis offset
309    for (uint8_t face = 0; face < 6; face++) {
310      float *ul = &coords[verts[face][0] * 3];     // 3D coordinates of matrix's
311      float *ur = &coords[verts[face][1] * 3];     // upper-left, upper-right
312      float *ll = &coords[verts[face][2] * 3];     // and lower-left corners.
313      for (int py = 0; py < matrix_size; py++) {   // For each pixel Y...
314        for (int px = 0; px < matrix_size; px++) { // For each pixel X...
315          for (uint8_t ay = 0; ay < aa; ay++) {    // " antialiased sample Y...
316            float yfactor =
317                mo + mr * (float)(py * aa + ay) / (float)(matrix_size * aa);
318            for (uint8_t ax = 0; ax < aa; ax++) { // " antialiased sample X...
319              float xfactor =
320                  mo + mr * (float)(px * aa + ax) / (float)(matrix_size * aa);
321              float x, y, z;
322              // Figure out the pixel's 3D position in space...
323              x = ul[0] + (ll[0] - ul[0]) * yfactor + (ur[0] - ul[0]) * xfactor;
324              y = ul[1] + (ll[1] - ul[1]) * yfactor + (ur[1] - ul[1]) * xfactor;
325              z = ul[2] + (ll[2] - ul[2]) * yfactor + (ur[2] - ul[2]) * xfactor;
326              // Then use trigonometry to convert to polar coords on a sphere...
327              longitude[i] =
328                  fmod((M_PI + atan2(-z, x)) / (M_PI * 2.0) * (float)map_width,
329                       (float)map_width);
330              latitude[i] = (int)((M_PI * 0.5 - atan2(y, sqrt(x * x + z * z))) /
331                                  M_PI * (float)map_height);
332              i++;
333            }
334          }
335        }
336      }
337    }
338  
339    // INITIALIZE RGB MATRIX CHAIN and OFFSCREEN CANVAS ----------------------
340  
341    if (!(matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt))) {
342      fprintf(stderr, "%s: couldn't create matrix object\n", argv[0]);
343      return 1;
344    }
345    if (!(canvas = matrix->CreateFrameCanvas())) {
346      fprintf(stderr, "%s: couldn't create canvas object\n", argv[0]);
347      return 1;
348    }
349  
350    // OTHER MINOR INITIALIZATION --------------------------------------------
351  
352    signal(SIGTERM, InterruptHandler);
353    signal(SIGINT, InterruptHandler);
354  
355    struct timeval startTime, now;
356    gettimeofday(&startTime, NULL); // Program start time
357  
358    // LOOP RUNS INDEFINITELY OR UNTIL CTRL+C or run_time ELAPSED ------------
359  
360    uint32_t frames = 0;
361    int prevsec = -1;
362  
363    while (!interrupt_received) {
364      gettimeofday(&now, NULL);
365      double elapsed = ((now.tv_sec - startTime.tv_sec) +
366                        (now.tv_usec - startTime.tv_usec) / 1000000.0);
367      if (run_time > 0.0) { // Handle time limit and fade in/out if needed...
368        if (elapsed >= run_time)
369          break;
370        if (elapsed < fade_time) {
371          matrix->SetBrightness((int)(max_brightness * elapsed / fade_time));
372        } else if (elapsed > (run_time - fade_time)) {
373          matrix->SetBrightness(
374              (int)(max_brightness * (run_time - elapsed) / fade_time));
375        } else {
376          matrix->SetBrightness(max_brightness);
377        }
378      }
379      float loffset =
380          fmod(elapsed, fabs(spin_time)) / fabs(spin_time) * (float)map_width;
381      if (spin_time > 0)
382        loffset = map_width - loffset;
383      i = 0; // Index into longitude[] and latitude[] arrays
384      for (uint8_t face = 0; face < 6; face++) {
385        int xoffset = (face % matrix_options.chain_length) * matrix_size;
386        int yoffset = (face / matrix_options.chain_length) * matrix_size;
387        for (int py = 0; py < matrix_size; py++) {
388          for (int px = 0; px < matrix_size; px++) {
389            uint16_t r = 0, g = 0, b = 0;
390            for (uint16_t s = 0; s < samples_per_pixel; s++) {
391              int sx = (int)(longitude[i] + loffset) % map_width;
392              int sy = latitude[i];
393              uint8_t *src = &map_data[(sy * map_width + sx) * 3];
394              r += src[0];
395              g += src[1];
396              b += src[2];
397              i++;
398            }
399            canvas->SetPixel(xoffset + px, yoffset + py, r / samples_per_pixel,
400                             g / samples_per_pixel, b / samples_per_pixel);
401          }
402        }
403      }
404      canvas = matrix->SwapOnVSync(canvas);
405      frames++;
406      if (now.tv_sec != prevsec) {
407        if (prevsec >= 0) {
408          printf("%f\n", frames / elapsed);
409        }
410        prevsec = now.tv_sec;
411      }
412    }
413  
414    canvas->Clear();
415    delete matrix;
416  
417    return 0;
418  }