Introduction
One of the fun aspects of implementing a path tracing engine is playing around with different objects using different materials and under multiple light conditions to generate render images. Being able to see the results of different settings is when you learn about the limitations of your rendering engine.
I have rendered a huge number of different scenes with Light using various primitives like spheres, planes, toruses and other. And, while complexes scenes can be build using the built-in primitives, those need to be built in the code itself, or using a very basic JSON specification that I initialluy implemented.
However, I always anted to be able to render scenes using 3D meshes available in multiple onine sources. So, to achieve this, I took the task of implementing a couple of new features in Light: an object file builder supporting the PLY format and a new scene format that allows for importing meshes as well as using the internal predefined solids.
Implementing a PLY loader
PLY, is a text file format used for describing 3D meshes. The file structure is simple but powerful enough to represent large 3D meshes build out of triangles, the file structure is composed of a Header, Vertex List and Face List, you can read more about it here.
The reson of choosing this format is that I wanted Light to be able to load some of the most well known models used to test 3D renderers; the ones created by the Standford University: Bunny, Dragon, Happy Buddha and many others. Implementing PLY as the first supported file format was a not a difficult choice.
My implementation is not complete, but it is good enough to load the files I am interested in.
The parts that are supported right now are:
- Header parsing
- Vertices list
- Faces list
- Triangle reconstruction
The full implementation can be found here: ply.rs.
Scene Format
Because loading meshes is not enough to render a complete scene, I also implemented a simple descriptor format that can be used to build a complex setting, importing PLY models and configuring the default camera parameters.
The new scene format supported by Light, has the following structure:
scene-folder
|
|- camera.json
|- scene.json
|- mesh.ply
Both camera.json and scene.json are files that are mandatory for a scene folder to be complete, each has their own mandatory fields, which are detailed here: config_types.rs.
I am using Rust's Serde library, which handles parsing and enforcement of the format.
Optionally, the scene-folder can have one or multiple .ply files describing meshes. Those .ply files need to be referenced in the scene.json file: SceneConfig, as a regular mesh solid.
Additionally, to facilitate distribution of scenes, this implementation also supports Zip files, that internally stores the scene description using this structure:
scene.zip
|
|- camera.json
|- scene.json
|- mesh.ply
Try it now!
The Dragon mesh was authored by Stanford University and available freely for download at The Stanford 3D Scanning Repository.
Thoughts
It took me quite a while to get all this working as intended. The PLY implementation was easy to build, but then I faced the problem of creating scenes with them, hence, I worked on a more robust scene descriptor format.
Building a specification for the scene took me quite a while, mainly because I restarted multiple times when the outcome was not flexible enough. But in the end, I think the results are very good.
And, since I wanted to show case this in my blog, I spent some time building a very simple visualization component. This library, runs the Light WebAssembly in a WebWorker to avoid blocking the webpage. The implementation is not pretty but it works for now.
Next steps
One thing that is not that great about the WebAssembly implementation of Light is that, while the native version can use all the cores in the CPU, this implementation only supports a single thread. This constrains the use of resources and severerly limits the performance on modern processors.
Multithreading is not yet part of the WebAssembly standard, and requires workarounds to implement using WebWorkers. This means that a multithreaded implementation is not as simple as using rayon and converting an interator into a multithreaded parallel one.
Adding to that, using shared memory space between WebAssembly and WebWorkers is complicated. So, in a multiple WebWorker implementation, the main method of communication through message passing. This way of sending information between different contexts has analog in Rust through Channels.
Channels support in Rust is very robust; changing the Light multithreading implementation from rayon to Channels would better match the WebWorkers paradigm, but I suspect that performance will suffer.
The next step is then to find out how much of a performance penalty there is by implementing a multi-threaded version of the main rendering loop in Light using Channels. Then use the resulting performances numbers and compare with the rayon implementation.
The performance analysis will help me decide whether to go back to rayon and look for another solution to implement multithreading in WebAssembly, or to keep it and work on a small framework that can be used to leverage multiple threads in the web browser.
Nevertheless, the main problem now is finding time for all this.
Eccentric Developments