Part 4 of my ongoing series creating a 3D software renderer for Pico 8. Click here to give it a try, and here to download it. Here are the previous posts:
In this post I implemented back face culling. This is where you do not draw triangles that are facing away from you. When drawing this wireframe cube it makes it appear solid. In fact I’ve implemented culling of either front or back faces, it’s configurable in the Pico 8 menu. You access this menu by pressing the enter key or the start button. Culling the front faces makes the cube appear like a diorama, as if you are peering into it seeing through the walls.
All of the triangles in the cube are written to be clockwise when you are looking at them facing the cube. This means when drawing if we detect any triangles to be anti-clockwise we know that it is facing away from us, and we can skip drawing it.
First I added this table in the _init function. It is to store the currently selected options for which faces to draw or cull.
cull={front=false,back=true}
I wrote this function to add a Pico 8 menu item, given the index of where it appears and the key in the cull table that holds the data. This contains a local function which returns the text to display in the menu. A local function is one that can only be called in the scope in which it’s declared. Since this helper function is only of use for creating the menu item I thought this was a nice bit of encapsulation.
The menuitem function is part of the Pico 8 API. It takes a number from 1 to 5 which controls where it is placed in the menu. It then takes a string, which is the text to be displayed. The third argument is a function which Pico 8 will call when the player selects the option. Here I’ve used an anonymous lambda function in which I toggle the boolean state of the particular option, and update the text in the menu. The call to menuitem with an underscore is a request to update the current item. Finally returning true tells Pico 8 to keep the menu open rather than automatically closing it.
function menu(i,key)
local function lbl(key)
return "cull "..key..": "..(
cull[key] and "on" or "off")
end
menuitem(i,lbl(key),function ()
cull[key]=not cull[key]
menuitem(_,lbl(key))
return true
end)
end
I call this function inside _init twice, once for front culling and once for back.
menu(1,"front")
menu(2,"back")
This is the function which detects which way we are facing the triangle. It works on 2D triangles, so is designed to be called after projection.
It calculates two vectors, a->b and a->c. It then performs a cross product on them. This actually calculates the size of the parallelogram which these vectors form. Half of this size is the size of the triangle, but we don’t actually need the size. The result of this formula is a signed size, as in it returns a positive or negative number based on the orientation of the vectors. When you do want the actual size the next step would be to convert this to an absolute value, but the sign is the part we are after here. Comparing the result with 0 means I get a boolean value which is true when the triangle is facing us.
The comparison with 0 is flipped compared to the standard calculation because the screen on Pico 8 is Y pointing down rather than up. This means the clockwise direction of the triangles is actually flipped.
function front(a,b,c)
return (b.x-a.x)
*(c.y-a.y)
-(b.y-a.y)
*(c.x-a.x)<0
end
The lines of code in _draw which draw the lines of the triangle are now wrapped in an if statement. If the triangle is front facing, and the front facing triangles are not being culled then draw it. The same goes for back facing, and the back facing option.
local f=front(a,b,c)
if (f and not cull.front)
or (not f and not cull.back) then
line(a.x,a.y,b.x,b.y,11)
line(b.x,b.y,c.x,c.y,11)
line(c.x,c.y,a.x,a.y,11)
end