import turtle import math import random """ Simeon Tran UCSC ID: 1731877 Programming Assignment #1 for CSE 30 NOTE: for the interactive mode, use the left and right arrow keys to adjust the angles and the up and down keys to adjust the tilt. """ def transform(x, y, z, angle, tilt): #Animation control (around y-axis). If considered as a view of earth from space, it's moving over the equator. s, c = math.sin(angle), math.cos(angle) x, z = x * c - z * s, x * s + z * c #Camera tilt (around x-axis). If considered as a view of earth from space, the tilt angle is measured from the equator. s, c = math.sin(tilt), math.cos(tilt) y, z = y * c - z * s, y * s + z * c # Setting up View Parameters z += 5 #Fixed Distance from top FOV = 20000 #Fixed Field of view f = FOV / z sx, sy = x * f, y * f return sx, sy class Line(): # NOTE: points can either be 2D or 3D depending on the size of the point tuples def __init__(self, point1 ,point2): self.point1 = point1 self.point2 = point2 self.points = [self.point1, self.point2] # Generate information regarding the midpoint self.mid_x = (self.point1[0] + self.point2[0]) / 2 self.mid_y = (self.point1[1] + self.point2[1]) / 2 self.midpoint = (self.mid_x, self.mid_y) self.midpoint_offset = 0 # the offset value of its midpoint def __repr__(self): return f"Line with points {self.point1} and {self.point2}" def __eq__(self, other): # This method will see if self and other have EXACTLY the same points # even if the values for point1 and point2 are switched self_points = [self.point1, self.point2] other_points = [other.point1, other.point2] self_points.sort() other_points.sort() if self_points == other_points: return True else: return False def __gt__(self, other): points_list = [self.point1, self.point2, other.point1, other.point2] points_list.sort() if points_list[3] in self.points: return True else: return False def __lt__(self, other): points_list = [self.point1, self.point2, other.point1, other.point2] points_list.sort() if points_list[0] in self.points: return True else: return False class Triangle(): def __init__(self, line1, line2, line3): self.line1 = line1 self.line2 = line2 self.line3 = line3 self.sides = [self.line1, self.line2, self.line3] self.sides.sort() # sort the lines in self.sides in a definable order # Get the vertices of the triangle from line1, line2, line3 self.vertices = [] for side in self.sides: for point in side.points: if point in self.vertices: continue else: self.vertices.append(point) self.vertices.sort() # To sort the points in the list into a definable order # NOTE: I believe the order is (not confirmed): leftmost pt, "tip" of triangle, rightmost pt def __repr__(self): return f"Triangle with vertices at {self.vertices[0]}, {self.vertices[1]}, {self.vertices[2]}" all_triangles = [] # a list of ALL triangles generated by triangle_midpoint_displacement already_displaced = [] # list of line object whose midpoints have already been displaced def triangle_midpoint_displacement(big_triangle, recur_level, previous_triangles = [], smoothness = 6, offset_value = 1): """@param: big_triangle -> The starting triangle as a triangle object recur_level -> recursive level (how many times to divide the triangle to subtriangles) smoothness -> Higher the value, the more smooth the displacements will be ( after trial and error, smoothness 6 looks ideal) previous_triangles -> subtriangles that were found in previous recursive iteration """ iteration_level = 3 # opposite of recur_level; keeps track of how many recursive calls have happened # iteration_level is also factor that displacement is divided by iteration_level += recur_level global all_triangles # reference to list of ALL triangles generated by triangle_midpoint_displacement global already_displaced # list of lines object whose midpoints have already been displaced triangles_to_draw = [] # Holds all the subtriangles found in current recursive iteration """- If recur_level == 0, then return triangles_to_draw - If not, do the following: - Create a triangle made up of midpoints in big_triangle " - Append that midpoint triangle, along with the 3 new triangles that were created above, left, and right of the midpoint triangle that is inside big_triangle, into triangles_to_draw - Decrement recur_level - Iterate through each triangle in triangles_to_draw, and in each triangle, call triangle_midpoint_displacement on it """ if recur_level == 0: if triangles_to_draw == [] and offset_value == 1: # if recur_level is initially == 0 all_triangles.append(big_triangle) return else: # Find the midpoint of each side in big_triangle, and add a triangle with those midpoint vertices # into triangles_to_draw big_triangle_midpoints = [] for line in big_triangle.sides: # See if line is already displaced for displaced_line in already_displaced: if already_displaced == []: break elif line == displaced_line: # Use the already established midpoint offset big_triangle_midpoints.append((displaced_line.mid_x, displaced_line.mid_y, displaced_line.midpoint_offset)) break else: # Give each midpoint a z-component, which is a random number # between between 0 and 1 # divide by iteration_level to make the displacements smaller each time # We can increase smoothness by making iteration_level bigger by multiplying it by smoothness z = random.random() / (iteration_level * smoothness) line.midpoint_offset = z big_triangle_midpoints.append((line.mid_x, line.mid_y, z)) already_displaced.append(line) midpoint_triangle = Triangle(Line(big_triangle_midpoints[0], big_triangle_midpoints[1]), Line(big_triangle_midpoints[1], big_triangle_midpoints[2]), Line(big_triangle_midpoints[2], big_triangle_midpoints[0]) ) triangles_to_draw.append(midpoint_triangle) # Append the three subtriangles, above, to the left, and to the right of midpoint_triangle # We are using the observation (not confirmed) that the sorted order of the points for each triangle # is: leftmost pt, "tip" of triangle/intermediary pt, rightmost pt # to find the 3 remaining subtriangles midpoint_pts = midpoint_triangle.vertices # aliases to easily access list of pts for the triangles big_triangle_pts = big_triangle.vertices left_triangle = Triangle(Line(midpoint_pts[0], midpoint_pts[1]), Line(midpoint_pts[0], big_triangle_pts[0]), Line(midpoint_pts[1], big_triangle_pts[0])) right_triangle = Triangle(Line(midpoint_pts[1], big_triangle_pts[2]), Line(big_triangle_pts[2], midpoint_pts[2]), Line(midpoint_pts[2], midpoint_pts[1])) upper_triangle = Triangle(Line(midpoint_pts[0], midpoint_pts[2]), Line(midpoint_pts[2], big_triangle_pts[1]), Line(big_triangle_pts[1], midpoint_pts[0])) triangles_to_draw.append(left_triangle) triangles_to_draw.append(right_triangle) triangles_to_draw.append(upper_triangle) """This worked by some stroke of luck. Basically, at the LAST recursive iteration, append the triangles MADE ONLY IN THE LAST RECURSIVE ITERATION and REMOVE that were in the PREVIOUS recursive iterations""" if recur_level <= 1: for triangle in triangles_to_draw: all_triangles.append(triangle) for triangle in previous_triangles: if triangle in all_triangles: all_triangles.remove(triangle) # decrement recursion level recur_level -= 1 offset_value += 1 """Iterate through each triangle in triangles_to_draw, and in each triangle, call triangle_midpoint_displacement on it""" for triangle in triangles_to_draw: triangle_midpoint_displacement(triangle, recur_level, triangles_to_draw, smoothness, offset_value) def draw_layers(triangle_list, angle, tilt, color = None): global already_drawn # Keep tracks of all the triangles and lines that have already been drawn if color != None: turtle.fillcolor(color) # fill color # Draw each triangle in triangle_list individually, and do nothing else for triangle in triangle_list: if color != None: turtle.begin_fill() # fill each triangle with color after it is created # Draw each line in triangle for line in triangle.sides: # Convert the 3D points into 2D points point1_x, point1_y = transform(line.point1[0], line.point1[1], line.point1[2], angle, tilt) point2_x, point2_y = transform(line.point2[0], line.point2[1], line.point2[2], angle, tilt) # Now draw the line turtle.penup() turtle.goto(point1_x, point1_y) turtle.pendown() turtle.goto(point2_x, point2_y) if color != None: turtle.end_fill() """ Extra Functions to Control Interactive Movement I follow this tutorial: https://techwithtim.net/tutorials/python-module-walk-throughs/turtle-module/key-presses-events/ """ def up(): global tilt tilt += 0.1 turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt) # draw the landscape given a certain angle and tilt turtle.update() # update screen def down(): global tilt tilt -= 0.1 turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt) # draw the landscape given a certain angle and tilt turtle.update() # update screen def left(): global angle angle -= 0.1 turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt) # draw the landscape given a certain angle and tilt turtle.update() # update screen def right(): global angle angle += 0.1 turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt) # draw the landscape given a certain angle and tilt turtle.update() # update screen def default(): global angle # If no key is pressed turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt) # draw the landscape given a certain angle and tilt turtle.update() # update screen """ ROTATION FUNCTIONS: """ def overworld_underworld(): """Since we are rotating over a terrain, I thought it would be a cool idea to make it look like if you were viewing the underground and above ground of a terrain in an objective point of view """ global tilt global angle color = "#5FDA68" # Start at over-world colors """Through trial and error, I found the ranges in which the tilt of transform displays which side of the terrain. From there, on one side, it will be in green color, the other side, brown color. """ if (tilt > 6.2999994) or (tilt < 0): tilt = 0 # reset tilt if 1.8 <= tilt <= 4.7: # Switch to the underground color = "#D2A05D" else: # Switch back to overworld colors color = "#5FDA68" turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt, color) # draw the landscape given a certain angle and tilt turtle.update() # update screen tilt += 0.009 # --------------- Main ------------------- # TEST TRIANGLES: """NOTE TO SELF: If you are going to make any new triangles, MAKE SURE YOU HAVE 3D COORDINATES! - Also, if you want to use other triangles, be sure to adjust the FOV according for each triangle, as some of the triangles may be too small/too big on screen with the wrong FOV. """ # ---- Test Triangles Not Currently Using ------------------------------ testTriangle3D = Triangle(Line((-1, 0, 0), (1, 0, 0)), Line((1, 0, 0), (0, 1, 0)), Line((0, 1, 0), (-1, 0, 0))) triangle3D_2 = Triangle(Line((-1, -0.5, 0), (1, -0.5, 0)), Line((1, -0.5, 0), (0, 0.5, 0)), Line((0, 0.5, 0), (-1, -0.5, 0))) small1 = Line((-0.1, -0.1, 0), (0.1, -0.1, 0)) small2 = Line((0.1, -0.1, 0), (0, 0.1, 0)) small3 = Line((0, 0.1, 0), (-0.1, -0.1, 0)) trianglePiazza = Triangle(Line((-1, -0.75, 0), (1.25, -0.5, 0)), Line((1.25, -0.5, 0), (0, 1.5, 0) ), Line((0, 1.5, 0), (-1, -0.75, 0))) # ---------------------------------------------------------------------- triangle3D_small = Triangle(small1, small2, small3) # User Interface to choose what they want to do level = input("Enter Recursion Level: \n" "In my opinion, choose recursion level 2 for fast animations, " "choose levels 3 and 4 for cool looking terrains, \n" "choose level 6 and above to break the recursion program: ") smooth = input("\nPlease enter the smoothness level for the terrain \n (level 5-6 for ideal looks): ") triangle_midpoint_displacement(triangle3D_small, int(level), smoothness = int(smooth)) print("\n\nType WORLD to see overworld/underworld rotations, ") print("type INTERACTIVE for keyboard interactive rotations and tilt,") print("type ROTATION to see a normal rotation view on the terrain, ") print("type TOP VIEW to see the top view of the terrain, ") print("type ALTERNATE VIEW to see a random alternate view of the terrain, ") print("type 2D to see a 2D top view of the terrain. ") print("NOTE: Please do not enter spaces after input, as program won't take input if you do so.") user_input = input() user_input = user_input.upper() # Setting everything up turtle.setup(1800, 1000) #turtle.speed(10) turtle.tracer(0, 0) turtle.hideturtle() turtle.stamp() # So I know where the origin (0, 0) is turtle.pencolor("brown") turtle.pensize("5") if user_input == "WORLD": # The setup angle = 0 tilt = 0 turtle.bgcolor("#61CBE7") # overworld bg color while True: if 1.8 < tilt < 1.9: turtle.bgcolor("#F1EA5B") elif 4.7 < tilt < 4.8: turtle.bgcolor("#61CBE7") overworld_underworld() elif user_input == "INTERACTIVE": angle = 0 tilt = 0 while True: turtle.listen() turtle.onkey(up, "Up") turtle.onkey(down, "Down") turtle.onkey(left, "Left") turtle.onkey(right, "Right") default() turtle.mainloop() elif user_input == "ROTATION": angle = 0 tilt = -0.5 while True: if angle == 360: # reset angle once it completes one loop angle = 0 turtle.clear() # clear the screen draw_layers(all_triangles, angle, tilt) # draw the landscape given a certain angle and tilt turtle.update() # update screen angle += 0.009 # control speed of animation elif user_input == "TOP VIEW": draw_layers(all_triangles, 0, 0) turtle.update() elif user_input == "ALTERNATE VIEW": angle = random.randint(0, 360) tilt = random.randint(-30, 30) draw_layers(all_triangles, angle, tilt) elif user_input == "2D": # Remove z-axis from all points for triangle in all_triangles: for line in triangle.sides: for point in line.points: temp = list(point) temp[2] = 0 point = temp draw_layers(all_triangles, 0, 0) turtle.update() print("\n\nTurtle Done!") turtle.done()