Scanline Rasterization
Looping trough every edge for every pixel is very inefficient. In this section we're going to optimize the DrawPolygon
function by tightly coupling it with the code PointInPolygon
function.
Since all we care about are the edges that cross the current scanline, an easy optimization is to keep an active list of edges and only loop trough those. As we're moving from scanline to scanline, we can check if an edge started or stopped crossing the scanline, and update the active edge list accordingly.
We will need to find where each active edge crosses the scanline, and sort the active edge list left to right based on this coordinate. This makes sure we count every edge as it's passed when rasterizing the scanline. Any time an edge is passed, update the winding number of the scanlie accordingly.
Implementation
We will need to know where each edge intersects the scanline. We're going to create a RenderEdge
structure to help keep track of this data.
struct RenderEdge { Point p0; Point p1; float x; };
Follow these steps to implement the algorithm described above. The sample code adds 0.5f
to all pixel coordinates because it assumes the pixel coordinate represents the center of the pixel.
- Sort all edges by their topmost y coordinate
- Maintain a list of active edgess for the current scanline
- For each scanline
- Add any edge whose top-most point is above the scanline to the active edge list
- Remove any edge whose bottom-most point is above the scanline from the active edge list
- Find where each eactive edge crosses the scanline (we know the y coordiante is that of the scan line, find the x coordinate)
- Sort active edges (left to right) by their intersection point with the scanline
- Initialize winding number to 0 and loop trough all intersection (x coordinates)
- Any time the x coordinate passes an edge, update the winding number
- Shade pixel if the winding number is not 0.
void DrawPolygon(int _x, int _y, const Polygon& poly, unsigned char r, unsigned char g, unsigned char b) { BoundingBox bbox = GetBoundingBox(poly); int s_x = bbox.min.x < 0.0f ? bbox.min.x - 0.5f : bbox.min.x + 0.5f; int e_x = bbox.max.x < 0.0f ? bbox.max.x - 0.5f : bbox.max.x + 0.5f; int s_y = bbox.min.y < 0.0f ? bbox.min.y - 0.5f : bbox.min.y + 0.5f; int e_y = bbox.max.y < 0.0f ? bbox.max.y - 0.5f : bbox.max.y + 0.5f; // Build a list of edges for the polygon std::vector< RenderEdge > edges; for (int c = 0, cSize = poly.size(); c < cSize; ++c) { const Contour& countour = poly[c]; int numPoints = countour.size(); for (int p = 0; p < numPoints; ++p) { const Point& point = countour[p]; const Point& next = countour[(p + 1) % numPoints]; edges.push_back(RenderEdge{ point, next, 0.0f }); } } std::sort(edges.begin(), edges.end(), TopmostY()); std::list< RenderEdge > active; unsigned int numEdges = edges.size(); unsigned int firstEdgeBelowScanline = 0; for (int y = s_y; y <= e_y; ++y) { // Sample from center of pixel float sample_y = float(y) + 0.5f; // Add any edges that start above the scanline while (firstEdgeBelowScanline < numEdges) { const RenderEdge& edge = edges[firstEdgeBelowScanline]; float min_y = edge.p0.y < edge.p1.y ? edge.p0.y : edge.p1.y; if (min_y <= sample_y) { // Inclusive of topmost point // firstEdgeBelowScanline was above the scanline active.push_back(edges[firstEdgeBelowScanline]); firstEdgeBelowScanline += 1; } else { // firstEdgeBelowScanline is below the scanline break; } } // Horizontal edges are not a problem, // they are added above and removed immediateley below // Remove any active edges that end above the scanline for (std::list< RenderEdge >::iterator it = active.begin(); it != active.end();) { float max_y = it->p0.y > it->p1.y ? it->p0.y : it->p1.y; if (max_y <= sample_y) { // Exclusive of bottom most point it = active.erase(it); } else { // Iterator crosses scanline, move to next one it++; } } // Find the x value of where each edge intersects the scanline for (std::list< RenderEdge >::iterator it = active.begin(), end = active.end(); it != end; ++it) { if (fabs(it->p0.x - it->p1.x) < 0.0001f) { // Vertical it->x = it->p0.x; } else { float m = (it->p1.y - it->p0.y) / (it->p1.x - it->p0.x); float b = it->p0.y - m * it->p0.x; it->x = (sample_y - b) / m; } } // Sort edges by ascending x intersection order active.sort(LeftmostXIntersection); // Init winding for scanline int winding = 0; // Draw scanline std::list< RenderEdge >::iterator it = active.begin(); std::list< RenderEdge >::iterator end = active.end(); for (int x = s_x; x <= e_x; ++x) { float sample_x = float(x) + 0.5f; // If any edges are on the left of the sample point // update the running winding number and remove the edge while (it != end && it->x < sample_x) { // Crossing scanline top to bottom if (it->p0.y <= sample_y && it->p1.y > sample_y) { winding -= 1; } // Crossing scanline bottom up else if (it->p1.y <= sample_y && it->p0.y > sample_y) { winding += 1; } ++it; } if (winding != 0) { DrawPixel(_x + x, _y + y, r, g, b); } } } }
These are the sorting functions for the above sample:
struct TopmostY { inline bool operator() (const RenderEdge& left, const RenderEdge& right) { float lY = left.p0.y < left.p1.y ? left.p0.y : left.p1.y; float rY = right.p0.y < right.p1.y ? right.p0.y : right.p1.y; return lY < rY; } }; bool LeftmostXIntersection(const RenderEdge& left, const RenderEdge& right) { return left.x < right.x; }