Rendering Engine's Support Scheme for Web

background

WebAssembly

To put it simply, WebAssembly can help us compile C++ into a .wasm binary format that can be called by JS, and use it on the Web side to call C++ modules on the Web side, giving full play to the efficient language advantages of C++.

Install Emscripten3.1.0

Emscripten is a complete WebAssembly compiler toolchain.
Official website: https://emscripten.org/index.html
Official installation documentation: https://emscripten.org/docs/getting_started/downloads.html
Follow the installation steps in the official documentation to successfully complete the Emscripten installation, and the official documentation There are more detailed instructions on the usage of Emscripten. It is recommended to search and view in the official documentation if necessary.
In addition, the reference document is as follows:
https://www.cntofu.com/book/150/zh/ch1-quick-guide/readme.md

Interaction between C++ and Javascript

to bind

original way

The methods in C++ can be extern "C" EMSCRIPTEN_KEEPALIVEmodified and can be called in html.
extern "C" - Converted to C interface,
EMSCRIPTEN_KEEPALIVEmacro - indicates that this method needs to be kept from being optimized all the time.
Add "_" in front of the method when calling, and the changeColor function name can be searched in the glue code generated by emcc.

//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>

In the middle

Embind is used to bind C++ functions and classes to JavaScript so that compiled code can be used in js in a natural way:

  1. A header file needs to be added to the C/C++ code #include <emscripten/bind.h>.
  2. Use EMSCRIPTEN_BINDINGS()blocks to create bindings for functions, classes, value types, pointers (both raw and smart pointers), enums, and constants
  3. Add the --bind parameter when compiling

For binding classes, functions, properties, and structures, refer to the following examples:

//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堆将无限增长

smart pointer

1. Binding of ordinary smart pointers

//绑定智能指针的两种方式
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. Binding of custom smart pointers
2.1. Write a custom smart_ptr_traittemplate class to implement your own smart pointer type.

//编写自定义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. Add in the smart pointer class element_type. To construct smart_ptr_trait, this variable must exist in the smart pointer.

    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);
};

interface class

The following example shows the binding method of the interface class.

//定义一个接口类,该接口需要由子类来实现
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");
}

In the above code, a **"glue" class** used to connect the code and the environment **wrapper**is constructed through the template class . Inside this class, the implementation process of the interface class in the code and the subclass in the environment is indirectly bound by calling the subclass interface implemented in the code . When internally binding the abstract method defined in the interface class, it is necessary to provide the method with a named strategy flag, which will identify the binding process of the pure virtual function and provide it with the corresponding exception capture capability. Provides us with two local function methods that can be used to implement the C/C++ interface in the code, namely the sum method. But the premise of using these two methods is that when binding the interface class, it is necessary to explicitly declare through the method that the specific implementation process of the interface class will be completed in the environment. Next, you can use these two methods to implement the specific logic of the interface in the environment.C/C++JavaScriptJavaScriptC++JavaScriptEMSCRIPTEN_BINDINGSfunction**pure_virtual()**
EmbindJavaScript**extend****implement****allow_subclass**JavaScriptJavaScriptC/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 : In this code, the subclass implementation process of the interface class is first **extend**completed using the method . InterfaceSimilar to C/C++the implementation process of the maintenance class, here you can also selectively use the __constructor __destructmethod to add corresponding constructors and destructors to the entity class.
**implement:** Compared with extendmethods, **implement**methods are more suitable for simple interface classes that do not require constructors and destructors. JavaScriptIt can be seen that here only need to wrap the function with the same signature as the pure virtual function in the interface class with the object structure, and pass it to implementthe method derived from the bound class object to complete the implementation process of the interface class. More conveniently, this method will directly return an instantiated subclass object, which also saves the need for another newprocess.

Overriding a non-pure virtual function

//定义一个接口类,该接口需要由子类来实现
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");
}

On the whole, the only difference between this code and the previous code is that when binding the non-pure virtual function of the abstract class, the pointer of the corresponding function cannot be passed directly to the function method, but the calling process of the function needs to be encapsulated by the optional _verridemethod In a special anonymous function and passed to the function method as a whole. In addition, unlike the process of implementing an interface class, we can selectively override or directly use the default implementation of the invoke function in the JavaScript environment . The specific process of overriding can only **extend**be realized by means of inheritance .

//通过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");

Derived classes in 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);
}

overloaded function

Use select_overload()the helper function to select the appropriate signature.

//示例说明
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))
     ;
}

The following demonstrates the constructor of SGGeoPoint and the overloaded function of 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))
     ;

Function parameter default values

For constructors , the binding of default parameters can be implemented relatively easily, and ordinary functions can optional_overridecreate special Lambda functions through methods.
For the following function with parameters

//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);//带默认值
}

