Rendering 3d from Scratch Chapter 2 - Polyhedrons
Last time we talked about points, and we defined a Vec3 primitive to represent them. In this article, we’ll talk about how to use points to create solid 3d objects, which will eventually be used to render 3d scenes.
To represent a shape in 2d space, we take a list of points and draw lines between consecutive points. The interior of these lines is a shape or polygon:
In 3d, it’s very similar, but instead of just one polygon, there are several conjoined polygons. In the context of a 3d shape, these polygons are often referred to as faces.
Let’s create a few more classes to represent these things:
class Face { std::vector<Vec3> vertices; public: Face(std::vector<Vec3> vertices) : vertices(vertices) {} const std::vector<Vec3>& getVertices() const { return vertices; } }; class Mesh { std::vector<Face> faces; public: Mesh(std::vector<Face> faces) : faces(faces) {} const std::vector<Face>& getFaces() const { return faces; } };
These are our Face and Mesh classes (by the way, a “mesh” is just another word for a 3d object). As you can see, a Face is simply a list of vertices, and a Mesh is a list of Faces. Now, this is great for our purposes, as it’s easy to wrap your head around, but I would like to point out for more curious readers that this is a horribly inefficient way to represent 3d shapes.
For example, picture a cube mesh. Well, I’ll do it for you:
Let’s count up the number of faces and vertices. 6, and 8. With our representation, we’ll use a Mesh with 6 square Faces to create a cube. That’s 24 vertices! We’re creating 16 extra vertices! You can imagine this inefficiency would only get worse as 3d objects gets more complex.
There are other problems, too. There’s no way to tell if two faces share the same vertex, and there’s no way to tell if the vertices on a face are in the correct order. This is pretty useful information, but these classes don’t capture it.
So, there are definite drawbacks to this simple representation of 3d meshes, but for our purposes, these classes will work admirably.
Let’s create our first shape, the ubiquitous cube:
Face makeFace(const Vec3& v1, const Vec3& v2, const Vec3& v3, const Vec3& v4) { std::vector<Vec3> pointsInFace; pointsInFace.push_back(v1); pointsInFace.push_back(v2); pointsInFace.push_back(v3); pointsInFace.push_back(v4); return Face(pointsInFace); } Mesh createCube(Vec3 center, Vec3 size) { float left = center.x - size.x * .5f; float right = center.x + size.x * .5f; float bottom = center.y - size.y * .5f; float top = center.y + size.y * .5f; float front = center.z - size.z * .5f; float back = center.z + size.z * .5f; std::vector<Vec3> points; points.push_back(Vec3(left, bottom, front)); points.push_back(Vec3(left, bottom, back)); points.push_back(Vec3(right, bottom, back)); points.push_back(Vec3(right, bottom, front)); points.push_back(Vec3(left, top, front)); points.push_back(Vec3(left, top, back)); points.push_back(Vec3(right, top, back)); points.push_back(Vec3(right, top, front)); std::vector<Face> faces; // bottom/top faces.push_back(makeFace(points.at(0), points.at(1), points.at(2), points.at(3))); faces.push_back(makeFace(points.at(4), points.at(5), points.at(6), points.at(7))); // left/right faces.push_back(makeFace(points.at(0), points.at(1), points.at(5), points.at(4))); faces.push_back(makeFace(points.at(3), points.at(2), points.at(6), points.at(7))); // front/back faces.push_back(makeFace(points.at(1), points.at(2), points.at(6), points.at(5))); faces.push_back(makeFace(points.at(0), points.at(3), points.at(7), points.at(4))); return Mesh(faces); }
Awesome! We have our first 3d shape. But right now, it’s just 1s and 0s living in memory. Not very interesting. How do we actually draw this thing? Well, we’re getting to it. In our next chapter, we’ll talk about a few powerful maths that are vital to 3d rendering!