Last active
February 18, 2018 01:56
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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