3D Graphics in Processing. Learning objectives and exercises from the University of London BSc in Creative Computing.
LEARNING OBJECTIVES
1. Explain how 3D images are represented and displayed on a two dimensional screen.
2D objects have only two dimensions, their length and height, and any point on the object can be specified by two coordinates, x and y. When we consider 3D objects, we have a third dimension, depth and a point within a 3D object is represented by three coordinates, x, y and z.
When we view 3D objects on a 2D screen we view them through a viewport or viewing rectangle, which is a 2D plane with some orientation to the object. This is analogous to looking through a window at a scene, we see a 2D representation of the 3D scene. When viewing a 2D object on a 2D plane, the object is actually on the viewing plane (although it could be rotated, and at any point along the length and height of the plane), analogous to looking at a stain-glassed window art work. When we view a 3D object the 2D viewport plane could be on, above or below the object (as well as being rotated around the object, with the object at any point along the length and height of the plane).
2. Describe and use image rendering
The objects that we see on our 2D viewport or screen are rendered onto it, a process of calculating which points of the 3D object would be visible from the viewport.
To continue the window analogy, the same 3D scene viewed through different windows of a house will appear differently. Some objects are only viewable through certain windows because other objects get in the way (walls, other buildings, trees etc).
The rendering process also allows for perspective ensuring that objects that are further away from the viewpoint appear smaller than objects that are closer to the viewpoint.
The following code, ©UoL creates a scene that visualises a 3D space showing the inside of a cube with a sphere at the origin using the line() and sphere() methods. The code has been adjusted from the original to add the right-hand side and ceiling of the cube. The viewport in this example is at the origin (top-left) perpendicular to the z-axis. It is convention to assume that the positive z axis is toward the viewer and therefore the negative z axis is into the screen away from the viewer.
Because the sphere is centred at the origin, only a proportion of it can be seen in the sketch.
CODE: ©UoL grid_sphere.pde
import processing.opengl.*;
int sz = 512;
size(sz, sz, OPENGL);
background(255);
strokeWeight(3);
noFill();
int step=64;
for(int k=0; k<=sz; k+=step)
{
//bottom grid
line(k, sz, 0, k, sz, -sz);
line(0, sz, -k, sz, sz, -k);
//top grid
line(k, 0, 0, k, 0, -sz);
line(0, 0, -k, sz, 0, -k);
//back grid
line(0, k, -sz, sz, k, -sz);
line(k, sz, -sz, k, 0, -sz);
//left grid
line(0, sz, -k, 0, 0, -k);
line(0, k, 0, 0, k, -sz);
//right grid
line(sz,0,-k,sz,sz,-k);
line(sz, k, 0, sz, k, -sz);
}
sphereDetail(20);
sphere(128);
The code above creates the following image:

3. Use various OpenGL methods in creating 3D images
The code below ©UoL again uses the sphere() OpenGL method, along with the box() method which creates a cube automatically.
CODE: ©UoL red_sphere_blue_box.pde
import processing.opengl.*;
void setup()
{
size(512, 512, OPENGL);
sphereDetail(20);
noFill();
}
void draw()
{
background(255);
translate(width/4, height/2, 0);
stroke(255, 0, 0);
sphere(100);
translate(width/2, 0);
stroke(0, 0, 255);
box(100);
}
This code creates the following sketch:

In this code we use the sphereDetail() method which specifies the density of the triangles used to create the sphere wireframe. Note also the import statement at the beginning of the code which allows the usage of the OpenGL classes and methods, along with the OPENGL attribute in the size() method that tells the computer to use specialised 3D hardware in the computer, rather than software, to render the sketch. OpenGL was developed by Silicon Graphics Ltd (SGI) as an open specification API (Application Programming Interface) for software to use specially-designed 3D graphics hardware and typically provides higher performance than software-only graphic rendering.
The OpenGL methods, sphere() and box() are always rendered at the origin, so to construct sketches using these methods we must translate() the origin as we have in the example above.
An additional method, camera() can be used to manipulate the position of the viewport rather than the object itself. It has nine 3D coordinate parameters, three sets of three. The first set positions the camera, the second sets the point where the camera is aiming and the third specifies which axis is pointing upwards (usually the y-axis).
If we consider the previous sketch, with the red and blue sphere, we can change the scene by repositioning the camera. The following code repositions the camera to the origin (0,0,0), the top-left hand corner of the screen and aims it at the bottom-right hand corner of the screen, still on the z-axis origin (width, height, 0) so that instead of looking at the scene we are actually looking across the scene. Finally we tell Processing that the y-axis is ‘up’.
CODE: red_sphere_blue_box_camera.pde
import processing.opengl.*;
void setup()
{
size(512, 512, OPENGL);
sphereDetail(20);
noFill();
camera(0,0,0,width,height,0,0,1,0);
}
void draw()
{
background(255);
translate(width/4, height/2, 0);
stroke(255, 0, 0);
sphere(100);
translate(width/2, 0);
stroke(0, 0, 255);
box(100);
}
The code above creates the following image:

