Last active
July 18, 2023 19:38
-
-
Save connornishijima/cf8db8b1058f6980502511ce6dafe4a6 to your computer and use it in GitHub Desktop.
Anti-aliased 2D Wireframe Rendering Function
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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