/ M4_Eyes / tablegen.cpp
tablegen.cpp
  1  // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  //34567890123456789012345678901234567890123456789012345678901234567890123456
  6  
  7  #include "globals.h"
  8  
  9  // Code in this file calculates various tables used in eye rendering.
 10  
 11  // Because 3D math is probably asking too much of our microcontroller,
 12  // the round eyeball shape is faked using a 2D displacement map, a la
 13  // Photoshop's displacement filter or old demoscene & screensaver tricks.
 14  // This is not really an accurate representation of 3D rotation,
 15  // but works well enough for fooling the casual observer.
 16  
 17  void calcDisplacement() {
 18    // To save RAM, the displacement map is calculated for ONE QUARTER of
 19    // the screen, then mirrored horizontally/vertically down the middle
 20    // when rendering. Additionally, only a single axis displacement need
 21    // be calculated, since eye shape is X/Y symmetrical one can just swap
 22    // axes to look up displacement on the opposing axis.
 23    if(displace = (uint8_t *)malloc((DISPLAY_SIZE/2) * (DISPLAY_SIZE/2))) {
 24      float    eyeRadius2 = (float)(eyeRadius * eyeRadius);
 25      uint8_t  x, y;
 26      float    dx, dy, d2, d, h, a, pa;
 27      uint8_t *ptr = displace;
 28      // Displacement is calculated for the first quadrant in traditional
 29      // "+Y is up" Cartesian coordinate space; any mirroring or rotation
 30      // is handled in eye rendering code.
 31      for(y=0; y<(DISPLAY_SIZE/2); y++) {
 32        yield(); // Periodic yield() makes sure mass storage filesystem stays alive
 33        dy  = (float)y + 0.5;
 34        dy *= dy; // Now dy^2
 35        for(x=0; x<(DISPLAY_SIZE/2); x++) {
 36          // Get distance to origin point. Pixel centers are at +0.5, this is
 37          // normal, desirable and by design -- screen center at (120.0,120.0)
 38          // falls between pixels and allows numerically-correct mirroring.
 39          dx = (float)x + 0.5;
 40          d2 = dx * dx + dy;                 // Distance to origin, squared
 41          if(d2 <= eyeRadius2) {             // Pixel is within eye area
 42            d      = sqrt(d2);               // Distance to origin
 43            h      = sqrt(eyeRadius2 - d2);  // Height of eye hemisphere at d
 44            a      = atan2(d, h);            // Angle from center: 0 to pi/2
 45            //pa     = a * eyeRadius;        // Convert to pixels (no)
 46            pa = a / M_PI_2 * mapRadius;     // Convert to pixels
 47            dx    /= d;                      // Normalize dx part of 2D vector
 48            *ptr++ = (uint8_t)(dx * pa) - x; // Round to pixel space (no +0.5)
 49          } else {                           // Outside eye area
 50            *ptr++ = 255;                    // Mark as out-of-eye-bounds
 51          }
 52        }
 53      }
 54    }
 55  }
 56  
 57  void calcMap(void) {
 58    int pixels = mapRadius * mapRadius;
 59    if(polarAngle = (uint8_t *)malloc(pixels * 2)) { // Single alloc for both tables
 60      polarDist = (int8_t *)&polarAngle[pixels];     // Offset to second table
 61  
 62      // CALCULATE POLAR ANGLE & DISTANCE
 63  
 64      float mapRadius2  = mapRadius * mapRadius;  // Radius squared
 65      float iRad        = screen2map(irisRadius); // Iris size in in polar map pixels
 66      float irisRadius2 = iRad * iRad;            // Iris size squared
 67  
 68      uint8_t *anglePtr = polarAngle;
 69      int8_t  *distPtr  = polarDist;
 70  
 71      // Like the displacement map, only the first quadrant is calculated,
 72      // and the other three quadrants are mirrored/rotated from this.
 73      int   x, y;
 74      float dx, dy, dy2, d2, d, angle, xp;
 75      for(y=0; y<mapRadius; y++) {
 76        yield(); // Periodic yield() makes sure mass storage filesystem stays alive
 77        dy  = (float)y + 0.5;        // Y distance to map center
 78        dy2 = dy * dy;
 79        for(x=0; x<mapRadius; x++) {
 80          dx = (float)x + 0.5;       // X distance to map center
 81          d2 = dx * dx + dy2;        // Distance to center of map, squared
 82          if(d2 > mapRadius2) {      // If it exceeds 1/2 map size, squared,
 83            *anglePtr++ = 0;         // then mark as out-of-eye-bounds
 84            *distPtr++  = -128;
 85          } else {                   // else pixel is within eye area...
 86            angle  = atan2(dy, dx);  // -pi to +pi (0 to +pi/2 in 1st quadrant)
 87            angle  = M_PI_2 - angle; // Clockwise, 0 at top
 88            angle *= 512.0 / M_PI;   // 0 to <256 in 1st quadrant
 89            *anglePtr++ = (uint8_t)angle;
 90            d = sqrt(d2);
 91            if(d2 > irisRadius2) {
 92              // Point is in sclera
 93              d = (mapRadius - d) / (mapRadius - iRad);
 94              d *= 127.0;
 95              *distPtr++ = (int8_t)d; // 0 to 127
 96            } else {
 97              // Point is in iris (-dist to indicate such)
 98              d = (iRad - d) / iRad;
 99              d *= -127.0;
100              *distPtr++ = (int8_t)d - 1; // -1 to -127
101            }
102          }
103        }
104      }
105  
106      // If slit pupil is enabled, override iris area of polarDist map.
107      if(slitPupilRadius > 0) {
108        // Iterate over each pixel in the iris section of the polar map...
109        for(y=0; y < mapRadius; y++) {
110          yield(); // Periodic yield() makes sure mass storage filesystem stays alive
111          dy  = y + 0.5;            // Distance to center, Y component
112          dy2 = dy * dy;
113          for(x=0; x < mapRadius; x++) {
114            dx = x + 0.5;           // Distance to center point, X component
115            d2 = dx * dx + dy2;     // Distance to center, squared
116            if(d2 <= irisRadius2) { // If inside iris...
117              yield();
118              xp = x + 0.5;
119              // This is a bit ugly in that it iteratively calculates the
120              // polarDist value...trial and error. It should be possible to
121              // algebraically simplify this and find the single polarDist
122              // point for a given pixel, but I've not worked that out yet.
123              // This is only needed once at startup, not a complete disaster.
124              for(int i=126; i>=0; i--) {
125                float ratio = i / 128.0; // 0.0 (open) to just-under-1.0 (slit) (>= 1.0 will cause trouble)
126                // Interpolate a point between top of iris and top of slit pupil, based on ratio
127                float y1 = iRad - (iRad - slitPupilRadius) * ratio;
128                // (x1 is 0 and thus dropped from equation below)
129                // And another point between right of iris and center of eye, inverse ratio
130                float x2 = iRad * (1.0 - ratio);
131                // (y2 is also zero, same deal)
132                // Find X coordinate of center of circle that crosses above two points
133                // and has Y at 0.0
134                float xc = (x2 * x2 - y1 * y1) / (2 * x2);
135                dx = x2 - xc;       // Distance from center of circle to right edge
136                float r2 = dx * dx; // center-to-right distance squared
137                dx = xp - xc;       // X component of...
138                d2 = dx * dx + dy2; // Distance from pixel to left 'xc' point
139                if(d2 <= r2) {      // If point is within circle...
140                  polarDist[y * mapRadius + x] = (int8_t)(-1 - i); // Set to distance 'i'
141                  break;
142                }
143              }
144            }
145          }
146        }
147      }
148    }
149  }
150  
151  // Scale a measurement in screen pixels to polar map pixels
152  float screen2map(int in) {
153    return atan2(in, sqrt(eyeRadius * eyeRadius - in * in)) / M_PI_2 * mapRadius;
154  }
155  
156  // Inverse of above
157  float map2screen(int in) {
158    return sin((float)in / (float)mapRadius) * M_PI_2 * eyeRadius;
159  }