首先,交代一下背景。Webots是一个开源机器人模拟器。为了满足需求,Webots拥有自己的渲染引擎:Wren(WebotsRenderingEngine)。Wren是用C++编写的,而且依赖OpenGL3.3。但是,Wren的公共API是用C编写的,这一点很重要,稍后我们会介绍原因。
此外,Webots还支持将模拟的动画录制下来,或者进行直播。然后,你就可以在浏览器中查看生成的动画或直播了。之前,我们使用Three.js作为渲染引擎在Web上显示Webots模拟。Three.js的运行良好,但它与Wren有一些本质上的区别,因此很难在桌面和Web上获得相同质量的图形显示。
经过一次彻底的分析后,我们决定将Wren移植到WebAssembly。
通过下面的图片,你可以看出编译成WebAssembly后,Wren的图形质量有了巨大的飞跃。
左:桌面版;中:Three.js显示的Web版;右:编译成WebAssembly后的Web版
总体的规划
我将项目分成了两个主要部分:
最后,我还会介绍一些我遇到的主要问题,并提供一些常见的建议。
第一步:准备代码
为了能够使用WebAssembly编译代码,首先我需要做一系列的准备。主要工作包括以下三项:
依赖关系
在使用Emscripten导出代码时,依赖关系很快就会变成一场噩梦。然而,我很幸运,Wren只有三个依赖项:OpenGL、glad和glm。
由于上述原因,我几乎不需要担心依赖关系。但是,我仍然想提一下,因为我觉得如果你的代码有庞大的依赖关系,例如物理引擎,则依赖关系很可能会成为一个巨大的挑战。
修改头文件并排除有问题的函数
首先,我必须修改头文件,才能使用Emscripten。在这一步中,大部分的修改都是将glad的头文件换成纯OpenGL头文件。
#ifdef__EMSCRIPTEN__#include#include#else#include#endif
接下来,我排除了渲染引擎中的一些不兼容Web的函数。由于事先无法得知哪些函数不能用Emscripten编译,所以我只能反复试验。
最终,我发现必须排除的函数主要分为两大类:
准备导出函数和/或枚举
我必须在Makefile中设置一些正确的标志(主要是为了OpenGL),这样就可以编译渲染引擎了。然而,事情远非这么简单。
问题是,用这种方法编译之后,我无法通过JavaScript访问Wren的任何功能。其背后原因是,Emscripten在编译你提供的文件时,会将其作为可执行文件:一旦编译完成,启动该文件,就应该执行些什么。但是,这不是我想要的使用方式,我希望将其作为库来使用,我需要访问其中的各个函数。
幸运的是,这个问题有现成的解决方案。事实上,如果你使用C,则有一种解决方案;如果使用C++,则有两种解决方案。
C的解决方案更为简单,实现速度也更快。你只需在链接时指定所有希望能够在JavaScript中使用的函数的名称。请不要忘记,必须在函数名称的开头添加下划线。
接下来,我只需要将下列选项添加到链接中:
-sEXPORTED_FUNCTIONS=’[$(shellcatfunctions_to_export.txt)]’
然后,我就可以从JavaScript访问每个函数了。
至此,我成功编译了整个渲染引擎。结果得到了三个文件:
请注意,虽然我已经编译了渲染引擎,但不意味着没有任何问题。前路还很漫长……
第二步:修改代码
乏味的工作开始了。
Webots使用了Three.js,可在Web上运行,我的目标是使用WebAssembly编译Wren。问题在于:Three.js和Wren之间没有一对一的映射关系。所以,我们不得不从头开始。首先,我建立了一个非常简单的概念证明:一个白色的立方体。然后,我在其之上构建了其他几何图形、外观、光照、阴影……
但文本主要讨论的是WebAssembly部分。
我采用的方法如下:
下面,我们来谈一谈我所遇到的问题。
问题
我遇到的绝大多数问题可分为以下两类:
指针
Wren的C接口中使用了大量的指针:函数接收指针参数,并返回指针。
而另一方面,JavaScript中没有指针。为了在Web上使用Wren,我必须编写它与JavaScript的接口。
如果将C函数的返回值(一个指针)赋给一个JavaScript变量,会怎么样呢?JavaScript会将其当成一个简单的整数。其实这样正合适,因为如果将这个整数传递给另一个接受指针的C函数,就能顺利地运行。
如果将JavaScript数组或对象作为参数,发送给需要指针的C函数,就会出现问题。C函数会尽最大努力理解收到的数据,但大多数情况下都会失败。
例子
我在项目开始时遇到了如下案例:
我可以加载网页,而且没有任何错误和警告。
虽然背景颜色已设置,但无论JavaScript数组中的值是什么,背景始终为红色。
发生这种情况,是因为C函数在努力翻译我传递过去的数组,最终它认为整个JavaScript结构为红色值。
如果你打算在JavaScript中使用Emscripten编译函数,那么可以通过这个案例学到一个重要的教训:小心没有任何错误和警告的问题!对于我遇到的这种情况,很明显什么地方出问题了,但你有可能会遇到不同的问题,有的时候甚至会让你抓狂!
幸运的是,有一些解决方案可以将JavaScript对象传递给Emscripten。在这个项目中,我结合使用了以下三种方案:
1、使用Emscripten的方法ccall和cwrap。为此,必须添加以下编译选项:
-s‘EXPORTED_RUNTIME_METHODS=[“ccall”,“cwrap”]’
如下所示,我们可以使用ccall和cwrap调用一些C函数,但需要指定参数的类型。这种方法非常适合处理字符串,但如果需要传递数组,那么这些函数的用途就会很有限。
Module.ccall('wr_post_processing_effect_pass_set_name',null,['number','string'],[colorPassTrough,"colorPassThrough"]);
请注意this.previousInverseViewMatrixPointer,这是我们从另一个C函数获得的指针,我们将其定义为类型为number的指针。
2、对于数组,你可能需要使用Emscripten内置的malloc和free实现。这个方法稍微有点复杂:
varbuf=Module._malloc(myTypedArray.length*myTypedArray.BYTES_PER_ELEMENT);Module.HEAPU8.set(myTypedArray,buf);Module.ccall('my_function','number',['number'],[buf]);Module._free(buf);
这段代码会分配一个缓冲区,填充,然后传递给C函数,然后再释放。
3、使用一个C静态函数作为过渡。我们再来看看上面的例子,我想将颜色向量传递给C函数。由于这个操作会频繁进行,所以我不想每一次都分配和释放缓冲区。另一种方法是设计一个额外的C函数,如下所示:
float*wrjs_array3(floatelement0,floatelement1,floatelement2){staticfloatarray[3];array[0]=element0;array[1]=element1;array[2]=element2;returnarray;}
这个函数可以接收三个单独的元素(在这个例子中为颜色),并返回一个指向包含这些元素的静态数组的指针。然而,这种方式也有一些缺点。例如,仅适用于预定义大小的数组,并且一次只能有一个(颜色)数组。
OpenGL
这个问题的原因是Webots使用的是OpenGL3.3,而在Web上我们使用的是WebGL2。WebGL2是OpenGL3.3的对应版本,但二者并不完全相同。此外,Emscripten使用OpenGLES3编译函数,这与OpenGL3.3也略有不同。
这意味着,WebGL2中不包含部分OpenGL3.3中的函数,或者OpenGL3.3和WebGL2中都存在的某个函数,却不包含在OpenGLES3中。或者三个版本都有某个函数,但接收的参数却不相同。
如何解决这些问题?
我没有找到通用的解决方案,如下是我使用过的一些技巧:
建议结果
从Three.js到WebAssembly,编译渲染引擎的转变极大地提高了Web模拟的图形质量。
通过以下图形可以看出桌面版、使用Three.js的Web版,以及使用Emscripten编译的渲染引擎的Web版之间的差异。