P3D
This tutorial is for Processing's Python Mode. If you see any errors or have comments, please let us know. This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
What is P3D?In Processing, there are many render modes: the default renderer, P2D, P3D, and PDF. To use a non-default renderer, you can specify via the size() function. def setup(): size(200,200,P3D) Now, you may be wondering: "Which render mode should I choose and why?" The mode itself tells Processing what to do behind the scenes when drawing the display window. For example, the default renderer employs existing Java (the language Processing.py is built on) 2D libraries to draw shapes, set colors, display text, etc. When deciding which renderer to use, you are balancing a number of factors: speed, accuracy, and general usefulness of the available features. In most cases, particularly when you are first learning Processing, you will want to use the default renderer setting. It provides the most elegant and accurate results when drawing in 2D. Switching to P2D or P3D is advisable given one of the following scenarios:
3D TransformationsBefore we begin drawing in 3D, it's important to note that as soon as we give ourselves over to the illusion of 3D space, a certain amount of control must be relinquished to the P3D renderer. You can no longer know exact pixel locations as you might with 2D shapes, because the 2D locations will be adjusted to create the illusion of 3D perspective. In order to draw something at a point in three dimensions the coordinates are specified in the order you would expect: x, y, z. Cartesian 3D systems are often described as "left-handed" or "right-handed." If you point your index finger in the positive y direction (up) and your thumb in the positive x direction (to the right), the rest of your fingers will point towards the positive z direction. It's left-handed if you use your left hand and do the same. In Processing, the system is left-handed, as follows: In other words, positive is coming at you, negative is moving away from you. Let's say we want to draw a rectangle that moves towards the viewer using P3D. We know that to draw a rectangle, the rect() function takes four arguments: x location, y location, width, and height. rect(x,y,w,h) Our first instinct might be to add another argument to the rect() function.
This, however, is incorrect. In order to specify 3D coordinates for shapes in Processing, you have to use translate(). Now, translate() is not exclusive to 3D sketches and is quite commonly used in 2D. In fact, there's an entire 2D transformations tutorial that I suggest you stop and read right now unless you are already comfortable with the concept of translation (and rotation) in Processing. Assuming, however, that you are already familiar with how translate() works in 2D, there isn't a lot to learn here beyond the addition of a single argument. In 2D, translate looks like: "translate(x,y)", and 3D we add one more argument: "translate(x,y,z)". def setup(): global x,y,z size(200,200,P3D) x = width/2 y = height/2 z = 0 def draw(): global x,y,z translate(x,y,z) rectMode(CENTER) rect(0,0,100,100) z += 1 # The rectangle moves forward as z increments. The third dimension also opens up the possibility of rotating around different axes. When we say plain old rotate() in Processing, what we are really saying is rotate around the Z axis (i.e. spin on the plane of the window itself). In 3D, the equivalent is rotateZ(). def setup(): size(200, 200, P3D) background(100) rectMode(CENTER) fill(51) stroke(255) translate(100, 100, 0) rotateZ(PI/8) rect(0, 0, 100, 100) We can also rotate around the x and y axes. rotateX(PI/8) rotateY(PI/8) As well as multiple axes at a time. translate(100, 100, 0) rotateX(PI/8) rotateY(PI/8) rotateZ(PI/8) rect(0, 0, 100, 100) 3D ShapesOnce you know how to translate and rotate around a three-dimensional coordinate system, you are ready to draw some three-dimensional shapes. You are probably quite comfortable with drawing shapes in 2D whether primitive (line(), rect(), ellipse(), triangle(), etc.) or custom (beginShape(), endShape(), and vertex()). The good news is that shapes in 3D work pretty much the same way. There are primitive shapes that you get for free such as box() and sphere() as well as custom shapes you can make with calls to vertex(). def setup(): size(640,360,P3D) background(0) lights() pushMatrix() translate(130, height/2, 0) rotateY(1.25) rotateX(-0.4) noStroke() box(100) popMatrix() pushMatrix() translate(500, height*0.35, -200) noFill() stroke(255) sphere(280) popMatrix() See also: Primitives3D In the above example, note that the functions box() and sphere() each only take one argument: size. These 3D shapes cannot be positioned via arguments, rather you should use the translate() and rotate() methods described previously. Custom 3D shapes are drawn using beginShape(), endShape(), and vertex() by placing multiple polygons side by side. In 3D, the vertex() function takes 3 arguments: x, y, and z. Let's say we want to draw a four-sided pyramid made up of four triangles, all connected to one point (the "apex") and a flat plane (the "base"). def setup(): size(640, 360, P3D) background(0) drawShape() def drawShape(): translate(width/2, height/2, 0) stroke(255) rotateX(PI/2) rotateZ(-PI/6) noFill() beginShape() vertex(-100, -100, -100) vertex( 100, -100, -100) vertex( 0, 0, 100) vertex( 100, -100, -100) vertex( 100, 100, -100) vertex( 0, 0, 100) vertex( 100, 100, -100) vertex(-100, 100, -100) vertex( 0, 0, 100) vertex(-100, 100, -100) vertex(-100, -100, -100) vertex( 0, 0, 100) endShape() Note above how it's often simpler to specify vertex locations using a standardized unit of measure (i.e. 1 pixel) and relative to a point of origin (0,0,0). The size and position of the shape is then set using matrix transformations: translate(), rotate(), and scale(). For some examples of more sophisticated custom shapes built in 3D, take a look at these examples: RGB Cube, Vertices, Toroid, Isocahedra. TexturesIn P3D mode, you can load and display images just like you do in 2D (see: Images and Pixels Tutorial). Everything we covered under transformations can be applied to images; they can be translated, rotated, and scaled in a virtual 3D space. In addition to drawing an image the old-fashioned way, however, images can be made into "textures" and applied to a shape. This is particularly useful when you want a 3D shape to resemble a real-world object. For example, applying an image of earth as a texture on a sphere will result in a globe. To apply an image as a texture to a shape, we first need to define the shape using beginShape() and endShape() as demonstrated in the previous section. Let's say you are drawing a rectangle, like so:
def setup(): size(640, 360,P3D) background(0) translate(width/2, height/2) stroke(255) fill(127) beginShape() vertex(-100, -100, 0) vertex( 100, -100, 0) vertex( 100, 100, 0) vertex(-100, 100, 0) endShape(CLOSE) The above example is a simple square with four vertices, a white outline, and a grey fill. To apply an image to the shape, we have to follow three steps. 1) Load an image into a PImage object. def setup(): global img size(640, 360, P3D) img = loadImage("berlin-1.jpg") # PImage object 2) Call texture(). The texture() function must be called between beginShape() and endShape() and before any calls to vertex(). The texture() function receives only one argument, the PImage that will be applied as a texture. def draw(): global img background(0) translate(width / 2, height / 2) stroke(255) fill(127) beginShape() texture(img) Once we have specified the texture itself, we have to then define the mapping of the image to the shape itself. This is a simple problem when the shape is rectangular (four corners of a shape map to four corners of an image), but grows more complex when you have many more vertices in a shape (such as in the globe example above). To define the mapping, two more arguments (usually referred to as u and v are added to each call to vertex(). By default, the coordinates used for u and v are specified in relation to the image's size in pixels, but this relation can be changed with textureMode(). In addition, when textures are in use, the fill color is ignored. Instead, use tint() to specify the color of the texture as it is applied to the shape. def draw(): background(0) translate(width / 2, height / 2) beginShape() texture(img) vertex(-100, -100, 0, 0, 0) vertex( 100, -100, 0, 400, 0) vertex( 100, 100, 0, 400, 400) vertex(-100, 100, 0, 0, 400) endShape() If the above seems like a trivial example, that's because it is. After all, if we're just going to texture a square, we can just draw the image using image(). Nevertheless, understanding the above process opens up a world of possibilities as we can now apply image textures to arbitrary 2D and 3D shapes. For some examples of more sophisticated mappings, check out Texture Triangle, Texture Cylinder, and Texture Cube. LightingIn P3D, you can also manipulate the lighting of the elements in your scene. Of course, just as drawing in three-dimensions is an illusion, the addition of lighting to a Processing sketch is a simulation of the idea of real world lighting for the purpose of creating a variety of effects. This is particularly useful since some objects (such as a sphere) do not appear three-dimensional until they are lit. If you don't want to get into the details of setting custom lighting for a 3D scene you can use Processing's lights() function which sets default lighting. Take a look at the following example, where a sphere is lit with default lighting only when the mouse is pressed. def setup(): size(200, 200, P3D) def draw(): background(0) translate(100, 100, 0) if (mousePressed): lights() noStroke() fill(255) sphere(50) Note how the call to the lights() function is included in draw(). Just as with matrix transformations, the 3D scene is reset each time through draw() any therefore any lighting must be included in order to remain persistent. In order to set custom lighting for your scene there are four different kinds of lights.
ambientLight(0,0,255) directionalLight(0, 255, 0, 0, -1, 0)
All together it looks like: spotLight(255, 0, 0, width/2, height/2, 400, 0, 0, -1, PI/4, 2) pointLight(255, 0, 0, width/2, height/2, 400) For additional examples of lighting in Processing, take a look at: Lights on off, Spot, Directional, Reflection. PerspectiveP3D mode consists of two different "projection" modes which control the way the renderer creates the 3D illusion. "Perspective" mode is the default and uses the trick of displaying objects that are farther away as smaller. This effect is commonly referred to as "foreshortening." In most cases, you don't need to specify the parameters of perspective projection, but you can with the perspective() function. The parameters of the function define a viewing volume with the shape of truncated pyramid. Objects near to the front of the volume appear their actual size, while farther objects appear smaller. The triangle has a field of view (angle in radians), an aspect ratio, and a maximum and minimum z location that defines the clipping plane (how far and how close Processing will actually bother to try to render things). An example (that recreates the default perspective) looks like so: fov = PI/3 cameraZ = (height/2.0) / tan(fov/2.0) perspective(fov, float(width)/float(height), cameraZ/10.0, cameraZ*10.0) It's not too often you'll need to change these parameters, but if you do, altering the field of view ("fov") tends to have the effect of zooming objects in and out (as the viewing volume grows and shrinks) and changing the aspect ratio can skew the rendering of objects making the appear fatter or skinnier. For an example of this, take a look at: Perspective. The other projection mode available in P3D is known as "orthographic" projection. In orthographic mode, all objects with the same dimension appear the same size, regardless of whether they are near or far from the camera. This is commonly used to achieve a certain visual style, such as one found in early video games like Q-Bert. To enable orthographic projection, all you need to do is call ortho() ortho() doesn't require any arguments, although you'll see there are some optional ones to define a clipping plane. Following is an example that shows a scene in both projection modes, depending on whether the mouse is pressed.
def setup(): size(640, 360, P3D) noStroke() fill(204) def draw(): background(0) lights() if(mousePressed): fov = PI/3.0 cameraZ = (height/2.0) / tan(fov/2.0); perspective(fov, float(width)/float(height), cameraZ/2.0, cameraZ*2.0) else: ortho(0, width, 0, height) translate(width/2, height/2, 0) rotateX(-PI/6) rotateY(PI/3) box(160) CameraWhen looking at a 3D scene in a Processing window, we can think of our view of the scene as a camera. Zoom in closer to the objects and we can imagine a camera zooming in. Rotate around the scene and the camera rotates. Of course, there is no actual camera, this is just a convenient device to help us understand how to traverse a 3D scene. Simulating a camera can be done through clever transformations at the beginning of draw() by using translate(), rotate(), and scale() to manipulate our view of the scene. Nevertheless, for convenience there is also a camera() function whose purpose is also to simulate a camera. The function defines a camera as having an "eye position", i.e. the camera location, a scene "center" which tells the camera which way to point, and an upward axis which aligns the camera vertically. The default camera position is essentially right between your eyes: a location out in front of the window aligned straight up and pointing towards the screen. Here are the numbers for the default position.
When written in code, this looks like: camera(width/2, height/2, (height/2) / tan(PI/6), width/2, height/2, 0, 0, 1, 0); Any of the arguments in the camera() function can be made into a variable to simulate camera movements. For example, by moving the x position of the eye according to the mouse, you can rotate around an object to see it from a different angle.
def setup(): size(640, 360, P3D) def draw(): background(0) camera(mouseX, height/2, (height/2) / tan(PI/6), width/2, height/2, 0, 0, 1, 0) translate(width/2, height/2, -100) stroke(255) noFill() box(200) If you move both the eye position and the scene's center according to the mouse, you can create the effect of panning.
def setup(): size(640, 360, P3D) def draw(): background(0) camera(mouseX, height/2, (height/2) / tan(PI/6), mouseX, height/2, 0, 0, 1, 0) translate(width/2, height/2, -100) stroke(255) noFill() box(200) For other examples that make use of the camera() function, take a look at: Move Eye. This tutorial is for Processing's Python Mode. If you see any errors or have comments, please let us know. This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. |
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License