React 优化技巧在 Web 版光线追踪里的应用(上)
2019-07-18 09:39:15
我在使用 Web 技术去实践 Ray Tracing 相关的知识的时候,遇到了很多问题。
在克服了那些问题后,我发现,它跟 React 优化技巧如出一辙(这里的 React 优化技巧,不是指优化用 React 实现的 Web APP 的技巧,而是 React 内部实现中包含的优化技巧)。
与其说是 React 优化技巧在 Web 版光线追踪里的应用。其实不如说,UI 渲染的优化技巧,在 React ,Ray Tracing 还是其它 UI 平台,是通用的。
只是闻道有先后,我个人先有了 React 背景,再去学习 Ray Tracing,故有此标题。如果反过来,则是《Ray Tracing 优化技巧在 React 里的应用》了。
接下来,我们来一一例举遇到的问题及其解决方案吧。
问题:Operator Overloading
JavaScript 还没有 C++ 等语言里的操作符重载特性,在编写向量、矩阵计算时,代码既繁琐,又不直观。
在 C++ 里,color 函数写起来是这样的。
数字跟向量相乘,只需要用 * 号即可。然而在 JS 里你得这样写:
在 JS 里,+-/* 不能用在 Float32Array 等非数值类型上。我们只好抽象成 add, sub, mul, div 等计算函数。写起来索然无味,调试起来也异常痛苦。
那么,如何克服这个问题?
解决方案:Babel Plugin
目前可以搜索到几个 Babel Plugin 能让 JS 支持操作符重载,但它们都有各自的问题。
比如:babel-plugin-overload,它的思路是参考 Python,在 JS 里用 Symbol.for('+') 表示 overloading。
它的原理是,把所有 +-/* 代码都编译成下面这样:
四则运算表达式,变成了匿名函数表达式。检查 left 是否包含重载属性。
它的完成度很低。既不能处理 number * vector 这种 left 值为数字的情况,还有非常严重的性能问题,每次执行都会创建一个匿名函数。基本上是不可用的。
另一个是,operator-overloading-js,年代更久远,连 Symbol 都没用,使用的是 __plus 这种约定做法。
它的思路是,在运行时解析函数的 AST,然后编译和生成新的函数并执行它。
使用它意味着我们会依赖 esprima,escodegen,分别用来解析代码和生成代码。这部分体积不小。
并且,因为它生成新的函数,我们的函数内部就不能随意访问闭包里的变量。这基本告别了 ES2016 模块写法,因为顶部的 import 语句引入的变量,对于模块内的函数来说,就是闭包里的变量。在它的示例里,模块写法都是 commonjs 的。
如上图所示,overload 函数包裹了几乎所有代码。它的运行时开销,会随着代码规模的增加而增加。总体而言得不偿失。
考察了多个方案后,我没有成功找到可以直接使用的方案。因此,我亲自动手写了一个 babel plugin,发现它其实可以非常简单。
在我的场景里,只用到了几十行代码,如下所示:
它做的事情非常简单,就是把 +-*/ 的表达式,按照 mapping 关系,替换成 _add_, _sub_, _mul_ 和 _div_ 四个函数调用。把 left, right 的操作值,作为参数传入。
如此,我们只需要实现四个 runtime-helper 函数,就可以自定义 +-*/ 的行为了。
比如我们为3个维度的向量实现相加的行为,首先判断是不是向量,不是向量就直接 return left + right,走默认行为的分支。如果是,则归一化为两个向量,然后用 gl-matrix 里提供的方法进行相加操作。
_sub_, _mul_ 和 _div_ 的代码几乎一样,只是调用不同的 vec3 方法罢了。
这里只展示了 vector 的部分,没有涉及 matrix,因为恰好我目前的场景里用不到 matrix。如有需要,添加起来也很容易,多一个判断分支而已。我们还可以做成插件注册机制。
overloading 内部会根据注册的插件,构造出对应的 runtime-helper,依次检测出匹配的插件,调用它的处理函数即可。
使用效果,我们可以写直观的代码,不用再管四则运算的类型跟 left, right 顺序:
Babel 插件把它编译成:
跟我们最初的代码进行对比:
只看 return 语句,我们会发现,它的编译结果,跟最初我们手写,总体上是一致的。这正是我们想要的效果,我们得到了跟我们手写一样的结果,但我们实际写的是更简洁直观的代码。
问题:Blocking The Main UI Thread
使用 Web 技术来学习和实现光线追踪或者 3D 图形渲染,最吸引人的优势是,我们可以很容易地在浏览器里部署和浏览效果。
然而,像光线追踪这种高度耗费运算量的算法,它需要长时间计算,才能得出结果。如果采用同步计算,它会卡住主线程,页面无法交互,甚至难以关闭。
上面这幅图渲染了 500+ 小球到 800 * 400 的分辨率中,使用 node.js v8.9.4,渲染了近 2 小时。即便减少小球,只用 20 来个,也得十几分钟。
如此,即便我们解决了 JS 没有 operator overloading 的问题,也无济于事。没人想看一个会卡死浏览器的光线追踪 DEMO。
我们得想办法解决这个问题。
具体如何解决,请看下回分解。后面我将介绍 Time Slicing 和 Streaming Rendering 优化策略。
扫描二维码分享到微信