Introduction: 3D Wireframe Engine Using Processing
This is a instructable about writing your own 3D engine from scratch.
Skills needed:
- Basic understanding of Java and Processing environment
- Basic understanding of geometry and trigonometry
Skills that are useful
- Matrix calculations
- understanding radians
The reason I picked the Processing as the programming environment for the project are for example the simplicity, readiness and easy graphic implementations. It is easy to set up and fast to start sketching. The full code can be found on github: https://github.com/Erbonator3000/Wireframe
Step 1: Getting Started
At start we are gonna make a new project and call it Wireframe. Then we add our setup() and draw() functions. If you are not familiar with Processing, the setup() is called once when we run the code and draw() is run once every frame update. Lets make a window appear on the setup() by calling size(600, 480);. this opens up an empty window with width of 600 and height of 480 when we run our code.
We then ofcourse want to draw something on the screen. Lets now just paint the whole window black by calling background(0,0,0); on the draw function. This sets the background of the window to desired color (Red, Green, blue), in this case black.
We can now draw dots, lines, rectangles and ellipses. More info about drawing can be found from Prosessings tutorials, for example from: https://processing.org/tutorials/drawing/
Lets just draw a little crosshair to the center of the screen. First we set the drawing line color to white by writing stroke(255); (white) any color can be chosen by using the (Red, Green, Blue) notation just like with background. To make drawing easier in the future lets declare two more screenWidth=600; and screenHeight=480;. These should be same as your windows width and height. Now we can draw two lines to make up the crosshair:
line(screenWidth/2-5, screenHeight/2,screenWidth/2+5, screenHeight/2);
line(screenWidth/2, screenHeight/2-5,screenWidth/2, screenHeight/2+5);
Now we have covered theneeded skills about drawing that we'll need further on.
Step 2: Bsics of 3Dspace
For describing a location in 3D space, we are gonna need 3 coordinates, x, y and z. For that purpose we are declaring a class called position, which will include all needed information and functions for a single point in space.
First we are just setting a couple of float values for the coordinates and a constructor.
class Position{
float x;
float y;
float z;
Position(float X,float Y,float Z){
x=X;
y=Y;
z=Z;
}
}
Then in main Wirefram code we create a new position object camPosition(0,0,0); to describe our cameras position in 3D space. Lets also set a float number direction=0; to tell the direction the camera is pointing to. Now we can turn the camera around (in theory) and look around.
Step 3: Things to Space
We ofcourse want there to be something in the space we can look at. Lets make a most basic wireframe part, Wire.
So lets make a new class for it. Wire is basically two points in space connected by visible line.
class Wire{
Position start;
Position end;
Wire(Position s, Position e){
start = s;
end = e;
}
}
Now when we have the basics set up we can make it into more complicated things, like for example Cube. For easier creation of wires lets make a relative(dx, dy, dz) function to position. With this we can get a position relative to current position. This allows us to make more complicated structures easier.
Position relative(float dx, float dy, float dz){
return new Position(x+dx, y+dy, z+dz);
}
Cube consists of 12 wire objects which we can store in an array. These wires are between eightcorners which coordinates are always half of the cubes side away from the coordinate axis.
Step 4: Camera Functioning
This is gonna be the mathematically hardest part of this tutorial. I will cover the mathematics needed to make the objects in space to appear on our window. There is probably other more or less similar methods with little differences that could be used but I found this to be simple enough and do just decent work.
We are gonna be using radians instead of degrees for angles. If you are not familiar with radians you should look into them for example from wikipedia: https://en.wikipedia.org/wiki/Radian
You'll manage if you understand that 360° is same as 2*PI, 180° is PI, 90° is PI/2 and so on.
For beginning lets add constant fov=PI/2 (90°)for field of view. You can change this later on to fit your own preferences.
Then lets get started with the function pointOnCanvas(Position) which will do the projecting the wires from the space to camera window. It will take in a position in space and give us its position in the window.
Position pointOnCanvas(Position pos){
float angleH = atan2(pos.y, pos.x);
float angleV = atan2(pos.z, pos.x);
return new Position(screenWidth/2-angleH*screenWidth/fov, screenHeight/2-angleV*screenWidth/fov, 0); }
The mathematics behind this is explained in the drawing.
Now we also want to see something drawn with this cool function, right? so lets create ourselves a nice little cube in to the positive X-axis a 6 units away so it should sit right in front of us.
In the draw() function we then must make this cube to be drawn on to our screen. That can be easily achieved with a for loop going trough all wires of the cube. We first transform each wires start and end positions to display coordinates by using pointOnCanvas then we draw the wire using line, same we used with the corsshair.
//loop to draw all of our wires on the screen
for(int i=0; i
//projection of start and endpoints to camera
Position drawStart = pointOnCanvas(camPosStart);
Position drawEnd = pointOnCanvas(camPosEnd);
//drawing lines on screen
line(drawStart.x, drawStart.y, drawEnd.x, drawEnd.y); }
Now when we run the code we should see our cube to be drawn on the screen.
Step 5: Moving the Camera Around
At the moment we are tied to the center of our space, staring towards positive X-axis. Wouldn't it be nice to move around a bit? For the view of the world correspond to our cameras location, we need to always give projection function the positions according to cameras position. For this purpose we can use some coordinate transformations.
So lets make a function to do this math for us.
Position toCamCoords(Position pos){
Position rPos = new Position(pos.x-camPosition.x, pos.y-camPosition.y, pos.z-camPosition.z);
//calculating rotation
float rx=rPos.x; float ry=rPos.y;
float rz=rPos.z;
//rotation z-axis
rPos.x=rx*cos(-direction)-ry*sin(-direction);
rPos.y=rx*sin(-direction)+ry*cos(-direction);
return rPos; }
For more information about rotating coordinates can be found for example from wikipedia:
https://en.wikipedia.org/wiki/Rotation_matrix
toCamCoords(Position) will take in a position in our space and transform it into position in our camera space. For this to take an effect to our view we must apply this to all of our wires before projecting and drawing them by transforming starts and ends of the wires in our drawing loop.
Position camPosStart = toCamCoords(cube.wires[i].start);
Position camPosEnd = toCamCoords(cube.wires[i].end);
Now we can change our cameras position and direction and see the effect! I chose to set my camera to x=0, y=-2, z=0, taking two steps left and turning PI/8 radians (45°) counter-clockwise.
Step 6: Controls
Now we can view our world from anywhere but we are still kind of stuck. Lets add some controls to move around. There are many ways to implement this and I have chosen the simplest(not necessarily the best) method that came in to my mind.
if(keyPressed){
switch(key){
case 'w': camPosition.move(speed*cos(direction), speed*sin(direction), 0); break;
case 's': camPosition.move(-speed*cos(direction), -speed*sin(direction), 0); break;
case 'a': camPosition.move(-speed*sin(direction), speed*cos(direction), 0); break;
case 'd': camPosition.move(speed*sin(direction), -speed*cos(direction), 0); break;
}
}
We can make the camera to look around following the cursor movement by changing the direction by cursor movements. We also want to normalize the direction always to be between 0 and 2*PI (0°-360°) . This is not a must but it makes things more reliable. Now you can run the code and turn your camera around.
First lets add a new method for position called move(x,y,z).
void move(float dx, float dy, float dz){
x+=dx;
y+=dy;
z+=dz;
}
As parameters for this function we simply give x, y and z values of how much we want to move our position into some direction. Then we set a speed=0.1; for our camera. You can play around with this value too. With a simple switch statement we can track keys being pressed and move camera. If we want to move according to cameras direction we must include some simple math. Since the movement is still controlled in world coordinates
When you start moving around you might notice some serious fishbowl effect on your cube. this can be easily fixed on projection function by adding two lines of code:
angleH/=abs(cos(angleH));
angleV/=abs(cos(angleV));
Step 7: Looking Up and Down
Okay so now we can move and look around. Lets make this a bit more interesting and add looking up and down. We need one more value to describe our rotation up and down. Lets just call it rotationY because we are rotating around Y-axis. We will control this with same kind of mechanism as we controlled rotating around Z-axis (direction) this time we just want it to stay between -PI/2 (-90°, straight up) and PI/2 (90°, straight down).
We also need to make some changes to our toCamCoords function, since we are no longer dealing with single axis rotation. We add some more trigonometry to handle the rotationY:
//rotation y-axis
rx=rPos.x;
rz=rPos.z;
rPos.x=rx*cos(-rotationY)+rz*sin(-rotationY);
rPos.z=rz*cos(-rotationY)-rx*sin(-rotationY);
Step 8: Finished, for Now
Now about everything needed for a simple wire frame modeling engine has been covered. Now you can move onward for example by adding new kind of wire frame objects like pyramids etc. If you have any questions or want better explanation on some subject, I'll be happy to help.