For this project, you are going to create, load and render some basic 3D models. In addition, you will create a simple orbit camera to help navigate a 3D scene.
In the end, your program will look something like the following:
When rendering in 3D, you have to change the size() function that you use to start up a Processing application, by adding P3D as a third parameter:
size(1600, 1000, P3D);
Perhaps the most important aspect of any 3D environment is the camera; without one (even a simple one), you can’t see anything.
The camera affects how the user views the information, and the implementation details of a camera system can make or break an application. In this assignment your camera is going to be a simple orbit camera which rotates around, and looks at, a specific point (an x, y, z location). This will be accomplished by using spherical coordinates, which is very similar to the concept of polar coordinates, just with an additional component to calculate.
Whether we’re talking about Processing, DirectX, or any other rendering system, you will need two main pieces of information for a camera. A projection matrix, and a view matrix. Most graphics APIs have functions to allow the programmer to easily create these, and Processing is no different.
The projection matrix can be created (and set for you, behind the scenes), by calling this function:
perspective(radians(50.0f), width/(float)height, 0.1, 1000);
This function only needs to be called once, unless you want or need to change some of the values. The first value is typically the one that would change—a smaller angle is a narrower field of view, which can simulate zooming in on a target. 50 might be a “normal” field of view, while 10 would be zoomed in, and 90 would be much wider.
The second function, one that you will typically call every frame (unless you have a fixed camera that never needs to change position or look at something else):
camera(positionX, positionY, positionZ, // Where is the camera? target.x, target.y, target.z, // Where is the camera looking?
0, 1, 0); // Camera Up vector (0, 1, 0 often, but not always, works)
In this assignment you are going to create a class for the camera functionality. There are existing camera libraries available for use in Processing; YOU MAY NOT use them for this assignment. Your class should contain the following functions:
Update() – Called every frame from within the main draw() function, calculates values to pass to the camera() function
AddLookAtTarget(PVector) – Add a target to the list of positions to cycle through
CycleTarget() – Move to the next target in the list
Zoom(float) – Move toward or away from the look at target
Theta – has a range of 0 to 180 degrees (or 0 to π radians). If the angle is 0, that refers to straight up, along the Y axis. If the angle is 90 degrees, or π/2 radians, the vector would lie flat along the X/Z plane, and if the angle were 180 degrees, the final point would lie somewhere on the -Y axis.
Phi – has a range of 0-360 degrees (or 0 to 2π radians)
The radius in this application, will be the camera’s offset from the target. For this application the range is 30 to 200, but your own program could use any value you like. (Though generally you wouldn’t want to have a negative radius as some of the controls would then feel inverted.)
A basic implementation of getting the angles would be to use the map() function for the mouse X and Y positions
The X, Y, and Z positions are relative to wherever the sphere is centered—in this assignment, that will the “look at” target. So the final position of the camera will be:
cameraPosition.x = lookatTarget.x + derivedX; cameraPosition.y = lookatTarget.y + derivedY; cameraPosition.z = lookatTarget.z + derivedZ;
The intended use of the camera class would be to create an instance, add look at targets in the setup function, and then call Update() every frame in the draw function to calculate the proper location
The value retrieved in that function can be sent to your camera’s Zoom function. You may want to scale the value, as one “tick” of a mouse wheel might need to be many units in a 3D environment.
Navigating 3D space without some frame of reference can be an exercise in frustration. While a simulation or game will have a lot of rendered content to represent the details of some environment, many stages of development won’t yet have that content. So what do you do? Create some! A simple grid to represent a ground of sorts, centered on
the origin (0, 0, 0) will suffice.
To create one, you can use the line() function and a couple of loops to create something like the image on the right. That grid has minimum and maximum values of -100 and 100 along the X and Z axes, with lines every 10 units. Depending on what you were working on you might use other values for a larger or denser grid, and you might have some colors or other indicators of which axis is which. (It’s very common in 3D applications to color-code the axes such that X is red, Y is green, and Z is blue—just think XYZ -> RGB.)
Processing handles collections of vertices in a class called PShape. The data for such an object can be created manually, or loaded from a file. In addition, data can be dynamically created and rendered on- demand, but this process is slower, and shouldn’t be used where application performance is critical.
It’s possible to create and render shapes immediately, as needed—this is something referred to as immediate mode rendering. This is typically a slower process, performance-wise, but it can be very helpful for the programmer as it allows them to get something up and running with minimal effort. In Processing this can be accomplished by using the beginShape() and endShape() functions, which you have already used in previous assignments. The only difference here is that in 3D, the vertex() function will need a 3rd component.
For example, if you wanted to create a single triangle, you might write:
You can also set per-vertex colors when creating a custom shape. This is done by calling the fill() function before each call to vertex.
This process can be used to create arbitrarily complex shapes, including shapes which are made of multiple polygons.
When creating complex shapes, it’s important to use the beginShape() function properly. Up until this point you haven’t had to pass it any
parameters, but you can pass a variety of values to it, which will determine how the data you store in the shape gets processed at render time.