#---------------------------------------------------------------------- # Linear blending - second version # # Copyright (C) November 15, 2017 -- Dr. William T. Verts #---------------------------------------------------------------------- import time #********************************************************************** # Helper Functions #********************************************************************** #---------------------------------------------------------------------- # Return closest integer to N #---------------------------------------------------------------------- def INT (N): return int(round(N)) #---------------------------------------------------------------------- # Plot a centered circle at with radius R. Coordinates may be # either ints or floats; if floats the closest integer is used. #---------------------------------------------------------------------- def addCircleFilled (Canvas, X, Y, R, NewColor=black): addOvalFilled(Canvas, INT(X-R), INT(Y-R), INT(2*R), INT(2*R), NewColor) return #---------------------------------------------------------------------- # Plot a centered circle at 2D point P with radius R # Point P is a list [X,Y], where X is at P[0] and Y is at P[1] #---------------------------------------------------------------------- def addCircleFilledPoint (Canvas, P, R, NewColor=black): addCircleFilled(Canvas, P[0], P[1], R, NewColor) return #---------------------------------------------------------------------- # Compute the Euclidean distance between 2D points P0 and P1. # P0 and P1 are both [X,Y] lists. #---------------------------------------------------------------------- def DistancePoints2D (P0,P1): DeltaX = P1[0] - P0[0] DeltaY = P1[1] - P0[1] return math.sqrt(DeltaX*DeltaX + DeltaY*DeltaY) #---------------------------------------------------------------------- # Compute the Euclidean distance between points P0 and P1. # P0 and P1 are both lists, but may be of any number of dimensions. #---------------------------------------------------------------------- def DistancePoints (P0,P1): Total = 0.0 for I in range(min(len(P0),len(P1))): Delta = P1[I] - P0[I] Total = Total + Delta * Delta return math.sqrt(Total) #********************************************************************** # Blending Functions #********************************************************************** #---------------------------------------------------------------------- # Blend linearly between numbers P0 and P1 (which may be either ints # or floats). A float is always returned. #---------------------------------------------------------------------- def Blend (P0,P1,T): return (P1 - P0) * float(T) + P0 #---------------------------------------------------------------------- # Blend linearly between numbers P0 and P1 (which may be either ints # or floats). The closest integer is always returned. #---------------------------------------------------------------------- def BlendInt (P0,P1,T): return INT(Blend(P0,P1,T)) #---------------------------------------------------------------------- # Blend linearly between points P0 and P1 (which may be of any number # of dimensions). #---------------------------------------------------------------------- def BlendPoints (P0,P1,T): return [Blend(P0[I],P1[I],T) for I in range(min(len(P0),len(P1)))] #---------------------------------------------------------------------- # Blend parabolically between numbers P0, P1, P2, so that we return # P0 when T=0, P1 when T=0.5, P2 when T=1. #---------------------------------------------------------------------- def BlendParabola (P0,P1,P2,T): A = 2*P0 - 4*P1 + 2*P2 B = -3*P0 + 4*P1 - P2 C = P0 T = float(T) return A*T*T + B*T + C def BlendParabolaInt (P0,P1,P2,T): return INT(BlendParabola(P0,P1,P2,T)) def BlendParabolaPoints (P0,P1,P2,T): # P0, P1, and P2 are points in any dimension return [BlendParabola(P0[I],P1[I],P2[I],T) for I in range(min(len(P0),len(P1),len(P2)))] def BlendParabolaColor (C0,C1,C2,T): R = BlendParabolaInt(C0.getRed(), C1.getRed(), C2.getRed(), T) G = BlendParabolaInt(C0.getGreen(), C1.getGreen(), C2.getGreen(), T) B = BlendParabolaInt(C0.getBlue(), C1.getBlue(), C2.getBlue(), T) return makeColor(R,G,B) #---------------------------------------------------------------------- # Blend linearly between two JES colors. #---------------------------------------------------------------------- def BlendColor (C0,C1,T): R = BlendInt(C0.getRed(), C1.getRed(), T) G = BlendInt(C0.getGreen(), C1.getGreen(), T) B = BlendInt(C0.getBlue(), C1.getBlue(), T) return makeColor(R,G,B) #---------------------------------------------------------------------- # Orthographic Projection System # # Origin2D and Scale2D are global so that values set by SetOrigin2D # and SetScale2D can be used by Project3D (which is the heart of the # code here). The projection angle is fixed at 30 degrees (which # "looks right"), and precomputed so we don't have to compute the # expensive sine and cosine of the same angle for every point being # transformed. #---------------------------------------------------------------------- Origin2D = [0,0] # Position on the Canvas of 3D point [0,0,0] Scale2D = 1.0 # Pixels per 3D unit Cosine30 = math.sqrt(3)/2.0 # Same as: math.cos(Radians(30.0)) Sine30 = 0.5 # Same as: math.sin(Radians(30.0)) def Project3D (P3D): # P3D is [X,Y,Z], returns a 2D point [X,Y] global Origin2D, Scale2D XOffset = P3D[0] + P3D[2] * Cosine30 YOffset = P3D[1] + P3D[2] * Sine30 X = Origin2D[0] + XOffset * Scale2D Y = Origin2D[1] - YOffset * Scale2D return [X,Y] def SetOrigin2D (P2D): # P2D is [X,Y] indicating location on canvas of [0,0,0] global Origin2D Origin2D = P2D return def SetScale2D (N): # N is number of pixels per 3D Unit global Scale2D Scale2D = N return #---------------------------------------------------------------------- # Start building tools to use the 3D system. # addLine2D draws a line between two 2D points, with different colors. # addLine3D draws a line between two 3D points, using addLine2D. #---------------------------------------------------------------------- def addLine2D(Canvas, P0, P1, C0=black, C1=black, R=2): # P0 and P1 are both [X,Y] points D = DistancePoints(P0, P1) Steps = D / R for I in range(Steps+1): T = float(I) / Steps P = BlendPoints(P0,P1,T) C = BlendColor(C0,C1,T) addCircleFilledPoint(Canvas, P, R, C) return def addLine3D(Canvas, P0, P1, C0=black, C1=black, R=2): # P0 and P1 are both [X,Y,Z] points addLine2D(Canvas, Project3D(P0), Project3D(P1), C0, C1, R) 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 filled polygon with an outline. # ParameterList2D is a list of 2D points. #---------------------------------------------------------------------- def addFilledOutlinedPolygon2D (Canvas, ParameterList2D, NewColor=black, OutlineColor=black): addFilledPolygon2D(Canvas, ParameterList2D, NewColor) addPolygon2D (Canvas, ParameterList2D, OutlineColor) 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 #---------------------------------------------------------------------- # Draw a filled 3D polygon with an Outline. # ParameterList3D is a list of 3D points. #---------------------------------------------------------------------- def addFilledOutlinedPolygon3D (Canvas, ParameterList3D, NewColor=black, OutlineColor=black): ParameterList2D = [Project3D(P) for P in ParameterList3D] addFilledOutlinedPolygon2D(Canvas, ParameterList2D, NewColor, OutlineColor) return #---------------------------------------------------------------------- # Plot a house as a bunch of 3D line segments (Wire-Frame) #---------------------------------------------------------------------- def addHouse3DWireFrame (Canvas, Origin=[0,0,0], Scale=1.0): def Fix(P): X = P[0] * Scale + Origin[0] Y = P[1] * Scale + Origin[1] Z = P[2] * Scale + Origin[2] return [X,Y,Z] LLF = Fix([ 0, 0, 0]) # Lower Left Front LRF = Fix([20, 0, 0]) # Lower Right Front ULF = Fix([ 0,20, 0]) # Upper Left Front URF = Fix([20,20, 0]) # Upper Right Front RF = Fix([10,30, 0]) # Roofline Front LLB = Fix([ 0, 0,30]) # Lower Left Back LRB = Fix([20, 0,30]) # Lower Right Back ULB = Fix([ 0,20,30]) # Upper Left Back URB = Fix([20,20,30]) # Upper Right Back RB = Fix([10,30,30]) # Roofline Back #addLine3D(Canvas, LLB, LRB, gray, gray) #addLine3D(Canvas, LLB, ULB, gray, gray) addLine3D(Canvas, LRB, URB) #addLine3D(Canvas, ULB, RB, gray, gray) addLine3D(Canvas, URB, RB) #addLine3D(Canvas, LLF, LLB, gray, gray) #addLine3D(Canvas, ULF, ULB, gray, gray) addLine3D(Canvas, LRF, LRB) addLine3D(Canvas, URF, URB) addLine3D(Canvas, RF, RB) addLine3D(Canvas, LLF, LRF) addLine3D(Canvas, LLF, ULF) addLine3D(Canvas, LRF, URF) addLine3D(Canvas, ULF, RF) addLine3D(Canvas, URF, RF) return #---------------------------------------------------------------------- # Plot a 3D house as a series of filled 3D polygons (Solid Model) #---------------------------------------------------------------------- def addHouse3D (Canvas, Origin=[0,0,0], Scale=1.0): def Fix(P): X = P[0] * Scale + Origin[0] Y = P[1] * Scale + Origin[1] Z = P[2] * Scale + Origin[2] return [X,Y,Z] LLF = Fix([ 0, 0, 0]) # Lower Left Front LRF = Fix([20, 0, 0]) # Lower Right Front ULF = Fix([ 0,20, 0]) # Upper Left Front URF = Fix([20,20, 0]) # Upper Right Front RF = Fix([10,30, 0]) # Roofline Front LLB = Fix([ 0, 0,30]) # Lower Left Back LRB = Fix([20, 0,30]) # Lower Right Back ULB = Fix([ 0,20,30]) # Upper Left Back URB = Fix([20,20,30]) # Upper Right Back RB = Fix([10,30,30]) # Roofline Back FrontWall = [RF,ULF,LLF,LRF,URF] RightWall = [URF,LRF,LRB,URB] RightRoof = [RF,URF,URB,RB] LeftWall = [LLF,ULF,ULB,LLB] BackWall = [RB,ULB,LLB,LRB,URB] LeftRoof = [RF,ULF,ULB,RB] addFilledOutlinedPolygon3D(Canvas, BackWall, red) repaint(Canvas) # Remove later time.sleep(0.25) # Remove later addFilledOutlinedPolygon3D(Canvas, LeftWall, red) repaint(Canvas) # Remove later time.sleep(0.25) # Remove later addFilledOutlinedPolygon3D(Canvas, LeftRoof, gray) repaint(Canvas) # Remove later time.sleep(0.25) # Remove later addFilledOutlinedPolygon3D(Canvas, FrontWall, red) repaint(Canvas) # Remove later time.sleep(0.25) # Remove later addFilledOutlinedPolygon3D(Canvas, RightWall, red) repaint(Canvas) # Remove later time.sleep(0.25) # Remove later addFilledOutlinedPolygon3D(Canvas, RightRoof, gray) repaint(Canvas) # Remove later time.sleep(0.25) # Remove later return #********************************************************************** # Test program. Plot some 3D Houses. #********************************************************************** def Main(): Canvas = makeEmptyPicture(1200,700) SetOrigin2D([getWidth(Canvas)/2, getHeight(Canvas)/2]) SetScale2D(5.0) show(Canvas) MyStyle = makeStyle(sansSerif, bold, 24) #------------------------------------------------------------------ # Show neighborhood of houses as 3D wireframe #------------------------------------------------------------------ addTextWithStyle(Canvas,10,30,"3D WIREFRAME",MyStyle) addLine3D(Canvas, [-10,0,0], [+10,0,0], red, green, 3) # X axis addLine3D(Canvas, [0,-10,0], [0,+10,0], green, blue, 3) # Y axis addLine3D(Canvas, [0,0,-10], [0,0,+10], blue, red, 3) # Z axis repaint(Canvas) time.sleep(1.0) for Z in range(40,-41,-40): # Paint back-to-front for X in range(-30,31,30): # Paint left-to-right addHouse3DWireFrame(Canvas, [X,0,Z], 1.0) repaint(Canvas) time.sleep(1.0) #------------------------------------------------------------------ # Clear everything #------------------------------------------------------------------ repaint(Canvas) time.sleep (2.0) setAllPixelsToAColor(Canvas, white) #------------------------------------------------------------------ # Show neighborhood of houses as 3D filled polygons (on green lawn) #------------------------------------------------------------------ addTextWithStyle(Canvas,10,30,"3D POLYGON FILL",MyStyle) repaint(Canvas) time.sleep(1.0) addFilledPolygon3D(Canvas, [[-60,0,-60], [-60,0,80], [80,0,80], [80,0,-60]], green) repaint(Canvas) time.sleep(1.0) for Z in range(40,-41,-40): # Paint back-to-front for X in range(-30,31,30): # Paint left-to-right addHouse3D(Canvas, [X,0,Z], 1.0) repaint(Canvas) time.sleep(1.0) return