arduino_gfx_helpers
Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| arduino_gfx_helpers [2025/03/25 21:05] – created kenson | arduino_gfx_helpers [2025/03/28 03:08] (current) – kenson | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | < | + | ====== Arduino_GFX helper functions ====== |
| + | |||
| + | ===== Good References ===== | ||
| + | |||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | |||
| + | ===== Embrient Logo ===== | ||
| + | |||
| + | - X(𝑡) = cos(𝑡) − 0.5cos(5𝑡) | ||
| + | - Y(𝑡) = sin(𝑡) + 0.5sin(5𝑡) | ||
| + | - Z(𝑡) = 1.32204cos(3𝑡) | ||
| + | |||
| + | Where 𝑡 is in degrees, i.e., 𝑡 ∈ [0,360) | ||
| + | |||
| + | To normalize the diameter to ' | ||
| + | |||
| + | - X(𝑡) = 0.378122(cos(t) − 0.5cos(5t)) | ||
| + | - Y(𝑡) = 0.378122(sin(t) + 0.5sin(5t)) | ||
| + | - Z(𝑡) = 0.5cos(3t) | ||
| + | | ||
| + | < | ||
| + | Xt = 0.378122*(cos(t)−0.5*cos(5*t)); | ||
| + | Yt = 0.378122*(sin(t)+0.5*sin(5*t)); | ||
| + | Zt = 0.5*cos(3t); | ||
| + | </ | ||
| + | |||
| + | |||
| + | |||
| + | ===== Thick Lines ===== | ||
| + | Arduino_GFX doesn' | ||
| + | |||
| + | This is a " | ||
| + | <code cpp> | ||
| // Draw a thick line with rounded ends using Adafruit_GFX primitives | // Draw a thick line with rounded ends using Adafruit_GFX primitives | ||
| - | void DrawNaiveThickLine(int x1, int y1, int x2, int y2, int thickness, int color) { | + | void DrawNaiveThickLineRounded(int x1, int y1, int x2, int y2, int thickness, int color) { |
| // Calculate the differences between endpoints | // Calculate the differences between endpoints | ||
| int deltaX = x1 - x2; | int deltaX = x1 - x2; | ||
| Line 36: | Line 70: | ||
| </ | </ | ||
| - | < | + | This is more optimized code focusing on eliminating redundant calculations. |
| + | < | ||
| // Draw a thick line with rounded ends using a unified scanline fill approach. | // Draw a thick line with rounded ends using a unified scanline fill approach. | ||
| // Focuses more on integer optimization, | // Focuses more on integer optimization, | ||
| Line 204: | Line 239: | ||
| } | } | ||
| } | } | ||
| + | </ | ||
| + | |||
| + | ===== Put in practice as ===== | ||
| + | |||
| + | <code cpp> | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | |||
| + | using std:: | ||
| + | |||
| + | #define TFT_CS | ||
| + | #define TFT_RST | ||
| + | #define TFT_DC | ||
| + | Adafruit_ST7796S tft = Adafruit_ST7796S(TFT_CS, | ||
| + | |||
| + | #define SCREEN_WIDTH | ||
| + | #define SCREEN_HEIGHT 480 | ||
| + | |||
| + | GFXcanvas16 canvas(SCREEN_WIDTH, | ||
| + | |||
| + | const float PI_F = 3.14159265; | ||
| + | const float SCALE = 0.378122; | ||
| + | const float Z_SCALE | ||
| + | const float Z_OFFSET = 2.0; | ||
| + | const int | ||
| + | |||
| + | float baseThickness = 24.0; | ||
| + | const uint16_t blackish = 0x0000; | ||
| + | |||
| + | float rotation = 0.0; | ||
| + | |||
| + | struct Vec3 { | ||
| + | float x, y, z; | ||
| + | }; | ||
| + | |||
| + | struct Segment3D { | ||
| + | int x1, y1, x2, y2; | ||
| + | float avgZ; | ||
| + | int thickness; | ||
| + | }; | ||
| + | |||
| + | // Rotate a 3D point around the X-axis. | ||
| + | Vec3 rotateX(const Vec3& v, float angleDeg) { | ||
| + | float a = angleDeg * PI_F / 180.0; | ||
| + | float cosA = cos(a); | ||
| + | float sinA = sin(a); | ||
| + | return { | ||
| + | v.x, | ||
| + | cosA * v.y - sinA * v.z, | ||
| + | sinA * v.y + cosA * v.z | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | // Rotate a 3D point around the Y-axis. | ||
| + | Vec3 rotateY(const Vec3& v, float angleDeg) { | ||
| + | float a = angleDeg * PI_F / 180.0; | ||
| + | float cosA = cos(a); | ||
| + | float sinA = sin(a); | ||
| + | return { | ||
| + | cosA * v.x + sinA * v.z, | ||
| + | v.y, | ||
| + | -sinA * v.x + cosA * v.z | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | // Rotate a 3D point around the Z-axis. | ||
| + | Vec3 rotateZ(const Vec3& v, float angleDeg) { | ||
| + | float a = angleDeg * PI_F / 180.0; | ||
| + | float cosA = cos(a); | ||
| + | float sinA = sin(a); | ||
| + | return { | ||
| + | cosA * v.x - sinA * v.y, | ||
| + | sinA * v.x + cosA * v.y, | ||
| + | v.z | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | // Project a 3D point into 2D canvas coordinates. | ||
| + | void project3D(const Vec3& pt, int centerX, int centerY, float size, int &px, int &py) { | ||
| + | baseThickness = size / 5; | ||
| + | float target_size = size - baseThickness; | ||
| + | float f = 1.0 / (1.15 - pt.z); | ||
| + | float x_proj = pt.x * f * target_size; | ||
| + | float y_proj = pt.y * f * target_size; | ||
| + | px = static_cast< | ||
| + | py = static_cast< | ||
| + | } | ||
| + | |||
| + | inline uint16_t rgb888_to_565(uint8_t r, uint8_t g, uint8_t b) { | ||
| + | uint16_t r5 = (r * 249 + 1014) >> 11; | ||
| + | uint16_t g6 = (g * 253 + 505) >> 10; | ||
| + | uint16_t b5 = (b * 249 + 1014) >> 11; | ||
| + | return (r5 << 11) | (g6 << 5) | b5; | ||
| + | } | ||
| + | |||
| + | // Draw a thick line with rounded ends using a unified scanline fill approach. | ||
| + | void DrawThickLineRoundedHLines(GFXcanvas16& | ||
| + | int r = thickness >> 1; | ||
| + | int dxLine = x2 - x1; | ||
| + | int dyLine = y2 - y1; | ||
| + | int d2 = dxLine * dxLine + dyLine * dyLine; | ||
| + | |||
| + | if (d2 == 0) { | ||
| + | canvas.fillCircle(x1, | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | float len = sqrt((float)d2); | ||
| + | float lx = dxLine / (float)len; | ||
| + | float ly = dyLine / (float)len; | ||
| + | float factor = (thickness / 2.0f) / len; | ||
| + | float offX = factor * dyLine; | ||
| + | float offY = factor * -dxLine; | ||
| + | |||
| + | float Ax = x1 + offX, Ay = y1 + offY; | ||
| + | float Bx = x1 - offX, By = y1 - offY; | ||
| + | float Cx = x2 - offX, Cy = y2 - offY; | ||
| + | float Dx = x2 + offX, Dy = y2 + offY; | ||
| + | |||
| + | float bboxMinX = min(min(min(Ax, | ||
| + | float bboxMaxX = max(max(max(Ax, | ||
| + | float bboxMinY = min(min(min(Ay, | ||
| + | float bboxMaxY = max(max(max(Ay, | ||
| + | |||
| + | int iMinY = (int)floor(bboxMinY); | ||
| + | int iMaxY = (int)ceil(bboxMaxY); | ||
| + | |||
| + | auto processEdge = [=](float x0, float y0, float x1, float y1, int y) -> float { | ||
| + | if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) { | ||
| + | float t = (y - y0) / (y1 - y0); | ||
| + | return x0 + t * (x1 - x0); | ||
| + | } | ||
| + | return NAN; | ||
| + | }; | ||
| + | |||
| + | struct Segment { float x0, x1; }; | ||
| + | |||
| + | for (int y = iMinY; y <= iMaxY; y++) { | ||
| + | Segment segments[3]; | ||
| + | int segCount = 0; | ||
| + | float inters[4]; | ||
| + | int count = 0; | ||
| + | float ttemp; | ||
| + | ttemp = processEdge(Ax, | ||
| + | ttemp = processEdge(Bx, | ||
| + | ttemp = processEdge(Cx, | ||
| + | ttemp = processEdge(Dx, | ||
| + | if (count >= 2) { | ||
| + | float qx0 = inters[0], qx1 = inters[0]; | ||
| + | for (int i = 1; i < count; i++) { | ||
| + | if (inters[i] < qx0) qx0 = inters[i]; | ||
| + | if (inters[i] > qx1) qx1 = inters[i]; | ||
| + | } | ||
| + | segments[segCount++] = { qx0, qx1 }; | ||
| + | } | ||
| + | |||
| + | if (y >= y1 - r && y <= y1 + r) { | ||
| + | float dyCap = y - y1; | ||
| + | float dxCap = sqrt((float)(r * r - dyCap * dyCap)); | ||
| + | float capStart = x1 - dxCap; | ||
| + | float capEnd | ||
| + | if (fabs(lx) > 1e-6) { | ||
| + | float xBoundary = x1 - ((y - y1) * ly) / lx; | ||
| + | if (lx > 0) { | ||
| + | capEnd = min(capEnd, xBoundary); | ||
| + | } else { | ||
| + | capStart = max(capStart, | ||
| + | } | ||
| + | } else { | ||
| + | if ((ly > 0 && y >= y1) || (ly < 0 && y <= y1)) { | ||
| + | capStart = 1, capEnd = 0; | ||
| + | } | ||
| + | } | ||
| + | if (capEnd >= capStart) | ||
| + | segments[segCount++] = { capStart, capEnd }; | ||
| + | } | ||
| + | |||
| + | if (y >= y2 - r && y <= y2 + r) { | ||
| + | float dyCap = y - y2; | ||
| + | float dxCap = sqrt((float)(r * r - dyCap * dyCap)); | ||
| + | float capStart = x2 - dxCap; | ||
| + | float capEnd | ||
| + | if (fabs(lx) > 1e-6) { | ||
| + | float xBoundary = x2 - ((y - y2) * ly) / lx; | ||
| + | if (lx > 0) { | ||
| + | capStart = max(capStart, | ||
| + | } else { | ||
| + | capEnd = min(capEnd, xBoundary); | ||
| + | } | ||
| + | } else { | ||
| + | if ((ly > 0 && y <= y2) || (ly < 0 && y >= y2)) { | ||
| + | capStart = 1, capEnd = 0; | ||
| + | } | ||
| + | } | ||
| + | if (capEnd >= capStart) | ||
| + | segments[segCount++] = { capStart, capEnd }; | ||
| + | } | ||
| + | |||
| + | if (segCount == 0) | ||
| + | continue; | ||
| + | |||
| + | for (int i = 0; i < segCount - 1; i++) { | ||
| + | for (int j = i + 1; j < segCount; j++) { | ||
| + | if (segments[j].x0 < segments[i].x0) { | ||
| + | Segment tmp = segments[i]; | ||
| + | segments[i] = segments[j]; | ||
| + | segments[j] = tmp; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | float mergedStart = segments[0].x0; | ||
| + | float mergedEnd = segments[0].x1; | ||
| + | for (int i = 1; i < segCount; i++) { | ||
| + | if (segments[i].x0 <= mergedEnd + 1) { | ||
| + | if (segments[i].x1 > mergedEnd) | ||
| + | mergedEnd = segments[i].x1; | ||
| + | } else { | ||
| + | int ix0 = (int)ceil(mergedStart); | ||
| + | int ix1 = (int)floor(mergedEnd); | ||
| + | if (ix1 >= ix0) | ||
| + | canvas.drawFastHLine(ix0, | ||
| + | mergedStart = segments[i].x0; | ||
| + | mergedEnd = segments[i].x1; | ||
| + | } | ||
| + | } | ||
| + | int ix0 = (int)ceil(mergedStart); | ||
| + | int ix1 = (int)floor(mergedEnd); | ||
| + | if (ix1 >= ix0) | ||
| + | canvas.drawFastHLine(ix0, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // | ||
| + | // 1. Create the shape (a list of 3D points) defined by sin/cos functions. | ||
| + | vector< | ||
| + | vector< | ||
| + | for (int i = 0; i <= NUM_SEGMENTS; | ||
| + | float t = (360.0 / NUM_SEGMENTS) * i; | ||
| + | float rad = t * PI_F / 180.0; | ||
| + | float y = -SCALE * (cos(rad) - 0.5 * cos(5 * rad)); | ||
| + | float x = SCALE * (sin(rad) + 0.5 * sin(5 * rad)); | ||
| + | float z = Z_SCALE * cos(3 * rad); | ||
| + | shape.push_back({ x, y, z }); | ||
| + | } | ||
| + | return shape; | ||
| + | } | ||
| + | |||
| + | // | ||
| + | // 2. Rotate the shape | ||
| + | vector< | ||
| + | vector< | ||
| + | for (size_t i = 0; i < shape.size(); | ||
| + | // Apply rotations in order: X, then Y, then Z. | ||
| + | Vec3 point = rotateX(shape[i], | ||
| + | point = rotateY(point, | ||
| + | point = rotateZ(point, | ||
| + | rotated.push_back(point); | ||
| + | } | ||
| + | return rotated; | ||
| + | } | ||
| + | |||
| + | |||
| + | // | ||
| + | // 3. Render the shape by projecting the rotated 3D points into 2D, building segments, | ||
| + | // sorting them by depth, and drawing them with thick lines. | ||
| + | void renderShape(const vector< | ||
| + | int segmentIndex = 0; | ||
| + | int prev_px_i = 0, prev_py_i = 0; | ||
| + | float prev_z = 0; | ||
| + | bool firstPoint = true; | ||
| + | Segment3D segmentsLocal[NUM_SEGMENTS]; | ||
| + | |||
| + | for (size_t i = 0; i < shape.size(); | ||
| + | int px, py; | ||
| + | project3D(shape[i], | ||
| + | | ||
| + | if (!firstPoint && px >= 0 && px < SCREEN_WIDTH && py >= 0 && py < SCREEN_HEIGHT) { | ||
| + | float avgZ = (shape[i].z + prev_z) / 2.0; | ||
| + | float perspectiveScale = 1.0f / (Z_OFFSET - avgZ); | ||
| + | int thickness = baseThickness * perspectiveScale; | ||
| + | if (thickness < 1) thickness = 1; | ||
| + | if (thickness > 20) thickness = 20; | ||
| + | segmentsLocal[segmentIndex++] = { prev_px_i, prev_py_i, px, py, avgZ, thickness }; | ||
| + | } | ||
| + | | ||
| + | firstPoint = false; | ||
| + | prev_px_i = px; | ||
| + | prev_py_i = py; | ||
| + | prev_z = shape[i].z; | ||
| + | } | ||
| + | | ||
| + | // Depth sort (back to front) | ||
| + | for (int i = 0; i < segmentIndex - 1; ++i) { | ||
| + | for (int j = i + 1; j < segmentIndex; | ||
| + | if (segmentsLocal[j].avgZ < segmentsLocal[i].avgZ) { | ||
| + | Segment3D tmp = segmentsLocal[i]; | ||
| + | segmentsLocal[i] = segmentsLocal[j]; | ||
| + | segmentsLocal[j] = tmp; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | // Draw each segment with color based on depth. | ||
| + | for (int i = 0; i < segmentIndex; | ||
| + | float depthFactor = (Z_OFFSET - segmentsLocal[i].avgZ - 1.5f); | ||
| + | depthFactor = constrain(depthFactor, | ||
| + | uint8_t brightnessg = (uint8_t)(99.0 - (depthFactor * 99.0 * 0.5)); | ||
| + | uint8_t brightnessb = (uint8_t)(168.0 - (depthFactor * 168.0 * 0.5)); | ||
| + | uint16_t color = rgb888_to_565(0, | ||
| + | DrawThickLineRoundedHLines( | ||
| + | canvas, | ||
| + | segmentsLocal[i].x1, | ||
| + | segmentsLocal[i].x2, | ||
| + | segmentsLocal[i].thickness, | ||
| + | color | ||
| + | ); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | void setup() { | ||
| + | tft.init(320, | ||
| + | tft.setSPISpeed(40000000); | ||
| + | tft.invertDisplay(true); | ||
| + | tft.setRotation(0); | ||
| + | } | ||
| + | |||
| + | void loop() { | ||
| + | canvas.fillScreen(blackish); | ||
| + | |||
| + | // Create the base shape only once (static initialization). | ||
| + | static vector< | ||
| + | vector< | ||
| + | renderShape(rotatedShape, | ||
| + | canvas.setCursor(5, | ||
| + | canvas.setTextColor(rgb888_to_565(0, | ||
| + | canvas.print(" | ||
| + | | ||
| + | // Draw the off-screen canvas onto the display. | ||
| + | tft.drawRGBBitmap(0, | ||
| + | |||
| + | // Increment rotation for next frame. | ||
| + | rotation += 4.0; | ||
| + | if (rotation >= 360.0) rotation -= 360.0; | ||
| + | } | ||
| + | |||
| </ | </ | ||
arduino_gfx_helpers.1742936737.txt.gz · Last modified: 2025/03/25 21:05 by kenson
