Getting ready to upload the release version.
Thought I’d go into a little technical details regarding my render pipeline.
That is correct. There’s no screen buffer, but unlike TAS which uses an 8-bit scanline (uint8_t scanline[220]
) mine uses a 16-bit one (uint16_t scanline[220]
).
Not quite as that would be painfully slow to test each pixel against each shape (I have points, lines, circles, and triangles). So the way it works is each shape defines it’s first and last scanlines and each scanline has a maxShapes
value to help optimize rendering.
Before I dive into the shapes I have a 16-bit alpha blending routine borrowed from Adafruit’s graphics library:
Alpha Blending Routine
//uint16_t converted to uint32_t temporarily so as to separate each color and give room for overflow.
uint16_t Display::alphaBlend(uint32_t fg, uint32_t bg, uint8_t alpha)
{
//Fully transparent so return background color
if (alpha == 0)
return (uint16_t)bg;
//Fully opaque so return foreground color
if (alpha == 32)
return (uint16_t)fg;
fg = (fg | fg << 16) & 0x07e0f81f;
bg = (bg | bg << 16) & 0x07e0f81f;
bg += (fg - bg) * alpha >> 5;
bg &= 0x07e0f81f;
return (uint16_t)(bg | bg >> 16);
}
From there points are obviously simple:
Draw Point
void Display::drawPoint(uint16_t *scanline, int y, InternalPoint &point, uint16_t color, uint8_t alpha)
{
if (y == point.y)
scanline[point.x] = alphaBlend(color, scanline[point.x], alpha);
}
Lines are semi-complex as they use Bresenham’s Line Algorithm but they store the current point, dx, dy, sx, and end point (more RAM usage but much faster frame rate):
Draw Line
struct InternalLine
{
InternalPoint p;
InternalPoint end;
InternalPoint d;
int16_t sx;
int16_t err;
};
void Display::drawLine(uint16_t *scanline, int y, InternalLine &line, uint16_t color, uint8_t alpha)
{
uint16_t *pixel = scanline + line.p.x;
while (line.p.y == y)
{
if (line.p.x >= 0 && line.p.x < 220)
*pixel = alphaBlend(color, *pixel, alpha);
if (line.p.x == line.end.x && line.p.y == line.end.y) break;
if (line.err >= line.d.y)
{
line.err += line.d.y * 2;
line.p.x += line.sx;
pixel += line.sx;
}
if (line.err <= line.d.x && line.p.y != line.end.y)
{
line.err += line.d.x * 2;
++line.p.y;
}
}
}
Circles aren’t terribly complicated as each line starts with a width equal to the circle’s radius and then shrinks both ends until they are within the circle:
Draw Circle
void Display::drawCircle(uint16_t *scanline, int y, InternalCircle &circle, uint16_t color, uint8_t alpha)
{
int16_t start = circle.center.x - circle.radius;
int16_t end = circle.center.x + circle.radius;
int16_t dist2 = (circle.center.y - y) * (circle.center.y - y);
int16_t r2 = circle.radius * circle.radius;
uint16_t *pixel;
while ((circle.center.x - start) * (circle.center.x - start) + dist2 > r2)
{
++start;
--end;
}
if (start < 0)
start = 0;
if (end > 219)
end = 219;
pixel = scanline + start;
for (int16_t x = start; x <= end; ++x, ++pixel)
*pixel = alphaBlend(color, *pixel, alpha);
}
Triangles is where things get a little interesting as I played around with several ways to do them and this is the fastest method I’ve found:
Draw Line
struct InternalPoint
{
int16_t x;
int16_t y;
};
struct InternalTriangle
{
InternalPoint p1;
InternalPoint p2;
InternalPoint p3;
};
bool Display::getIntersect(int16_t &x /*OUT*/, int16_t y, const InternalPoint &p1, const InternalPoint &p2)
{
if (p1.y < y && p2.y < y)
return false;
if (p1.y > y && p2.y > y)
return false;
if (p1.y == p2.y) //shouldn't receive this value
return false;
x = p1.x + (p2.x - p1.x) * (y - p1.y) / (p2.y - p1.y);
return true;
}
void Display::getTriangleStartEnd(int16_t &start /*OUT*/, int16_t &end /*OUT*/, int16_t y, InternalTriangle &triangle)
{
if (triangle.p1.y == y)
{
if (triangle.p2.y == y)
{
start = triangle.p1.x;
end = triangle.p2.x;
}
else if (triangle.p3.y == y)
{
start = triangle.p1.x;
end = triangle.p3.x;
}
else
{
start = triangle.p1.x;
end = triangle.p1.x; //in case p2->p3 doesn't intersect scanline
getIntersect(end, y, triangle.p2, triangle.p3);
}
}
else if (triangle.p2.y == y)
{
if (triangle.p3.y == y)
{
start = triangle.p2.x;
end = triangle.p3.x;
}
else
{
start = triangle.p2.x;
end = triangle.p2.x; //in case p3->p1 doesn't intersect scanline
getIntersect(end, y, triangle.p3, triangle.p1);
}
}
else if (triangle.p3.y == y)
{
start = triangle.p3.x;
end = triangle.p3.x; //in case p1->p2 doesn't intersect scanline
getIntersect(end, y, triangle.p1, triangle.p2);
}
else
{
if (getIntersect(start, y, triangle.p1, triangle.p2))
{
if (!getIntersect(end, y, triangle.p2, triangle.p3))
getIntersect(end, y, triangle.p3, triangle.p1);
}
else
{
getIntersect(start, y, triangle.p2, triangle.p3);
getIntersect(end, y, triangle.p3, triangle.p1);
}
}
if (start > end)
swapWT(int16_t,start,end);
if (start < 0)
start = 0;
if (end > 219)
end = 219;
}
void Display::drawTriangle(uint16_t *scanline, int y, InternalTriangle &triangle, uint16_t color, uint8_t alpha)
{
int16_t start, end;
uint16_t *pixel;
getTriangleStartEnd(start, end, y, triangle);
pixel = scanline + start;
for (int16_t x = start; x <= end; ++x, ++pixel) //TODO enable alpha blending
*pixel = alphaBlend(color, *pixel, alpha);
}
All of the shapes are passed with a Transform
type:
struct Transform
{
VectorF offset;
VectorF xAxis;
VectorF yAxis;
};
This way all the models can be stored in FLASH space and transformed to screen space based on individual locations/rotations. To optimize the routines I’m using a 2D matrix in the form of the localized X-axis and Y-axis plus offset.
The movement mechanics were rather interesting to figure out and came out incredibly smoothly even when using the dpad. To accomplish this I first determine which direction the player wants to go based on the combination of dpad buttons (diagonals are implemented too). From there I do a vector slerp (spherical linear interpolation) between the player’s current rotation and the desired direction (each frame it steps 0.15 closer to the desired direction). Then I just move the player forward along their y-axis causing a smooth curved movement.
Body parts along the chain are updated based on the model’s link
coordinate (location where next model’s origin should be). Then they’re slerped towards the previous piece’s direction so they slowly rotate to catch up.
I’ve updated github with the latest version for any who wish to examine the source (look for the link in the Game’s page).