Lab Exercise 11: 3D Turtle
The purpose of this lab is to give you one more chance to extend your drawing system. You should have a reasonable understanding of how all the pieces fit together, so now we're going to swap out the standard 2D turtle and put in a 3D turtle. All of your 2D turtle programs should continue to work just fine, but now you can make 3D shapes as well. In addition, you'll be able to rotate your completed drawings with the mouse.
The lab consists of three parts.
- First, edit your TurtleInterpreter class to make use of the 3D turtle. Once this is complete, you can try drawing some simple 3D shapes.
- Second, implement the 3D symbols for L-systems so you can draw 3D trees.
- Third, update your Shape class and possibly some of its children to make use of the 3rd dimension.
The goal is to be able to make 3D shapes and scenes as easily as 2D. You should be able to create a Cube class, for example, as well as 3D trees and other L-system shapes that work exactly the same way as their 2D counterparts.
Create a new working folder. Copy your lsystem.py, turtle_interpreter.py, shapes.py, and tree.py files from last week. Label them as version 5. Then download the 3D turtle file (documentation at the bottom of this page).
- Update the TurtleInterpreter Class to Use the 3D Turtle
One difference between the standard turtle and the 3D turtle is that the latter is implemented as a class. Therefore, you need to create a single instance of the turtle object that will be used by all TurtleInterpreter objects. With appropriate naming, all of the rest of your turtle code will work fine.
At the top of turtle_interpreter.py, import turtleTk3D instead of turtle, and create a global variable called turtle (or t if you called the turtle module t) and initialize it to None.
Second, before calling any other turtle functions in your __init__ method, but after the test of TurtleInterpreter.initialized, put the following two lines.
global turtle turtle = turtleTk3D.Turtle3D(dx, dy)
The first line tells the function to use the turtle variable in the global symbol table, and the second line creates a new turtleTk3D object. By putting the 3D turtle object in a variable called turtle, expressions like turtle.left(angle) or turtle.forward(distance) still work as expected.
- Update TurtleInterpreter to Handle 3D Rotations
Make the following additional changes to turtle_interpreter.py.
- Edit the hold method so that it calls only turtle.mainloop(). Delete everything else.
- If you have not already done so, make the '[' and ']' cases in drawString to store and restore the turtle width in addition to the heading and position.
Edit the orient method so it takes two additional optional arguments: roll and pitch. The definition should look like:
def orient(self, angle, roll=0, pitch=0)
The function should first call the turtle method setheading with an argument of 0, then roll by the roll argument (e.g. turtle.roll(roll)), pitch by the pitch argument, and yaw by the angle argument. Executing a yaw rotation is the same as a left rotation in the planar turtle.
- If you have a goto method, add an optional parameter zpos with a default value of 0. Then change the turtle.goto call to include zpos as the third argument.
Do the same with the place method. It should have two required arguments (xpos and ypos) and four optional arguments (angle, roll, pitch, and zpos).
The other change to the place method is in the case where angle is not None, call the turtle interpreter's orient function (self.orient) with angle, roll, and pitch as the arguments.
Design Note: if you set up the arguments as place(xpos, ypos, zpos=0, angle=None, pitch=0, roll=0) it will break your prior code that assumes angle is the third argument. It's probably better to make zpos the last argument, even if it seems out of place. Give the new arguments (pitch, roll, and zpos) default values of 0. Backward compatibility is often a requirement when making changes to an existing system, even if it leads to some odd decisions.
- Create additional turtle interpreter methods called roll, pitch, and yaw, that call the 3D turtle functions roll, pitch and yaw. These functions will look like your existing width or color functions.
- The turtle.position() method now returns the tuple (x, y, z) instead of just (x, y). There are likely some places in the TurtleInterpreter's forward method that need to be updated to handle the z coordinate. In the jitter and jitter3 cases, you probably want to add a random offset in z, in addition to x and y, and make all of your goto calls include the z coordinate.
Add cases in in drawString method for pitch ( & and ^ ) and roll (\ and /). The & symbol means to execute the pitch method with a positive angle (down), and ^ should execute the pitch method with a negative angle (up). The backslash '\' should execute the roll with a positive angle (right), and the forward slash '/' should execute roll with a negative angle (left).
Yaw is still + and -.
Syntax Note: you will have to use the string '\\' to represent the backward slash character because it is a special character (e.g., '\n' is a newline and '\t' is a tab). Two backslashes in a row tells Python to insert a single backslash in the string.
- The 3D turtle color function works slightly differently from the regular turtle. The primary difference is that it takes as input r, g, b values, a color string, or a single tuple (r, g, b) and it returns only a single color value. You'll need to edit your angle bracket cases '<' and '>' to take into account that there is no pencolor/fillcolor separation.
- You may need to update your code in the turtle interpreter to use functions provided by the 3D turtle but not the planar turtle, but most of the functions and their variations are supported. Change these as they crop up.
- When you are done with these changes, try out first test program. (Note that is assumes your TurtleInterpreter has a method named width. If you have a method named setWidth, then update the test code to use the method you have. You don't want to just change the name of your method to width because you can't have a method and a data field with the same name.)
- Update the Shape Class for 3D
Make the following changes to shapes.py.
- Add three optional parameters to the Shape class draw function. The arguments should be called roll, pitch, and zpos and be added to the parameter list in that order. Given them all default values of 0. The orientation parameter already holds the yaw information.
- Add pitch, roll, and zpos to the place call before drawString.
Run test function two. It is a simple example of how to begin building a 3D scene. It should look like a poorly designed shelving unit.
- Update the Tree Class to Use 3D
Update your tree.py file. Add roll, pitch, and zpos to the parameter list of the Tree class draw method, giving them all default values of 0. Then pass the three parameters on to the parent draw function in the proper order.
Run the test function 3 using one of the L-systems below. SystemZ works well with 3 or 4 iterations. SystemZ2 and systemZ3 work well with 7-9 iterations.
When you are done with the lab exercises, you may begin the project.
Appendix: Turtle3D Documentation
The Turtle3D class implements a 3D turtle abstraction using the Tkinter package.
The default turtle window supports three user operations.
- Once drawing is complete, the user can rotate the image using the first mouse button and clicking and dragging.
- The user can close the window and quit the program by typing Command-q or by typing just q.
- The user can reset the view to the default orientation by typing Command-r or by typing just r.
The Turtle3D class includes the following methods for public use.
__init__: the constructor function has six optional arguments.
- winx (default 800) is the horizontal window size
- winy (default 800) is the vertical window size
- title (default 'Turtle 3D')
- position (default (0, 0, 0) ) is the initial 3D position of the turtle
- heading (default (1.0, 0.0, 0.0)) is the initial forward direction of the turtle
- up (default (0.0, 0.0, 1.0)) is the initial up direction, which defines left and right (yaw) relative to the forward direction. The default values mean that left and right work identically to the standard 2D turtle as long as the turtle does not pitch or roll.
- reset(): deletes all drawing and resets the turtle to its initial position.
- forward(distance): goes forward in the current turtle direction. If the pen is down, it creates a line.
goto(xnew, ynew, znew): The function can take one, two or three parameters. If the pen is down, it creates a line.
- One parameter: xnew is treated as a two-element tuple (x, y) and the turtle is placed at (x, y, 0).
- Two parameters: xnew and ynew are used and znew is set to 0.
- Three parameters: the turtle is placed at (xnew, ynew, znew).
- lt | left(angle): turn left (yaw) as defined by the current heading and up direction.
- rt | right(angle): turn right (yaw) as defined by the current heading and up direction.
- yaw(angle): go left (positive) or right (negative) relative to the current turtle orientation.
- pitch(angle): go up (positive) or down (negative) relative to the current turtle orientation.
- roll(angle): rotate right (positive) or left (negative) about the current turtle heading.
- pensize | width(w): set the width of the pen to w. If called with no arguments, it simply returns the current width.
- color(r, g, b): sets the pen color. Arguments can be the standard X11 color strings defined in rgb.txt or r, g, b values. The first argument can be a tuple (r, g, b) or the color values can be given individually. Color values should be in [0, 1].
- penup | pu | up(): pick up the pen
- pendown | pd | down(): put down the pen
- tracer(blah): does nothing (no visible turtle anyway)
- circle(radius, theta): draws a circle of the given radius. The theta argument is optional and permits drawing only the angular fraction of the circle specified.
- pos | position(): returns a tuple with (x, y, z)
- heading(): returns a tuple of two tuples with the current heading and up vectors. ( (hx, hy, hz), (ux, uy, uz) )
- seth | setheading(heading): ideally heading should be two vectors representing the turtle's forward and up directions. If a single scalar is provided, the turtle is set so the turtle is in the drawing plane (up of (0, 0, 1)) and is rotated according to the argument. This gives it the same functionality as the 2D turtle.
- begin_fill | end_fill | fill(q): if q is True, the turtle will begin to store points until the fill function is called with False as the argument. Then it fills in the area defined by the points. The turtle must visit at least three points after fill(True) is called and before fill(False) is called in order to generate a polygon.
- nudge(n): allows the user to adjust the turtle's coordinate system by nudging the forward vector in the specified direction. The argument n should be a 3-element sequence (list or tuple). A value like (0.0, -0.1, 0.0) nudges the turtle's orientation down (i.e. like gravity).
- cube(distance): takes one optional parameter (size) and draws a cube.
- setRightMouseCallback(func): takes one argument, which should be a function with an argument (other than self for a class method) called event. The mouse click location will be in event.x and event.y. Note that the click location will be in window coordinates, not turtle coordinates.
- window2turtle(x, y): takes in window coordinates (like those in event.x and event.y above) and returns a tuple (x', y', 0) with the corresponding turtle coordinates in the default view.
- hold(): goes into a main loop waiting for user input.
- wait(): goes into a main loop waiting for user input.
- updateCanvas(): updates the Canvas to draw any new shapes.