Using vector cross product to hide 3D faces
Question:
The code creates 3D coordinates for the cube and then displays them on the 2D screen but you can still see the back faces of the cube. What is the best method for hiding these faces?
points = [(-1,-1,-1),(-1,-1,1),(-1,1,1),(-1,1,-1),
(1,-1,-1),(1,-1,1),(1,1,1),(1,1,-1), ]#coords for points
faces = [(3,0,4,7),(1,5,6,2),(2,1,0,3),(5,4,7,6),(3,2,6,7),(0,1,5,4)]
These are the coordinates of the points and which points should be joined to which
def flattenPoint(point):
(x, y, z) = (point[0], point[1], point[2])
xnew = x#x axis rotation
ynew = y * math.cos(rotatedanglex) - z * math.sin(rotatedanglex)
znew = y * math.sin(rotatedanglex) + z * math.cos(rotatedanglex)
xnew = znew * math.sin(rotatedangley) + xnew * math.cos(rotatedangley)
ynew = ynew #y axis rotation
znew = ynew * math.cos(rotatedangley) - xnew * math.sin(rotatedangley)
projectedY = int(height / 2 + ((ynew * distance) / (znew + distance)) * scale)
projectedX = int(width / 2 + ((xnew * distance) / (znew + distance)) * scale)
return (projectedX, projectedY, znew)
def createOutline(points):
a, b, c, d = points[0], points[1], points[2], points[3]
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, blue, coords, 1)
”’
The FlattenPoint function rotates the 3D points and then turns them into 2D coordinates that are displayed.
”’
def createFace(points):
a, b, c, d = points[0], points[1], points[2], points[3]
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, green, coords)
createFace joins up the 2D coordinates.
def render(points, faces):
coords = []
for point in points:
coords.append(flattenPoint(point))
screen.fill(screencolour)
for face in faces:
createFace((coords[face[0]], coords[face[1]], coords[face[2]], coords[face[3]]))
for face in faces:#must draw outline after all the faces have been drawn
createOutline((coords[face[0]], coords[face[1]], coords[face[2]],coords[face[3]]))
”’
Answers:
Compute the normal vector to of a face and cull the faces where the normal vector points away from the view. The normal vector can be computed with the Cross product:
def cross(a, b):
return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
Use the cross product and cull the faces:
def createFace(points):
a, b, c, d = points[0], points[1], points[2], points[3]
v1 = b[0]-a[0], b[1]-a[1], b[2]-a[2]
v2 = c[0]-a[0], c[1]-a[1], c[2]-a[2]
n = cross(v1, v2)
if n[2] < 0:
return
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, green, coords)
You have to ensure that the winding order of all the faces in counter clockwise. See also Face Culling and Back-face culling
Change the vertices and indices as follows:
points = [(-1,-1,-1),( 1,-1,-1), (1, 1,-1),(-1, 1,-1),
(-1,-1, 1),( 1,-1, 1), (1, 1, 1),(-1, 1, 1)]
faces = [(0,1,2,3),(5,4,7,6),(4,0,3,7),(1,5,6,2),(4,5,1,0),(3,2,6,7)]
However, I recommend to implement a depth test. See Pygame rotating cubes around axis and Does PyGame do 3d?.
Complete example:
import pygame
import math
points = [(-1,-1,-1),( 1,-1,-1), (1, 1,-1),(-1, 1,-1),
(-1,-1, 1),( 1,-1, 1), (1, 1, 1),(-1, 1, 1)]
faces = [(0,1,2,3),(5,4,7,6),(4,0,3,7),(1,5,6,2),(4,5,1,0),(3,2,6,7)]
def flattenPoint(point):
(x, y, z) = (point[0], point[1], point[2])
xnew = x#x axis rotation
ynew = y * math.cos(rotatedanglex) - z * math.sin(rotatedanglex)
znew = y * math.sin(rotatedanglex) + z * math.cos(rotatedanglex)
xnew = znew * math.sin(rotatedangley) + xnew * math.cos(rotatedangley)
ynew = ynew #y axis rotation
znew = ynew * math.cos(rotatedangley) - xnew * math.sin(rotatedangley)
projectedY = int(height / 2 + ((ynew * distance) / (znew + distance)) * scale)
projectedX = int(width / 2 + ((xnew * distance) / (znew + distance)) * scale)
return (projectedX, projectedY, znew)
def cross(a, b):
return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
def createOutline(points):
a, b, c, d = points[0], points[1], points[2], points[3]
v1 = b[0]-a[0], b[1]-a[1], b[2]-a[2]
v2 = c[0]-a[0], c[1]-a[1], c[2]-a[2]
n = cross(v1, v2)
if n[2] < 0:
return
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, blue, coords, 3)
def createFace(points):
a, b, c, d = points[0], points[1], points[2], points[3]
v1 = b[0]-a[0], b[1]-a[1], b[2]-a[2]
v2 = c[0]-a[0], c[1]-a[1], c[2]-a[2]
n = cross(v1, v2)
if n[2] < 0:
return
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, green, coords)
def render(points, faces):
coords = []
for point in points:
coords.append(flattenPoint(point))
screen.fill(screencolour)
for face in faces:
createFace((coords[face[0]], coords[face[1]], coords[face[2]], coords[face[3]]))
for face in faces:#must draw outline after all the faces have been drawn
createOutline((coords[face[0]], coords[face[1]], coords[face[2]],coords[face[3]]))
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
rotatedanglex = 0.0
rotatedangley = 0.0
width, height = screen.get_size()
distance = 200.0
scale = 75.0
green = (0, 255, 0)
blue = (0, 0, 255)
screencolour = (0, 0, 0)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
screen.fill(0)
render(points, faces)
pygame.display.flip()
rotatedanglex += 0.01
rotatedangley += 0.02
pygame.quit()
exit()
The code creates 3D coordinates for the cube and then displays them on the 2D screen but you can still see the back faces of the cube. What is the best method for hiding these faces?
points = [(-1,-1,-1),(-1,-1,1),(-1,1,1),(-1,1,-1),
(1,-1,-1),(1,-1,1),(1,1,1),(1,1,-1), ]#coords for points
faces = [(3,0,4,7),(1,5,6,2),(2,1,0,3),(5,4,7,6),(3,2,6,7),(0,1,5,4)]
These are the coordinates of the points and which points should be joined to which
def flattenPoint(point):
(x, y, z) = (point[0], point[1], point[2])
xnew = x#x axis rotation
ynew = y * math.cos(rotatedanglex) - z * math.sin(rotatedanglex)
znew = y * math.sin(rotatedanglex) + z * math.cos(rotatedanglex)
xnew = znew * math.sin(rotatedangley) + xnew * math.cos(rotatedangley)
ynew = ynew #y axis rotation
znew = ynew * math.cos(rotatedangley) - xnew * math.sin(rotatedangley)
projectedY = int(height / 2 + ((ynew * distance) / (znew + distance)) * scale)
projectedX = int(width / 2 + ((xnew * distance) / (znew + distance)) * scale)
return (projectedX, projectedY, znew)
def createOutline(points):
a, b, c, d = points[0], points[1], points[2], points[3]
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, blue, coords, 1)
”’
The FlattenPoint function rotates the 3D points and then turns them into 2D coordinates that are displayed.
”’
def createFace(points):
a, b, c, d = points[0], points[1], points[2], points[3]
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, green, coords)
createFace joins up the 2D coordinates.
def render(points, faces):
coords = []
for point in points:
coords.append(flattenPoint(point))
screen.fill(screencolour)
for face in faces:
createFace((coords[face[0]], coords[face[1]], coords[face[2]], coords[face[3]]))
for face in faces:#must draw outline after all the faces have been drawn
createOutline((coords[face[0]], coords[face[1]], coords[face[2]],coords[face[3]]))
”’
Compute the normal vector to of a face and cull the faces where the normal vector points away from the view. The normal vector can be computed with the Cross product:
def cross(a, b):
return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
Use the cross product and cull the faces:
def createFace(points):
a, b, c, d = points[0], points[1], points[2], points[3]
v1 = b[0]-a[0], b[1]-a[1], b[2]-a[2]
v2 = c[0]-a[0], c[1]-a[1], c[2]-a[2]
n = cross(v1, v2)
if n[2] < 0:
return
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, green, coords)
You have to ensure that the winding order of all the faces in counter clockwise. See also Face Culling and Back-face culling
Change the vertices and indices as follows:
points = [(-1,-1,-1),( 1,-1,-1), (1, 1,-1),(-1, 1,-1),
(-1,-1, 1),( 1,-1, 1), (1, 1, 1),(-1, 1, 1)]
faces = [(0,1,2,3),(5,4,7,6),(4,0,3,7),(1,5,6,2),(4,5,1,0),(3,2,6,7)]
However, I recommend to implement a depth test. See Pygame rotating cubes around axis and Does PyGame do 3d?.
Complete example:
import pygame
import math
points = [(-1,-1,-1),( 1,-1,-1), (1, 1,-1),(-1, 1,-1),
(-1,-1, 1),( 1,-1, 1), (1, 1, 1),(-1, 1, 1)]
faces = [(0,1,2,3),(5,4,7,6),(4,0,3,7),(1,5,6,2),(4,5,1,0),(3,2,6,7)]
def flattenPoint(point):
(x, y, z) = (point[0], point[1], point[2])
xnew = x#x axis rotation
ynew = y * math.cos(rotatedanglex) - z * math.sin(rotatedanglex)
znew = y * math.sin(rotatedanglex) + z * math.cos(rotatedanglex)
xnew = znew * math.sin(rotatedangley) + xnew * math.cos(rotatedangley)
ynew = ynew #y axis rotation
znew = ynew * math.cos(rotatedangley) - xnew * math.sin(rotatedangley)
projectedY = int(height / 2 + ((ynew * distance) / (znew + distance)) * scale)
projectedX = int(width / 2 + ((xnew * distance) / (znew + distance)) * scale)
return (projectedX, projectedY, znew)
def cross(a, b):
return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
def createOutline(points):
a, b, c, d = points[0], points[1], points[2], points[3]
v1 = b[0]-a[0], b[1]-a[1], b[2]-a[2]
v2 = c[0]-a[0], c[1]-a[1], c[2]-a[2]
n = cross(v1, v2)
if n[2] < 0:
return
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, blue, coords, 3)
def createFace(points):
a, b, c, d = points[0], points[1], points[2], points[3]
v1 = b[0]-a[0], b[1]-a[1], b[2]-a[2]
v2 = c[0]-a[0], c[1]-a[1], c[2]-a[2]
n = cross(v1, v2)
if n[2] < 0:
return
coords = ((b[0], b[1]), (a[0], a[1]), (d[0], d[1]),(c[0], c[1]))
pygame.draw.polygon(screen, green, coords)
def render(points, faces):
coords = []
for point in points:
coords.append(flattenPoint(point))
screen.fill(screencolour)
for face in faces:
createFace((coords[face[0]], coords[face[1]], coords[face[2]], coords[face[3]]))
for face in faces:#must draw outline after all the faces have been drawn
createOutline((coords[face[0]], coords[face[1]], coords[face[2]],coords[face[3]]))
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
rotatedanglex = 0.0
rotatedangley = 0.0
width, height = screen.get_size()
distance = 200.0
scale = 75.0
green = (0, 255, 0)
blue = (0, 0, 255)
screencolour = (0, 0, 0)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
screen.fill(0)
render(points, faces)
pygame.display.flip()
rotatedanglex += 0.01
rotatedangley += 0.02
pygame.quit()
exit()