7.3 KiB
Corkscrew Pipeline: computeTile and multiSample Integration
Overview
The FastLED corkscrew allows the user to write to a regular rectangular buffer and have it displayd on a dense corkscrew of LEDs.
Dense 144LED @ 3.28 are cheap and readily avialable. They are cheap, have high density.
Pipeline Components
1. User Paints to XY Grid
Users create patterns on a 2D rectangular grid (fl::Grid<CRGB>) using standard XY coordinates.
// Grid dimensions calculated from corkscrew parameters
uint16_t width = input.calculateWidth(); // LEDs per turn
uint16_t height = input.calculateHeight(); // Total vertical segments
fl::Grid<CRGB> sourceGrid(width, height);
2. LED Projection: Corkscrew → XY Grid
Each LED in the corkscrew has a corresponding floating-point position on the XY grid:
// From corkscrew.cpp - calculateLedPositionExtended
vec2f calculateLedPosition(uint16_t ledIndex, uint16_t numLeds, uint16_t width) {
const float ledProgress = static_cast<float>(ledIndex) / static_cast<float>(numLeds - 1);
const uint16_t row = ledIndex / width; // Which turn (vertical position)
const uint16_t remainder = ledIndex % width; // Position within turn
const float alpha = static_cast<float>(remainder) / static_cast<float>(width);
const float width_pos = ledProgress * numLeds;
const float height_pos = static_cast<float>(row) + alpha;
return vec2f(width_pos, height_pos);
}
3. computeTile: Splat Pixel Rendering
The splat function implements the "computeTile" concept by converting floating-point positions to Tile2x2_u8 structures representing neighbor intensities:
// From splat.cpp
Tile2x2_u8 splat(vec2f xy) {
// 1) Get integer cell indices
int16_t cx = static_cast<int16_t>(floorf(xy.x));
int16_t cy = static_cast<int16_t>(floorf(xy.y));
// 2) Calculate fractional offsets [0..1)
float fx = xy.x - cx;
float fy = xy.y - cy;
// 3) Compute bilinear weights for 4 neighbors
float w_ll = (1 - fx) * (1 - fy); // lower-left
float w_lr = fx * (1 - fy); // lower-right
float w_ul = (1 - fx) * fy; // upper-left
float w_ur = fx * fy; // upper-right
// 4) Build Tile2x2_u8 with weights as intensities [0..255]
Tile2x2_u8 out(vec2<int16_t>(cx, cy));
out.lower_left() = to_uint8(w_ll);
out.lower_right() = to_uint8(w_lr);
out.upper_left() = to_uint8(w_ul);
out.upper_right() = to_uint8(w_ur);
return out;
}
4. Tile2x2_u8: Neighbor Intensity Representation
The Tile2x2_u8 structure represents the sampling strength from the four nearest neighbors:
class Tile2x2_u8 {
uint8_t mTile[2][2]; // 4 neighbor intensities [0..255]
vec2<int16_t> mOrigin; // Base grid coordinate (cx, cy)
// Access methods for the 4 neighbors:
uint8_t& lower_left(); // (0,0) - weight for pixel at (cx, cy)
uint8_t& lower_right(); // (1,0) - weight for pixel at (cx+1, cy)
uint8_t& upper_left(); // (0,1) - weight for pixel at (cx, cy+1)
uint8_t& upper_right(); // (1,1) - weight for pixel at (cx+1, cy+1)
};
5. Cylindrical Wrapping with Tile2x2_u8_wrap
For corkscrew mapping, the tile needs cylindrical wrapping:
// From corkscrew.cpp - at_wrap()
Tile2x2_u8_wrap Corkscrew::at_wrap(float i) const {
Tile2x2_u8 tile = at_splat_extrapolate(i); // Get base tile
Tile2x2_u8_wrap::Entry data[2][2];
vec2i16 origin = tile.origin();
for (uint8_t x = 0; x < 2; x++) {
for (uint8_t y = 0; y < 2; y++) {
vec2i16 pos = origin + vec2i16(x, y);
// Apply cylindrical wrapping to x-coordinate
pos.x = fmodf(pos.x, static_cast<float>(mState.width));
data[x][y] = {pos, tile.at(x, y)}; // {position, intensity}
}
}
return Tile2x2_u8_wrap(data);
}
6. multiSample: Weighted Color Sampling
The readFromMulti method implements the "multiSample" concept by using tile intensities to determine sampling strength:
// From corkscrew.cpp - readFromMulti()
void Corkscrew::readFromMulti(const fl::Grid<CRGB>& source_grid) const {
for (size_t led_idx = 0; led_idx < mInput.numLeds; ++led_idx) {
// Get wrapped tile for this LED position
Tile2x2_u8_wrap tile = at_wrap(static_cast<float>(led_idx));
uint32_t r_accum = 0, g_accum = 0, b_accum = 0;
uint32_t total_weight = 0;
// Sample from each of the 4 neighbors
for (uint8_t x = 0; x < 2; x++) {
for (uint8_t y = 0; y < 2; y++) {
const auto& entry = tile.at(x, y);
vec2i16 pos = entry.first; // Grid position
uint8_t weight = entry.second; // Sampling intensity [0..255]
if (inBounds(source_grid, pos)) {
CRGB sample_color = source_grid.at(pos.x, pos.y);
// Weighted accumulation
r_accum += static_cast<uint32_t>(sample_color.r) * weight;
g_accum += static_cast<uint32_t>(sample_color.g) * weight;
b_accum += static_cast<uint32_t>(sample_color.b) * weight;
total_weight += weight;
}
}
}
// Final color = weighted average
CRGB final_color = CRGB::Black;
if (total_weight > 0) {
final_color.r = static_cast<uint8_t>(r_accum / total_weight);
final_color.g = static_cast<uint8_t>(g_accum / total_weight);
final_color.b = static_cast<uint8_t>(b_accum / total_weight);
}
mCorkscrewLeds[led_idx] = final_color;
}
}
Complete Pipeline Flow
1. User draws → XY Grid (CRGB values at integer coordinates)
↓
2. LED projection → vec2f position on grid (floating-point)
↓
3. computeTile → Tile2x2_u8 (4 neighbor intensities)
(splat) ↓
4. Wrap for → Tile2x2_u8_wrap (cylindrical coordinates + intensities)
cylinder ↓
5. multiSample → Weighted sampling from 4 neighbors
(readFromMulti) ↓
6. Final LED color → CRGB value for corkscrew LED
Key Insights
Sub-Pixel Accuracy
The system achieves sub-pixel accuracy by:
- Using floating-point LED positions on the grid
- Converting to bilinear weights for 4 nearest neighbors
- Performing weighted color sampling instead of nearest-neighbor
Cylindrical Mapping
- X-coordinates wrap around the cylinder circumference
- Y-coordinates represent vertical position along the helix
- Width = LEDs per turn, Height = total vertical segments
Anti-Aliasing
The weighted sampling naturally provides anti-aliasing:
- Sharp grid patterns become smoothly interpolated on the corkscrew
- Reduces visual artifacts from the discrete→continuous→discrete mapping
Performance Characteristics
- Memory: O(W×H) for grid (O(N) for corkscrew LEDs where O(N) <= O(WxH) == O(WxW))
- Computation: O(N) with 4 samples per LED (constant factor)
- Quality: Sub-pixel accurate with built-in anti-aliasing
Future Work
Often led strips are soldered together. These leaves a gap between the other leds on the strip. This gab should be accounted for to maximize spatial accuracy with rendering straight lines (e.g. text).