Micro Sub Pixel Rendering

Micro Sub Pixel Rendering (mspxr)
Copyright (C) 2024 Canmi, all rights reserved.

Introduction

mspxr is a lightweight graphic rendering library. It leverages the unique properties of LCDs to display 3x times more pixels using limited physical pixels. Additionally, it employs anti-aliasing techniques to enhance the detail and smoothness of fonts and images.


Dot

Here is a 1px dot.


Line


Rectangle

We can use these dots to construct a rectangle.


Circle?

Without anti-aliasing, it may seem like your screen resolution is too low. But do you really need such a high resolution?


Midpoint Circle Algorithm

The Midpoint Circle Algorithm is an efficient method for drawing a circle on a grid by using symmetry and a decision-making process to determine the next pixel to plot based on a midpoint criterion.

Initialization:

Update Rule:

Draw the following points:

Now, let’s implement this concept in C.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void mspxr_draw_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, uint16_t color)
{
int16_t x = 0;
int16_t y = radius;
int16_t decision = 3 - 2 * radius;

// Use Bresenham's algorithm to draw the circle
while (y >= x)
{
// Plot points in all octants
mspxr_draw_dot(center_x + x, center_y + y, color);
mspxr_draw_dot(center_x - x, center_y + y, color);
mspxr_draw_dot(center_x + x, center_y - y, color);
mspxr_draw_dot(center_x - x, center_y - y, color);
mspxr_draw_dot(center_x + y, center_y + x, color);
mspxr_draw_dot(center_x - y, center_y + x, color);
mspxr_draw_dot(center_x + y, center_y - x, color);
mspxr_draw_dot(center_x - y, center_y - x, color);

// Update decision parameter
if (decision <= 0)
{
decision += 4 * x + 6;
}
else
{
decision += 4 * (x - y) + 10;
y--;
}
x++;
}
}

Now we have a circle, but it’s not exactly what we want.


Rounded Rectangle

We can divide this circle into four pieces to create a corner for a rounded rectangle.

Now we can use a circle divided into 4 corners and 5 rectangles to form a rounded rectangle.

Rounded Rectangle:

To draw a rounded rectangle, the algorithm combines straight lines for the rectangle’s edges and circular arcs for its corners. The edges of the rectangle are drawn using straight lines:

Draw horizontal lines from , maintaining the y-coordinates at and , and draw vertical lines from to , maintaining the x-coordinates at and .

Top Edge:

Bottom Edge:

Left Edge:

Right Edge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void mspxr_draw_rounded_rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t corner, uint16_t color, uint8_t argument)
{
// Draw horizontal edges
mspxr_draw_straight_line(x1 + corner, y1, x2 - corner, y1, color);
mspxr_draw_straight_line(x1 + corner, y2, x2 - corner, y2, color);

// Draw vertical edges
mspxr_draw_straight_line(x1, y1 + corner - argument, x1, y2 - corner + argument, color);
mspxr_draw_straight_line(x2, y1 + corner - argument, x2, y2 - corner + argument, color);

int16_t dx, dy;

// Draw corner arcs
for (dx = 0; dx <= corner; dx++)
{
dy = (int16_t)(sqrt(corner * corner - dx * dx));

// Upper-left corner
mspxr_draw_dot(x1 + corner - dx, y1 + corner - dy, color);
// Lower-left corner
mspxr_draw_dot(x1 + corner - dx, y2 - corner + dy, color);
// Upper-right corner
mspxr_draw_dot(x2 - corner + dx, y1 + corner - dy, color);
// Lower-right corner
mspxr_draw_dot(x2 - corner + dx, y2 - corner + dy, color);
}
}

Rounded Rectangle Filled:

Top Section:
For lines where the current y-coordinate is within the top rounded corner area, the x-coordinates are calculated based on the circle equation:

Horizontal lines are drawn between:

Middle Section:
For lines in the middle rectangle (outside the corner areas), the horizontal lines span directly between:

Bottom Section:
For lines in the bottom rounded corner area, the x-coordinates are calculated similarly to the top section, but using the offset from the bottom:

