After the last post we have solid triangles. The cube is no longer a wireframe. Unfortunately it’s possible to count the frame rate by eye. In this post I replace the implementation with another approach, called scanline rasterisation, which is significantly faster when implemented with a CPU.
As always, click here to give it a try, and here to download it. Here are the previous posts:
We’re back to 60fps, and we’re barely even touching the sides. This cart has a CPU load of 0.07 to 0.1 according to the performance overlay of Pico 8 (press Ctrl-P to see), as opposed to the 6.5 of the previous implementation.
function _update60()
I made a small change to the projection function. I have wrapped the x and y
coordinates in a call to flr. This makes all of the screen positions integer
values, and causes the calculations on neighbouring triangles to work more
consistently. This means that there should be no gaps between them.
--project to screen
return {
x=flr((x/z+1)/2*128),
y=flr((y/z+1)/2*128)
}
This first triangle drawing function is for the special case where it has a flat bottom. Points b and c share a y coordinate. I added a little ASCII art in the comment to help visualise it. This makes it really quite simple to render since there can only be two slopes to work through. The lines on either side of the triangle will only ever go in a single direction.
First we measure the height of the triangle. This is the amount of scanlines to iterate through. Then we calculate two slopes, from the top to the bottom left point, and from the top to the bottom right point. This is calculated by working out the width of that distance, and then dividing it by the height. Either of these slopes could be positive or negative.
We then have two variables to walk through those slopes, both of which start at the x position of the point a. We then iterate through all of the scanlines from the top to the bottom of the triangle drawing lines, adding the slope to each x coordinate along the way.
This is a single 1-dimensional loop. Inside the loop we are doing two very simple additions, and making a single call to a line function. Compared to the previous implementation where we were iterating by pixel, and performing significantly more calculations for each.
function ⬆️tri(a,b,c,clr)
local height=b.y-a.y
local slpx1=(b.x-a.x)/height
local slpx2=(c.x-a.x)/height
--flat bottomed, so all the
--starting points are at a
--
-- a
--b c
local curx1=a.x
local curx2=a.x
for y=a.y,b.y do
line(curx1,y,curx2,y,clr)
curx1+=slpx1
curx2+=slpx2
end
end
This function is for the other special case, where the top of the triangle is flat and the bottom is the point. The process is basically identical, but we calculate the width of the slopes slightly differently and then start the slopes at the x coordinates for a and b.
function ⬇️tri(a,b,c,clr)
local height=c.y-a.y
local slpx1=(c.x-a.x)/height
local slpx2=(c.x-b.x)/height
--flat topped, so the starting
--points are at a and b
--
--a b
-- c
local curx1=a.x
local curx2=b.x
for y=a.y,c.y do
line(curx1,y,curx2,y,clr)
curx1+=slpx1
curx2+=slpx2
end
end
This function then is the top-level one. It is the one that is called in the _draw function. Firstly we reuse the previous culling code. We then sort the points so that a is always the highest point on the screen. This is an assumption made in the previous two functions. We then work out if we can call either of the special case functions for easy rendering of the triangle.
We now do a little trick. If this is a triangle where a, b, and c all have different heights we cut it in two. We create a new point, d, which will end up creating two new triangles. One of these will have a flat top, and one of them will have a flat bottom. The y coordinate for point d is shared with b, since we sorted the points we know that c is lower on the screen. The x coordinate requires a small calculation. First we work out the ratio between the heights of a->b and a->c. We then use that ratio to work out how much to add to the x coordinate of a. We can then call the two previous functions substituting one of the points with d.
function draw_tri(a,b,c,clr)
local f=front(a,b,c)
if (f and cull.front)
or (not f and cull.back) then
return
end
--sort the points so that a
--is the highest point on
--screen
if (a.y>b.y) a,b=b,a
if (b.y>c.y) b,c=c,b
if (a.y>b.y) a,b=b,a
if a.y==b.y then
⬇️tri(a,b,c,clr)
elseif b.y==c.y then
⬆️tri(a,b,c,clr)
else
local ac_ratio=(b.y-a.y)
/(c.y-a.y)
local d={
x=a.x+ac_ratio*(c.x-a.x),
y=b.y
}
⬆️tri(a,b,d,clr)
⬇️tri(b,d,c,clr)
end
end