Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to pinwheel algorithm, should work on any size and should also be faster #4185

Open
wants to merge 5 commits into
base: 0_15
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@ uint16_t mode_running_random(void) {

unsigned z = it % zoneSize;
bool nzone = (!z && it != SEGENV.aux1);
for (unsigned i=SEGLEN-1; i > 0; i--) {
for (int i=SEGLEN-1; i > 0; i--) {
if (nzone || z >= zoneSize) {
unsigned lastrand = PRNG16 >> 8;
int16_t diff = 0;
Expand Down
216 changes: 120 additions & 96 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -637,37 +637,29 @@ uint16_t IRAM_ATTR_YN Segment::nrOfVStrips() const {

// Constants for mapping mode "Pinwheel"
#ifndef WLED_DISABLE_2D
constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16
constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium"
constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32
constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big"
constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50
constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL"
constexpr int Pinwheel_Steps_XL = 368;
constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians
constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians
constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians
constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians

constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction)

// Pinwheel helper function: pixel index to radians
static float getPinwheelAngle(int i, int vW, int vH) {
int maxXY = max(vW, vH);
if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small;
if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med;
if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big;
// else
return float(i) * Int_to_Rad_XL;
}
uint32_t* pinWheelBitMap = NULL; // bit-map to track drawn pixels (managed in service() )
constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction)
constexpr float stepFactor = 1.6; // number of angle steps (rays = stepFacor * maxXY)

// Pinwheel helper function: matrix dimensions to number of rays
static int getPinwheelLength(int vW, int vH) {
static int getPinwheelSteps(int vW, int vH) {
int maxXY = max(vW, vH);
if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small;
if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium;
if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big;
// else
return Pinwheel_Steps_XL;
unsigned stepfactor = unsigned(stepFactor * Fixed_Scale);
return (maxXY * stepfactor) / Fixed_Scale;
}

static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) {
int steps = getPinwheelSteps(vW, vH);
int baseAngle = 0xFFFF / steps; // 360° / steps, in 16 bit scale
int rotate = 0;
if(getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color
for(int k = 0; k < 2; k++) // angular steps for two consecutive rays
{
cosVal[k] = (cos16((i + k) * baseAngle + rotate) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF
sinVal[k] = (sin16((i + k) * baseAngle + rotate) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable)
}
startx = (vW * Fixed_Scale) / 2 + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point)
starty = (vH * Fixed_Scale) / 2 + sinVal[0] / 4;
}
#endif

Expand All @@ -689,7 +681,7 @@ uint16_t IRAM_ATTR Segment::virtualLength() const {
vLen = sqrt16(vH*vH + vW*vW); // use diagonal
break;
case M12_sPinwheel:
vLen = getPinwheelLength(vW, vH);
vLen = getPinwheelSteps(vW, vH);
break;
default:
vLen = vW * vH; // use all pixels from segment
Expand Down Expand Up @@ -763,51 +755,84 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
for (int y = 0; y < i; y++) setPixelColorXY(i, y, col);
break;
case M12_sPinwheel: {
// i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small)
float centerX = roundf((vW-1) / 2.0f);
float centerY = roundf((vH-1) / 2.0f);
float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians
float cosVal = cos_t(angleRad);
float sinVal = sin_t(angleRad);

// avoid re-painting the same pixel
int lastX = INT_MIN; // impossible position
int lastY = INT_MIN; // impossible position
// draw line at angle, starting at center and ending at the segment edge
// we use fixed point math for better speed. Starting distance is 0.5 for better rounding
// int_fast16_t and int_fast32_t types changed to int, minimum bits commented
int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit
int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit
int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit
int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit

int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint
int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint

// Odd rays start further from center if prevRay started at center.
static int prevRay = INT_MIN; // previous ray number
if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) {
int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel
posx += inc_x * jump;
posy += inc_y * jump;
if(pinWheelBitMap == NULL) break; // safety check
int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale
setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal);

unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors
uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram
int lineLength[2] = {0};

// draw two lines starting at at x0/y0, save the coordinates. based on idea by @Brandon502
for (int lineNr = 0; lineNr < 2; lineNr++) {
int x0 = startX; // x / y coordinates in fixed scale
int y0 = startY;
int x1 = (startX + (cosVal[lineNr] * vW)); // outside of grid
int y1 = (startY + (sinVal[lineNr] * vH));
const int dx = abs(x1-x0), sx = x0<x1 ? 1 : -1; // x distance & step
const int dy = -abs(y1-y0), sy = y0<y1 ? 1 : -1; // y distance & step
uint16_t* coordinates = lineCoords[lineNr]; // 1D access is faster
int* length = &lineLength[lineNr]; // faster access
x0 /= Fixed_Scale; // convert to pixel coordinates
y0 /= Fixed_Scale;

// Bresenham's algorithm
int idx = 0;
int err = dx + dy;
while (true) {
if (unsigned(x0) >= vW || unsigned(y0) >= vH) break; // stop if outside of grid (exploit unsigned int overflow)
coordinates[idx++] = x0;
coordinates[idx++] = y0;
(*length)++;
// note: since endpoint is out of grid, no need to check if endpoint is reached
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
prevRay = i;

// draw ray until we hit any edge
while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) {
// scale down to integer (compiler will replace division with appropriate bitshift)
int x = posx / Fixed_Scale;
int y = posy / Fixed_Scale;
// set pixel
if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different
lastX = x;
lastY = y;
// advance to next position
posx += inc_x;
posy += inc_y;

// fill up the shorter line with missing coordinates, so block filling works correctly and efficiently
int diff = lineLength[0] - lineLength[1];
int longLineIdx = (diff > 0) ? 0 : 1;
int shortLineIdx = longLineIdx ? 0 : 1;
if (diff != 0) {
int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index
int lastX = lineCoords[shortLineIdx][idx++];
int lastY = lineCoords[shortLineIdx][idx++];
bool keepX = lastX == 0 || lastX == vW - 1;
for (int d = 0; d < abs(diff); d++) {
lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx];
idx++;
lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY;
idx++;
}
}

// draw and block-fill the line oordinates. Note: block filling only efficient if angle between lines is small
for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx!
int x1 = lineCoords[0][idx];
int x2 = lineCoords[1][idx++];
int y1 = lineCoords[0][idx];
int y2 = lineCoords[1][idx++];
int minX, maxX, minY, maxY;
(x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1);
(y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1);
// fill the block between the two x,y points
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
int index = y * vW + x; // calculate bitmap index, int_index, and bitmask for the current (x, y)
uint32_t int_index = index >> 5; // equivalent to index / 32
uint32_t bitmask = 1U << (index & 31); // equivalent to index % 32
// Check if the pixel has already been drawn
if (!(pinWheelBitMap[int_index] & bitmask)) {
pinWheelBitMap[int_index] |= bitmask; // set bit in map
setPixelColorXY(x, y, col);
}
}
}
}
break;
}
break;
}
return;
} else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) {
Expand Down Expand Up @@ -929,31 +954,17 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const
break;
case M12_sPinwheel:
// not 100% accurate, returns pixel at outer edge
// i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small)
float centerX = roundf((vW-1) / 2.0f);
float centerY = roundf((vH-1) / 2.0f);
float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians
float cosVal = cos_t(angleRad);
float sinVal = sin_t(angleRad);

int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit
int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit
int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit
int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit
int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint
int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint

// trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor
int x = INT_MIN;
int y = INT_MIN;
while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) {
// scale down to integer (compiler will replace division with appropriate bitshift)
x = posx / Fixed_Scale;
y = posy / Fixed_Scale;
// advance to next position
posx += inc_x;
posy += inc_y;
int x, y, cosVal[2], sinVal[2];
setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true);
int maxX = (vW-1) * Fixed_Scale;
int maxY = (vH-1) * Fixed_Scale;
// trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates
while ((x < maxX) && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) {
x += cosVal[0]; // advance to next position
y += sinVal[0];
}
x /= Fixed_Scale;
y /= Fixed_Scale;
return getPixelColorXY(x, y);
break;
}
Expand Down Expand Up @@ -1364,6 +1375,15 @@ void WS2812FX::service() {
// when cctFromRgb is true we implicitly calculate WW and CW from RGB values
if (cctFromRgb) BusManager::setSegmentCCT(-1);
else BusManager::setSegmentCCT(seg.currentBri(true), correctWB);

#ifndef WLED_DISABLE_2D
if (seg.is2D() && seg.map1D2D == M12_sPinwheel) {
int bitArraySize = (seg.virtualHeight() * seg.virtualWidth() + 31) / 32; // 1 bit per pixel
//uint32_t tempmap[bitArraySize] = {0}; // put the bit-map for overdraw tracking on stack
//pinWheelBitMap = tempmap; // assign global pointer Note: using stack leads to crashes on larger sizes (write access error)
pinWheelBitMap = new uint32_t[bitArraySize](); // allocate the bit-map for overdraw tracking on heap and initialize to zero
}
#endif
// Effect blending
// When two effects are being blended, each may have different segment data, this
// data needs to be saved first and then restored before running previous mode.
Expand Down Expand Up @@ -1392,6 +1412,10 @@ void WS2812FX::service() {
seg.next_time = nowUp + delay;
}
_segment_index++;
#ifndef WLED_DISABLE_2D
if(pinWheelBitMap != NULL) delete[] pinWheelBitMap; // free the memory
pinWheelBitMap = NULL; // invalidate pointer
#endif
}
_virtualSegmentLength = 0;
_isServicing = false;
Expand Down