4. Discuss the representation of lines in three dimensions and also how this can give the viewer the perception of depth and perspective
A postulate by a famous 17th century mathematician, Desargues, states that all parallel lines appear to meet at a point at infinity. This postulate from the mathematics of projective geometry, the mathematics used in calculating perspective, is the basis for all 3D graphics.
Parallel lines are drawn so that they converge at a vanishing point. This process causes objects that are further away from the viewport to appear smaller than closer objects, creating the illusion of depth and perspective.
As a point of interest and a request from the study notes, this postulate is based on axioms of projective geometry, one of which holds:
Any two lines in a plane have at least one point of the plane (which may be the point at infinity) in common.
Source: Weisstein, Eric W. “Projective Geometry.” From MathWorld–A Wolfram Web Resource.
Projective geometry is an evolution of the more intuitive Euclidean geometry, which is based on the assumption that the x, y and z planes are inherently flat (as opposed to be being spherical, hyperbolic or some other construct). Within Euclidean geometry, a 5th, or parallel postulate holds:
If two lines are drawn which intersect a third in such a way that the sum of the inner angles on one side is less than two right angles, then the two lines inevitably must intersect each other on that side if extended far enough.
Source: Weisstein, Eric W. “Euclid’s Postulates.” From MathWorld–A Wolfram Web Resource.
Both geometries are used in the rendering algorithm to project a 3D object onto a 2D viewport.
5. Perform various manipulations, including scaling, rotation and transformations to change the way an image is presented, in both 2D and 3D
Considering the scale() method first, that allows us to stretch and contract the x, y and z-axes in a Processing sketch. The code to draw the red sphere and blue box from the previous example has been modified to include the scale() method that increases the z-axis scale by 4. Because we have inserted this code after the sphere has been drawn but before the box, only the box has been scaled, elongating the box along the z-axis by a factor of 4.
When the scale() method is used within a draw() method loop, each time the loop starts again, the transformation is reset so that the effect is not cumulative, as scale(2) followed by scale(2) is the equivalent to scale(4).
CODE: red_sphere_blue_box_scaled.pde
import processing.opengl.*;
void setup()
{
size(512, 512, OPENGL);
sphereDetail(20);
noFill();
}
void draw()
{
background(255);
translate(width/4, height/2, 0);
stroke(255, 0, 0);
sphere(100);
translate(width/2, 0);
stroke(0, 0, 255);
scale(1, 1, 4); //scale z-axis by 4
box(75); //reduce size of box to stay viewable with scaled
//dimensions (from 100 to 75)
}
The code above produces the following sketch.

Let us now consider the rotateX(), rotateY() and rotateZ() methods. Each of these methods allow us to rotate a scene around the x, y or z axes. If we think about a airplane in flight: rotating around the x-axis is the equivalent of pitching, climbing or diving; rotating around the y-axis is the equivalent of yawing, to the left or right; rotating around the z-axis is the equivalent of rolling, to the left or right.
Unlike the scale() or transform() methods, the order in which we apply the rotate() methods has an effect on the resulting image.
The following code illustrates the concept of rotating about the y-axis.
CODE: ©UoL rotateY_sketch.pde
size(512, 512, P3D);
rotateY(PI/3);
noStroke();
background(255);
fill(0);
ellipse(256, 256, 512, 512);
rect(512,0, 512, 512);
rect(1024, 512, 512, 512);
rect(1536, 0, 512, 512);
rect(2048, 512, 512, 512);
rect(2560, 0, 512, 512);
rect(3072, 512, 512, 512);
stroke(0);
strokeWeight(3);
line(0, 0, 1e6, height);
line(0, height, 1e6, height);
line(0, 2*height, 1e6, 2*height);
The code above produces the following image:

Finally, consider the transform() method. We have seen this used in different code and images already in this post, but the code below uses the translate() and rotate() methods within three for() loops to construct a cube of spheres (©UoL). In this example, after each sphere is rendered, the translation and rotation applied is reversed to allow the for()loops to reposition the next sphere easily relative to the original.
CODE: ©UoL cube_of_spheres.pde
import processing.opengl.*;
int sz=512;
void setup()
{
size(sz, sz, OPENGL);
fill(0, 127, 255);
stroke(0);
noLoop();
sphereDetail(12);
}
int boxVol=2*sz/3;
int nBoxes=8;
int boxSZ=boxVol/nBoxes;
float ang1=PI/(4/sqrt(2));
float ang2=PI/(4/((1+sqrt(5))/2));
void draw()
{
background(255);
translate(sz/4, 0 , -sz/4); //position
for(int i=0; i
{
for(int j=0; j
{
for(int k=0; k&rt;-boxVol; k-=boxSZ)
{
rotateY(ang1);
rotateX(ang2);
translate(i, j, k);
sphere(boxSZ/2);
translate(-i,-j,-k);
rotateX(-ang2);
rotateY(-ang1);
}
}
}
}
The code above creates the following image:

To avoid having to restore the coordinate system after every transform() we can use the methods pushMatrix() and popMatrix() to store and restore the current coordinate system. Up to 20 calls to pushMatrix() can be made before a popMatrix() is required and because the coordinate data is stored on a stack, the last item on the stack is the first item off the stack, so coordinates are restored in reverse order.
We can see these methods in use in the following code ©UoL.
CODE: ©UoL popMatrix_3D.pde
import processing.opengl.*;
int sz=512;
void setup()
{
size(sz, sz, OPENGL);
fill(200,200,255);
noStroke();
noLoop();
sphereDetail(12);
}
float boxVol=sz;
float nBoxes=8.0;
float boxSZ=boxVol/nBoxes;
float ang1=PI/(4/((1+sqrt(5))/2));
void draw()
{
background(255);
rotateY(ang1);
translate(sz/2, sz/2, sz/2);
directionalLight(255,255,255,1,1,-1);
for(int j=0; j
{
for(float i=sz/2 - j*boxSZ/2; i <= sz/2+ j*boxSZ/2; i+=boxSZ)
{
for(float k=-sz/2 + j*boxSZ/2; k&rt;=-sz/2 - j*boxSZ/2; k-=boxSZ)
{
pushMatrix();
translate(i, j*sqrt(2)*boxSZ/2, k);
sphere(boxSZ/2);
popMatrix();
}
}
}
}
In this code, we construct a pyramid from spheres a row at a time, using the pushMatrix() and popMatrix() methods. Also included in this code is a method called directionalLight(). This method has six parameters. The first three set the colour of the light, either RGB or HSB depending on the current colorMode(). The second three set the direction the light is facing along each axis which is usually 0, 1 or -1.
The code creates the following image:

Expanding lighting further, another useful method is lights(). This method activates various lighting methods with default values. Firstly it sets ambientLight(125, 125, 125). This method lights objects equally from all directions and the parameters set the RGB or HSB values for the light. In addition lights() sets directionalLight(128, 128, 128, 0, 0, -1). Two other methods are initiated, lightFalloff(1, 0, 0) and lightSpecular(0, 0, 0). lightFalloff(1, 0, 0) reduces the amount of light reaching an object as a proportion of its distance from the light source. In the default case the amount of light is inversely proportional to the distance from the source. lightSpecular() is a method that controls the colour of light that bounces off objects in a certain direction due to the properties of those objects (for instance, shiny objects) rather than diffusing in all directions. At default values the light is black so there is no specular light set through the lights() method.
It is important for both the lights() and directionalLight() method in a dynamic-mode Processing sketch to include the them within the draw() section, rather than the setup() section, otherwise they will only be set during the first loop and not adjust from that point.
An example of using lights() and the directionalLight() method in a dynamic sketch can be seen below. This is based on code ©UoL, but with orange directional light applied in the directional of negative-z and negative-x (from behind and to the right).
CODE: sphere_lighting.pde
import processing.opengl.*;
float theta=0;
float dTheta=TWO_PI/360;
float radius=200;
//Lights, Camera, Action
void setup()
{
size(800, 600, OPENGL);
noStroke();
//camera(width/2, height/2, width, width/2, height/2, 0, 0, -1, 0);
fill(255,255,255);
}
void draw()
{
lights();
background(0);
float x, z;
x=radius*cos(theta);
z=radius*sin(theta);
directionalLight(255, 127, 0, -1, 0, -1);
pushMatrix();
translate(width/2+x, height/2, z);
sphere(50);
popMatrix();
pushMatrix();
translate(width/2-x, height/2, -z);
sphere(50);
popMatrix();
pushMatrix();
x=radius*cos(theta+PI/2);
z=radius*sin(theta+PI/2);
translate(width/2, height/2+x, -z);
sphere(50);
popMatrix();
theta=(theta+dTheta)%TWO_PI;
}
This code creates the following animation of three spheres rotating around the x- and y-axis.
6. Incorporate texture into images
We can apply ‘texture’ to 3D objects by overlaying 2D images to the surfaces of the objects. We can only use P3D mode for this (not supported in OpenGL). We anchor the 2D images to the vertices of the object and the algorithm renders the image texture in 3D in the same way it does primitive objects.
Some new methods are required to utilise this technique in Processing. Firstly, we initialise a PImage type variable with the loadImage() method. This method is capable of loading a .gif, .jpg, .tga, or .png file (the files must be in the same directory as the Processing sketch).
We use the texture() method to apply the image to the object face. It must be called between beginShape() and endShape() methods and before end calls to vertex(). The vertex() method has two additional parameters when using the texture() method which are the x- and y-coordinates in pixels of the point in the texture image that should be fixed to the vertex.
The code below show the texture() method being used. In this code we have created a cube using vertices and textured with images of dice, originally designed by Evgeniy Grigoriev in 2010 (Source)
CODE: vertex_texture_dice.pde
int sz = 336; //this is the size of the di face in pixels
int x1 = (width-sz)/2; //position the di in the middle of the screen
int y1 = (height-sz)/2; //position the di in the middle of the screen
int z1 = 0; //position the front of the di on the z-axis origin
int x2 = x1+sz; //set the relative positions of the other points of
//the cube
int y2 = y1+sz;
int z2 = z1-sz; //z-axis negative into screen
size(500,500, P3D);
background(255, 127, 0); //orange background
camera(width, -300, 100, width/2, height/2, -200, 0, 1, 0);
/* camera positioned at the far right hand edge of the x-axis screen,
300 pixels above the origin and 100 pixel back from the origin.
Looking at the centre of the screen 200 pixels behind the origin on
the z-axis with the y-axis as up.
*/
//face1 - front
beginShape();
PImage dice_1 = loadImage("dice-1.gif");
texture(dice_1);
vertex(x1,y1,z1,0,0);
vertex(x2,y1,z1,sz,0);
vertex(x2,y2,z1,sz,sz);
vertex(x1,y2,z1,0,sz);
endShape();
//face4 - top
beginShape();
PImage dice_4 = loadImage("dice-4.gif");
texture(dice_4);
vertex(x1,y1,z1,0,0);
vertex(x2,y1,z1,sz,0);
vertex(x2,y1,z2,sz,sz);
vertex(x1,y1,z2,0,sz);
endShape();
//face6 - back
beginShape();
PImage dice_6 = loadImage("dice-6.gif");
texture(dice_6);
vertex(x1,y1,z2,0,0);
vertex(x2,y1,z2,sz,0);
vertex(x2,y2,z2,sz,sz);
vertex(x1,y2,z2,0,sz);
endShape();
//face3 - bottom
beginShape();
PImage dice_3 = loadImage("dice-3.gif");
texture(dice_3);
vertex(x1,y2,z1,0,0);
vertex(x2,y2,z1,sz,0);
vertex(x2,y2,z2,sz,sz);
vertex(x1,y2,z2,0,sz);
endShape();
//face5 - right
beginShape();
PImage dice_5 = loadImage("dice-5.gif");
texture(dice_5);
vertex(x2,y1,z1,0,0);
vertex(x2,y2,z1,sz,0);
vertex(x2,y2,z2,sz,sz);
vertex(x2,y1,z2,0,sz);
endShape();
//face2 - left
beginShape();
PImage dice_2 = loadImage("dice-2.gif");
texture(dice_2);
vertex(x1,y1,z1,0,0);
vertex(x1,y2,z1,sz,0);
vertex(x1,y2,z2,sz,sz);
vertex(x1,y1,z2,0,sz);
endShape();
The code above creates the following image of a 3D dice:

EXERCISES
The exercises for this chapter concern matrix algebra which forms part of the Mathematics for Computing course and will be covered in that section of the notes in time. Therefore the exercises for this chapter have been omitted.