#---------------------------------------------------------------------- # Introduction to Orthographic 3D Projections # Updated with Polygon Fill April 10, 2017 # # Copyright (C) April 7, 2017 -- Dr. William T. Verts #---------------------------------------------------------------------- Origin2D = [0,0] # Location of [0,0,0] on canvas Scale2D = 1.0 # Pixels per 3D Unit Cosine30 = cos(math.radians(30.0)) # Projection Angle Sine30 = sin(math.radians(30.0)) # Projection Angle def SetOrigin2D (P2D): # Location on the canvas of the [0,0,0] point global Origin2D Origin2D = P2D return def SetScale2D (N): # Scale (size) multiplier global Scale2D Scale2D = N return def Project3D (P3D): # P3D is [X,Y,Z] in space, function returns [X,Y] on the canvas global Scale2D,Origin2D X = Origin2D[0] + (P3D[0] + P3D[2] * Cosine30) * Scale2D Y = Origin2D[1] - (P3D[1] + P3D[2] * Sine30) * Scale2D return [X,Y] #---------------------------------------------------------------------- # Higher level geometric functions #---------------------------------------------------------------------- def INT(N): return int(round(N)) def addLine2D (Canvas, P1, P2, NewColor=black): # P1 and P2 are both [X,Y] lists addLine(Canvas, INT(P1[0]), INT(P1[1]), INT(P2[0]), INT(P2[1]), NewColor) return def addLine3D (Canvas, P1, P2, NewColor=black): # P1 and P2 are both [X,Y,Z] lists addLine2D (Canvas, Project3D(P1), Project3D(P2), NewColor) return #---------------------------------------------------------------------- # Fill a 2D polygon defined by a list of 2D points. For # example, to fill a triangle: # addFilledPolygon2D (Canvas, [[3,4],[12,5],[6,20]], red) # or: # P0 = [3,4] # P1 = [12,5] # P2 = [6,20] # addFilledPolygon2D (Canvas, [P0,P1,P2], red) #---------------------------------------------------------------------- import copy def addFilledPolygon2D (Canvas, ParameterList2D, NewColor): Epsilon = 0.000001 # Error adjustment List2D = copy.deepcopy(ParameterList2D) # Make local copy of points so # changes aren't passed back # Must "import copy" at top of program. #---------------------------------- # Find the bounding box around the # polygon. #---------------------------------- MinX = List2D[0][0] MinY = List2D[0][1] MaxX = MinX MaxY = MinY for P in List2D: X = float(P[0]) Y = float(P[1]) MinX = min(MinX, X) MinY = min(MinY, Y) MaxX = max(MaxX, X) MaxY = max(MaxY, Y) MidY = (MaxY + MinY) / 2.0 #---------------------------------- # Adjust all points with integer # Y-coordinate values above the mid # point up a tiny bit, and all # points below down a tiny bit, so # that scan lines never exactly hit # a vertex - guarantees an even # number of line crossings. #---------------------------------- for I in range(len(List2D)): Y = float(List2D[I][1]) if (Y == round(Y)): if (Y < MidY): Y = Y - Epsilon else: Y = Y + Epsilon MaxY = max(MaxY, Y) MinY = min(MinY, Y) List2D[I][1] = Y MinScan = int(MinY) + 1 MaxScan = int(MaxY) #---------------------------------- # Scan from top raster to bottom # raster, for each raster line find # those polygon segments that cross # the raster line, record all the # intersections in XTable, sort the # table, then draw lines on screen # at that raster line for every # pair of intersections in XTable. #---------------------------------- for Yscan in range(MinScan, MaxScan): XTable = [] P = List2D[len(List2D)-1] X1 = round(P[0]) Y1 = round(P[1]) for P in List2D: X2 = round(P[0]) Y2 = round(P[1]) if ((Yscan < Y1) <> (Yscan < Y2)): XValue = ((Yscan - Y1) / (Y2 - Y1)) * (X2 - X1) + X1 XTable = XTable + [int(round(XValue))] X1 = X2 Y1 = Y2 # Sort XTable - not necessary for convex # polygons (only required for polygons # with one or more concave sections). XTable.sort() # Step through list of intersections, # two at a time, drawing lines between # each pair. for I in range(0,len(XTable),2): addLine(Canvas, XTable[I], Yscan, XTable[I+1], Yscan, NewColor) return #---------------------------------------------------------------------- # Draw a polygon outline. ParameterList2D is a list of 2D points. #---------------------------------------------------------------------- def addPolygon2D (Canvas, ParameterList2D, NewColor=black): Last = ParameterList2D[-1] for P in ParameterList2D: addLine2D(Canvas, Last, P, NewColor) Last = P return #---------------------------------------------------------------------- # Draw a polygon outline. ParameterList3D is a list of 3D points. #---------------------------------------------------------------------- def addPolygon3D (Canvas, ParameterList3D, NewColor=black): ParameterList2D = [Project3D(P) for P in ParameterList3D] addPolygon2D(Canvas, ParameterList2D, NewColor) return #---------------------------------------------------------------------- # Draw a filled 3D polygon. ParameterList3D is a list of 3D points. #---------------------------------------------------------------------- def addFilledPolygon3D (Canvas, ParameterList3D, NewColor=black): ParameterList2D = [Project3D(P) for P in ParameterList3D] addFilledPolygon2D(Canvas, ParameterList2D, NewColor) return #---------------------------------------------------------------------- # addCube changed over from wireframe to solidly filled polygons # Offset3D indicates the coordinates of the front bottom left corner # of the cube, added to all points. #---------------------------------------------------------------------- def addCube (Canvas, N, Offset3D=[0,0,0]): # Offset3D is origin of the cube def PlotOffset (Polygon, NewColor=black): L = [] for P in Polygon: NewP = [P[0]+Offset3D[0], P[1]+Offset3D[1], P[2]+Offset3D[2]] L = L + [NewP] addFilledPolygon3D (Canvas, L, NewColor) # Fill the polygon addPolygon3D(Canvas, L, black) # Draw a black outline return Back = [[N,0,N], [N,N,N], [0,N,N], [0,0,N]] # Back face of cube (not needed) Front= [[N,0,0], [N,N,0], [0,N,0], [0,0,0]] # Front face of cube Top = [[0,N,0], [0,N,N], [N,N,N], [N,N,0]] # Top face of cube Right= [[N,0,0], [N,N,0], [N,N,N], [N,0,N]] # Right face of cube # No need to define or paint left side or bottom PlotOffset(Back, red) PlotOffset(Front, green) PlotOffset(Top, blue) PlotOffset(Right, cyan) return #---------------------------------------------------------------------- # Simple test program #---------------------------------------------------------------------- def Main(): Canvas = makeEmptyPicture(800,600) SetOrigin2D ([100,500]) SetScale2D (4) show(Canvas) addLine3D(Canvas, [0,0,0], [100,0,0], red) # X Axis addLine3D(Canvas, [0,0,0], [0,100,0], green) # Y Axis addLine3D(Canvas, [0,0,0], [0,0,100], blue) # Z Axis addCube(Canvas, 30, [0,0,50]) # Left back cube addCube(Canvas, 30, [50,0,50]) # Right back cube addCube(Canvas, 30) # Left front cube addCube(Canvas, 20, [50,0,0]) # Right front cube repaint(Canvas) return