'graf-w.bas 'wireframe 3d engine 'kinem
'$lang:"fb"
const pi = atn(1)*4 'pi radians = 180 degrees
#define key_esc multikey(1)
#define key_rt multikey(77)
#define key_lt multikey(75)
#define key_up multikey(72)
#define key_dn multikey(80)
#define key_a multikey(30)
#define key_z multikey(44)
#define key_alt multikey(56)
'A vector has direction and size. In 3d, it is defined by 3 numbers,
'which are its components in the x,y,z directions.
Type Vector
As double x, y, z
End Type
Operator + (ByRef lhs As Vector, ByRef rhs As Vector) As Vector
Return Type(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z)
End Operator
Operator - (ByRef lhs As Vector, ByRef rhs As Vector) As Vector
Return Type(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z)
End Operator
Operator * (ByRef lhs As double, ByRef rhs As Vector) As Vector
Return Type(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z)
End Operator
'a dot b = |a| |b| cosine(angle between them)
function dot(byref a as vector, byref b as vector) as double
dot = a.x*b.x + a.y*b.y + a.z*b.z
end function
'"a cross b" is perpendicular to both a and b
'|a cross b| = |a| |b| sine(angle between them)
function cross(byref a as vector, byref b as vector) as vector
cross = Type(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x)
end function
'normal vector perpendicular to the plane a tile is in
#define normal(p1,p2,p3) cross(p2 - p1, p3 - p1)
#define faceside(q) sgn(dot(normal(rel3d(q.p1),rel3d(q.p2),rel3d(q.p3)) , rel3d(q.p1)))
type quad 'Quadrilateral - better than triangle for boxy objects.
'Points must be coplanar, or object will look distorted.
'If p4 = 0, the quadrilateral becomes a triangle with points p1,p2,p3.
'Points are often shared by more than one face of an object,
'so they will be given here as the index of the point in the point3d/rel3d arrays.
'That way when a point is rotated or moved, it automatically affects all faces.
as integer p1,p2,p3,p4
as uinteger col 'color
end type
' wall
' |------------(x,-z)
' | /
' | /
' | /
' | /
' |screen /
' |column/
' |-----o
' | /|
' | / |
' | / | eye distance to screen = constant
' | / |
' |/ |
'----------------o-----------------
' (your eye)
'
'(screen column position - center column) / (distance to screen) = -x/z = tangent(Angle)
'in the coordinate system to be used here,
'+x is to the right, -z is forward,
'+y is up but note that the screen coordinate in the y direction
'increases as you go down the screen
dim shared as single centerx, centery, scrscale
#define scrx(pt) (centerx - scrscale * pt.x/pt.z)
'the - sign is from -z being in front of you
#define scry(pt) (centery + scrscale * pt.y/pt.z)
'second - sign from screen y being down cancels the one from -z
declare sub setupscr(byref s as integer): setupscr(18)
declare sub wire(byval p1 as vector, byval p2 as vector, byref c as uinteger)
declare sub wirequad(byref q as quad, byref c as uinteger)
dim as integer maxpt = 8 * 3 ' = 8 * # of cubes
redim shared as vector point3d(maxpt), rel3d(maxpt) 'rel3d for points relative to eye
redim shared as quad qface(maxpt * 6/8)
dim as uinteger gray = rgb(50,50,50), wcol
dim as ubyte red, green, blue
dim as vector eye = Type(250, 150, 400) 'starting viewpoint coordinates
dim as double theta = 0, tim = timer + 1 'view angle, timer
dim as vector forward = Type(0, 0, -1), rtdir = Type(1, 0, 0)
dim as integer i,j, f, fram = 0, ofram = 0 'utility variables
dim as vector dist = Type(50, 20, -350) 'from one cube to the next
dim as double sinth = sin(theta), costh = cos(theta)
for i=1 to 8: read point3d(i).x, point3d(i).y, point3d(i).z: next 'get cube points
for i=1 to 6: read qface(i).p1, qface(i).p2, qface(i).p3, qface(i).p4 'get tile data
read red,green,blue: qface(i).col = rgb(red,green,blue): next
for j = 1 to (maxpt\8 - 1)
for i=1+8*j to 8+8*j: point3d(i) = point3d(i-8) + dist: next 'setup points for more cubes
for i=1+6*j to 6+6*j:
qface(i).p1 = qface(i-6*j).p1 + 8*j 'copy quad tile data for the additional cubes
qface(i).p2 = qface(i-6*j).p2 + 8*j
qface(i).p3 = qface(i-6*j).p3 + 8*j
qface(i).p4 = qface(i-6*j).p4 + 8*j
qface(i).col = rgb(255*rnd,255*rnd,255*rnd)
next: next
do: for i=1 to maxpt
rel3d(i).x = (point3d(i).x - eye.x) * costh + (point3d(i).z - eye.z) * sinth
rel3d(i).z = -(point3d(i).x - eye.x) * sinth + (point3d(i).z - eye.z) * costh
rel3d(i).y = (point3d(i).y - eye.y): next
screenlock: cls: ?"x";int(eye.x),"y";int(eye.y),"z";int(eye.z),
? "angle"; int(theta*180/pi):? ofram; " frames/sec"
for i=1 to maxpt*6/8: f = faceside(qface(i))
if f<0 then wcol = qface(i).col else wcol=gray
wirequad(qface(i),wcol): next: screenunlock
if key_alt then
if key_rt then eye = eye + 5 * rtdir
if key_lt then eye = eye - 5 * rtdir
else
if key_rt then theta = theta + pi/180
if key_lt then theta = theta - pi/180
end if
if key_up then eye = eye + 5 * forward
if key_dn then eye = eye - 5 * forward
if key_a then eye.y = eye.y + 5
if key_z then eye.y = eye.y - 5
sinth = sin(theta): costh = cos(theta): forward.x = sinth: forward.z = -costh
rtdir.x = costh: rtdir.z = sinth
fram=fram+1: if timer>tim then tim=timer+1: ofram=fram: fram=0
loop until key_esc
data -100,-100,-100 'a cube is a good test shape for 3d graphics
data 100,-100,-100 'because we know what it should look like
data 100,100,-100 'and graphics bugs would tend to ruin its symmetry
data -100,100,-100
data -100,-100,-300
data 100,-100,-300
data 100,100,-300
data -100,100,-300
data 1,2,3,4, 255,0,0 'counterclockwise face = towards eye
data 5,8,7,6, 255,0,0 'p1,p2,p3,p4, red,green,blue
data 1,5,6,2, 0,255,0
data 3,7,8,4, 0,255,0
data 2,6,7,3, 0,0,255
data 4,8,5,1, 0,0,255
sub setupscr(byref s as integer)
screen s,32: dim as integer w, h, i: screeninfo w,h 'get max x,y on screen
scrscale = 500*w/640: centerx=(w-1)/2: centery=(h-1)/2
end sub
sub wire(byval p1 as vector, byval p2 as vector, byref c as uinteger)
dim as integer num,den
if cint(p1.z)>=0 and cint(p2.z)>=0 then exit sub 'both points to side or in back of you
if cint(p1.z)<0 and cint(p2.z)<0 then
'both points in front
line (scrx(p1),scry(p1))-(scrx(p2),scry(p2)),c
else 'p1 or p2 is in back or to the side of you
if cint(p1.z) >= 0 then swap p1,p2 'Now p1.z < 0, meaning it's in front of you.
'(change in x) / (change in y) = dx/dy =
'(scrx(p2)-scrx(p1)) / (scry(p2)-scry(p1)) =
'-(p2.x/p2.z - p1.x/p1.z) / (p2.y/p2.z - p1.y/p1.z)
'but since p2.z can be 0, making scrx(p2) and scry(p2) infinite, multiply
'numerator and denominator by the z's:
'dx/dy = num / den = -(p1.z*p2.x - p2.z*p1.x) / (p1.z*p2.y - p2.z*p1.y)
num = -(p1.z * p2.x - p2.z * p1.x): den = p1.z * p2.y - p2.z * p1.y
line (scrx(p1), scry(p1)) - (scrx(p1) + scrscale * num, scry(p1) + scrscale * den),c
'The line should be long enough to go off-screen,
'since p2 is to the side of you (p2.z = 0) or in back (p2.z > 0).
'That is not guaranteed with the above but in practice it's not a problem.
'To make sure, the end point could be chosen on the appropriate border of the screen.
end if
end sub
sub wirequad(byref q as quad, byref c as uinteger)
wire(rel3d(q.p1),rel3d(q.p2),c)
wire(rel3d(q.p2),rel3d(q.p3),c)
wire(rel3d(q.p3),rel3d(q.p1),c)
if q.p4>0 then
wire(rel3d(q.p3),rel3d(q.p4),c)
wire(rel3d(q.p4),rel3d(q.p1),c)
wire(rel3d(q.p2),rel3d(q.p4),c)
end if
end sub