Horizontal lines are drawn between:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void mspxr_draw_rounded_rectangle_filled(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t corner, uint16_t color)
{
for (uint16_t y = y1; y <= y2; y++)
{
if (y < y1 + corner)
{
int16_t dy = y1 + corner - y;
int16_t dx = (int16_t)(sqrt(corner * corner - dy * dy));
mspxr_draw_straight_line(x1 + corner - dx, y, x2 - corner + dx, y, color);
}
else if (y > y2 - corner)
{
int16_t dy = y - (y2 - corner);
int16_t dx = (int16_t)(sqrt(corner * corner - dy * dy));
mspxr_draw_straight_line(x1 + corner - dx, y, x2 - corner + dx, y, color);
}
else
{
mspxr_draw_straight_line(x1, y, x2, y, color);
}
}
}


FXAA

FXAA (Fast Approximate Anti-Aliasing) is a modern technique developed to reduce the jagged edges (aliasing) that appear in graphics, especially on diagonal or curved lines. Unlike traditional anti-aliasing methods, FXAA operates as a post-processing effect, smoothing out edges without the need for heavy computational power. It works by blending the colors of pixels along the edges, which reduces the visual impact of jaggedness. FXAA is widely used in real-time rendering applications, offering a balance between quality and performance.


Blend Color

In order to use FXAA, we must create some extra pixels for transitioning.

Blended Color =

Where:

is the foreground color (the pixel being drawn).

is the background color (the pixel already in place).

is the alpha value (ranging from 0 to 255), which controls the mix between the two colors.

This formula ensures a smooth gradient by proportionally mixing the two colors based on the alpha value, where higher alpha emphasizes the foreground color.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint16_t mspxr_blend_color(uint16_t color, uint16_t background_color, uint16_t alpha)
{
// Foreground color
uint8_t r1 = (color >> 11) & 0x1F; // (5 bits)
uint8_t g1 = (color >> 5) & 0x3F; // (6 bits)
uint8_t b1 = color & 0x1F; // (5 bits)

// Background color
uint8_t r2 = (background_color >> 11) & 0x1F; // (5 bits)
uint8_t g2 = (background_color >> 5) & 0x3F; // (6 bits)
uint8_t b2 = background_color & 0x1F; // (5 bits)

// Apply alpha blending
uint8_t r = (r1 * alpha + r2 * (255 - alpha)) / 255;
uint8_t g = (g1 * alpha + g2 * (255 - alpha)) / 255;
uint8_t b = (b1 * alpha + b2 * (255 - alpha)) / 255;

// Combine into 16-bit color (RGB565 format)
return (r << 11) | (g << 5) | b;
}

FXAA Circle

We can renders a filled circle with a smooth, anti-aliased edge using Fast Approximate Anti-Aliasing (FXAA). The function draws the solid interior of the circle first and then gradually blends the outer edge into the background, creating a smooth transition.

Circle Interior:

Iterate through all pixels within the radius of the circle, drawing them with the specified foreground color.
For a pixel at coordinates relative to the circle’s center, it is part of the interior if:

Anti-Aliased Edge:

Pixels in the transition zone are determined based on their distance from the circle’s edge.
The transition zone spans from to , where “expansion” controls the width of the blending area.

The alpha value for blending is calculated as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void mspxr_fxaa_circle(uint16_t center_x, uint16_t center_y, uint16_t radius, uint16_t expansion, uint16_t color, uint16_t background_color)
{
int16_t dx, dy;
uint16_t alpha;
float total_radius = radius + expansion;

// Draw the solid interior of the circle
for (dx = -radius; dx <= radius; dx++)
{
for (dy = -radius; dy <= radius; dy++)
{
if (sqrt(dx * dx + dy * dy) <= radius)
{
mspxr_draw_dot(center_x + dx, center_y + dy, color);
}
}
}

// Draw the anti-aliased edge
for (dx = -total_radius; dx <= total_radius; dx++)
{
for (dy = -total_radius; dy <= total_radius; dy++)
{
float distance = sqrt(dx * dx + dy * dy);

// Skip pixels already drawn in the interior
if (distance <= radius)
continue;

// Only process pixels in the transition zone
if (distance <= total_radius)
{
alpha = (uint16_t)((1.0f - ((distance - radius) / expansion)) * 255);

// Blend the foreground and background colors
uint16_t mixed_color = mspxr_blend_color(color, background_color, alpha);

mspxr_draw_dot(center_x + dx, center_y + dy, mixed_color);
}
}
}
}


FXAA Quarter Circle

In embedded applications with limited resources, optimizing for bufferless scenarios is essential. Therefore, rather than using four full circles to create a rounded rectangle, we generate four quarter-circle segments.

A quarter-circle is restricted to the first quadrant, expanding to the right and downward from the center point. The pixel coordinates

must satisfy:

