Ray-Tracing: Rendering a Triangle

Distributed under the terms of the CC BY-NC-ND 4.0 License.

  1. Why Are Triangles Useful?
  2. Geometry of a Triangle
  3. Ray-Triangle Intersection: Geometric Solution
  4. Single vs Double Sided Triangle and Backface Culling
  5. Barycentric Coordinates
  6. Möller-Trumbore algorithm
  7. Source Code (external link GitHub)

Single vs Double Sided Triangle and Backface Culling

Reading time: 6 mins.

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've previously discussed the term back-face culling in the lesson on rasterization, but let's revisit it. Imagine developing a routine to compute the intersection of a ray with a triangle. We create a scene using a right-hand coordinate system, where the triangle's vertices are declared in a counter-clockwise order. The triangle is situated five units away from the ray's origin, located at the world coordinate system's center (0, 0, 0). The ray is oriented along the negative z-axis. We know how to compute the triangle's normal, which, by construction, aligns with the z-axis if all the triangle's vertices lie in the x-y plane or a plane parallel to it. Remember, the orientation of the normal depends on the coordinate system's handedness and the order of the triangle's vertices creation (also known as winding). As depicted in Figure 1, the ray direction and the normal of the triangle face each other.

The surface of a primitive with its normal pointing outward is defined as the outside surface, and the opposite side as the inside surface. Changing the coordinate system's handedness or the order of the vertices' creation will flip the normal's direction, consequently altering the primitive's inside surface to the outside surface, and vice versa. This surface orientation has implications for surface visibility and shading. While shading is not our current focus, let's consider the visibility issue.

In a CG scene, primitives with their outside surfaces facing the camera are deemed front-facing. Conversely, when the outside surface of a primitive faces away from the camera, the surface is considered back-facing. Rendering programs often allow the discarding of any primitive that is back-facing during the visibility part of the rendering process, thereby making only those primitives whose outside surface faces the camera (front-facing) visible. For instance, OpenGL provides such controls. Such primitives are described as single-sided (visible only when their outside surface faces the viewer). To render a primitive visible regardless of whether its surface is facing towards or away from the camera, you may declare the primitive as double-sided. Some rendering programs don't offer the option to designate primitives as either single or double-sided; they either systematically discard all back-facing primitives (rendering them invisible in the final frame) or default to making them all double-sided. The RenderMan specifications allow you to specify on a per-primitive basis whether an object should be treated as a single or double-sided primitive (using the RiSide call). Additionally, RenderMan specifications offer the option to define the scene's coordinate system handedness (with the RiOrientation procedure) and to reverse the orientation of a primitive's surface in relation to the coordinate system's handedness (if desired) with the RiReverseOrientation call.

Real-time APIs such as Vulkan, Metal, or DirectX provide the capability to control whether the vertices for defining triangles are specified in a counter-clockwise or clockwise order and whether backfacing triangles should be discarded. This configuration is typically done while defining the graphics pipeline in the primitive/assembly state.

The term back-face culling (equivalent to removing, if you prefer) signifies that objects whose normals point away from the view direction will not be drawn on the screen. Enabling back-face culling ensures that polygons, triangles, surfaces, etc., whose normals do not face the view direction, won't be rendered. Only geometry with normals facing the camera will be visible in the final image. This technique can significantly increase rendering speed (and reduce memory usage) in z-buffer style renderers by potentially reducing the number of surfaces rendered by a substantial margin (for example, theoretically, 50% of the faces of a polygonal sphere could be culled). With ray tracing, this feature is not as beneficial since, generally, we want all scene geometry to cast shadows, regardless of the object surface's orientation relative to the ray direction. However, back-face culling is still desired for primary rays. If a polygon's surface normal deviates more than 90 degrees from the viewing vector (as illustrated), it could be excluded from intersection tests. A simple dot product test between the normal and the view direction suffices to decide whether the face should be culled. Naturally, we do not cull the face if the object is declared double-sided, and culling backface surfaces when objects are transparent may not be desirable either.

The image above illustrates the faces (in orange) that will be culled at render time if the object is opaque, rendered as a single-sided object, and if back-face culling is enabled.

When developing a rendering program, it's crucial to ensure it accommodates all potential scenarios. Providing users with the ability to select the coordinate system handedness, reverse surface orientation if necessary, and define if primitives are single or double-sided can drastically alter what's visible in a frame for the same geometry. For instance, the choice of coordinate system handedness affects the normal's direction, defined by vertices V0, V1, V2. Using a right-hand coordinate system, the normal (computed from the triangle's vertices) points away from the camera; the ray-triangle intersection routine will return false if back-face culling is enabled, even if the ray intersects the triangle. Switching to a left-hand coordinate system reverses the normal's orientation, making the test successful. In the first scenario, the triangle would not be visible, but in the second, it would be, even though it's the same triangle.

Here is an implementation for the single/double-sided feature. At the function's start, compute the dot product of the triangle's normal with the ray direction. If this dot product is negative, the vectors point in opposite directions, indicating the surface is front-facing. If the dot product is positive, the vectors point in the same direction, and we are looking at the inside (or the back) of the surface, making it back-facing. If the primitive is declared single-sided, it should not be visible in this case, and the function returns false.

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