OBJ Viewer App
About
I started making this after my linear algebra class. We learned a lot about vectors and transformations. One thing that stuck to me were rotation transformations, though we only learned about the 2D rotation matrix:
The way it works is pretty simple: just multiply the matrix by the vector you want to rotate. For example, to rotate \(\begin{bmatrix}1\\2\end{bmatrix}\) by radians would be to:
A then wondered about how 3D rotation matrices would work. I looked it up and they were pretty much what I thought; just the 2D matrix with some padding to just rotate certian planes. The way to do it is exactly what you'd do with 2D, but with one of these silly matrices:
You'd multiply one of the 3D rotation matrices by a vector to rotate that vector. You can then multiply another matrix by the rotated vector to rotate it again in another direction. I also wondered about 4D rotations, but that got complicated and started losing my mind a little. I understand a little, but I still don't get it. Heh, typical 3D brain user amirite.
Anyway, I saw this reddit post of someone who made a cube renderer using Google Sheets and saw that it wasn't toooo hard to do. After talking with a friend who knows a lot about this kinda thing, she said that all it really takes to render 3D objects are vectors and matrix multiplication. I almost didn't believe it at the time since it all seemed too simple. I had to try it myself. So I started in desmos....
Desmos
Each rendering in this section is live with desmos embedded!
I wanted to keep basic from the start, like reaaaly basic. I wanted to see if I can rotate a 2D square and project it onto a 1D line. I made 4 points for the corners, then I used a function to transform each vector with the 2D rotation matrix. Then I projected it onto a line. I used a polygon to draw the line since I can just put the transformed and projeced points into it.
Yep. Prety straight forward. I also managed any quadrilateral to be "rendered" by just moving the points. All I did was take each coordinate and make a function to multiply each one by the 2D rotation matrix. Next, time to make a 3D cube rotate on a 2D screen! The process wasn't too hard. It's really just an extention of what I had just done. This time, I was more systematic though. I made a table of each of the vectors of a cube. Then I made 3 functions for each of the 3 planes of rotation in 3D. I made a functoin to multiply a matrix by a vector. With these functions, I made another table to go through and go through each vector and transform it with each rotation matrix. I then drew a polygon connecting each of the 6 faces together with the transformed vectors.
This time, not too straight forward. It only works with a cube anyway. I'd have to go in and manually add every face to a table and update each vertex for it to render anything else. If only there was a way to get a 3D model's vertices and faces. A neat contained list would be so beneficial... INCOMES THE .OBJ FILE!!
The .obj file is a really simple file format. Remember when I said how I wished for a 3D model to have a neat contained list of it's vertices and faces not 17 words ago? Yep! The .obj file is exactly that! Behold! cube.obj:
g cube
# These are the vertices of the cube in x y z
# It is index starting at 1 for the faces.
v -1 -1 -1
v 1 -1 -1
v -1 1 -1
v 1 1 -1
v -1 -1 1
v 1 -1 1
v -1 1 1
v 1 1 1
# These are the faces that connect the vertices.
# Ex: f 1 3 4 connects (-1, -1, -1), (-1, 1, -1), and (1, 1, -1)
f 1 3 4
f 1 2 4
f 2 4 8
f 2 6 8
f 6 7 8
f 5 6 7
f 1 3 5
f 3 5 7
f 1 2 6
f 1 5 6
f 3 4 8
f 3 7 8
And that was what I needed to go more advanced! I took a .obj model for a dodecahedron. I was able to pretty much copy and paste the both llists into desmos. I simplified the polygon drawing with a for loop and drew each face as a traingle. everything else was pretty much the same as with the cube, it's just the vertices and faces are very easy to change.
I also may have started to go a little overboard with the .obj files and took some fun ones. One thing that I wasn't super happy with was that all the renderings were orthographic. That made some of the renderings look a little bit off; far away things don't get smaller. I consulted with my friend who knows a lot about 3D models and she told me about the perspective matrix. The setup to use a perspective matrix is similar, but slightly different. This time, you'd need a 4th component. There are a few different perspective matrices, but I chose to use this one:
In short, \(h\) and \(w\) are the height and width of the display and \(\theta\) is the FOV and determines how zoomed in you are. Don't worry about zNear and zFar if you don't know what they are; i'm not worrying about them in my graphs.
There is one more step that ust be done before rendering. The perspective matrix only gives a scale factor for each x, y, and z component. To do this, divide the each component by the 4th as follows:
After you do this, feel free to just project the vector onto the 2D plane for displaying! Far away vectors now get scaled smaller! See how the cube on the right actually looks like a cube instead of some wierd looking rectangle? Crazy what a pinch of z can do!
I wasn't done yet though...I wanted to be able to translate. Translations are luckily pretty simple.
This one requires no fancy division or anything; you'd use it just as you would with a rotation matrix. In fact, you need the translation matrix if you don't want to be inside the model! It also allows you to see the models from different angles without rotating:
Keep in mind that order of operations matters; first rotate, then translate. That ensures that the model rotates about the obj's origin. If you translate first, then it will rotate about the graph's orgin. The rendering on the left is rotate first then translate and the right is translate translate first then rotate:
Almost done with this silly desmos portion; I wanted a way to rotate about a pivot. By default, the models rotate about obj's origin. There's no reason for the model to be centered on the origin. I need to calculate the origin myself. Some ways to calculate the center of a 3D object is to:
- Just average all the vertices and call it a day
- Make use of my Calculus III class and . . . circular integrals and . . . stokes theorem and . . . and um yeah just do #1
Yeah I averaged all the vertices, yeah it's not really the center of the model, but you know what I have to say? So what‽ This is a linear algebra project, not a calculus one.
Rotating about the average center isn't so hard. I can actually translate the obj's origin to the graph's origin so that when it rotates, it will rotate about the graph's origin, then I can translate back to where ever it was. Notice that the obj's origin is between the pokémon's feet. The right is translating it to the graph's origin, rotating, then translating it back.
Ok, that's as good as I feel I'm getting done with desmos. It takes a bit to "convert" each obj file to get to desmos. Copying and pasting larger files get cumbersome since not all the vertices and faces are right by each other. There's other information for colors, textures, and the normals for the faces. Desmos is also starting to get slow for larger models as well.
In the end, I had some silly Desmos graphs
desmos released 3d graphing; this was all for naught
Java
With Desmos slowing for large models, I figured an actual program in Java could load them way faster. As a challange, I chose to code all the math myself. I started by making a Vertex
and
a Face
class. I then made a OBJReader
class to parse .obj files. It uses a BufferedReader
and puts all the vertex and face data into arrays. I then made a
Matrix
class to perform matrix operations. The root of the is a Double[][]
. I also added static methods to create new rotation matrices. I also made a method to multiply
arbitrary matrices together. I went back to my linear algebra notes and got the formula to calculate the product entrywise and converted it into for loops:
/**
* Multiplies two matrices in the order A × B.
*
* @param a Matrix A.
* @param b Matrix B.
*
* @return Returns the matrix product of A × B.
*/
public static Matrix multiply(final Matrix a, final Matrix b) throws DimensionMismatchException {
// check if compatible
if (a.col != b.row) {
throw new DimensionMismatchException("Incompatible multiplication;" +
"columns of A do not match the rows of B; " +
a.col + " != " + b.col);
} // if
Matrix c = new Matrix(a.row, b.col);
for (int i = 0; i < a.row; i++) {
for (int j = 0; j < b.col; j++) {
for (int k = 0; k < b.row; k++) {
c.matrix[i][j] += a.matrix[i][k] * b.matrix[k][j];
} // for
} // for
} // for
return c;
} // multiply
I also made a Vector
class that extends Matrix
since not everything needs to be a Matrix. Afterwards, I set up a JavaFX application and drew each face without any rotations just to make sure the drawing works. I used a dodecahedron model:
Then I implemented the rotation matrices with some sliders and it works perfectly!
After verifying is working as intended, I went on to add the perspective elmement of this silly viewer. I also added a file input field to load any file easily. The dodecahedron model can get a little confusing since there are a lot of planer faces. It takes 3 traignles to draw a pentagon and all the lines overlap each other. I used a Yoshi model which may be more easier to see:
Great! I had a properly working 3D model viewer, the math does what the math should and all, but the UI isn't super great. This is actually what I spent most time on, making the UI. One thing that I spent a while perfecting is using the mouse view from different perspectives. This video demos how the program can rotate, translate, and zoom in/out 3D models with using just the mouse controls. All the underlaying math is the same as before, just with better controls. The video also demos how the program automatically puts the model, regardless of its size, a respectable distance from the camera so it isn't too zoomed in when it loads.
In the end, I feel like this is as good as I can get the UI. I may come back to this with a library for matrix functions since they may be faster.