To check if a pixel (𝑑𝑥, 𝑑𝑦) falls within the extended circular area, compute its Euclidean distance from the circle’s center:

A pixel is part of the extended circle if:

Drawing Logic:
For pixels inside the original circle:

Draw with the solid foreground color.

For pixels within the transition (anti-aliased) zone:

Compute the alpha value for blending based on the distance:

Use this alpha value to blend the foreground and background colors for smooth edges.


FXAA Rounded Rectangle

Building on the previous method for drawing a quarter-circle, we can break down a rounded rectangle with anti-aliasing into four quarter-circles for the corners and three rectangles to complete the shape.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
void mspxr_fxaa_rounded_rectangle_filled(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t corner, uint16_t expansion, uint16_t color, uint16_t background_color) 
{
x2 -= 1;
y2 -= 1;
int16_t dx, dy;
float total_radius = corner + expansion;
uint16_t alpha;
uint16_t vertical_fill_start = y1 + corner;
uint16_t vertical_fill_end = y2 - corner;

for (dx = -corner; dx <= 0; dx++)
{
for (dy = -corner; dy <= 0; dy++)
{
float distance = sqrt(dx * dx + dy * dy);
if (distance <= corner + expansion && x1 + corner + dx >= x1 && y1 + corner + dy >= y1)
{
if (distance <= corner)
{
mspxr_draw_dot(x1 + corner + dx, y1 + corner + dy, color);
}
else
{
alpha = (uint16_t)((1.0f - ((distance - corner) / expansion)) * 255);
uint16_t blended_color = mspxr_blend_color(color, background_color, alpha);
mspxr_draw_dot(x1 + corner + dx, y1 + corner + dy, blended_color);
}
}
}
}

mspxr_draw_rectangle_filled(x1, vertical_fill_start, x1 + corner, vertical_fill_end, color);

for (dx = -corner; dx <= 0; dx++)
{
for (dy = 0; dy <= corner; dy++)
{
float distance = sqrt(dx * dx + dy * dy);
if (distance <= corner + expansion && x1 + corner + dx >= x1 && y2 - corner + dy <= y2)
{
if (distance <= corner)
{
mspxr_draw_dot(x1 + corner + dx, y2 - corner + dy, color);
}
else
{
alpha = (uint16_t)((1.0f - ((distance - corner) / expansion)) * 255);
uint16_t blended_color = mspxr_blend_color(color, background_color, alpha);
mspxr_draw_dot(x1 + corner + dx, y2 - corner + dy, blended_color);
}
}
}
}

mspxr_draw_rectangle_filled(x1 + corner, y1, x2 - corner, y2, color);

for (dx = 0; dx <= corner; dx++)
{
for (dy = -corner; dy <= 0; dy++)
{
float distance = sqrt(dx * dx + dy * dy);
if (distance <= corner + expansion && x2 - corner + dx <= x2 && y1 + corner + dy >= y1)
{
if (distance <= corner)
{
mspxr_draw_dot(x2 - corner + dx, y1 + corner + dy, color);
}
else
{
alpha = (uint16_t)((1.0f - ((distance - corner) / expansion)) * 255);
uint16_t blended_color = mspxr_blend_color(color, background_color, alpha);
mspxr_draw_dot(x2 - corner + dx, y1 + corner + dy, blended_color);
}
}
}
}

mspxr_draw_rectangle_filled(x2 - corner, vertical_fill_start, x2, vertical_fill_end, color);

for (dx = 0; dx <= corner; dx++)
{
for (dy = 0; dy <= corner; dy++)
{
float distance = sqrt(dx * dx + dy * dy);
if (distance <= corner + expansion && x2 - corner + dx <= x2 && y2 - corner + dy <= y2)
{
if (distance <= corner)
{
mspxr_draw_dot(x2 - corner + dx, y2 - corner + dy, color);
}
else
{
alpha = (uint16_t)((1.0f - ((distance - corner) / expansion)) * 255);
uint16_t blended_color = mspxr_blend_color(color, background_color, alpha);
mspxr_draw_dot(x2 - corner + dx, y2 - corner + dy, blended_color);
}
}
}
}
}


Summary

Building on the approach described above, we can add support for subpixel rendering by performing triple sampling of horizontal pixels, calculating weights, and compressing the subpixels back into place. While the specifics of the subpixel algorithm are not detailed here, the image below illustrates a comparison between a rounded rectangle calculated with subpixel and anti-aliasing techniques and a simple rounded rectangle.