Skip to content

Instantly share code, notes, and snippets.

@deadandhallowed
Last active February 18, 2018 01:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save deadandhallowed/620d580e79dd6559ca0ef14b8e43fb3e to your computer and use it in GitHub Desktop.
Save deadandhallowed/620d580e79dd6559ca0ef14b8e43fb3e to your computer and use it in GitHub Desktop.
Polygon Filling using tkinter, limited tp create_line. Other extensions (numpy, matrix math, etc.) is forbidden.
import math
from tkinter import *
#------------------------ CLASSES ------------------------#
class Pyramid:
# Pyramid class for pyramid objects.
def __init__(self,apex,base1,base2,base3,base4):
# Definition of the five underlying points.
self.apex = apex
self.base1 = base1
self.base2 = base2
self.base3 = base3
self.base4 = base4
# Polygon points defined in counter clockwise order when viewed from the outside.
self.frontpoly = [self.apex,self.base4,self.base3]
self.rightpoly = [self.apex,self.base3,self.base2]
self.backpoly = [self.apex,self.base2,self.base1]
self.leftpoly = [self.apex,self.base1,self.base4]
self.bottompoly1 = [self.base4,self.base2,self.base3]
self.bottompoly2 = [self.base4,self.base1,self.base2]
# Definition of the object shape.
self.shape = [self.bottompoly1, self.bottompoly2, self.frontpoly, self.rightpoly, self.backpoly, self.leftpoly]
# Definition of the Pyramid's underlying point cloud. No structure, just the points.
self.PointCloud = [self.apex, self.base1, self.base2, self.base3, self.base4]
self.OriginalPointCloud = [] # For resetting.
# Temporary object for use with in-place functions.
self.tempObject = [[0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0]]
# Maintain toggled values throughout functions.
self.backFaceCulling = False
self.polygonFilling = 0
#------------------------ OBJECTS and VARIABLES ------------------------#
CanvasWidth = 400
CanvasHeight = 400
d = 500
pyr1 = Pyramid (
[-50,0,50],
[-100,-100,0],
[0,-100,0],
[0,-100,100],
[-100,-100,100]
)
# I'm sure there's a better way to do this in the class itself.
pyr1.OriginalPointCloud = [
[-50,0,50],
[-100,-100,0],
[0,-100,0],
[0,-100,100],
[-100,-100,100]
]
#------------------------ FUNCTIONS ------------------------#
# The function will reset the object to original point cloud values.
def resetSelected(object):
for i in range(0,len(object.PointCloud)):
for j in range(0,len(object.PointCloud[i])):
object.PointCloud[i][j] = object.OriginalPointCloud[i][j]
# The function will draw an object by repeatedly callying drawPoly on each polygon in the object
def drawObject(object):
# Only calls for polygons that would show
for polygon in range(0,len(object.shape)):
if (object.polygonFilling == 0) and (not object.backFaceCulling):
drawPoly(object.shape[polygon],object.polygonFilling)
elif ((object.polygonFilling != 0) or (object.backFaceCulling)) and (not isItCulled(object.shape[polygon])):
drawPoly(object.shape[polygon],object.polygonFilling)
# This function will draw a polygon by repeatedly callying create_line on each pair of points in the object.
def drawPoly(poly,polygonFilling):
if (polygonFilling != 0): # If filling not on
polyFill(poly)
if (polygonFilling != 2):
# Only if not lineless polygon filling
for line in range(0,len(poly)):
if line != len(poly)-1:
start = convertToDisplayCoordinates(project(poly[line])) # Convert projected startpoints.
end = convertToDisplayCoordinates(project(poly[line+1])) # Convert projected endpoints.
else: # Between last and first.
start = convertToDisplayCoordinates(project(poly[line])) # Convert projected startpoints.
end = convertToDisplayCoordinates(project(poly[0])) # Convert projected endpoints.
w.create_line(start[0],start[1],end[0],end[1], fill='red')
# This function converts from 3D to 2D (+ depth) using the perspective projection technique.
def project(point):
ps = []
ps.append(d*point[0]/(d+point[2]))
ps.append(d*point[1]/(d+point[2]))
ps.append(point[2]/(d+point[2]))
return ps
# This function converts a 2D point to display coordinates in the tk system.
def convertToDisplayCoordinates(point):
displayXY = []
# Offset from center of canvas.
displayXY.append((CanvasWidth / 2) + point[0])
displayXY.append((CanvasHeight / 2) - point[1])
return displayXY
# Thsi function determines if a polygon, or face, is a backface to be culled.
def isItCulled(polygon):
surfaceNormal = [
(((polygon[1][1]-polygon[0][1]) * (polygon[2][2]-polygon[0][2])) - ((polygon[2][1]-polygon[0][1]) * (polygon[1][2]-polygon[0][2]))),
-(((polygon[1][0]-polygon[0][0]) * (polygon[2][2]-polygon[0][2])) - ((polygon[2][0]-polygon[0][0]) * (polygon[1][2]-polygon[0][2]))),
(((polygon[1][0]-polygon[0][0]) * (polygon[2][1]-polygon[0][1])) - ((polygon[2][0]-polygon[0][0]) * (polygon[1][1]-polygon[0][1])))
]
scalarD = (surfaceNormal[0] * polygon[0][0]) + (surfaceNormal[1] * polygon[0][1]) + (surfaceNormal[2] * polygon[0][2])
if (surfaceNormal[2] * (-d) - scalarD <= 0):
return True # Culled, not visible.
return False
def polyFill(polygon):
fillFace = [ # convert polygon's x,y,z points to drawable x,y points
convertToDisplayCoordinates(project(polygon[0])),
convertToDisplayCoordinates(project(polygon[1])),
convertToDisplayCoordinates(project(polygon[2]))
]
for j in range(0,len(fillFace)):
#fillFace[j][0] = math.trunc(fillFace[j][0]) + 0.5
fillFace[j][1] = math.trunc(fillFace[j][1]) + 0.5
fillFace = sorted(fillFace, key=lambda x: x[1]) # min y to max y
# determine "edges" between 2 points, represented with the following structure:
# [maximum y, minimum y, dx, initial x (max y's x + dx)]
if (fillFace[0][0] > fillFace[1][0]):
denom1 = (fillFace[0][1] - fillFace[1][1])
dx1 = (fillFace[0][0] - fillFace[1][0])/denom1 if (denom1 != 0) else 0
else:
denom1 = (fillFace[1][1] - fillFace[0][1])
dx1 = (fillFace[1][0] - fillFace[0][0])/denom1 if (denom1 != 0) else 0
x_init1 = fillFace[0][0] + (dx1/2)
edge1 = [fillFace[0][1], fillFace[1][1], dx1, x_init1]
if (fillFace[0][0] > fillFace[2][0]):
denom2 = (fillFace[0][1] - fillFace[2][1])
dx2 = (fillFace[0][0] - fillFace[2][0])/denom2 if (denom2 != 0) else 0
else:
denom2 = (fillFace[2][1] - fillFace[0][1])
dx2 = (fillFace[2][0] - fillFace[0][0])/denom2 if (denom2 != 0) else 0
x_init2 = fillFace[0][0] + (dx2/2)
edge2 = [fillFace[0][1], fillFace[2][1], dx2, x_init2]
if (fillFace[1][0] > fillFace[2][0]):
denom3 = (fillFace[1][1] - fillFace[2][1])
dx3 = (fillFace[1][0] - fillFace[2][0])/denom3 if (denom3 != 0) else 0
else:
denom3 = (fillFace[2][1] - fillFace[1][1])
dx3 = (fillFace[2][0] - fillFace[1][0])/denom3 if (denom3 != 0) else 0
x_init3 = fillFace[1][0] + (dx3/2)
edge3 = [fillFace[1][1], fillFace[2][1], dx3, x_init3]
startEdge = edge1 # temporary
endEdge = edge1 # temporary
# edges 1 and 2 touch the highest y
if (edge1[3] > edge2[3]):
# if edge1 is more right, edge2 starts
startEdge = edge2
elif (edge1[3] < edge2[3]):
# if edge1 is more left, edge2 ends
endEdge = edge2
else:
# if edge1[3] == edge2[3] (horizontal), skip it
startEdge = edge2
endEdge = edge3
startX = startEdge[3] # start
endX = endEdge[3] # end
y = startEdge[0]
# first portion
while (y < startEdge[1] and y < endEdge[1]):
# stop drawing when y is less than both lower ys
x = startX - 1 # incremented x
while (x < endX):
# stop drawing when reach endX
w.create_rectangle(x,y,x+1,y+1,width=0,fill='blue')
x += 1
startX += (startEdge[2])
endX += (endEdge[2])
y += 1
if (y < edge3[1]):
if (y >= startEdge[1]):
startEdge = edge3
elif (y >= endEdge[1]):
endEdge = edge3
startX = startEdge[3] # start
endX = endEdge[3] # end
y = startEdge[0]
# second portion
while (y < startEdge[1] and y < endEdge[1]):
# stop drawing when y is less than both lower ys
x = startX - 1 # incremented x
while (x < endX):
# stop drawing when reach endX
w.create_rectangle(x,y,x+1,y+1,width=0,fill='blue')
x += 1
startX += (startEdge[2])
endX += (endEdge[2])
y += 1
#------------------------ GUI FUNCTIONS ------------------------#
# Define functions called by GUI buttons, pre-processing them to
# be sent to the appropriate base function with specific settings.
def reset():
w.delete(ALL)
resetSelected(pyr1)
drawObject(pyr1)
def backFaceCulling():
w.delete(ALL)
pyr1.backFaceCulling = not pyr1.backFaceCulling
drawObject(pyr1)
def polygonFilling():
w.delete(ALL)
pyr1.polygonFilling += 1
if (pyr1.polygonFilling == 3):
pyr1.polygonFilling = 0
drawObject(pyr1)
#------------------------ GUI ------------------------#
# Defines and designs GUI with buttons to call functions.
root = Tk()
outerframe = Frame(root)
outerframe.pack()
w = Canvas(outerframe, width=CanvasWidth, height=CanvasHeight)
drawObject(pyr1)
w.pack()
controlpanel = Frame(outerframe)
controlpanel.pack()
resetcontrols = Frame(controlpanel, height=100, borderwidth=2, relief=RIDGE)
resetcontrols.pack(side=LEFT)
resetcontrolslabel = Label(resetcontrols, text="Reset")
resetcontrolslabel.pack()
resetButton = Button(resetcontrols, text="Reset", fg="green", command=reset)
resetButton.pack(side=LEFT)
# "back face culling" controls, label, and button.
backfacecullingcontrols = Frame(controlpanel, borderwidth=2, relief=RIDGE)
backfacecullingcontrols.pack(side=LEFT)
backfacecullingcontrolslabel = Label(backfacecullingcontrols, text="Back Face Culling")
backfacecullingcontrolslabel.pack()
backfacecullingButton = Button(backfacecullingcontrols, text="Toggle", command=backFaceCulling)
backfacecullingButton.pack()
# "polygon filling" controls, label, and button.
polygonfillingcontrols = Frame(controlpanel, borderwidth=2, relief=RIDGE)
polygonfillingcontrols.pack(side=LEFT)
polygonfillingcontrolslabel = Label(polygonfillingcontrols, text="Polygon Filling")
polygonfillingcontrolslabel.pack()
polygonfillingButton = Button(polygonfillingcontrols, text="Toggle", command=polygonFilling)
polygonfillingButton.pack()
root.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment