渲染引擎对Web的支持方案

背景

WebAssembly

简单来说,WebAssembly可以帮助我们将C++编译为可被JS调用的.wasm二进制格式,且在Web端运用,以在Web端调用C++模块,发挥C++高效的语言优势。

安装Emscripten3.1.0

Emscripten 是一个完整的 WebAssembly 编译器工具链。
官方网站:https://emscripten.org/index.html
官方安装文档:https://emscripten.org/docs/getting_started/downloads.html
按照官方文档中的安装步骤即可顺利完成Emscripten安装,且官方文档对Emscripten的用法有较为详细的说明,建议有需要时先在官方文档中搜索和查看。
另外附参考文档如下:
https://www.cntofu.com/book/150/zh/ch1-quick-guide/readme.md

C++与Javascript交互

绑定

原始方式

可对C++中方法以 extern "C" EMSCRIPTEN_KEEPALIVE修饰,即可在html中调用。
extern "C" ——转化为C接口,
EMSCRIPTEN_KEEPALIVE宏——表明此方法需一直保留不被优化。
调用时的方法前面加“_”,且emcc生成的胶水代码中可搜索到changeColor函数名。

//example.cpp
#include <emscripten.h>
extern "C" void EMSCRIPTEN_KEEPALIVE changeColor(){
    
    return 0;}

//emcc
emcc --bind example.cpp -o index.js -O3 -s WASM=1 
	  <script type='text/javascript'>
        var canv = document.getElementById('canvas');
        var Module = {
    
    canvas: canv};
    </script>    
    <!-- Call the javascript glue code (index.js) as generated by Emscripten -->
    <script src="index.js"></script>//调用index.js    
    <!-- Allow the javascript to call C++ functions -->
    <script type='text/javascript'>
        canv.addEventListener('click',    _changeColor, false);//使用index.js中方法
    </script>

Embind

Embind 用于绑定 C++ 函数和类到 JavaScript ,这样编译代码就能在 js 中以一种很自然的方式来使用:

  1. 需要在 C/C++ 代码中添加#include <emscripten/bind.h>头文件。
  2. 使用EMSCRIPTEN_BINDINGS()块来创建函数、类、值类型、指针(包括原始和智能指针)、枚举和常量的绑定
  3. 编译时加入–bind参数

绑定类、函数、属性、结构体可参考如下示例:

//example.cpp
#include <emscripten/bind.h>
using namespace emscripten;
struct Point {
    
    
    int x;
    int y;
};
 
Point getPoint() {
    
    
    Point point = {
    
    0};
       point.x = 100;
       point.x = 200;
       return point;
}

class MyClass {
    
    
public:
  MyClass(int num){
    
     m_num = num; };
  void CompareBig(int x, int y){
    
                    //普通函数
  	  printf("Big one is %d\n", x > y ? x : y);}
  static int getNum(const MyClass& instance){
    
       //静态函数
      return instance.printfNum;}
  int getNumValue() const{
    
                          //可与下函数绑定属性
		printf("getNumvalue:%d\n", m_num);return m_num;}
  void setNum(int num){
    
                             //可与上函数绑定属性
		printf("setNum:%d\n", num);m_num = num;}
private:
  int printfNum(){
    
    
	  return m_num;}
private:
  int m_num;
};
 
EMSCRIPTEN_BINDINGS(my_module) {
    
     //my_module可以随意填写
    //绑定结构体
    value_object<Point>("Point")
        .field("x", & Point::x)
        .field("y", & Point::y);
    //绑定函数
    function("_getPoint", &getPoint);
    //绑定类、类中函数、属性
    class_<MyClass>("MyClass")
    .constructor<int>() //构造函数
    .function("CompareBig", &MyClass::CompareBig) //普通类成员函数
    .class_function("getNum", &MyClass::getNum) //静态类成员函数
    .property("m_num", &MyClass::getNumValue, &MyClass::setNum)//绑定属性,将私有变量暴露
    ;
}