Corresponding binding writing:

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);
            }))
        ;

enumerate

embind supports C++98 enumerations and C++11 enumeration classes.

    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);
     }

The calling form in JavaScript is as follows:

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

constant

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

Embind reference link:
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

canvas

Emscripten provides support for EGL and OpenGL. For details, please refer to the official documentation: https://emscripten.org/docs/porting/multimedia_and_graphics/index.html

Emscripten Canvas

There is only one unique canvas in Emscripten, and the size can be specified by the canvas id.

EGL creates context

1. Obtain the handle of the object
EGLDisplay eglGetDisplay(EGL_DEFAULT_DISPLAY)
2. Initialize on the display
EGLBoolean eglInitialize()
3. Find the rendering target parameters
eglGetConfigs()/eglChooseConfig()
4. Create the main rendering target surface
EGLSurface eglCreateWindowSurface()
5. Create a GLES2 rendering context, the version can be specified as ES2/ES3 when creating
EGLContext eglCreateContext()
6. Activate the rendering context
eglMakeCurrent()

OpenGLES3

If OpenGLES3 is used, parameters need to be specified -s FULL_ES3=1.
Note that the first line of the shader used by opengles3 in the program should specify the version **#version 300 es**.

JS binding canvas

Bind the default canvas in C++ to the canvas element canvas of JS in the following way.

<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>

virtual file system

Emscripten file system

Emscripten provides the MEMFS memory file system for fopen()// and other libc/libcxx file fread()access fwrite()function calls.
Reference link:
https://www.cntofu.com/book/150/zh/ch3-runtime/ch3-03-fs.md
https://emscripten.org/docs/porting/files/packaging_files.html

emcc preload

File packaging can be done on the emcc command line, there are two ways: preload (preload) and embed (embed). Among them , the preload method is more efficient, and the file size is smaller after packaging. After packaging, a .data data file with the same name as .js is generated, which contains the binary data of all files. Download and load operations.
Example of use in emcc:

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

CMAKE package file

需要打包的文件:
![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. --preload-fileSpecify the package file/folder filedir in CMakeLists.txt
set_target_properties(test PROPERTIES LINK_FLAGS "--bind --preload-file shaders")
  1. The packaged files/folders need to be placed in the cmake directory of the project

  2. After compiling, the program will be able to call the shaders in the packaged file.

Memory

set memory

image.png
If the memory is insufficient when Em is compiled, we can modify the value in settings.js in the emsdk/upstream/emscripten/src directory INITIAL_MEMORYto set a larger memory.
Insufficient memory errors may also occur during runtime on the Web side, but the data size fluctuates enormously during runtime, and it would be very wasteful to specify a large memory during initial compilation. The variable memory parameter can be specified to expand the memory capacity at runtime.

variable memory

Using -s ALLOW_MEMORY_GROWTHmode, variable memory can affect performance when compiling to asm.js. But when the compilation target is wasm, using mutable memory mode is very efficient and will not affect performance.

debugging

compile project

CMake project

the code

  1. Set the rendering mode to OpenGLES3

SGERenderer::setCurrentRenderer(“OpenGLES3”);

  1. emscripten_set_main_loop sets the em message loop
#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. shader specific version

At the top (first line) of the shader file, the version should be declared:

# version 300 es 

library dependencies

During cmake project compilation, the dependent libraries need to be added to the CMakelists.txt file. If library A depends on library B, and the class or method of library B is used in the code, library B needs to be added as a dependency:

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")

After compiling, there are only .wasm files and .js files, which encapsulate the code in the project and do not include third-party library packages, so the file size will not be too large.

Em parameter

The following are some emscripten compilation parameters, which can be specified on demand.

--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

Perform static library compilation for each library that the program depends on.
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

Finally, three files: smartgis.3dexample.js, smartgis.3dexample.wasm, and smartgis.3dexample.data are compiled.

start web

Create a new index.html file in the running directory, import smartgis.3dexample.js, specify the canvas connection, and perform the following operations:

emrun --no_browser --port 8080 .

Open the URL output by the command window in the browser to view the effect in the browser. If there is no connection, change 0.0.0.0 in the address to localhost to refresh.
In addition to emrun, you can also use the following methods to start the service.

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

OpenGL rendering effect

At present, it has been preliminarily verified that the autonomous engine enables OpenGLES3 rendering mode to render on the Web side.

draw triangle

The following is the effect displayed on the Web side after drawing the triangle. Shaders are loaded in packfiles.
image.png

Load IFC

The following is the display of the ifc model rendered by the autonomous engine on the web. Affected by the continuous printing of information, the number of frames in the screenshot is small. But the number of frames is indeed a problem that the engine needs to optimize in the future.
gif-ifc楼.gif

Guess you like

Origin blog.csdn.net/qq_33377547/article/details/126596652