/ 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 }