arduino_gfx_helpers
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| arduino_gfx_helpers [2025/03/27 02:18] – kenson | arduino_gfx_helpers [2025/03/28 03:08] (current) – kenson | ||
|---|---|---|---|
| Line 241: | Line 241: | ||
| </ | </ | ||
| + | ===== Put in practice as ===== | ||
| - | In practice | + | < |
| - | < | + | |
| #include < | #include < | ||
| #include < | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | |||
| + | using std:: | ||
| #define TFT_CS | #define TFT_CS | ||
| Line 252: | Line 256: | ||
| Adafruit_ST7796S tft = Adafruit_ST7796S(TFT_CS, | Adafruit_ST7796S tft = Adafruit_ST7796S(TFT_CS, | ||
| - | #define SCREEN_WIDTH | + | #define SCREEN_WIDTH |
| - | #define SCREEN_HEIGHT | + | #define SCREEN_HEIGHT |
| GFXcanvas16 canvas(SCREEN_WIDTH, | GFXcanvas16 canvas(SCREEN_WIDTH, | ||
| - | |||
| - | uint8_t rotate = 0; | ||
| const float PI_F = 3.14159265; | const float PI_F = 3.14159265; | ||
| Line 263: | Line 265: | ||
| const float Z_SCALE | const float Z_SCALE | ||
| const float Z_OFFSET = 2.0; | const float Z_OFFSET = 2.0; | ||
| - | const int | + | const int |
| + | float baseThickness = 24.0; | ||
| const uint16_t blackish = 0x0000; | const uint16_t blackish = 0x0000; | ||
| float rotation = 0.0; | float rotation = 0.0; | ||
| + | |||
| + | struct Vec3 { | ||
| + | float x, y, z; | ||
| + | }; | ||
| struct Segment3D { | struct Segment3D { | ||
| Line 274: | Line 281: | ||
| int thickness; | int thickness; | ||
| }; | }; | ||
| - | Segment3D segments[NUM_SEGMENTS]; | ||
| - | struct | + | // Rotate a 3D point around the X-axis. |
| - | float x, y, z; | + | 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) { | Vec3 rotateY(const Vec3& v, float angleDeg) { | ||
| float a = angleDeg * PI_F / 180.0; | float a = angleDeg * PI_F / 180.0; | ||
| Line 291: | Line 306: | ||
| } | } | ||
| - | void project(const Vec3& pt, int &px, int &py) { | + | // Rotate a 3D point around the Z-axis. |
| - | float f = 1.0 / (Z_OFFSET | + | Vec3 rotateZ(const Vec3& v, float angleDeg) { |
| - | float x_proj = pt.x * f; | + | float a = angleDeg * PI_F / 180.0; |
| - | float y_proj = pt.y * f; | + | float cosA = cos(a); |
| - | px = (int)((x_proj | + | float sinA = sin(a); |
| - | py = (int)((1.0 - y_proj) * SCREEN_HEIGHT / 2.0); | + | 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< | ||
| } | } | ||
| Line 306: | Line 336: | ||
| } | } | ||
| - | void DrawThickLineRoundedHLines(GFXcanvas16& | + | // Draw a thick line with rounded ends using a unified scanline fill approach. |
| - | // Half thickness (using integer division is fine for even thicknesses) | + | void DrawThickLineRoundedHLines(GFXcanvas16& |
| int r = thickness >> 1; | int r = thickness >> 1; | ||
| int dxLine = x2 - x1; | int dxLine = x2 - x1; | ||
| int dyLine = y2 - y1; | int dyLine = y2 - y1; | ||
| int d2 = dxLine * dxLine + dyLine * dyLine; | int d2 = dxLine * dxLine + dyLine * dyLine; | ||
| - | + | ||
| - | // If endpoints coincide, draw a full circle. | + | |
| if (d2 == 0) { | if (d2 == 0) { | ||
| canvas.fillCircle(x1, | canvas.fillCircle(x1, | ||
| return; | return; | ||
| } | } | ||
| - | + | ||
| - | // Compute the length and normalized line direction. | + | |
| float len = sqrt((float)d2); | float len = sqrt((float)d2); | ||
| float lx = dxLine / (float)len; | float lx = dxLine / (float)len; | ||
| float ly = dyLine / (float)len; | float ly = dyLine / (float)len; | ||
| - | |||
| - | // Compute perpendicular offset (using (dy, -dx)) scaled to half thickness. | ||
| float factor = (thickness / 2.0f) / len; | float factor = (thickness / 2.0f) / len; | ||
| float offX = factor * dyLine; | float offX = factor * dyLine; | ||
| float offY = factor * -dxLine; | float offY = factor * -dxLine; | ||
| - | + | ||
| - | // Compute the four vertices (of the quadr) in clockwise order. | + | |
| - | // These define the main body of the line. | + | |
| float Ax = x1 + offX, Ay = y1 + offY; | float Ax = x1 + offX, Ay = y1 + offY; | ||
| float Bx = x1 - offX, By = y1 - offY; | float Bx = x1 - offX, By = y1 - offY; | ||
| float Cx = x2 - offX, Cy = y2 - offY; | float Cx = x2 - offX, Cy = y2 - offY; | ||
| float Dx = x2 + offX, Dy = y2 + offY; | float Dx = x2 + offX, Dy = y2 + offY; | ||
| - | + | ||
| - | // Determine overall bounding box (including the circular caps). | + | |
| float bboxMinX = min(min(min(Ax, | float bboxMinX = min(min(min(Ax, | ||
| float bboxMaxX = max(max(max(Ax, | float bboxMaxX = max(max(max(Ax, | ||
| float bboxMinY = min(min(min(Ay, | float bboxMinY = min(min(min(Ay, | ||
| float bboxMaxY = max(max(max(Ay, | float bboxMaxY = max(max(max(Ay, | ||
| - | + | ||
| int iMinY = (int)floor(bboxMinY); | int iMinY = (int)floor(bboxMinY); | ||
| int iMaxY = (int)ceil(bboxMaxY); | int iMaxY = (int)ceil(bboxMaxY); | ||
| - | + | ||
| - | // Lambda to process an edge of the quadr. | + | |
| - | // Returns the x coordinate where the horizontal scanline at ' | + | |
| - | // or NAN if there is no intersection. | + | |
| auto processEdge = [=](float x0, float y0, float x1, float y1, int y) -> float { | auto processEdge = [=](float x0, float y0, float x1, float y1, int y) -> float { | ||
| if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) { | if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) { | ||
| Line 355: | Line 375: | ||
| return NAN; | return NAN; | ||
| }; | }; | ||
| - | + | ||
| - | // For each scanline, we compute up to three segments: one from the quadr body, | + | |
| - | // one from the start cap, and one from the end cap. | + | |
| - | // We then merge overlapping segments and draw them. | + | |
| struct Segment { float x0, x1; }; | struct Segment { float x0, x1; }; | ||
| - | + | ||
| for (int y = iMinY; y <= iMaxY; y++) { | for (int y = iMinY; y <= iMaxY; y++) { | ||
| Segment segments[3]; | Segment segments[3]; | ||
| int segCount = 0; | int segCount = 0; | ||
| - | |||
| - | // 1. Quadr segment: process all four edges of the quadr. | ||
| float inters[4]; | float inters[4]; | ||
| int count = 0; | int count = 0; | ||
| Line 381: | Line 396: | ||
| segments[segCount++] = { qx0, qx1 }; | segments[segCount++] = { qx0, qx1 }; | ||
| } | } | ||
| - | + | ||
| - | // 2. Start cap (centered at (x1,y1)): if y is within the circle' | + | |
| if (y >= y1 - r && y <= y1 + r) { | if (y >= y1 - r && y <= y1 + r) { | ||
| float dyCap = y - y1; | float dyCap = y - y1; | ||
| - | float dxCap = sqrt((float)(r*r - dyCap*dyCap)); | + | float dxCap = sqrt((float)(r * r - dyCap * dyCap)); |
| float capStart = x1 - dxCap; | float capStart = x1 - dxCap; | ||
| float capEnd | float capEnd | ||
| - | // Clip to the half circle: include only points for which dot((x-x1, | ||
| - | // Solve: (x - x1)*lx + (y - y1)*ly < 0 => if lx != 0, x < x1 - ((y-y1)*ly)/ | ||
| - | // or x > x1 - ((y-y1)*ly)/ | ||
| if (fabs(lx) > 1e-6) { | if (fabs(lx) > 1e-6) { | ||
| float xBoundary = x1 - ((y - y1) * ly) / lx; | float xBoundary = x1 - ((y - y1) * ly) / lx; | ||
| Line 399: | Line 410: | ||
| } | } | ||
| } else { | } else { | ||
| - | // For near-vertical lines, if the condition isn't met, skip drawing the cap on this scanline. | ||
| if ((ly > 0 && y >= y1) || (ly < 0 && y <= y1)) { | if ((ly > 0 && y >= y1) || (ly < 0 && y <= y1)) { | ||
| - | capStart = 1, capEnd = 0; // empty | + | capStart = 1, capEnd = 0; |
| } | } | ||
| } | } | ||
| Line 407: | Line 417: | ||
| segments[segCount++] = { capStart, capEnd }; | segments[segCount++] = { capStart, capEnd }; | ||
| } | } | ||
| - | + | ||
| - | // 3. End cap (centered at (x2,y2)): if y is within the circle' | + | |
| if (y >= y2 - r && y <= y2 + r) { | if (y >= y2 - r && y <= y2 + r) { | ||
| float dyCap = y - y2; | float dyCap = y - y2; | ||
| - | float dxCap = sqrt((float)(r*r - dyCap*dyCap)); | + | float dxCap = sqrt((float)(r * r - dyCap * dyCap)); |
| float capStart = x2 - dxCap; | float capStart = x2 - dxCap; | ||
| float capEnd | float capEnd | ||
| - | // Clip to the half circle: include only points for which dot((x-x2, | ||
| if (fabs(lx) > 1e-6) { | if (fabs(lx) > 1e-6) { | ||
| float xBoundary = x2 - ((y - y2) * ly) / lx; | float xBoundary = x2 - ((y - y2) * ly) / lx; | ||
| Line 430: | Line 438: | ||
| segments[segCount++] = { capStart, capEnd }; | segments[segCount++] = { capStart, capEnd }; | ||
| } | } | ||
| - | + | ||
| - | // If no segments were found on this scanline, continue. | + | |
| if (segCount == 0) | if (segCount == 0) | ||
| continue; | continue; | ||
| - | + | ||
| - | // Merge segments: sort by starting x and then combine overlapping ones. | + | |
| - | // (Since there are few segments, a simple bubble sort is sufficient.) | + | |
| for (int i = 0; i < segCount - 1; i++) { | for (int i = 0; i < segCount - 1; i++) { | ||
| for (int j = i + 1; j < segCount; j++) { | for (int j = i + 1; j < segCount; j++) { | ||
| Line 446: | Line 451: | ||
| } | } | ||
| } | } | ||
| - | + | ||
| - | // Merge overlapping segments. | + | |
| float mergedStart = segments[0].x0; | float mergedStart = segments[0].x0; | ||
| float mergedEnd = segments[0].x1; | float mergedEnd = segments[0].x1; | ||
| for (int i = 1; i < segCount; i++) { | for (int i = 1; i < segCount; i++) { | ||
| - | if (segments[i].x0 <= mergedEnd + 1) { // overlapping or contiguous | + | if (segments[i].x0 <= mergedEnd + 1) { |
| if (segments[i].x1 > mergedEnd) | if (segments[i].x1 > mergedEnd) | ||
| mergedEnd = segments[i].x1; | mergedEnd = segments[i].x1; | ||
| } else { | } else { | ||
| - | // Draw the current merged segment. | ||
| int ix0 = (int)ceil(mergedStart); | int ix0 = (int)ceil(mergedStart); | ||
| int ix1 = (int)floor(mergedEnd); | int ix1 = (int)floor(mergedEnd); | ||
| Line 464: | Line 467: | ||
| } | } | ||
| } | } | ||
| - | // Draw the final merged segment. | ||
| int ix0 = (int)ceil(mergedStart); | int ix0 = (int)ceil(mergedStart); | ||
| int ix1 = (int)floor(mergedEnd); | int ix1 = (int)floor(mergedEnd); | ||
| Line 472: | Line 474: | ||
| } | } | ||
| - | void setup() { | + | // |
| - | | + | // 1. Create the shape (a list of 3D points) defined by sin/cos functions. |
| - | tft.setSPISpeed(40000000); | + | vector< |
| - | | + | |
| - | tft.setRotation(rotate); | + | 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; | ||
| } | } | ||
| - | void loop() { | + | // |
| - | | + | // 2. Rotate the shape |
| + | vector< | ||
| + | | ||
| + | for (size_t i = 0; i < shape.size(); ++i) { | ||
| + | // 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; | ||
| + | } | ||
| - | const float baseThickness | + | |
| + | //======================================================================== | ||
| + | // 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 segmentIndex = 0; | ||
| - | | + | |
| float prev_z = 0; | float prev_z = 0; | ||
| - | | + | |
| - | + | | |
| - | | + | |
| - | float t = (360.0 / NUM_SEGMENTS) * i; | + | |
| - | float rad = t * PI_F / 180.0; | + | |
| - | + | ||
| - | float x = SCALE * (cos(rad) - 0.5 * cos(5 * rad)); | + | |
| - | float y = SCALE * (sin(rad) + 0.5 * sin(5 * rad)); | + | |
| - | float z = Z_SCALE * cos(3 * rad); | + | |
| - | + | ||
| - | Vec3 point = { x, y, z }; | + | |
| - | Vec3 rotated = rotateY(point, | + | |
| + | for (size_t i = 0; i < shape.size(); | ||
| int px, py; | int px, py; | ||
| - | | + | |
| - | + | ||
| - | if (i > 0 && px >= 0 && px < SCREEN_WIDTH && py >= 0 && py < SCREEN_HEIGHT) { | + | if (!firstPoint |
| - | float avgZ = (rotated.z + prev_z) / 2.0; | + | float avgZ = (shape[i].z + prev_z) / 2.0; |
| float perspectiveScale = 1.0f / (Z_OFFSET - avgZ); | float perspectiveScale = 1.0f / (Z_OFFSET - avgZ); | ||
| int thickness = baseThickness * perspectiveScale; | int thickness = baseThickness * perspectiveScale; | ||
| if (thickness < 1) thickness = 1; | if (thickness < 1) thickness = 1; | ||
| if (thickness > 20) thickness = 20; | if (thickness > 20) thickness = 20; | ||
| - | + | segmentsLocal[segmentIndex++] = { prev_px_i, prev_py_i, px, py, avgZ, thickness }; | |
| - | segments[segmentIndex++] = { | + | |
| - | | + | |
| - | | + | |
| } | } | ||
| + | |||
| + | firstPoint = false; | ||
| prev_px_i = px; | prev_px_i = px; | ||
| prev_py_i = py; | prev_py_i = py; | ||
| - | prev_z = rotated.z; | + | prev_z = shape[i].z; |
| } | } | ||
| + | |||
| + | // Depth sort (back to front) | ||
| for (int i = 0; i < segmentIndex - 1; ++i) { | for (int i = 0; i < segmentIndex - 1; ++i) { | ||
| for (int j = i + 1; j < segmentIndex; | for (int j = i + 1; j < segmentIndex; | ||
| - | if (segments[j].avgZ < segments[i].avgZ) { | + | if (segmentsLocal[j].avgZ < segmentsLocal[i].avgZ) { |
| - | Segment3D tmp = segments[i]; | + | Segment3D tmp = segmentsLocal[i]; |
| - | | + | |
| - | | + | |
| } | } | ||
| } | } | ||
| } | } | ||
| + | |||
| + | // Draw each segment with color based on depth. | ||
| for (int i = 0; i < segmentIndex; | for (int i = 0; i < segmentIndex; | ||
| - | float depthFactor = (Z_OFFSET - segments[i].avgZ - 1.5f); | + | float depthFactor = (Z_OFFSET - segmentsLocal[i].avgZ - 1.5f); |
| depthFactor = constrain(depthFactor, | depthFactor = constrain(depthFactor, | ||
| - | uint8_t | + | uint8_t |
| - | uint16_t color = rgb888_to_565(0, | + | uint8_t brightnessb = (uint8_t)(168.0 - (depthFactor * 168.0 * 0.5)); |
| + | uint16_t color = rgb888_to_565(0, | ||
| DrawThickLineRoundedHLines( | DrawThickLineRoundedHLines( | ||
| canvas, | canvas, | ||
| - | | + | |
| - | | + | |
| - | | + | |
| color | 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, | tft.drawRGBBitmap(0, | ||
| - | | + | |
| + | // Increment rotation for next frame. | ||
| + | | ||
| if (rotation >= 360.0) rotation -= 360.0; | if (rotation >= 360.0) rotation -= 360.0; | ||
| } | } | ||
| + | |||
| </ | </ | ||
arduino_gfx_helpers.1743041900.txt.gz · Last modified: 2025/03/27 02:18 by kenson
