When I used Web technology to practice Ray Tracing, I encountered many problems.
After overcoming those issues, I found that it was very similar to the React optimization tip (not the tip for optimizing Web apps implemented with React, but the optimization tip included in the React internal implementation).
It’s not so much the React optimization techniques applied to ray Tracing on the Web. In fact, UI rendering optimization techniques, such as React, Ray Tracing, and other UI platforms, are common.
However, I first learned React background and then learned Ray Tracing, hence the title. If it’s the other way around, Ray Tracing applies Optimization Techniques to React.
Next, let’s list the problems and their solutions.
Problem: Operator Overloading
JavaScript does not have the operator overload feature in C++ and other languages, so when writing vector and matrix calculation, the code is cumbersome and not intuitive.
In C++, the color function is written like this.
When you multiply a number times a vector, you just use asterisks. Whereas in JS you have to write:
In JS, +-/* cannot be used for non-numeric types such as Float32Array. We have to abstract into add, sub, mul, div, etc. It’s boring to write and painful to debug.
So how do you overcome this problem?
Solution: Babel Plugin
Several Babel plugins can be found that enable JS to support operator overloading, but they all have their own problems.
For example, babel-plugin-overload, which is based on Python, uses Symbol. For (‘+’) to indicate overloading in JS.
It works by compiling all +-/* code as follows:
The four operational expressions become anonymous function expressions. Check if left contains overloaded attributes.
It’s very low on completion. Not being able to handle numbers like number * vector, where the left value is a number, has a very serious performance problem, creating an anonymous function every time it executes. It’s basically not available.
The other is operator-overloading-js, which is older than Symbol and uses the __plus convention.
The idea is to parse the AST of the function at run time, then compile and generate the new function and execute it.
Using it means we rely on Esprima, esCodeGen, for parsing and generating code, respectively. This is not a small volume.
And, because it generates new functions, variables in the closure cannot be arbitrarily accessed inside our function. This is essentially a departure from ES2016 module writing, because the import statement at the top introduces variables that, for functions inside a module, are inside a closure. In its example, modules are written in CommonJS.
As shown in the figure above, the Overload function wraps almost all of the code. Its runtime overhead increases as the code size increases. Overall, the gains outweigh the losses.
After looking at several solutions, I failed to find one that could be used directly. So I wrote the Babel Plugin myself and found that it could be quite simple.
In my scenario, only a few dozen lines of code were used, as follows:
What it does is very simple: replace the +-*/ expression with add, sub, mul, and div function calls according to the mapping relationship. Pass in the values of the left and right operations as arguments.
Thus, we only need to implement four run-time helper functions to customize the +-*/ behavior.
For example, if we add vectors of three dimensions, first check whether they are vectors or not, return left + right directly, and go to the branch of the default behavior. If so, it is normalized to two vectors and then added using the method provided in gl-matrix.
The code for sub, mul and div is almost the same, except that different VEC3 methods are called.
I only show the vector part here, not matrix, because I happen to not use matrix in my current scenario. It’s also easy to add if needed, just one more judgment branch. We can also make plugin registration mechanism.
Overloading internally constructs the run-time helper corresponding to the registered plug-in, detects the matching plug-in in turn, and invokes its processing function.
With effects, we can write intuitive code without worrying about the left and right types of the four operations:
The Babel plugin compiles it to:
Compare this to our original code:
If we just look at the return statement, we’ll see that it compiles exactly the same as it did when we first wrote it. This is exactly what we want, we get the same result as when we write by hand, but we’re actually writing much cleaner and more intuitive code.
Blocking The Main UI Thread
The most attractive advantage of using Web technologies to learn and implement raytracing or 3D graphics rendering is that effects can be easily deployed and viewed in a browser.
However, for highly computationally intensive algorithms like ray tracing, it takes a long time to produce results. If you use synchronous computing, it can get stuck in the main thread and the pages can’t interact or even be difficult to close.
The image above rendered 500+ balls to 800 * 400 resolution in nearly 2 hours using Node.js V8.9.4. Even if you reduce the number of balls, only 20 or so, it will take more than ten minutes.
Thus, even if we solve the problem that JS does not have operator overloading, it does not help. No one wants to see a raytracing DEMO that freezes the browser.
We have to find a way to solve this problem.
For specific solutions, please see the next decomposition. Later, I will introduce optimization strategies of Time Slicing and Streaming Rendering.