This article is divided into two parts:Path tracing code implementationandMicro-texture model.
We are HW.5 Constructed the Whitted-Style Ray Tracing algorithm ray tracing project, HW.6 The BVH acceleration structure is used to speed up the intersection process. This time, we built Path Tracing's ray tracing and used multi-threading to accelerate rendering. Finally, the micro-surface model is used to provide the project with a more rough material.
In addition, it should be noted that the content of this article on microsurface models is mainly derived from Ref.5 , mainly talks about the basic theory and code implementation of the Cook-Torrance model.
This article basically explains the entire content of the framework. If there are any errors in the content, please point them out. This project is about rendering a CornellBox scene, and the final effect is roughly as shown below:
Build a BVH to accelerate collision detection between rays and objects in the scene.
scene.buildBVH();
Finally, a renderer object r is created to render the scene and the rendering time is recorded.
Renderer r; auto start = std::chrono::system_clock::now(); r.Render(scene); auto stop = std::chrono::system_clock::now();
The above is the general process of the project.
Object abstract base class – Object
The Object class defines all the basic behaviors that an object needs in the ray tracing algorithm. It uses pure virtual functions to indicate that this is an interface that needs to be inherited by specific object classes (MeshTriangle, Sphere, and Triangle) and implement these methods.
For detailed instructions, see the following code comments:
Object() {} virtual ~Object() {} virtual bool intersect(const Ray& ray) = 0;// Used to determine whether a ray intersects with the object virtual bool intersect(const Ray& ray, float &, uint32_t &) const = 0;// Also used to detect whether a ray intersects with an object, but this function also returns a parameterized representation of the intersection and the intersection index virtual Intersection getIntersection(Ray _ray) = 0;// Returns the intersection information between the ray and the object virtual void getSurfaceProperties(const Vector3f &, const Vector3f &, const uint32_t &, const Vector2f &, Vector3f &, Vector2f &) const = 0;// This function is used to obtain the properties of the object surface, such as the surface normal, texture coordinates, etc. virtual Vector3f evalDiffuseColor(const Vector2f &) const =0;// Evaluates the diffuse color of an object at a specific texture coordinate virtual Bounds3 getBounds()=0;// Returns the bounding box of the object virtual float getArea()=0;// Returns the surface area of the object. The calculation method for each shape can be different virtual void Sample(Intersection &pos, float &pdf)=0;// Sample a point from the surface of the object for light source sampling. The `pos` parameter is the information of the sampling point, and `pdf` is the probability density function value of the point. virtual bool hasEmit()=0;// Determine whether the object emits light, that is, whether it is a light source.
Based on this class, we also created three specific object classes: MeshTriangle, Sphere and Triangle. These three classes are subclasses of the object class Object and are used to represent different geometric shapes in three-dimensional space.
Since we need to render the picture by ray tracing, we need to implement an important operation called intersect, which is used to detect whether a ray intersects with an object.
In addition, each class has a data member m of the Material type, which represents the material of the object. The material defines the object's color, texture, emitted light and other properties.
In Triangle.hpp, rayTriangleIntersect uses the Möller-Trumbore algorithm to determine whether a ray intersects a triangle in 3D space, and if so, it can calculate the exact location of the intersection. For a detailed code explanation, please see my other articlearticle , the main steps are written in the code comments.
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig, const Vector3f& dir, float& tnear, float& u, float& v){ // First, calculate the vectors of the two sides of the triangle (edge1 and edge2), and then calculate a new vector pvec based on the vector product (outer product) of the ray direction dir and the edge edge2. Vector3f edge1 = v1 - v0; Vector3f edge2 = v2 - v0; Vector3f pvec = crossProduct(dir, edge2); float det = dotProduct(edge1, pvec); if (det == 0 || det < 0) return false; // Then, by calculating the dot product (inner product) of pvec and edge edge1, a determinant value is obtained. If this value is 0 or negative, it means that the ray is parallel to the triangle or the ray is in the opposite direction of the triangle, and false should be returned at this time. Vector3f tvec = orig - v0; u = dotProduct(tvec, pvec); if (u < 0 || u > det) return false; // Then, calculate the cross product of tvec and edge1 to get qvec, and calculate its dot product with dir to get v. If v is less than 0 or u+v is greater than det, return false. Vector3f qvec = crossProduct(tvec, edge1); v = dotProduct(dir, qvec); if (v < 0 || u + v > det) return false; float invDet = 1 / det; // Finally, if all tests pass, the ray intersects the triangle. Calculate the depth tnear of the intersection point, as well as the barycentric coordinates (u, v) inside the triangle. tnear = dotProduct(edge2, qvec) * invDet; u *= invDet; v *= invDet; return true; }
Due to space constraints, we will only focus on these three categories.
Triangle
Next, a Triangle object represents a triangle in three-dimensional space.
Each triangle has three vertices (v0, v1, and v2), two edge vectors (e1 and e2), a normal vector (normal), an area (area), and a material pointer (m). In the triangle constructor, the edge vector, normal vector, and area are calculated based on the three vertices input. The area is calculated by half the modulus of the cross product of e1 and e2.
Triangle related operations
There are three functions (Sample, getArea and hasEmit) that are directly overridden.
The Sample function randomly samples a point on the surface of a triangle and returns:
Information about this point (including position and normal vector)
Probability density function value (pdf) of the sampling point
The getArea function returns the area of the triangle.
The hasEmit function checks whether the triangle's material emits light.
MeshTriangle
The MeshTriangle class is also a subclass of the Object class. It represents a 3D model or mesh composed of many triangles. In other words, a MeshTriangle object may contain many Triangle objects. For example, you can use 12 triangles (2 triangles per face, a total of 6 faces) to represent a cube model.
The MeshTriangle class also includes some additional functions, such as calculating the model's AABB bounding box, BVHAccel objects, etc.
Constructor
The following pseudo code succinctly describes the construction process of MeshTriangle:
MeshTriangle(string filename, Material* mt) { // 1. Load the model file loader.LoadFile(filename); // 2. Create a Triangle object for each face and store for (each face in model) { Triangle tri = create triangles (face vertices, mt); triangles.push_back(tri); } // 3. Calculate the bounding box and total area of the model bounding_box = calculate bounding box (all vertices of the model); area = calculate total area (all triangles); // 4. Create a BVH for fast intersection testing bvh = create BVH (all triangles); }
The constructor accepts a file name filename and a material mt, and then uses objl::Loader to load the 3D model. After loading the model, it traverses all the triangles of the model and creates corresponding Triangle objects. At the same time, the bounding box of the entire model and the total area of all triangles are calculated and stored.
In the framework, we use the objl::Loader class to read the .obj file. Call loader.LoadFile(filename) to complete the loading. Then access loader.LoadedMeshes to obtain the loaded 3D model data.
objl::Loader loader; loader.LoadFile(filename); ... auto mesh = loader.LoadedMeshes[0];
When loading, we noticed an assertion, which checks whether a model has only one mesh. If there are multiple meshes or no mesh, an assertion error is triggered.
assert(loader.LoadedMeshes.size() == 1);
Initializes the minimum and maximum values of the model's vertices, which are used to calculate the axis-aligned bounding box of the 3D model.
Next we need to understand the data structure of the mesh in the objl library, that is, what is stored in the objl::Loader loader.
MeshName: stores the name of the mesh
Vertices: Stores all vertex data in the model, including position, normal and texture coordinates.
Indices: Stores all face (usually triangle) data in the model. Each face consists of a set of indices pointing to vertices in Vertices.
MeshMaterial: Stores all material data in the model, including diffuse color, specular color, texture and other properties.
The vertex information of each triangle is stored continuously in Vertices, so we construct a Triangle with three vertices as a group, and then set the AABB.
for (int i = 0; i < mesh.Vertices.size(); i += 3) { std::array face_vertices; for (int j = 0; j < 3; j++) { auto vert = Vector3f(mesh.Vertices[i + j].Position.X, mesh.Vertices[i + j].Position.Y, mesh.Vertices [i + j].Position.Z); face_vertices[j] = vert; min_vert = Vector3f(std::min(min_vert.x, vert.x), std::min(min_vert.y, vert.y), std::min(min_vert.z, vert.z)); max_vert = Vector3f(std::max(max_vert.x, vert.x ), std::max(max_vert.y, vert.y), std::max(max_vert.z, vert.z)); } triangles.emplace_back(face_vertices[0], face_vertices[1], face_vertices[2], mt); } bounding_box = Bounds3(min_vert, max_vert);
Finally, the areas of all triangles are calculated and the BVH acceleration structure is constructed. In the loop, the code first stores the pointers of all triangles into ptrs, then calculates the sum of the areas of all triangles. Then all triangle pointers are passed into the BVH constructor.