And here are more obj 3D model files (only some of them have normals) which you may try with your program.
A box without front face
Shark
Happy Budaha
Dragon
Human Skull
Octopus
Roman
Sparrow
Unicycle
Armadillo
There are also many free online 3D models (try google "free 3D model files"). However most of them are not in OBJ formats, and you have to convert them using "quick 3D", which is already installed in your lab machine.
TriangleMesh is mainly used to OBJ 3D model file input.
All OBJ files have vertex and triangle face data, i.e V and F in our class.
For some OBJ files, vertex and face are their only data, and no vertex normal or texture coordinate data defined. If you are rendering this type of models, do not try to do lighting computation (although you can, using a math called cross production). However, depth cue can still be used to give you some 3D perception.
For some OBJ files, there is a normal for each vertex, and corresponding has_normal is set to true. You should do lighting computation for this type of models.
And still for some other OBJ files, there are also texture values defined. We do not use it though.
To support all these types of OBJ files, the TriangleMesh is updated as,
std :: vector<Vertex> V; // vertex (x, y, z) std :: vector<Vector> N; // normal (x, y, z) std :: vector<Vertex> T; // texture (s, t, w) std :: vector<TriangleFace> F; // triangle (i, j, k) int total_triangles; bool has_normal; bool has_texture;
Two containers are added, one for normals, the other for texture coordinates. Also two flags has_normal and has_texture are added.
And the TriangleFace is updated as,
struct TriangleFace { TriangleFace(int I, int J, int K, int Ti=-1, int Tj=-1, int Tk=-1, int Ni=-1, int Nj=-1, int Nk=-1) : i(I), j(J), k(K), ti(Ti), tj(Tj), tk(Tk), ni(Ni), nj(Nj), nk(Nk) { } int i, j, k; // indices of 3 vertices. int ni, nj, nk; // indices of 3 normals. int ti, tj, tk; // indices of 3 tex coords. };
TriangleMesh is supposed to do the OBJ file input for you, and you are not supposed to understand everything. But you have to understand how to use it.
To access data in a triangle mesh, usually you do,
for(int f = 0; f < msh.total_triangles; ++f) { int i = msh.F[f].i, j = msh.F[f].j, k = msh.F[f].k; // use msh.V[i], msh.V[j] and msh.V[k] to access the three vertices of this face. /* comment out this line if there are not normals for this mesh, or you dont want to use them */ int ni = msh.F[f].ni, nj = msh.F[f].nj, nk = msh.F[f].nk; // use msh.N[ni], msh.N[nj] and msh.N[nk] to access the three normals at the three verties of this face. /* comment out this line if there are not texture coordinates for this mesh, or you dont want to use them */ int ni = msh.F[f].ti, tj = msh.F[f].tj, tk = msh.F[f].tk; // use msh.T[ti], msh.T[tj] and msh.T[tk] to access the texture coordinates of three vertices of this face. }
Many OBJ models have -1#IND00 as their normals, which is a float point exception. To deal with this problem, please change the following code segment of the TriangleMesh construtor,
else if(ele_id == "vn") { no_normal = false; is >> x >> y >> z; N.push_back( Vector(x, y, z) ); }
into,
else if(ele_id == "vn") { no_normal = false; is >> x >> y >> z; if(! is.good()) // in case it is -1#IND00 { x = y = z = 0.0; is.clear(); skip_line(is); } N.push_back( Vector(x, y, z) ); }
There is one problem with our code so far if we try to do solid rendering. The order in which you draw all the triangles matters. The final color of a pixel is determined by then last triangle which is drawn on an area covering that pixel. This is not correct; instead, here is what we want.
// Ususlly there are more than one 3D space points, which write to the same pixel after the projection and // scan conversion process. Of all these 3D space points, the closest one, i.e with maximal z value, should // determines the final color of that pixel.
Here was our code doing perspective projection.
It returns a projected point, but not exactly, since the z component of it is the same as the original space point.inline Point Canvas :: perspectiveProjection(Point const& P) { double scale = eye_at / (eye_at - P.z); return Point(P.x * scale, P.y * scale, P.z); }
However, the z component is not used later when we call ScanLineSegment(),
void Canvas :: ScanLineSegment(int x1, int y1, RGB const& col1, int x2, int y2, RGB const& col2, multimap<int, pair<int, RGB> >* output)
Now, we change it into,
and also pass the z values of the two end points to it so it will interpolate the z value just the same way as it interpolates color. In this way, each scan-converted point has both color and z-value information, and when you try to write to the canvas, the writing is done only if the current z-value is larger than the old z value, and the old z value is also updated in that case.void Canvas :: ScanLineSegment(int x1, int y1, float z1, RGB const& col1, int x2, int y2, float z2, RGB const& col2, set<ScannedResult>* output)
// Z(Y + vReso/2, X + hReso/2) is the old z value at (X,Y). if( cur_z > Z(Y + vReso/2, X + hReso/2) ) { Z(Y + vReso/2, X + hReso/2) = cur_z; Pixel(Y + vReso/2, X + hReso/2) = col; } else // do nothing.
You have done 2D animation in the first week, and have generated some really interesing animations. Now do the same thing in a 3D space setting. You can either choose depth cue wireframe rendering, or solid shaded rendering (then you have to update your code from today's lecture).
Submit your source code and image file, before 7PM Monday, to zh2001 AT columbia DOT edu, and cc copy to xchen AT cs DOT utah DOT edu. Please do not pack your image file with your source code.
RGB compute_lighting(Vector const& normal, Vector const& light_dir, RGB const& material_color) { float dot_product = normal.x * light_dir.x +normal.y * light_dir.y +normal.z * light_dir.z; if(dot_product < 0) return background_color; else { float scale = dot_product / ( sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z) * sqrt(light_dir.x * light_dir.x + light_dir.y * light_dir.y + light_dir.z * light_dir.z) ); return material_color * scale; } }
So far we did not consider the color of the lighting source. But of course, the color of the light source matters in the color perception. One example is, if you are looking at an object with green material color, under a lighting of red color, you actually can not see the object. So here is the code to do lighting computation, considering the color of the light source.
RGB compute_lighting(Vector const& normal, Vector const& light_dir, RGB const& material_color, RGB const& light_color) { float dot_product = normal.x * light_dir.x +normal.y * light_dir.y +normal.z * light_dir.z; if(dot_product < 0) return background_color; else { RGB color(material_color); for(int i=0; i<3; i++) color[i] *= light_color[i]; float scale = dot_product / ( sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z) * sqrt(light_dir.x * light_dir.x + light_dir.y * light_dir.y + light_dir.z * light_dir.z) ); return color * scale; } }
The hidden line removal we did in the last lecture actually only works for if the lighting direction is at (0,0,1). Here is an algorithm which really solve this problem with the help of z-buffer algorithm.
By the way, this method is called polygon offset.//step1: render the model in solid mode, but only write the z-buffer value, not the color. //step2: render the model in wireframe mode, but make sure you offset the model a little bit positive z value, i.e. // move the model a little bit closer to the eye. This time write both the z-buffer and the color as usual.
When you try to do collision detection of a moving bullet with a moving sphere or box, you might need to answer the question whether the bullet(point) is inside or outside of the sphere(box), since an event from outside state to inside state usually means a collision.
bool is_inside(Point const& P, BBox const& box) { return (P.x >= box.min.x && P.x <= box.max.x) && (P.y >= box.min.y && P.y <= box.max.y) && (P.z >= box.min.z && P.z <= box.max.z); } bool is_inside(Point const& P, float sphere_radius, Point const& sphere_center) { return ( (P.x - sphere_center.x) *(P.x - sphere_center.x) + (P.y - sphere_center.y) *(P.y - sphere_center.y) + (P.z - sphere_center.z) *(P.z - sphere_center.z) ) <= sphere_radius * sphere_radius; }
You might want to rotate some of the downloaded models. The simplest way to do a 3D rotation is to do multiple 2D rotation. For example, a rotate of alpha degree around z-direction is nothing different from what I put on the previous lecture page.
// rotate around z-direction theta angle. new_x = cos(theta) * x - sin(theta) * y; new_y = sin(theta) * x + cos(theta) * y; new_z = z;
// rotate around y-direction theta angle. new_z = cos(theta) * z - sin(theta) * x; new_x = sin(theta) * z + cos(theta) * x; new_y = y;
// rotate around x-direction theta angle. y z x new_y = cos(theta) * y - sin(theta) * z; new_z = sin(theta) * y + cos(theta) * z; new_x = x;
When you do rotation, it is always a good idea to make sure the center of the model is at or close to (0,0,0).
1.3.6