Skip to content

Instantly share code, notes, and snippets.

@connornishijima
Last active July 18, 2023 19:38
Show Gist options
  • Save connornishijima/cf8db8b1058f6980502511ce6dafe4a6 to your computer and use it in GitHub Desktop.
Save connornishijima/cf8db8b1058f6980502511ce6dafe4a6 to your computer and use it in GitHub Desktop.
Anti-aliased 2D Wireframe Rendering Function
// Draws an open-ended polygon using anti-aliased strokes with user-defined subpixel positioning, scaling and opacity.
// The coordinates of the vertices of your polygon are floating point values on or between LED coordinates, not 0.0-1.0
// like UV coordinates. Output is grayscale values (0.0-1.0) written to a global "float mask[Y_SIZE][X_SIZE]" array.
// Polygons are defined as a 2D array of vertices that form a vector shape, no closed shapes officially supported yet.
// On an ESP32-S3, this rasterizes the polygon very, VERY quickly.
void lixie_aa_polygon(float vertices[][2], uint16_t num_vertices, float x_offset, float y_offset, float x_scale, float y_scale, float angle_deg, float opacity) {
// Line thickness
float stroke_width = 1.0; // Alter this
float stroke_width_inv = 1.0 / stroke_width; // Don't alter this
// Clear temp mask
memset(temp_mask, 0, sizeof(float) * (LEDS_X * LEDS_Y));
// How wide of a neighborhood each checked position should have
int16_t search_width = 1;
// Used to store bounding box of final shape
float min_x = LEDS_X; // Will shrink to polygon bounds later in this function
float min_y = LEDS_Y;
float max_x = -LEDS_X;
float max_y = -LEDS_Y;
// Iterate over all vertices of the polygon
for (uint16_t vert = 0; vert < num_vertices - 1; vert++) {
float rotated_x1, rotated_y1, rotated_x2, rotated_y2;
if(angle_deg == 0.0000){
// Preserve verts if no rotation needed
rotated_x1 = vertices[vert][0];
rotated_y1 = vertices[vert][1];
rotated_x2 = vertices[vert+1][0];
rotated_y2 = vertices[vert+1][1];
}
else{
// Convert angle from degrees to radians
float angle_rad = angle_deg * M_PI / 180.0;
angle_rad *= -1.0;
// Rotate verts (More computationally expensive)
rotated_x1 = vertices[vert][0] * cos(angle_rad) - vertices[vert][1] * sin(angle_rad);
rotated_y1 = vertices[vert][0] * sin(angle_rad) + vertices[vert][1] * cos(angle_rad);
rotated_x2 = vertices[vert + 1][0] * cos(angle_rad) - vertices[vert + 1][1] * sin(angle_rad);
rotated_y2 = vertices[vert + 1][0] * sin(angle_rad) + vertices[vert + 1][1] * cos(angle_rad);
}
// Line start coord
float x_start = rotated_x1 * x_scale + x_offset + 3;
float y_start = rotated_y1 * y_scale + y_offset + 7;
// Line end coord
float x_end = rotated_x2 * x_scale + x_offset + 3;
float y_end = rotated_y2 * y_scale + y_offset + 7;
// Update polygon bounding box
if (x_start < min_x) {
min_x = x_start;
}
if (x_end < min_x) {
min_x = x_end;
}
if (x_start > max_x) {
max_x = x_start;
}
if (x_end > max_x) {
max_x = x_end;
}
// Y axis too
if (y_start < min_y) {
min_y = y_start;
}
if (y_end < min_y) {
min_y = y_end;
}
if (y_start > max_y) {
max_y = y_start;
}
if (y_end > max_y) {
max_y = y_end;
}
// Get exact length of line segment
float vert_x_diff = x_end - x_start;
float vert_y_diff = y_end - y_start;
float line_segment_length = sqrt((vert_x_diff * vert_x_diff) + (vert_y_diff * vert_y_diff));
// Convert line length to integer number of iterations drawing will take, with at least one step per line
// This way, a line that stretches 7 pixels in the x or y axis will never have less than 7 drawing steps to avoid gaps while avoiding redundant work
uint16_t num_steps = line_segment_length;
if (num_steps < 1) {
num_steps = 1;
}
// Amount to increment along the line on each step
float step_size = 1.0 / num_steps;
// Iterate over length of line segment for num_steps
float progress = 0.0;
for (uint16_t step = 0; step <= num_steps; step++) {
float brightness_multiplier = 1.0;
// Half brightness at positions where line-segments meet,
// except for the beginning and end of the shape
if (num_steps > 1) {
if (vert != 0) {
if (step == 0) {
brightness_multiplier = 0.5;
}
}
if (vert != num_vertices - 1) {
if (step == num_steps) {
brightness_multiplier = 0.5;
}
}
}
// Get exact intermediate position along line
float x_step_pos = x_start * (1.0 - progress) + x_end * progress;
float y_step_pos = y_start * (1.0 - progress) + y_end * progress;
// Clear search markers
memset(searched, false, sizeof(bool) * (LEDS_X * LEDS_Y));
// Evaluate pixel neighborhood of current point to render step of line
for (int16_t x_search = search_width * -1; x_search <= search_width; x_search++) {
for (int16_t y_search = search_width * -1; y_search <= search_width; y_search++) {
// Integer pixel position to check
int16_t x_pos_current = x_step_pos + x_search;
int16_t y_pos_current = y_step_pos + y_search;
// If we haven't search this pixel yet
if (searched[y_pos_current][x_pos_current] == false) {
searched[y_pos_current][x_pos_current] = true;
// Only check position if on visible pixels
if (x_pos_current >= 0 && x_pos_current < LEDS_X) {
if (y_pos_current >= 0 && y_pos_current < LEDS_Y) {
// Calculate distance between currently searched pixel and exact position of line step point
float x_diff = fabs(x_pos_current - x_step_pos);
float y_diff = fabs(y_pos_current - y_step_pos);
float distance_to_line = sqrt((x_diff * x_diff) + (y_diff * y_diff));
// Convert distance to pixel mask brightness if close enough
if (distance_to_line < stroke_width) {
float brightness = 1.0 - (distance_to_line * stroke_width_inv);
brightness *= brightness_multiplier;
// Line segments shorter than 1px should proportionally contribute less to the raster
if (line_segment_length < stroke_width) {
brightness *= (line_segment_length * stroke_width_inv);
}
// Apply added brightness to mask, saturating at 1.0 (white)
temp_mask[y_pos_current][x_pos_current] += brightness;
if (temp_mask[y_pos_current][x_pos_current] > 1.0) {
temp_mask[y_pos_current][x_pos_current] = 1.0;
}
}
}
}
}
}
}
// Move one step forward along the line
progress += step_size;
}
}
// Apply search width padding to bounding box
min_x -= search_width;
max_x += search_width;
min_y -= search_width;
max_y += search_width;
// Double check that we're clipped to the visible screen area
if (min_x < 0) {
min_x = 0;
} else if (min_x > LEDS_X - 1) {
min_x = LEDS_X - 1;
}
if (max_x < 0) {
max_x = 0;
} else if (max_x > LEDS_X - 1) {
max_x = LEDS_X - 1;
}
if (min_y < 0) {
min_y = 0;
} else if (min_y > LEDS_Y - 1) {
min_y = LEDS_Y - 1;
}
if (max_y < 0) {
max_y = 0;
} else if (max_y > LEDS_Y - 1) {
max_y = LEDS_Y - 1;
}
// Use potentially smaller bounding box to write final shape more efficiently to the pixel mask
for (int16_t x = min_x; x <= max_x; x++) {
for (int16_t y = min_y; y <= max_y; y++) {
// If not fully black
if (temp_mask[y][x] > 0.0) {
// Write out final pixels to the mask with proper opacity now applied
mask[y][x] += temp_mask[y][x] * opacity;
// Saturate at 1.0 (white)
if (mask[y][x] > 1.0) {
mask[y][x] = 1.0;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment