Goals

  • Understanding 3D
  • The Z-Axis
    • Depth
    • Perspective
    • Camera
  • Challenges
    • Camera: Fixed or Floating
    • Generative Work
    • Games: Extra Work
  • The Return of the PVector

Resources

"What's troubling is that because the camera is 3D, the northern part of the screen isn't necessarily north anymore. So the jungle transforms into the true meaning of a jungle. For someone with no sense of direction like myself, I get lost in the caves every time." ~ Hideo Kojima

Depth

We already know in our 2D processing experience that the x axis moves left and right, and the y axis moves up and down. But what if we add another dimension to this system? Moving in and out of the screen, otherwise known as the z axis.

The top left wall of this image is our original processing screen, but now we have an extra dimension, how close or far our objects are.

Perspective

When working in 3D it can be useful to control the perspective. What actually ends up on screen is not always the same image.

Just like in the real world, perspectives can be changed, skewed, even distorted.

This image represents the "view frustum" which is the object that captures the 3D scene and decides how it will get translated into 2D before appearing on the screen.

In processing you control the size and shape of the view frustum using the perspective() method.

perspective(fov, aspect, z-near, z-far);
Where:
fov = field of view (default = PI/3.0, larger fov makes further objects smaller)
aspect = aspect ratio of screen (default = (float)width/(float)height)
z-near = z-near clipping plane, how close can the closest object be
z-far = z-far clipping plane, how far can the farthest object be

Camera

We also must specify where our camera is. This is done using the camera() method.

The camera method has 9 parameters, but can be broken down into 3 groups: position, target position, tilt.

Here's an example call to the perspective method using PVectors for each of the groups:

camera(pos.x, pos.y, pos.z,
       target.x, target.y, target.z,
       tilt.x, tilt.y, tilt.z);

Where:
pos = the position of the camera in 3-space
target = the viewing target of the camera (what is it looking at)
tilt = which way is up (default = (0, 1, 0) y-axis is up)

Challenges

Adding an extra dimension may sound like a good idea in the beginning. However, this can open up a number of new and unexpected challenges.

Camera: Fixed or Floating

There are lots of problems when it comes to choosing the type of camera for a 3D environment. Where is your camera going to be, will it capture all the action? Can a user see everything? Are there objects in the way? These are just a few of the issues that arise.

In 2D the worst problem we had to encounter was called the painter's algorithm. Each item that was drawn to the screen would overlap all other existing items. In this way, the surface of the screen was built up like a stack. The first items on were at the bottom of the stack, and the last item drawn would be at the top.

Fixed Camera

We can choose to fix our camera in 3D to a specific location, such that it never moves. This will basically leave us with the same problems of a 2D scene, although instead of the painter's algorithm, choosing the order in which to paint items, we'll have to order them along the z axis.

A fixed camera can also mean that the camera is fixed along a 1 or 2 axes, so the camera may move left and right, or up and down with the scene. The issues are the same as above. Objects must have the correct z depth to be visible.

Floating Camera

This is where things become difficult. Not only do you have to worry about the items in your scene being in the correct place to be visible, the camera as it floats around may get stuck behind items. When items are hidden from view, this is called occlusion.

If the user should interact with Block A, but the camera is behind Block B so that Block A is occluded, there's really not much that can happen.

Additionally, a constantly moving camera must be smooth and not jerky, but fast enough to catch the action. There are many challenges here.

Generative Work

Up until now, our generative pieces in 2D all have had something in common. We don't clear the background so that the image can grow as it is generated.

This is possible in 3D but with one significant drawback. The camera cannot move. Which begs the question, why use 3D at all?

If the camera is moved, the entire scene must be re-drawn to account for the changed perspective. With some generative pieces drawing millions of shapes, this is not an easy task.

Games: Extra Work

Think about a collision in 2D, we only have to check 2 conditions for a simple box collision. But in 3D we have that extra dimension to worry about. Now ever box has a width, height AND depth.

The bounds of the world are also an additional headache. we must make sure our objects don't go outside the x and y axis as well as the z axis. This simply means more checks and more if statements. With a large number of objects in a scene, this will theoretically slow down a program to 66% of it's 2D counterpart.

The only saving grace when moving to 3D is circular collisions / interactions. While the distance formula now needs to incorporate the z dimension. The check whether it is less than the combined radii of the two objects for a collision remains the same.

PVectors

PVectors are a special datatype in Processing that store x, y, and z components. They also have a lot of useful built in math functions.

Using methods, we can add, sub, mult, div, dot, normalize, mag, limit... etc... our PVector objects.

When working in 3D there's absolutely no reason to not use PVectors. There are so many small additions and multiplications that can be avoided by learning to use the PVector object efficiently.

A Note on C++ (and some other languages)

When working with vectors in C++, you can have vectors of any type (int, float, bool...). Also, the math operators (+, -, *, /, +=, -=, ...) can be overloaded, which means that adding two vectors is as easy as: Vec1 += Vec2.

    fill(red(c) - (255 - decay), green(c) - (255 - decay), blue(c) - (255 - decay));
  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
 
float bound = 512; //sets a boundary for objects
 
ArrayList ballList = new ArrayList();
//stores camera information so we can move around
Camera cam = new Camera();
 
void setup() {
  size(512, 512, OPENGL);
  sphereDetail(6); //Set low for low power systems
  perspective(PI/2.2, (float)width/(float)height, 0.2, bound * 4);
  lights();
}
 
void draw() {
  background(0);
 
  directionalLight(64, 64, 64, 0, 1, 0);
  directionalLight(255, 255, 220, 0, -1, 0);
  directionalLight(255, 255, 220, 1, -1, 0);
  directionalLight(255, 255, 220, -1, -1, 0);
 
  if (mousePressed) {
    ballList.add(new Ball());
  }
 
  cam.update();
  camera(cam.pos.x, cam.pos.y, cam.pos.z,
  cam.target.x, cam.target.y, cam.target.z,
  cam.tilt.x, cam.tilt.y, cam.tilt.z);
 
  //draw a bounding box for the scene
  stroke(255);
  noFill();
  box(bound);
  noStroke();
 
  //loop through ballList
  for (int i = 0; i < ballList.size(); i++) {
    Ball b = (Ball) ballList.get(i);
    for (int j = i; j < ballList.size(); j++) {
      Ball b2 = (Ball) ballList.get(j);
      b.repel(b2, 64);
    }
    b.updatePos();
    b.wallCollision();
    b.drawMe();
  }
}
 
//Will need a Camera object named cam to use this code
void keyPressed() {
  if (key == 'q') cam.up = true;
  if (key == 'e') cam.down = true;
  if (key == 'w') cam.in = true;
  if (key == 'a') cam.left = true;
  if (key == 's') cam.out = true;
  if (key == 'd') cam.right = true;
}
void keyReleased() {
  if (key == 'q') cam.up = false;
  if (key == 'e') cam.down = false;
  if (key == 'w') cam.in = false;
  if (key == 'a') cam.left = false;
  if (key == 's') cam.out = false;
  if (key == 'd') cam.right = false;
}
 
//A simple Ball Object
class Ball {
  //fields
  PVector pos, vel, acc;
  float mass, damp;
  int decay;
  color c;
 
  //constructor
  Ball() {
    //pos = new PVector(0, 0, 0);
    //vel = new PVector(random(-8, 8), random(-8, 8), random(-8, 8));
    vel = new PVector(0, 0, 0);
    acc = new PVector(0,0,0);
    mass = random(4, 16);
    pos = new PVector(random(-bound/2 + mass, bound/2 - mass),
                        random(-bound/2 + mass, bound/2 - mass),
                        random(-bound/2 + mass, bound/2 - mass));
    
    damp = 0.99;
    decay = 255;
    c = color(random(255), random(255), random(255));
  }
 
  void repel(Ball o, float force) {
    PVector dir = PVector.sub(pos, o.pos);
    float distSq = sq(dir.mag());
    if (distSq > 1) {
      dir.normalize();
      float f = 1/distSq * 32 * force;
      
      acc.add( PVector.mult( dir, f / mass ) );
      o.acc.sub( PVector.mult( dir, f / o.mass ) );
    }
  }
 
  //method to update velocity, position and clear forces from our acceleration
  void updatePos() {
    vel.add(acc);
    vel.mult(damp);
    pos.add(vel);
    acc.set(0,0,0);
 
    if (decay < 1) {
      ballList.remove(this);
    }
  }
 
  //wall collisions
  void wallCollision() {
    if (abs(pos.x) > bound/2 - mass) {
      vel.x *= -1;
    }
    if (abs(pos.y) > bound/2 - mass) {
      vel.y *= -1;
    }
    if (abs(pos.z) > bound/2 - mass) {
      vel.z *= -1;
    }
  }
 
  void drawMe() {
    fill(red(c) - (255 - decay), green(c) - (255 - decay), blue(c) - (255 - decay));
    pushMatrix();
    translate(pos.x, pos.y, pos.z);
    sphere(mass);
    popMatrix();
    decay--;
  }
}
 
//Stores and updates our camera information
//Keeps track of keyPresses, multiple keys can be down at one time
class Camera {
 
  PVector pos, target, tilt;
  boolean up, down, left, right, in, out;
  float angle, zoom, speed;
 
  Camera() {
    pos = new PVector(0, 0, 0); //place outside the boundary of our objects
    target = new PVector(0, 0, 0); //look at the origin
    tilt = new PVector(0, 1, 0); //tilt the y axis up
    up = down = left = right = in = out;
    angle = 0;
    zoom = bound;
    speed = 8;
  }
  void update() {
    if (up) pos.y -= speed;
    if (down) pos.y += speed;
    if (left) angle += speed/512;
    if (right) angle -= speed/512;
    if (in) zoom -= speed;
    if (out) zoom += speed;
    
    pos.x = zoom * cos(angle);
    pos.z = zoom * sin(angle);
  }
}