Now it’s time to add a texture to the cube. I’ve done this with the use of the tline function in Pico 8.
As always, click here to give it a try, and here to download it. Here are the previous posts:
When adding a texture to a polygon we need to know where we are getting the
colour data from, and how to map it across the face. In this implementation I
use tline which uses map coordinates for the starting position, and two values
to tell it how much to increment in both directions after each pixel it draws.
This allows it to sample colour data as it draws the lines of our triangles.
Since the function increments the map position by a linear amount after every
pixel it means that you get warping. This is known as affine texture mapping,
and is exactly what caused that behaviour on the PlayStation 1. To have
perspective correct textures for a triangle angled away from you tline would
need to increase the amount it’s stepping along the map as the triangle recedes
away from the screen.
The first change is in the setup of the verts table in _init. We now have two more coordinates: u and v. These have nothing to do with UV radiation, but are the horizontal and vertical texture coordinates. These point to tiles on the Pico 8 map data. I’ve gone with an 8x8 section of the map to give it a reasonable amount of detail.
There are also now significantly more vertices. This is because the points that used to be shared across triangles now need to have different texture coordinates from each other. Each corner of the old cube was used in three triangles, which worked well when it is only representing position. Now each face of the cube using that point needs to pin a different part of the texture to it.
verts={
--front
{x=-1,y=-1,z=-1,u= 0,v= 0},
{x=-1,y= 1,z=-1,u= 0,v= 8},
{x= 1,y= 1,z=-1,u= 8,v= 8},
{x= 1,y=-1,z=-1,u= 8,v= 0},
--back
{x= 1,y=-1,z= 1,u= 0,v= 0},
{x= 1,y= 1,z= 1,u= 0,v= 8},
{x=-1,y= 1,z= 1,u= 8,v= 8},
{x=-1,y=-1,z= 1,u= 8,v= 0},
--top
{x=-1,y=-1,z=-1,u= 0,v= 0},
{x= 1,y=-1,z=-1,u= 8,v= 0},
{x= 1,y=-1,z= 1,u= 8,v= 8},
{x=-1,y=-1,z= 1,u= 0,v= 8},
--bottom
{x=-1,y= 1,z= 1,u= 0,v= 0},
{x= 1,y= 1,z= 1,u= 8,v= 0},
{x= 1,y= 1,z=-1,u= 8,v= 8},
{x=-1,y= 1,z=-1,u= 0,v= 8},
--left
{x=-1,y=-1,z= 1,u= 0,v= 0},
{x=-1,y= 1,z= 1,u= 0,v= 8},
{x=-1,y= 1,z=-1,u= 8,v= 8},
{x=-1,y=-1,z=-1,u= 8,v= 0},
--right
{x= 1,y=-1,z=-1,u= 0,v= 0},
{x= 1,y= 1,z=-1,u= 0,v= 8},
{x= 1,y= 1,z= 1,u= 8,v= 8},
{x= 1,y=-1,z= 1,u= 8,v= 0},
}
The tris table has had its colour value removed, and has been updated to use the extra vertices.
tris={
{ 1, 2, 3},
{ 1, 3, 4},
{ 5, 6, 7},
{ 5, 7, 8},
{ 9,10,11},
{ 9,11,12},
{13,14,15},
{13,15,16},
{17,18,19},
{17,19,20},
{21,22,23},
{21,23,24}
}
The projection and rotation functions have had a small change to pass through the new u and v coordinates. Everything else is identical.
function project(v)
--move
local x=v.x+pos.x
local y=v.y+pos.y
local z=v.z+pos.z
--project to screen
return {
x=flr((x/z+1)/2*128),
y=flr((y/z+1)/2*128),
u=v.u,
v=v.v
}
end
function rotate_x(p,a)
local c=cos(a)
local s=sin(a)
return {
x=p.x,
y=p.y*c-p.z*s,
z=p.y*s+p.z*c,
u=p.u,
v=p.v
}
end
function rotate_y(p,a)
local c=cos(a)
local s=sin(a)
return {
x=p.x*c-p.z*s,
y=p.y,
z=p.x*s+p.z*c,
u=p.u,
v=p.v
}
end
function rotate_z(p,a)
local c=cos(a)
local s=sin(a)
return {
x=p.x*c-p.y*s,
y=p.x*s+p.y*c,
z=p.z,
u=p.u,
v=p.v
}
end
The calculation of d in the draw_tri function has been updated to also
interpolate the u and v coordinates for the new point.
local d={
x=a.x+ac_ratio*(c.x-a.x),
y=b.y,
u=a.u+ac_ratio*(c.u-a.u),
v=a.v+ac_ratio*(c.v-a.v)
}
The main change for this post is in the two flat triangle drawing functions. We
now have to check that the points are in order horizontally, otherwise the
tline function will not draw anything. This is because the mdx and mdy
arguments, which are used to walk along the map data, need to be in the same
direction as the line.
We interpolate the u and v values for each point in a very similar way to the x
and y values. They are effectively simply different pieces of data that we need
to perform the same calculations on. We then use these to tell tline where to
look in the map data for the pixel colour.
function ⬆️tri(a,b,c)
--if b.x is to the right of c.x
--then the increment for the
--texture sampling will be in
--the wrong direction.
if (b.x>c.x) b,c=c,b
local height=b.y-a.y
local slpx1=(b.x-a.x)/height
local slpx2=(c.x-a.x)/height
local slpu1=(b.u-a.u)/height
local slpu2=(c.u-a.u)/height
local slpv1=(b.v-a.v)/height
local slpv2=(c.v-a.v)/height
--flat bottomed, so all the
--starting points are at a
--
-- a
--b c
local curx1=a.x
local curx2=a.x
local curu1=a.u
local curu2=a.u
local curv1=a.v
local curv2=a.v
for y=a.y,b.y do
local width=curx2-curx1
if width>0 then
tline(
curx1,y,curx2,y,
curu1,curv1,
(curu2-curu1)/width,
(curv2-curv1)/width)
end
curx1+=slpx1
curx2+=slpx2
curu1+=slpu1
curu2+=slpu2
curv1+=slpv1
curv2+=slpv2
end
end
function ⬇️tri(a,b,c)
--if a.x is to the right of b.x
--then the increment for the
--texture sampling will be in
--the wrong direction.
if (a.x>b.x) a,b=b,a
local height=c.y-a.y
local slpx1=(c.x-a.x)/height
local slpx2=(c.x-b.x)/height
local slpu1=(c.u-a.u)/height
local slpu2=(c.u-b.u)/height
local slpv1=(c.v-a.v)/height
local slpv2=(c.v-b.v)/height
--flat topped, so the starting
--points are at a and b
--
--a b
-- c
local curx1=a.x
local curx2=b.x
local curu1=a.u
local curu2=b.u
local curv1=a.v
local curv2=b.v
for y=a.y,c.y do
local width=curx2-curx1
if width>0 then
tline(
curx1,y,curx2,y,
curu1,curv1,
(curu2-curu1)/width,
(curv2-curv1)/width)
end
curx1+=slpx1
curx2+=slpx2
curu1+=slpu1
curu2+=slpu2
curv1+=slpv1
curv2+=slpv2
end
end
This feels pretty cool. A textured cube just from drawing lines using maths. Next will either be fixing the issue mentioned in the inside test post where if you do not cull any face you get a corrupted image, or it might be drawing multiple cubes.