Abstract
This last chapter is an introduction to the 3D capabilities of Processing. Just as Processing provides ways to draw in the plane, we can build a sketch with objects that are rendered in space, that is, three dimensions. It is important to recall that once things have been drawn in the Processing window, any notion of how the pixels were colored is not maintained. That is why the coding has to keep track of food items and snake segments and rock, slingshot, and chicken, drawing them again as required. Processing does not provide a 3D modeling system, but the facilities provide considerable power. With that in mind, in this chapter, we’ll review programming and Processing concepts and then focus on two 3D sketches.
As with everything in programming, it is essential for your understanding that you experiment: Copy the simple sketches in the documentation and make changes, download or copy the examples in this book and make changes, and create your own sketches. I include extra sketches with the source code. See later in the chapter for screenshots of a dreidel (a top that spins and slows down) and a representation of the solar system, with nine planets rotating round the sun, including Pluto, although that makes for an even greater challenge.
Programming Concepts
Representation of 3D on the flat computer screen requires what is termed rendering. The 3D objects in Processing are collections of flat faces, in certain contexts called facets, made up of edges and vertices (corners) and information on what is the inside vs. the outside of the object. The locations of the vertices are specified using three values for the x, y, and z axes, analogous to x and y coordinates for 2D. In Processing, the standard orientation for the z axis is coming out of the plane of the screen. That is, values for the z dimension increase moving toward us; the default zero z position is at the screen; and values are more negative moving away from us. Although I could, with confidence, make the statement that most programming tools use the origin in the upper left corner, the upside-down arrangement that Processing uses for 2D work, 3D tools differ on the orientation of the three dimensions. Processing uses a left-handed coordinate system, whereas some languages and tools use a right-handed coordinate system.
The task of the renderer is to determine how the object is projected onto the screen to be viewed by the user. This often involves calculating when all or portions of edges and faces are blocked by other faces. This is known as the hidden line or hidden surface removal problem. The calculations use the eye, gaze location (the point in space that the eye is looking at), type of projection (standard perspective or something else), and, sometimes, lighting. Default settings mean that programmers might not need to specify everything to produce results.
Processing and other tools provide a set of primitive 3D shapes along with ways for programmers to construct their own shapes. Spheres are provided as faceted polyhedrons, with the standard setting providing so many facets that they appear round to us. Transformations, such as shown in 2D, are provided.
Processing Programming Features
The code has changed the original origin in the x and y dimensions, but not in z dimensions. This demonstrates the critical aspect of 3D in Processing: drawing is done by setting the origin.
Other transformations, such as rotateX, rotateY, rotateZ, and scale, change what we see in the window. The use of scale can change a sphere into an ellipsoid with different dimensions. You can change the number of facets using sphereDetail, and you can turn off seeing the facets using noStroke. This also removes all edges, which might or might not be what you want.
My next example demonstrates the use of the camera function along with fill and noFill. The first step is to write the setup function and include a call to noFill. The draw function erases the window, as you have seen many times before, and invokes translate to move the drawings to the center of the window. The effects of translate and any other transformation go away after each call of draw and must be repeated.
The code for the dreidel base applied colors using fill in the standard way. An alternative is to put what is called texture on a 3D shape by specifying the position in space of the 3D object and the corresponding position in a 2D image. This requires five numbers. Following the example in the Textured Cube in the Processing documentation, I use the technique of specifying the vertices using unit values and scale the object to be bigger using the scale function. You will find the entire sketch code in the “Program” section for the rotating cube example.
Processing also provides ways of specifying lighting for the scene. It appears that just using the single statement lights(); does improve the look of many sketches involving 3D. The statement must be in the draw function or some other function invoked for each frame, because the settings are reset at each invocation of draw.
I urge you to experiment with the primitives and transformations. I do caution you to proceed at a slow pace, though, as I did in the example with the two boxes. If you change where an object is located and change one or more of the camera parameters and apply rotations, it will not be easy to understand what is going on. If an object such as a sphere is very small, it might appear as a black ball as opposed to a faceted sphere. Making a snowman is a good place to start. You can find a snowman consisting of three spheres and a snowman on a box in the source code. The facets produce a crystal-like effect that fits the idea of a snowman.
Under the Covers
The rendering of 3D scenes into output on the 2D display is called the 3D graphics rendering pipeline. Today, much of the computation is done using special graphics processing units (GPUs) with much parallel computation. The increase in computer speed over the years has been considerable, but the demands from the gaming and movie industries also have increased, so this still is an area of research and development.
Rolling Ball at Alhambra Operation Overview
In the first sketch, the ball rolls down the left side, across the back, up the right side, and then goes back down the right side, across the back from right to left, and up on the left side (Figure 10-1). I do call this a cheap trick: the background is a 2D image from a postcard that I purchased at the Alhambra in Granada, Spain. I term this sketch a cheap trick because of the use of the 2D image from the postcard. Please do keep in mind that the size of the image had to match the size of the Processing window. The ball uses the Processing texture feature to appear to be wrapped in the Spanish flag. The 3D space is superimposed on the background.
The rolling ball at the Alhambra starts in motion. Pressing any key toggles back and forth between stopping and starting. The stop-and-start feature was added later by declaring a global variable, moving, and putting all the coding in the draw function within an if (moving) statement. After I did this, it was much easier for me to produce a screenshot of the sketch with the Spanish flag displayed as I wanted.
Implementing the Rolling Ball at Alhambra
Having been to the Alhambra and possessing the postcard, I decided to produce a sketch that superimposes the movement of a ball against the picture. The program just grew after that, with wrapping the ball in the Spanish flag to show the rotating and providing the stop-and-start feature.
Planning
The first task for the rolling ball was to determine the turning points, and this was done by trial and error. I wrote a sketch that made the postcard picture the background and then drew a sphere at each guessed location. This was an instance of what is termed throw-away code, and you need to be willing to do it. The next task was developing the movement back and forth over the three sides of the reflecting pool. I realized that the coordinates change in z or x. More exactly, the change is decreasing in z, then increasing in x, then increasing in z, then (turning around) decreasing in z, decreasing in x, and increasing in z. I was satisfied with this as an adequate example for my classes. However, when I was writing this book, I decided to do more.
Moving an object in 3D in Processing requires resetting of the origin. If the origin of a sphere is the center, then rotating the sphere on itself is easy once I decide along which axis I want to do the rotation. This creates a new challenge: My code can rotate the sphere, but how to make it visible that the ball is rotating if it is simply a sphere? The answer is to put something on the sphere using the texture facilities. I made use of an image of the Spanish flag. Note that it has file extension png.
I came to the realization that the path was made up of six segments, not three: going down the left side, across from left to right at the back, coming up the right side, going down the right side, across from right to left at the back, and coming up the left side. I used a switch statement in a function I named rotateAndDraw to handle the cases corresponding to the segments.
Function Table for Rolling Ball
Function | Invoked by | Invokes |
---|---|---|
setup | Underlying Java program | |
draw | Underlying Java program | forwardtravel, backwardtravel |
forwardtravel | draw | rotateAndDraw |
backwardtravel | draw | rotateAndDraw |
keyPressed | Underlying Java program |
Programming the Rolling Ball at Alhambra
Program for Rolling Ball
PImage bg; | This is the image from the postcard showing the Alhambra |
float x,y,z; | Used to indicate the parameters of translate to position the origin to draw the ball |
float xstart = 150; | The leftmost x position |
float xend = 330; | The rightmost x position |
float ylevel = 400; | The constant y level; that is, the height |
float zstart = -50; | The farthest away z position |
float zend = 450; | The closest z position |
boolean forward = true; | Indicating forward motion or not |
float a=0; | The a stands for angle; this is used for rotating the ball, initialized to 0 |
PShape ball; | Will hold the ball, with its texture |
boolean moving = true; | Used to indicate movement or not |
String msg = "Press any key to stop or restart."; | Instructions |
void setup() { | Header for setup |
size(500,740,P3D); | Set the dimension of window and 3D |
noStroke(); | Turn off stroke |
sphereDetail(15); | Set the amount of detail (faceting) |
bg = loadImage("alhambra.jpg"); | Load postcard image |
PImage design = loadImage("flag.png"); | Load Spanish flag image |
ball = createShape(SPHERE,10); | Create a shape |
ball.setTexture(design); | Give it texture; that is, wrap the flag around the sphere |
background(bg); | Set initial background |
x = xstart; | Initialize x |
y = ylevel; | Initialize y |
z = zend; | Initialize z |
textSize(20); | Set text size for instructions |
} | Close setup |
void draw() { | Header for draw |
if (moving) { | Only do something if moving is true |
background(bg); | Erase the window and redraw background |
fill(0); | Set color for clearing bottom of image |
rect(0,700,500,40); | Draw black rectangle |
fill(250,0,0); | Set color for instructions message |
text(msg,100,720); | Output instructions |
lights(); | Set lights |
if (forward) { | If forward |
forwardtravel(); | Invoke forwardtravel |
} | Close clause |
else { | else |
backwardtravel(); | Invoke backwardtravel |
} | Close clause |
} | Close the if (moving) clause |
} | Close draw |
void forwardtravel() { | Header for forwardtravel |
if ((z>zstart)&&(x==xstart)) { | Check if at first segment |
z--; | Decrement z (move away) |
rotateAndDraw(1); | Invoke rotateAndDraw with parameter 1 |
} | Close clause |
else { if (x<xend) { | Check if at segment at back |
x++; | Increment x |
rotateAndDraw(2); | Invoke rotateAndDraw with parameter 2 |
} | Close clause |
else { z++; | Increment z (move toward viewer) |
rotateAndDraw(3); | Invoke rotateAndDraw with parameter 3 |
if (z>zend) {forward = false;}; | If at the end, set forward to false |
} | Close clause |
} | Close clause |
} | Close forwardtravel |
void backwardtravel() { | Header for backwardtravel |
if ((z>zstart)&&(x>=xend)) { | If at fourth segment |
z--; | Decrement z |
rotateAndDraw(4); | Invoke rotateAndDraw with parameter 4 |
} | Close clause |
else { if (x>xstart) { | If in back segment |
x--; | Decrement x (move to the left) |
rotateAndDraw(5); | Invoke rotateAndDraw with parameter 5 |
} | Close clause |
else { z++; | Increment z, now moving toward viewer |
rotateAndDraw(6); | Invoke rotateAndDraw with parameter 6 |
if (z>zend) {forward = true;}; | If at end, set forward to true |
} | Close clause |
} | Close clause |
} | Close backwardtravel |
void rotateAndDraw(int p) { | Header for rotateAndDraw; parameter will indicate the segment and therefore what gets rotated positively or negatively |
a=a+PI/10; | Increment a (the angle) |
translate(x,y,z); | Position origin at x,y,z; these have been set previously |
switch(p) { | Switch on the parameter |
case 1: | First segment, going down the left |
rotateX(a); | Rotate around x axis |
break; | Leave switch |
case 2: | Second segment, going from left to right at the back |
rotateZ(a); | Rotate around z axis |
break; | Leave switch |
case 3: | Third segment, coming up the right |
rotateX(-a); | Rotate around x axis, negatively |
break; | Leave switch |
case 4: | Fourth segment, going back down the right, away from viewer |
rotateX(a); | Rotate around x axis |
break; | Leave switch |
case 5: | Fifth segment, going at the back right to left |
rotateZ(-a); | Rotate around z axis, negatively |
break; | Leave switch |
case 6: | Sixth segment, coming back up the left side |
rotateX(-a); | Rotate around x axis, negatively |
break; | Leave switch |
} | Close switch |
shape(ball); | Draw the ball |
} | Close rotateAndDraw |
void keyPressed() { | Header for keyPressed |
moving = !moving; | Toggle the moving Boolean; using !, which is logical not, changes true to false and false to true |
} | Close keyPressed |
Rotating Cube Operation Overview
The example, shown in Figure 10-2, is based on the Textured Cube described in the Processing documentation. I emphasize again that the sketch needs to be run to be appreciated. The user can rotate the cube using the mouse. Note that dragging the mouse to the right or left causes the cube to be rotated around the y axis. Dragging the mouse up or down the screen causes the cube to be rotated around the x axis. If the mouse is dragged diagonally, it is rotated along both axes.
I made the addition of having the cube rotate by itself after no action by the user after a specified amount of time. This is a nice, although perhaps creepy, effect, and it demonstrates a technique for handling the event of nothing happening.
Implementing the Rotating Cube
I include this as one of the featured examples because of my addition and because I felt it merited extra attention beyond what was provided in the documentation. The main Processing feature demonstrated is applying texture, in the form of images, to faces of a cube.
Planning
I decided to use three images for texture, each for the pair of opposing sides of the cube. Following the Textured Cube example, applying a texture (i.e., an image) to a portion of a 3D shape can be done using unit measurements as opposed to the exact pixel dimensions to relate a set of 3D coordinates (three numbers) to a set of 2D coordinates (two numbers). Each face of the cube is associated with one of the three images.
A horizontal (x) movement will set off a rotation around the y axis, and a vertical (y) movement will set off a rotation around the x axis. The expressions involving the previous mouse positions are different because of the upside-down coordinate system. Do not take my word for this. Change mouseDragged and move the mouse and see what seems correct to you.
The rate variable determines how much moves of the mouse affect rotations. You can experiment with the value. Do keep in mind that the mouseDragged function is called at every frame, so you don’t want small moves to lead to big rotations.
I could have put these two statements in the if clause, but I generally favor defining functions for distinct tasks.
To produce the cube, I modified the function in the Processing documentation in two ways. I gave the function three parameters for the three sets of opposite sides of the cube. Then I used beginShape(QUADS) and endShape() three times, referencing a different one of the parameters each time. I used the approach of defining the cube as occupying the space from -1 to 1 along the x axis, -1 to 1 along the y axis, and -1 to 1 along the z axis. The vertices of the images are indicated by (0,0), (1,0), (1,1), and (0,1). It is important to note two things. First, this is a very tiny cube. The reason we can see it is that there is a call to scale(200) before the TextureCube function is invoked. Second, there is some distortion of the images I use for this because they are not squares.
Function Table for Rotating Cube
Function | Invoked by | Invokes |
---|---|---|
setup | Underlying Java program | |
draw | Underlying Java program | setRotation, TexturedCube |
mouseDragged | Underlying Java program | |
setRotation | draw | |
TexturedCube | draw |
Programming the Rotating Cube
Program of Rotating Cube
PImage frog, flowers, makeup; | For the three images |
float rotx = PI/4; | Initial rotation around x axis |
float roty = PI/4; | Initial rotation around y axis |
int last; | Hold time last thing was done |
int interval = 6000; | Amount of wait before rotation “by itself” |
void setup() { | Header for setup |
size(1000, 1000, P3D); | Set the dimensions of window and set up for 3D |
frog = loadImage("AnnikaFrog.JPG"); | Load frog image |
flowers = loadImage("AnnikaFlowers.JPG"); | Load flowers image |
makeup = loadImage("AnnikaMakeup.jpg"); | Load makeup image |
textureMode(NORMAL); | Set normal texture mode |
last = millis(); | Initial setting of last |
} | Close setup |
void draw() { | Header for draw |
background(0); | Erase window |
textSize(20); | Set the text size |
text("Drag using mouse anywhere on screen to rotate cube. If no action, cube will rotate by itself.", 17,14); | Give instructions |
noStroke(); | No stroke |
translate(width/2.0, height/2.0, -100); | Move origin to center and back away from viewer |
if ((millis()-last) > interval) { | Has there been nothing happening for a long enough time? |
setRotation(); | Set the rotations |
} | Close if clause |
rotateX(rotx); | Rotate around x whatever rotx is |
rotateY(roty); | Rotate around y whatever roty is |
scale(200); | Scale up (because cube defined is tiny) |
TexturedCube(frog,flowers,makeup); | Invoke TexturedCube to draw the cube with the images |
} | Close draw |
void setRotation() { | Header for setRotation |
rotx += PI/400; | Increment rotx |
roty += PI/400; | Increment roty |
} | Close setRotation |
void TexturedCube(PImage tex1, PImage tex2, PImage tex3) { | Header for TexturedCube; the parameters are the three images for opposing sides of the cube; the cube is 2 pixels by 2 pixels by 2 pixels |
beginShape(QUADS); | Begin a shape, using the QUADS parameter indicating how the images will be applied |
texture(tex1); | Use the first image; it will be applied to the faces of the cube that are at z equal to 1 and -1; these are the front face and the back face |
vertex(-1, -1,1, 0, 0); | Connect the corners of the face to the corners of the image; the upper left corner of the face is connected to the top left corner of the image |
vertex( 1, -1,1, 1, 0); | The upper right corner of the face is connected to the top right corner of the image |
vertex( 1,1,1, 1, 1); | The bottom right corner of the face is connected to the bottom right corner of the image |
vertex(-1,1,1, 0, 1); | The bottom left corner of the face is connected to the bottom left corner of the image |
This still uses tex1; now applied to the back face; the x and y specifications will be opposite the previous set of images to display the images correctly | |
vertex( 1, -1, -1, 0, 0); | Connect the corners of the face to the corners of the image; the upper right corner of the face is connected to the top left corner of the image |
vertex(-1, -1, -1, 1, 0); | The upper left corner of the face is connected to the top right corner of the image |
vertex(-1,1, -1, 1, 1); | The bottom left corner of the face is connected to the bottom right corner of the image |
vertex( 1,1, -1, 0, 1); | The bottom right corner of the face is connected to the bottom left corner of the image |
endShape(); | End the shape assigning the image as texture in two parts, for the two opposing faces |
beginShape(QUADS); | Begin a shape, using the QUADS parameter indicating how the images will be applied |
texture(tex2); | Use the second image; it will be applied to the faces of the cube that are at y equal to -1 and 1; these are the top face and the bottom face |
vertex(-1, -1, -1, 0, 0); | Now the coordinate that stays the same is the y coordinate for the cube; it is at -1 for the first four (the top face) and then will be at 1 for the next four; the image vertex at 0,0 is at the cube vertex -1,-1,-1 |
vertex( 1, -1, -1, 1, 0); | The image vertex at 1,0 is at the cube vertex 1,-1,-1 |
vertex( 1, -1,1, 1, 1); | The image vertex at 1,1 is at the cube vertex 1, -1,1 |
vertex(-1, -1,1, 0, 1); | The image vertex at 0,1 is at the cube vertex -1, -1,1 |
Using the same image, tex2, the next four vertices describe the bottom face | |
vertex(-1,1,1, 0, 0); | The image vertex at 0,0 is at the cube vertex -1,1,1 |
vertex( 1,1,1, 1, 0); | The image vertex at 1,0 is at the cube vertex 1,1,1 |
vertex( 1,1, -1, 1, 1); | The image vertex at 1,1 is at the cube vertex 1,1,-1 |
vertex(-1,1, -1, 0, 1); | The image vertex at 0,1 is at the cube vertex -1,1,-1 |
endShape(); | End the shape |
beginShape(QUADS); | Begin a shape, using the QUADS parameter, indicating how the images will be applied |
texture(tex3); | Use the third image; it will be applied to the faces of the cube that are at x equal to -1 and 1; these are the left and right faces |
vertex(-1, -1, -1, 0, 0); | Now the coordinate that stays the same is the x coordinate for the cube; it is at -1 for the first four (the left face) and then will be at 1 for the next four; the image vertex at 0,0 is at the cube vertex -1,-1,-1 |
vertex(-1, -1,1, 1, 0); | The image vertex at 1,0 is at the cube vertex -1,-1,1 |
vertex(-1,1,1, 1, 1); | The image vertex at 1,1 is at the cube vertex -1,1,1 |
vertex(-1,1, -1, 0, 1); | The image vertex at 0,1 is at the cube vertex -1,1,-1 |
Using the same image, tex3, the next four vertices describe the right face | |
vertex( 1, -1,1, 0, 0); | The image vertex at 0,0 is at the cube vertex 1,-1,1 |
vertex( 1, -1, -1, 1, 0); | The image vertex at 1,0 is at the cube vertex 1,-1,-1 |
vertex( 1,1, -1, 1, 1); | The image vertex at 1,1 is at the cube vertex 1,1,-1 |
vertex( 1,1,1, 0, 1); | The image vertex at 0,1 is at the cube vertex 1,1,1 |
endShape(); | End the shape |
} | |
void mouseDragged() { | Header for mouseDragged |
float rate = 0.01; | Used to scale the change in mouse positions |
last = millis(); | Set last to indicate time when something happened |
rotx += (pmouseY-mouseY) * rate; | Increment rotx |
roty += (mouseX-pmouseX) * rate; | Increment roty |
} | Close mouseDragged |
Things to Look Up
You will become comfortable with the coordinate system if and when you build on my examples and examples in the Processing documentation and when you design and build your own projects.
Investigate how to make custom shapes and how to apply textures. Review the use of transformations, especially using scale after creating a custom shape using unit dimensions.
Proceed slowly and study the different ways to use the camera function to change the various parameters for calculating the display and how to specify the different ways of lighting. At the risk of repeating myself, proceed step by step. If you change shapes and make transformations and change camera parameters all at once, you probably will get confused.
How to Make This Your Own
You certainly can do your own cheap tricks, making objects move against interesting flat backgrounds. You can use your own pictures for textures on rotating cubes or give the user the option to use images on the local computer or on the Web.
A good next challenge would be bouncing things in a five-sided box. You can decide if there is an invisible sixth side. Another challenge would be a shooter game, perhaps a version of slingshot.
One addition for the rotating cube could provide users a way to upload images from their own local computer. See Chapter 7 for background on how to do this.
I have provided additional examples in the source code section. These include a simple snowman consisting of just three spheres, the simple snowman on a box, the original rolling ball around the Alhambra, a dreidel, and a (crude) solar system.
What You Learned
This chapter was an introduction to 3D using Processing. You learned about the 3D coordinate system and transformations such as translations and rotations. You learned about the 3D primitives, applying texture, and creating custom 3D shapes. Objects are positioned by transformations of the coordinate system. What you learned to do in 2D can apply to the 3D domain. This applies to the mouseDragged event and the mouseX, mouseY, pmouseX, and pmouseY variables and using millis to insert a wait for something to happen.
What’s Next
This is the last chapter! I hope this book was a satisfactory introduction to programming and the Processing language and it made you want to create your own sketches.
In the Appendix, I provide an introduction to p5js, a companion project by the Processing development community to provide a way to publish (disseminate) Processing sketches on the Web. That is, it is a way to use JavaScript with Processing functions. I offer three examples. The first is my familiar Daddy Logo. The second is a coin-toss type of application, alternating between two photos taken when we were in the Wall Street area in New York City. The third is a 3D helix that can be rotated just as the cube is rotated.