1/15) 🧵 Breaking down a quick experiment: Simple but slow displacement for ray-traced triangles
#MetalRT #rendering

Top-left: Typical path-tracer with a simple triangle-based model.
Other three: Same model input, with increasing displacement added while tracing the rays.

2/15) Taking a closer look, we can see the SDF displacement clearly. HW RT is tracing each ray against the BLAS and a custom intersection function runs a raymarcher in compute, using a triangle distance estimator with an arbitrary displacement operator.
3/15) We can raymarch using any distance estimator. For example: cubes or spheres.
4/15) The concept is simple: instead of adding each triangle to the BLAS, we fit a bounding box around the triangle, expand it as needed to allow extra space for the displacement, add the triangle data to the primitive buffer.
5/15) For the primitive data, I chose these for simplicity: 3 vertex positions, 1 color, roughness, metallic, emission. It means that each triangle will have a single color instead of a texture. This can be mitigated by subdividing large triangles.
6/15) The color and material attributes are fetched at load time from the material textures, using the UV at the center of the triangle. 
Each texture is loaded on the GPU, a compute shader samples it to fill a small grid of RGBA colors in a buffer that the CPU will then use for look-up.
7/15) For actual texture support, a scheme would be needed to generate the UVs in the ray marcher. Maybe by adding vertex UVs to the primitive data, finding the closest point on the triangle from the displaced world position and using barycentric coordinates as normal. Not sure it would look good.
8/15) I used a couple of sliders in the app UI to specify the displacement scale and frequency.
In this case I used iq's displacement function: scale * sin(freq * p.x)*sin(freq * p.y)*sin(freq * p.z).
It doesn't always work well depending on the model, so it would be fun to design other operators.
9/15) There are several important issues with this technique. Starting with:
- It's very very slow: the bounding boxes are large around the triangles so the ray marcher needs many steps and there is a lot of overlap between boxes.
- There are a lot of artifacts from the displacement.
10/15) Example performance metrics:
- Normal triangles: 1.0x reference (~1080p30 at 4 rays/pixel).
- Ray marched triangles: 15x slower.
- Small displacement: 25x slower.
- Large displacement: 50x slower.
11/15) Using it on a complex scene
The Sponza scene is full of large triangles, it takes too long to converge cleanly even after leaving it to chew for a day. 
It can create a large amount of detail that would be difficult to achieve with geometry, though.
12/15) Previous work:
This is a small evolution from a 2022 concept that I tried out in my toy renderer when it was only a ray marcher with no support for HW RT. At the time I had an option to add displacement to any SDF. The advancement here is to apply it to triangle geometry loaded from a model.
13/15) Conclusion
The results can be pretty nice and it's easy to implement (given an existing path tracer with ray marching support).
It is not displacement mapping: it does not use a displacement map but instead relies on the world space intersection point.
The performance cost is prohibitive.
14/15) References
- iq's classic SDF primitive functions, including triangle distance estimator and a simple displacement operation:  https://iquilezles.org/articles/distfunctions/
- CC0 model of Head of David: https://www.myminifactory.com/object/3d-print-head-of-michelangelo-s-david-52645
15/15) Proper papers on displacement mapping for RT:
- Nonlinear Ray Tracing for Displacement and Shell Mapping, Shinji Ogaki
- Tessellation-free displacement mapping for ray tracing, Théo Thonat et al.