Ray Tracing: Rendering a Triangle

News (August, 31): We are working on Scratchapixel 3.0 at the moment (current version of 2). The idea is to make the project open source by storing the content of the website on GitHub as Markdown files. In practice, that means you and the rest of the community will be able to edit the content of the pages if you want to contribute (typos and bug fixes, rewording sentences). You will also be able to contribute by translating pages to different languages if you want to. Then when we publish the site we will translate the Markdown files to HTML. That means new design as well.

That's what we are busy with right now and why there won't be a lot of updates in the weeks to come. More news about SaP 3.0 soon.

We are looking for native Engxish (yes we know there's a typo here) speakers that will be willing to readproof a few lessons. If you are interested please get in touch on Discord, in the #scratchapixel3-0 channel. Also looking for at least one experienced full dev stack dev that would be willing to give us a hand with the next design.

Feel free to send us your requests, suggestions, etc. (on Discord) to help us improve the website.

And you can also donate). Donations go directly back into the development of the project. The more donation we get the more content you will get and the quicker we will be able to deliver it to you.

6 mns read.

Single vs Double Sided Triangle

Figure 1: The outside surface of a primitive is the side from which the normal points outward; the inside surface is the opposite side.

We already mentioned the term back-face culling in the lesson on rasterization but let's introduce it here one more time. Let's say that to develop a routine that computes the intersection of a ray with a triangle, we create a scene using a right-hand coordinate system and where the triangle's vertices were declared in counter-clockwise order. The triangle is 5 units away from the ray's origin which is located at the center of the world coordinate system (0, 0, 0). The ray is oriented along the negative z axis. We know how to compute the triangle's normal, which by construction is oriented along the z-axis if all the triangles vertices lie in the x-y plane or a plane a parallel to it (remember that the orientation of the normal depends on the coordinate system's handedness and the triangle's vertices creation order, also called winding). As you see, the ray direction and the triangle normal are facing each other (figure 1).

The surface of a primitive whose normal is pointing outward is defined as the outside surface. the opposite side is defined as the inside surface. Note that if you change the coordinate system handedness or the vertices creation order, you will flip the normal direction. Consequently the inside surface of a primitive will become the outside surface, and the outside surface the inside surface. The surface orientation has implications for surface visibility and shading. We are not concerned by shading yet, but let's have a look at the visibility problem.

In a CG scene, you may have the case of primitives for which the outside surface is facing the camera. The surface is said to be front-facing. In the opposite case (when the outside surface of a primitive is facing away from the camera), the surface is said to be back-facing. Rendering programs often give you the option to discard during the visibility part of the rendering process, any primitive whose outside surface is not facing the camera (back-facing) making only visible, primitives whose outside surface is facing the camera (front-facing). OpenGL for example has such controls. It is said that such primitives are single-sided (single sided primitives are only visible when their outside surface is facing the viewer). If you wish to make a primitive visible in both cases (when the surface of the object is either facing the camera or in the opposite direction) you may have to declare the primitive as double-sided. Some rendering programs do not give the option to the user to declare primitives as being either single or double sided. They will either systematically discard all the backfacing primitives (they won't be visible in the final frame) or will make them all double sided by default. In the RenderMan specifications though, you have an option to declare on a primitive basis, if an object should be treated as a single or double sided primitive (using the RiSide call). Note that the RenderMan specifications also give you the option to specify the coordinate system handedness of the scene (with the RiOrientation procedure) and reverse the orientation of a primitive's surface in regards to the coordinate system handedness (if desired) with the RiReverseOrientation call.

The term back-face bulling (synonymous of removing if you prefer) means that objects whose normals are pointing away from the view direction will not be drawn to the screen. Turning back-face culling on, means that polygons, triangles, surfaces, etc. whose normal are not facing the view direction won't be rendered. Only geometry whose normals are facing the camera will be visible in the final image. This technique can lead to significant speedup (and memory savings) in z-buffer style renderers because it can reduce the number of surfaces actually being rendered by a pretty large factor (for example for a polygon sphere, in theory 50% of the faces could be culled). With ray tracing, this feature is not as useful. Generally with ray tracing, we want geometry in the scene to cast shadows for example, regardless of the orientation of the object's surface with respect to the ray direction however the backface culling option might still be desired for primary rays. If the polygon's surface normal points more than 90 degrees away from the viewing vector (see adjacent figure) then we could avoid testing this polygon for intersections. A simple dot product test between the normal and the view direction is enough to determine if the face should be culled or not. Of course, we don't cull the face if the object it belongs to is declared double sided and culling back face surfaces when objects are transparent is also not necessarily desirable.

The image above shows the faces (in orange) that will be culled at render time if the object is not transparent, rendered as a single sided object and that back-face culling is turned on.

If you develop a rendering program you should be very careful about making sure it handles all possible cases. It is best to give users the ability to choose the coordinate system handedness, to reverse the surface orientation if they need to and to define if primitives are single or double sided. For the same geometry, these settings can change what's visible in the frame in the end. For example, the choice of coordinate system handedness changes the direction on the normal defined by the vertices V0, V1, V2. If when you use a right-hand coordinate system, the normal (computed from the triangles vertices) points away from the camera, the ray-triangle intersection routine will return false if back-face culling is turned on, even if the ray actually intersects the triangle. Switching to a left-hand coordinate system though, would change the orientation of the normal and the test in this case would be successful. In the first case, the triangle wouldn't be visible but in the second, it would, even though the triangle is the same.

Here is how you would implement the single/double sided feature. At the beginning of the function, we compute the dot product of the triangle's normal with the the ray direction. If this dot product is lower than 0, it means that the two vectors are pointing in opposite directions. Thus, the surface is front-facing. If the dot product is greater than 0 the vectors are pointing in the same direction. We are looking at the inside of the surface (or the back of it, it's back-facing). If the primitive was declared single-sided, it shouldn't then be visible. In this case, the function returns false.

bool intersectTriangle(point v0, ..., const bool &isSingleSided) { Vec3f e0 = v1 - v0; Vec3f e1 = v2 - v0; Vec3f N = crossProduct(e0, e1); normalize(N); ... // implementing the single/double sided feature if (dotProduct(dir, N) > 0 && isSingleSided) return false; // back-facing surface ... return true; }