emcc --bind -o index.js example.cpp -O3 -s WASM=1 
//html中的js代码
//调用函数
var oPoint = Module._getPoint();
var ix = oPoint.x;
var iy = oPoint.y;
//调用类
var instance = new Module.MyClass(10); //声明类的对象
instance.CompareBig(1,2); //类函数
Module.MyClass.getNum(instance); //静态函数
instance.m_num = 20; //绑定属性后可直接对变量赋值
instance.delete(); //使用后需释放,否则Emscripten堆将无限增长

智能指针

1.普通智能指针的绑定

//绑定智能指针的两种方式
EMSCRIPTEN_BINDINGS(module) {
    
    
  class_<Class>("Class")
      .constructor<int>()
      .smart_ptr<std::shared_ptr<Class>>("shared_ptr<Class>")//智能指针
      .property("x", &Class::getX, &Class::setX);
}

EMSCRIPTEN_BINDINGS(module) {
    
    
  class_<Class>("Class")
       //这里将智能指针与类对象的创建过程进行绑定 
      .smart_ptr_constructor("shared_ptr<Class>", &std::make_shared<Class, int>)
      .property("x", &Class::getX, &Class::setX);
};

2.自定义智能指针的绑定
2.1.编写自定义smart_ptr_trait模板类,实现自己的智能指针类型。

//编写自定义smart_ptr_trait模板类,实现自己的智能指针类型
template<typename PointeeType>
struct smart_ptr_trait<dan::Smart_Object<PointeeType>> {
    
    
    typedef dan::Smart_Object<PointeeType> PointerType;
    typedef typename PointerType::element_type element_type;

    static element_type* get(const PointerType& ptr) {
    
    
        return ptr.get();
    }

    static sharing_policy get_sharing_policy() {
    
    
        return sharing_policy::BY_EMVAL;
    }

    static dan::Smart_Object<PointeeType>* share(PointeeType* p, EM_VAL v) {
    
    
        return new dan::Smart_Object<PointeeType>(
            p,
            val_deleter(val::take_ownership(v)));
    }

    static PointerType* construct_null() {
    
    
        return new PointerType;
    }

private:
    class val_deleter {
    
    
    public:
        val_deleter() = delete;
        explicit val_deleter(val v)
            : v(v)
        {
    
    }
        void operator()(void const*) {
    
    
            v();
            // eventually we'll need to support emptied out val
            v = val::undefined();
        }
    private:
        val v;
    };
};

2.2.在智能指针类中添加element_type,要构造smart_ptr_trait,智能指针中须有此变量。

    using element_type = typename remove_extent<T>::type;

![image.png](https://img-blog.csdnimg.cn/img_convert/f5bfd22713555967e83677aded7a96da.png#crop=0&crop=0&crop=1&crop=1&height=181&id=Li64e&margin=[object Object]&name=image.png&originHeight=181&originWidth=707&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12542&status=done&style=none&title=&width=707)
2.3.在EMSCRIPTEN_BINDINGS中绑定。

EMSCRIPTEN_BINDINGS(my_module) {
    
      
     class_<SGGeoPoint>("SGGeoPoint")   
             .constructor()
             .smart_ptr<SGGeoPointPtr>("SGGeoPointPtr")//自定义智能指针
             .function("point",&SGGeoPoint::point)
             .function("getX",&SGGeoPoint::getX)
             .function("getY",&SGGeoPoint::getY);
};

接口类

以下示例了接口类的绑定方法。

//定义一个接口类,该接口需要由子类来实现
class MyInterface{
    
    
    public:      
        virtual void invoke(const std::string &str) = 0;  //纯虚函数
}; 
//定义一个胶水类用来链接C/C++与js代码
class DerivedClass : public wrapper<MyInterface> {
    
    
    public:
        EMSCRIPTEN_WRAPPER(DerivedClass);
        void invoke(const std::string &str) override{
    
           	
            return call<void>("invoke",str);} //间接调用在js中实现的方法       
};
//绑定 
EMSCRIPTEN_BINDINGS(module){
    
    
    class_<MyInterface>("MyInterface")
    //纯虚函数:绑定父类中的抽象接口
    .function("invoke",&MyInterface::invoke,pure_virtual())
    //通过allow_subclass方法向绑定的接口添加俩个js方法extend和inplement,用于实现定义在c++代码中的接口
    .allow_subclass<DerivedClass>("DerivedClass");
}

上面的代码中,通过**wrapper**模板类构建了一个用于连接C/C++代码与JavaScript环境的**“胶水”类**。在该类内部,通过调用在JavaScript代码中实现的子类接口这种方式来间接地绑定C++代码中的接口类与JavaScript环境中的子类实现过程。而在EMSCRIPTEN_BINDINGS内部绑定接口类中定义的抽象方法时,需要为function方法提供一个名为**pure_virtual()**的策略标志,该标志会标识纯虚函数的绑定过程,并为其提供相应的异常捕获能力。
Embind为我们提供了两个可用于在JavaScript代码中实现C/C++接口的本地函数方法,即**extend****implement**方法。但使用这两个方法的前提是在绑定接口类时,需要通过**allow_subclass**方法显式地声明将要在JavaScript环境中完成接口类的具体实现过程。接下来,便可以借助这两个方法,在JavaScript环境中实现C/C++接口的具体逻辑。

//通过extend方法来实现子类         
            var DerivedClass = Module.MyInterface.extend("MyInterface",
            {
    
    
                //构造方法(可选)
                __construct: function(){
    
    
                    this.__parent.__construct.call(this); //调用父类的构造函数
                },
                //析构函数(可选)
                __destruct: function(){
    
    
                    this.__parent.__destruct.call(this); //调用父类的析构函数
                },
                //对接口中纯虚函数的具体实现
                invoke: function(str){
    
    
                    console.log("js_invoke_ing" + str);
                },
            });
						//调用子类方法
            var instanceExtend = new DerivedClass;
            instanceExtend.invoke("i'm extend");
//通过implement方法来构造子类
            var x = {
    
    
                invoke:function(str){
    
    
                    console.log("invoking with:"+ str);
                }
            };
            var interfacePbject = Module.MyInterface.implement(x);
						//调用子类方法
            interfacePbject.invoke("i'm implement");

extend:在这段代码中,首先使用**extend**方法完成了Interface接口类的子类实现过程。与C/C++中维承类的实现过程类似,这里也可以选择性地使用__construct__destruct方法来为该实体类添加相应的构造函数和析构函数。
**implement:**相对于extend方法而言,**implement**方法则更适用于不需要构造函数与析构函数的简单接口类。可以看到,这里只需要将与接口类中纯虚函数其签名完全一致的JavaScript函数以对象结构进行包裹,并传递给从绑定类对象中导出的implement方法,即可完成对接口类的实现过程。更为方便的是,该方法会直接返回一个已经实例化好的子类对象,这样同时也省去了需要另外再new的过程。

覆写非纯虚函数

//定义一个接口类,该接口需要由子类来实现
class MyInterface{
    
    
    public:
    	//非纯虚函数
    	virtual void invokeN(const std::string &str){
    
    
            std::cout << str + " - from 'c++'"<<std::endl;}
}; 
//定义一个胶水类用来链接C/C++与js代码
class DerivedClass : public wrapper<MyInterface> {
    
    
    public:
        EMSCRIPTEN_WRAPPER(DerivedClass); 
   		void invokeN(const std::string &str) override{
    
           	
            return call<void>("invokeN",str);} //间接调用在js中实现的方法
};
//绑定 
EMSCRIPTEN_BINDINGS(module){
    
    
    class_<MyInterface>("MyInterface")
    //非纯虚函数:需要通过optional_override方法来创建特殊的Lambda函数,防止js代码与Wrapper函数之间产生循环递归调用问题 
    .function("invoke",optional_override([](MyInterface &self,const std::string &str){
    
    
        return self.MyInterface::invoke(str);
    }))
    //通过allow_subclass方法向绑定的接口添加俩个js方法extend和inplement,用于实现定义在c++代码中的接口
    .allow_subclass<DerivedClass>("DerivedClass");
}

从整体上看,这段代码与前面代码唯一的差别是,当绑定抽象类的非纯虚函数时,不能直接向function 方法传递对应函数的指针,而是需要通过optional _verride方法将函数的调用过程封装在个特殊的匿名函数中并整体传递给 function 方法。另外,不同于实现接口类的过程,我们可以在 JavaScript 环境中选择性地覆写或直接使用 invoke 函数的默认实现,覆写的具体过程只能以**extend**即继承的方式来实现

//通过extend方法来实现子类         
            var DerivedClass = Module.MyInterface.extend("MyInterface",
            {
    
    
                //选择性地对接口中非纯虚函数的具体实现
                invokeN: function(str){
    
    
                    console.log("js_invokeN_ing" + str);
                }
            });
						//调用子类方法
            var instanceExtend = new DerivedClass;
            instanceExtend.invokeN("i'm extend");

C++中派生类

//定义一个基类(父类) 
class MyBaseClass{
    
    
    public:
        MyBaseClass() = default;
        virtual std::string invoke(const std::string &str){
    
    
            return str + " - from 'MyBaseClass'"; };
};
//定义继承的子类
class MyDerivedClass : public MyBaseClass{
    
    
    public:
        MyDerivedClass() = default; 
        std::string invoke(const std::string &str) override{
    
    
            return str + " - from 'MyDerivedClass'"; };
};
//绑定 
EMSCRIPTEN_BINDINGS(module){
    
    
    //绑定基类 
    class_<MyBaseClass>("MyBaseClass")
    .constructor<>()
    .function("invoke",&MyBaseClass::invoke);
    //绑定子类
    class_<MyDerivedClass,base<MyBaseClass>>("MyDerivedClass")
    .constructor<>()
    .function("invoke",&MyDerivedClass::invoke);
}

重载函数

使用select_overload()帮助函数选中合适的签名。

//示例说明
struct Example {
    
    
    void foo();
    void foo(int i);
    void foo(float f) const;
}; 
EMSCRIPTEN_BINDING(overloads) {
    
    
   class_<Example>("Example")
     .function("foo", select_overload<void()>(&Example::foo))
     .function("foo_int", select_overload<void(int)>(&Example::foo))
     .function("foo_float", select_overload<void(float)const>(&Example::foo))
     ;
}

下面演示了 SGGeoPoint 的构造函数和 createObject重载函数。

class_<SGGeoPoint>("SGGeoPoint")
     .constructor()
     .constructor<double,double>()//此行对应参数3有默认值的C++构造函数
     .constructor<double,double,double>()
     .smart_ptr<SGGeoPointPtr>("SGGeoPointPtr")
     .class_function("createObject",select_overload<SGGeoPointPtr()>(&SGGeoPoint::createObject))
     .class_function("createObjectByXYZ",select_overload<SGGeoPointPtr(double,double,double)>(&SGGeoPoint::createObject))
     ;

函数参数默认值

对于构造函数来说,可以较简单的实现默认参数的绑定,而普通函数可通过optional_override方法来创建特殊的Lambda函数。
对于下面的带参函数

//SGGeoPoint类构造函数、静态函数的参数有默认值
class SMART_GEOMETRY_EXPORT SGGeoPoint :public SGAbstractGeometry
{
    
    
public:
	SGGeoPoint();
	SGGeoPoint(double x, double y, double z = 0.0);//带默认值
	static dan::Smart_Object<SGGeoPoint> createObject();
	static dan::Smart_Object<SGGeoPoint> createObject(double x, double y, double z = 0.0);//带默认值
}

对应绑定写法:

class_<SGGeoPoint>("SGGeoPoint")
        .constructor()
        .constructor<double,double>()//构造函数直接这样写即可
        .constructor<double,double,double>()
        .smart_ptr<SGGeoPointPtr>("SGGeoPointPtr")
        .class_function("createObject",select_overload<SGGeoPointPtr()>(&SGGeoPoint::createObject))
        .class_function("createObject_1",select_overload<SGGeoPointPtr(double,double,double)>(&SGGeoPoint::createObject))
        //普通函数通过lamda函数可达到需求
        .class_function("createObject_2",optional_override([](double x,double y){
    
    
                return SGGeoPoint::createObject(x,y);
            }))
        ;

枚举

embind支持C++98枚举和C++11枚举类。

    enum OldStyle {
    
    
        OLD_STYLE_ONE,
        OLD_STYLE_TWO
    }; 
    enum class NewStyle {
    
    
        ONE,
        TWO
    };
    EMSCRIPTEN_BINDINGS(my_enum_example) {
    
    
        enum_<OldStyle>("OldStyle")
            .value("ONE", OLD_STYLE_ONE)
            .value("TWO", OLD_STYLE_TWO);
        enum_<NewStyle>("NewStyle")
            .value("ONE", NewStyle::ONE)
            .value("TWO", NewStyle::TWO);
     }

JavaScript中调用形式如下:

    Module.OldStyle.ONE;
    Module.NewStyle.TWO;

常量

    EMSCRIPTEN_BINDINGS(my_constant_example) {
    
    
        constant("SOME_CONSTANT", SOME_CONSTANT);
    }

Embind参考链接:
https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html
https://www.osheep.cn/3952.html
https://www.jianshu.com/p/a03444bf9e97
https://www.cnblogs.com/catwin/p/13337074.html

画布

Emscripten提供了对EGL和OpenGL的支持。详情可参考官方文档:https://emscripten.org/docs/porting/multimedia_and_graphics/index.html

Emscripten画布

Emscripten中只有唯一画布,可通过画布id为其指定大小。

EGL创建上下文

1.获取对象的句柄
EGLDisplay eglGetDisplay(EGL_DEFAULT_DISPLAY)
2.在显示器上初始化
EGLBoolean eglInitialize()
3.查找渲染目标参数
eglGetConfigs()/eglChooseConfig()
4.创建主渲染目标表面
EGLSurface eglCreateWindowSurface()
5.创建 GLES2渲染上下文,创建时可指定版本为ES2/ES3
EGLContext eglCreateContext()
6.激活渲染上下文
eglMakeCurrent()

OpenGLES3

若使用OpenGLES3,需指定参数-s FULL_ES3=1
注意,程序中opengles3所用着色器的第一行应指定版本**#version 300 es**

JS绑定画布

为JS的画布元素canvas通过以下方式绑定C++中默认画布。

<body>    
    <!-- Create the canvas that the C++ code will draw into -->
    <canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
    <!-- Allow the C++ to access the canvas element --> 
    <script type='text/javascript'>
        var canv = document.getElementById('canvas');
        var Module = {
    
    
        canvas: canv      
        };
    </script>
    <script type='text/javascript' src="smartgis.3dexample.js"></script>
</body>

虚拟文件系统

Emscripten文件系统

Emscripten提供了MEMFS内存文件系统,供fopen()/fread()/fwrite()等libc/libcxx文件访问函数调用。
参考链接:
https://www.cntofu.com/book/150/zh/ch3-runtime/ch3-03-fs.md
https://emscripten.org/docs/porting/files/packaging_files.html

emcc preload

文件打包可以在emcc命令行中完成,有 preload(预加载)以及embed(嵌入)两种方式。其中preload方式效率更高,打包后文件体积更小,打包后生成与.js同名的.data数据文件,其中包含了所有文件的二进制数据,同时在.js文件的胶水代码中包含对文件包的下载、装载操作。
emcc中使用示例:

--preload-file hello.txt //指定打包文件
--preload-file filedir //指定打包文件夹下所有文件,包含下级文件夹中的文件

CMAKE打包文件

需要打包的文件:
![image.png](https://img-blog.csdnimg.cn/img_convert/7f3a9b3c37f91b01e25f66487c29aa73.png#clientId=u275b0154-1d4f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=59&id=F2zgW&margin=[object Object]&name=image.png&originHeight=118&originWidth=1180&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36560&status=done&style=none&taskId=ued147289-c81b-4c43-a57f-43612e5ca5b&title=&width=590)

  1. CMakeLists.txt 中 --preload-file 指定打包文件/文件夹filedir
set_target_properties(test PROPERTIES LINK_FLAGS "--bind --preload-file shaders")
  1. 被打包的文件/文件夹需放在该项目cmake目录中

  2. 编译后程序将能调用到打包文件中的着色器。

内存

设置内存

image.png
若Em编译时内存不足,我们可以修改emsdk/upstream/emscripten/src目录里的settings.js中的INITIAL_MEMORY值来设置更大的内存。
Web端运行时也可能出现内存不足的错误,但运行时数据大小的波动是巨大的,在初始编译时指定超大内存会非常浪费。可以通过可变内存参数指定在运行时扩大内存容量。

可变内存

使用-s ALLOW_MEMORY_GROWTH模式,当编译目标是asm.js时,可变内存会影像性能。但当编译目标是wasm时,使用可变内存模式非常高效,不会影响性能。

调试

编译工程

CMake工程

代码

  1. 设置渲染模式为OpenGLES3

SGERenderer::setCurrentRenderer(“OpenGLES3”);

  1. emscripten_set_main_loop设置em消息循环
#include <emscripten.h>
std::function<void()> loop;
void main_loop() {
    
     loop(); }
int main()
{
    
    
    //.....
	loop = [&]
    {
    
    
		//draw......
    };
    emscripten_set_main_loop(main_loop, 0, true);
	return 0;
}
  1. 着色器指定版本

在着色器文件的顶部(第一行),应声明版本:

# version 300 es 

库依赖

在cmake工程编译中,需要将依赖到的库加入CMakelists.txt文件中。如果库A依赖了库B,且代码中使用到了库B的类或方法,则需要将库B也加入依赖:

link_directories(/mnt/hgfs/smartgis.all/smartgis.all/bin/x64/Release)
link_directories(/home/czw/3rd_a)
target_link_libraries(testtwo stdc++ dl smartgis.core.a smartgis.common.a
smartgis.data.a smartgis.geometry.a geos.a m)
set_target_properties(testtwo PROPERTIES LINK_FLAGS "--bind --experimental-wasm-simd")

编译后只有.wasm文件和.js文件,是对工程内代码的封装,不包含第三方库包,文件体积不会过大。

Em参数

以下是一些emscripten编译参数,可按需指定编译参数。

--bind 	                    #执行C++到JS地绑定
--experimental-wasm-simd    #SIMD
-std=c++11  
-s WASM=1                   #生成.wasm而不是asm.js
-s FULL_ES3=1               #使用gles3.0
--preload-file shaders      #打包文件
-s LLD_REPORT_UNDEFINED     #指定对undefined类型错误更详细地输出
-s ALLOW_MEMORY_GROWTH      #允许程序运行时内存增长
-s ASSERTIONS=1             #断言,指定后Web运行时可输出更多错误信息
-v                          #编译时输出更多信息
-O3                         #优化编译

CMAKE

对程序依赖的各个库进行静态库编译。
windows:

emsdk_env.bat   //注册em环境
emcmake cmake ..   //cmake
emmake make     ///make

linux:

#1.注册环境:进入emsdk目录后执行emsdk_env.sh
source ./emsdk_env.sh
#2.cmake
emcmake cmake ..
#3.make
emmake make

最终编译出smartgis.3dexample.js、smartgis.3dexample.wasm、smartgis.3dexample.data三个文件。

启动Web

在运行目录中新建index.html文件,引入smartgis.3dexample.js,指定canvas连接,执行以下操作:

emrun --no_browser --port 8080 .

将命令窗输出的网址在浏览器打开,即可在浏览器查看效果。若无连接,将地址中0.0.0.0修改为localhost刷新即可。
除了emrun外,也可以使用以下方式开启服务。

python -m http.server 8080
//如果是python2,则使用下面代码
//python -m SimpleHTTPServer 8080

OpenGL呈现效果

目前已初步验证了自主引擎启用OpenGLES3渲染模式在Web端呈现。

绘制三角形

以下是绘制三角形后在Web端显示的效果。着色器在打包文件中进行了加载。
image.png

加载IFC

以下是自主引擎渲染ifc模型在web端的显示。受不断打印信息的影响,截图中帧数较小。但帧数确实是引擎后续需优化的问题。
gif-ifc楼.gif

猜你喜欢

转载自blog.csdn.net/qq_33377547/article/details/126596652