Sixth tutorial: 3D and output.
Today we are going to take the step from 2d to 3d, and also to look at how to export data from Processing into dxf. To be able to do this two things easier, we will also look at how to use libraries.
Drawing in 3D:
Drawing in 3D in Processing is in principle relatively simple, and we have actually already looked at some 2D code that we can reuse and make into 3D. Both in 2D and 3D there are two basic forms of drawing: one giving coordinates directly, as we have done when using line() or point(), or using transformations, such as translate(), rotate() (and related variations such as rotateX(), rotateY(), rotateZ()), scale(), pushMatrix() popMatrix() and resetMatrix(). All these affect the current transformation matrix the program uses to draw, a concept that you can think as working similarly to the coordinate systems of CAD programs. In this sense, what one rotates are not objects, but the coordinate system we use to draw them. This is a standard way of drawing things in many programming and scripting languages and environments. Lets remind then how these transformations work in 2D, which is easier to understand, before we take the step into 3D:
1. Transformations:
We can draw a simple rectangle like this (we also draw lines along the x and y axis to visualise it better):
size(100,100); //draw X Axis (blue) stroke(0,0,255); line(0,0,50,0); //draw Y Axis (red) stroke(255,0,0); line(0,0,0,50); //draw rectangle stroke(0,0,0); rect(40,30,20,40);
The result will be then something like this:
But if we also rotate and translate the coordinate system before we draw the rectangle like this:
size(100,100); translate(40,5); rotate(radians(45)); //"radians()" converts degrees to radians, which is the kind of angles rotate takes... //after these transformations, the coordinate //system will be rotated and translated //draw X Axis (blue) stroke(0,0,255); line(0,0,50,0); //draw Y Axis (red) stroke(255,0,0); line(0,0,0,50); //draw rectangle stroke(0,0,0); rect(40,30,20,40);
It is important to remember that the order of the transformation matters! it is not the same to move the coordinate system first and then rotate it, than to rotate it first, and then move it. (you can easily see this by switching the order of those two lines). Transformations can be a bit counterintuitive, but a good way of thinking about them is to read them backwards starting from the last one. We could visualise the previous transformations then as rotating the coordinate system 45 degrees clockwise first, and then moving it to position (40, 5).
Transformations accumulate, which allow us to compose them, as we can show if we draw the same rectangle twice, with transformations between the two calls to the rectangle() function:
size(100,100); translate(40,5); rotate(radians(45)); //draw rectangle (observe, at 0,0) rect(0,0,20,40); //rotate and translate coordinate system rotate(radians(30)); translate(40,5); //draw another rectangle, with the same parameters rect(0,0,20,40);
There are a number of functions to deal with this behaviour. We can reset transformations with the function resetMatrix(), any time. We can also store the transformations at some stage in our code with pushMatrix() and restore the transformations at that stage with popMatrix(). All calls to pushMatrix() need to be matched with a popMatrix() at some point in the code, or Processing will complain. like brackets, they can be included in each other, and composed in different ways, like in this example:
We first create a simple function to draw an arc, which we call drawArm(), using rotate(), which translates and rotates the coordinate system, and then draws a rectangle and a line in the middle of it at each iteration of a for loop:
void drawArm(float angle, int times){ stroke(0); fill(255); //for every iteration in the loop, it translates //the length of a segment, and it rotates it rads... for(int i=0;i<times;i++){ translate(0,10); rotate(angle); rect(-2,0,4,10); line(0,0,0,10); } }
We also have defined a function to draw the coordinate system at any point (as we have done above), which takes a number that it outputs as text, so we can keep track of the transformations, like this:
void drawCoordinateSystem(int number){ //draw X Axis (blue) stroke(0,0,255); line(0,0,20,0); //draw Y Axis (red) stroke(255,0,0); line(0,0,0,20); fill(0); text(number,5,10); }
Now we can test how pushMatrix() and popMatrix work, and how we can use them to compose transformations, like this:
void setup(){ size(300,300); //set the font and font size PFont myFont = createFont("Helvetica", 32); textFont(myFont); textSize(10); } void draw(){ background(255,255,255); drawCoordinateSystem(0); translate(20,150); drawArm(radians(-10), 10); drawCoordinateSystem(1); //just to see. pushMatrix(); //we want to get back here. drawArm(radians(10), 10); drawCoordinateSystem(2); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. //we do it again pushMatrix(); //we want to get back here... drawArm(radians(-7), 20); drawCoordinateSystem(3); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. //we do it again without pushing drawArm(radians(-3), 20); drawCoordinateSystem(4); //we are here before calling popMatrix() //now we push and pop... pushMatrix(); drawArm(radians(5), 6); drawCoordinateSystem(5); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. //and once more... pushMatrix(); drawArm(radians(-20), 6); drawCoordinateSystem(6); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. } void drawArm(float angle, int times){ stroke(0); fill(255); //for every iteration in the loop, it translates //the length of a segment, and it rotates it rads... for(int i=0;i<times;i++){ translate(0,10); rotate(angle); rect(-2,0,4,10); line(0,0,0,10); } } void drawCoordinateSystem(int number){ //draw X Axis (blue) stroke(0,0,255); line(0,0,20,0); //draw Y Axis (red) stroke(255,0,0); line(0,0,0,20); fill(0); text(number,5,10); }
And this is the result:
2. 3D, actually.
And now that we have looked at how transformations work, lets take the step into 3D. The basic logic is the same, but obviously, in 3D. The default mode of Processing is 2D, so we need to specify that we are going to use 3D, like this:
size(500,300,P3D); //this tells processing that the sketch will use 3D, besides specifying the size. translate (100,100,0); //we make the first translation, to point 100,100,0 (3D!) box(50,50,50); translate (100,0,0); //we translate the coordinate system 100 to the right... box(50,50,50); translate (100,0,0); box(50,50,50); translate (100,0,0); box(50,50,50);
As you can see, doing something in 3D is basically the same as doing it in 2D, but with an extra dimension. It would be however really nice to be able to be able to rotate our 3D geometry, and to have a basic modelling interface for it. In processing there exist a camera() function that enables some control over the way we can visualise our objects in 3D. However, the way this camera may change in relation to mouse or keyboard input, so it can rotate pan, or do any other transformation to those we are accustomed to in modelling, cad programs or computer games needs to be written, a code that is not trivial, since it requires a good understanding of the way transformations are performed in Processing (or for that matter in any other programming environment with a similar paradigm). This also produces lots of “boilerplate” code, that needs to either be included in each project, or otherwise developed into a separate library (more about libraries in a second), a process that also requires some level of expertise. As it is often the case in Processing, luckily there is a library (actually a few) to be able to do this easily. We are going to use one such library called PeasyCam. But first an introduction to what libraries are:
3. Libraries:
Libraries are code that extends the functionality of a programming language. They consist, depending of the language and context, of predefined functions, classes or variables that make certain services and behaviours available to our programs, once properly included and linked. Libraries are one of the most common forms of reusing code, besides copying and pasting code or including code directly into our projects. Libraries are essential also if we want to make complex code available to others. Usually libraries are made available under a legal license that specifies how they can be used in different projects, as for example the Open Source LGPL license, under which most of Processing is released.
If you want to include someone else’s libraries in your code using the Processing IDE (the program we are using to write code in this course), there are a number of options: some libraries come already pre-installed, such as dxf, net, pdf or serial, and we don’t need to do much with these, except, like with any other library, include them in our code. Some contributed libraries can be installed through the the Processing IDE, and some need to be installed manually. Rather than repeating things in this text, the best reference can be found in the processing webpage section for libraries. A detailed explanation on how to install libraries can be found here.
4. PeasyCam.
We are going to look at two different libraries in this tutorial, a preinstalled one for dxf output, called Dxf Export, and a contribution library for managing views in 3D, called PeasyCam, by Jonathan Feinberg.
We will start with PeasyCam. PeasyCam is a contributed library that can be easily installed through the Processing IDE, rather than following the manual process. For installing PeasyCam, go to the “Sketch” menu and navigate to:
“Sketch -> Import Library -> Add Library…”
And there either scroll or type PeasyCam on the search field. Select it and click the “Install” button. that’s it. Now that it is installed, we can easily use it by writing on the top of our program:
import peasy.*;
Which is the standard way in java of importing a library. For actually using it in our program we need to create a PeasyCam object for the sketch. This is a easy as:
import peasy.*; PeasyCam cam; void setup(){ size(500, 500, P3D); cam = new PeasyCam(this, 100); noStroke(); } void draw(){ background(0); lights();//turn on the lights! rotateX(radians(45)); //this is only so the lights look good rotateY(radians(30)); box(45); }
it will look like this, though obviously this says nothing about the interaction:
Now you can rotate by dragging the mouse with the left button pressed, you can zoom in and out with the right button, and the middle button (command while you drag on a mac) will pan. Double click will restore the initial view, and shift will lock the rotation to one axis… For more details, and possibilities look at PeasyCam webpage.
Observe also that we are using lights for rendering our geometry… you can see all things that can be done with lights on the reference to lights() and related functions. The subject of lights could easily become another tutorial, so we won’t get into it.
5. Dxf.
We are covering much terrain in this tutorial… finally we are going to look at how to output dxf data from our Processing sketches. This is actually quite easy, even if all data we can output will be converted into line segments and triangular faces. This data can then be imported into a modeller or CAD program and further manipulated. For doing this we will use the pre-installed Dxf Export library. We can include it in our code by writing:
import processing.dxf.*;
At the beginning of our sketch, or, if you find it easier, by going to the menu and doing: “Sketch -> Import Library… -> dxf”. Which will have the same effect. For using the dxf library we just need to write
beginRaw(DXF, "yourfilename.dxf");
before we draw the geometry we want to save (change “yourfilename.dxf” for the name of the dxf file you want ) , and
endRaw();
after.
In a normal drawing loop this means that we will be saving a dxf everytime we draw, so it is recommended to have some way of controlling this, for examples so we only save when the “s” key is pressed. Here is an example of the above program, with a save key. It is a bit silly because it will only save a cube, but it is a simple illustration, and you can easily see the result also:
import processing.dxf.*; import peasy.*; PeasyCam cam; boolean doSave=false; void setup(){ size(200, 200, P3D); cam = new PeasyCam(this, 100); cam.setSuppressRollRotationMode(); noStroke(); } void draw(){ background(0); lights();//turn on the lights! if(doSave){ beginRaw(DXF, "output.dxf"); //start recording to the file. } rotateX(radians(45)); //this is only so the lights look good rotateY(radians(30)); //we also check if the transformations are saved... box(45); if(doSave){ endRaw();//stop recording to the dxf file. println("file saved."); //turn saving off...otherwise it will anyway keep on saving for ever doSave=false; } } //we add this to listen to keys pressed... void keyPressed() { if (key == 's' || key == 'S') { //lower or uppercase doSave=true; } }
6. Saving images.
Before we finish, just some useful functions to save the screen from Processing are save() and saveFrame(). you can easily save everything you have drawn before you call the function, in either TIFF (.tif), TARGA (.tga), JPEG (.jpg), or PNG (.png) formats.
7. All together now…
You may have started seen that lots of code is often the result of copy and pasting snippets of code in the right places…If you wonder what happens when we combine all the code we have seen today, in that manner, this is the result! (there are a few changes in how to draw with transformations, to give them a 3D effect…)
import processing.dxf.*; import peasy.*; PeasyCam cam; boolean doSave=false; void setup(){ size(500,500, P3D); cam = new PeasyCam(this, 100); cam.setSuppressRollRotationMode(); //set the font PFont myFont = createFont("Helvetica", 32); textFont(myFont); textSize(10); } void draw(){ background(255,255,255); if(doSave){ beginRaw(DXF, "output.dxf"); //start recording to the file. } drawCoordinateSystem(0); translate(20,150); drawArm(radians(-10),radians(-20), 10); drawCoordinateSystem(1); //just to see. pushMatrix(); //we want to get back here. drawArm(radians(10),radians(2), 50); drawCoordinateSystem(2); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. //we do it again pushMatrix(); //we want to get back here... drawArm(radians(-1),radians(2), 100); drawCoordinateSystem(3); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. //we do it again without pushing drawArm(radians(-3),radians(5), 70); drawCoordinateSystem(4); //we are here before calling popMatrix() //now we push and pop... pushMatrix(); drawArm(radians(5),radians(2), 20); drawCoordinateSystem(5); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. //and once more... pushMatrix(); drawArm(radians(-10),radians(5), 30); drawCoordinateSystem(6); //we are here before calling popMatrix() popMatrix(); //now we are back to where we pushed. if(doSave){ endRaw();//stop recording to the dxf file. save("Trasformations.png"); //save also an image! println("file saved."); //turn saving off...otherwise it will anyway keep on saving for ever doSave=false; } } void drawArm(float radsZ,float radsY, float times){ stroke(0); fill(255); //for every iteration in the loop, it translates //the length of a segment, and it rotates it rads... for(int i=0;i<times;i++){ translate(0,10,0); rotateZ(radsZ); rotateY(radsY); rect(-2,0,4,10); line(0,0,0,10); } } void drawCoordinateSystem(int number){ //draw X Axis (blue) stroke(0,0,255); line(0,0,0, 20,0,0); //draw Y Axis (red) stroke(255,0,0); line(0,0,0, 0,20,0); //draw Z Axis (green) stroke(0,255,0); line(0,0,0, 0,0,20); fill(0); text(number,5,10); } //we add this to listen to keys pressed... void keyPressed() { if (key == 's' || key == 'S') { //lower or uppercase doSave=true; } }
And it looks like this:
This is the last tutorial of this course… now it is just a question of putting all this into practice…