fundo
WebAssembly
Simplificando, WebAssembly pode nos ajudar a compilar C++ em um formato binário .wasm que pode ser chamado por JS e usá-lo no lado da Web para chamar módulos C++ no lado da Web, aproveitando ao máximo as vantagens de linguagem eficiente do C++ .
Instalar Emscripten3.1.0
Emscripten é uma cadeia de ferramentas completa do compilador WebAssembly.
Site oficial: https://emscripten.org/index.html
Documentação oficial de instalação: https://emscripten.org/docs/getting_started/downloads.html
Siga as etapas de instalação na documentação oficial para concluir com êxito a instalação do Emscripten e o documentação oficial Existem instruções mais detalhadas sobre o uso do Emscripten, recomenda-se pesquisar e consultar a documentação oficial, se necessário.
Além disso, o documento de referência é o seguinte:
https://www.cntofu.com/book/150/zh/ch1-quick-guide/readme.md
Interação entre C++ e Javascript
ligar
maneira original
Os métodos em C++ podem ser extern "C"
EMSCRIPTEN_KEEPALIVE
modificados e podem ser chamados em html.
extern "C"
- Convertido para interface C,
EMSCRIPTEN_KEEPALIVE
macro - indica que este método precisa ser mantido para não ser otimizado o tempo todo.
Adicione "_" na frente do método ao chamar, e o nome da função changeColor pode ser pesquisado no código de cola gerado pelo 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>
No meio
Embind é usado para vincular funções e classes C++ a JavaScript para que o código compilado possa ser usado em js de maneira natural:
- Um arquivo de cabeçalho precisa ser adicionado ao código C/C++
#include <emscripten/bind.h>
. - Use
EMSCRIPTEN_BINDINGS()
blocos para criar associações para funções, classes, tipos de valor, ponteiros (tanto ponteiros brutos quanto inteligentes), enums e constantes - Adicione o parâmetro --bind ao compilar
Para vincular classes, funções, propriedades e estruturas, consulte os seguintes exemplos:
//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堆将无限增长
ponteiro inteligente
1. Ligação de ponteiros inteligentes comuns
//绑定智能指针的两种方式
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. Vinculação de ponteiros inteligentes personalizados
2.1 Escreva uma smart_ptr_trait
classe de modelo personalizada para implementar seu próprio tipo de ponteiro inteligente.
//编写自定义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 Adicionar na classe smart pointer element_type
Para construir smart_ptr_trait
, esta variável deve existir no 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&originWidWid =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);
};
classe de interface
O exemplo a seguir mostra o método de ligação da classe de interface.
//定义一个接口类,该接口需要由子类来实现
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");
}
No código acima, uma **classe "glue"** usada para conectar o código e o ambiente **wrapper**
é construída por meio da classe template . Dentro dessa classe, o processo de implementação da classe de interface no código e da subclasse no ambiente é vinculado indiretamente chamando a interface de subclasse implementada no código . Ao vincular internamente o método abstrato definido na classe de interface, é necessário fornecer ao método um sinalizador de estratégia nomeado, que identificará o processo de vinculação da função virtual pura e fornecerá a capacidade de captura de exceção correspondente. Fornece dois métodos de função local que podem ser usados para implementar a interface C/C++ no código, ou seja, o método sum . Mas a premissa de usar esses dois métodos é que, ao vincular a classe de interface, é necessário declarar explicitamente por meio do método que o processo de implementação específico da classe de interface será concluído no ambiente. Em seguida, você pode usar esses dois métodos para implementar a lógica específica da interface no ambiente.C/C++
JavaScript
JavaScript
C++
JavaScript
EMSCRIPTEN_BINDINGS
function
**pure_virtual()**
Embind
JavaScript
**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 : Neste código, o processo de implementação da subclasse da classe de interface é primeiro **extend**
concluído usando o método . Interface
Semelhante ao C/C++
processo de implementação da classe de manutenção, aqui você também pode usar seletivamente o método __construct
ou __destruct
para adicionar construtores e destruidores correspondentes à classe de entidade.
**implemento:** em comparação com extend
os métodos, **implement**
os métodos são mais adequados para classes de interface simples que não requerem construtores e destruidores. JavaScript
Pode-se ver que aqui só é necessário agrupar a função com a mesma assinatura que a função virtual pura na classe de interface com a estrutura do objeto e passá-la para implement
o método derivado do objeto de classe vinculado para concluir o processo de implementação da interface aula. Mais convenientemente, esse método retornará diretamente um objeto de subclasse instanciado, o que também evita a necessidade de outro new
processo.
Substituindo uma função virtual não pura
//定义一个接口类,该接口需要由子类来实现
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");
}
No geral, a única diferença entre este código e o código anterior é que, ao vincular a função virtual não pura da classe abstrata, o ponteiro da função correspondente não pode ser passado diretamente para o método da função, mas o processo de chamada da classe abstrata A função precisa ser encapsulada pelo optional _verride
método em uma função anônima especial e passada para o método da função como um todo. Além disso, ao contrário do processo de implementação de uma classe de interface, podemos substituir seletivamente ou usar diretamente a implementação padrão da função de chamada no ambiente JavaScript.O processo específico de substituição só pode **extend**
ser realizado por meio de herança .
//通过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");
Classes derivadas em 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);
}
função sobrecarregada
Use select_overload()
a função auxiliar para selecionar a assinatura apropriada.
//示例说明
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))
;
}
O seguinte demonstra o construtor de SGGeoPoint e a função sobrecarregada de 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))
;
Valores padrão do parâmetro de função
Para construtores , a associação de parâmetros padrão pode ser implementada com relativa facilidade e funções comuns podem optional_override
criar funções especiais do Lambda por meio de métodos.
Para a seguinte função com parâmetros
//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);//带默认值
}
Escrita obrigatória correspondente:
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);
}))
;
enumerar
embind suporta enumerações C++98 e classes de enumeração 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);
}
O formulário de chamada em JavaScript é o seguinte:
Module.OldStyle.ONE;
Module.NewStyle.TWO;
constante
EMSCRIPTEN_BINDINGS(my_constant_example) {
constant("SOME_CONSTANT", SOME_CONSTANT);
}
Link de referência do 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
tela
Emscripten fornece suporte para EGL e OpenGL. Para obter detalhes, consulte a documentação oficial: https://emscripten.org/docs/porting/multimedia_and_graphics/index.html
Tela escrita
Existe apenas uma tela única no Emscripten e o tamanho pode ser especificado pelo id da tela.
EGL cria contexto
1. Obtenha o identificador do objeto
EGLDisplay eglGetDisplay(EGL_DEFAULT_DISPLAY)
2. Inicialize na exibição
EGLBoolean eglInitialize()
3. Encontre os parâmetros de destino de renderização
eglGetConfigs()/eglChooseConfig()
4. Crie a superfície de destino de renderização principal
EGLSurface eglCreateWindowSurface()
5. Crie um contexto de renderização GLES2, a versão pode ser especificada como ES2/ES3 ao criar
EGLContext eglCreateContext()
6. Ative o contexto de renderização
eglMakeCurrent()
OpenGLES3
Se o OpenGLES3 for usado, os parâmetros precisam ser especificados -s FULL_ES3=1
.
Observe que a primeira linha do shader usado pelo opengles3 no programa deve especificar a versão **#version 300 es**
.
Tela de encadernação JS
Vincule a tela padrão em C++ ao elemento canvas canvas de JS da seguinte maneira.
<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>
sistema de arquivos virtual
Sistema de arquivos Emscripten
Emscripten fornece o sistema de arquivos de memória MEMFSfopen()
para // e outras chamadas de função fread()
de acesso a arquivos libc/libcxx. Link de referência: https://www.cntofu.com/book/150/zh/ch3-runtime/ch3-03-fs.md https://emscripten.org/docs/porting/files/packaging_files.htmlfwrite()
pré-carga emcc
O empacotamento do arquivo pode ser feito na linha de comando do emcc, existem duas formas: preload (preload) e embed (embed). Entre eles , o método de pré-carregamento é mais eficiente e o tamanho do arquivo é menor após o empacotamento. Após o empacotamento, é gerado um arquivo de dados .data com o mesmo nome de .js, que contém os dados binários de todos os arquivos. Operações de download e carregamento .
Exemplo de uso em emcc:
--preload-file hello.txt //指定打包文件
--preload-file filedir //指定打包文件夹下所有文件,包含下级文件夹中的文件
arquivo de pacote CMAKE
需要打包的文件:
![image.png](https://img-blog.csdnimg.cn/img_convert/7f3a9b3c37f91b01e25f66487c29aa73.png#clientId=u275b0154-1d4f-4&crop=0&crop=1&cropid=0&paste=0&cropight=0&cropight=0&cropight=0&cropight=0&cropight=0&cropight=0&cropight=0&cropight=0&cropight=0&cropid= 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-43612e&5wica5dbth=9)
--preload-file
Especifique o arquivo/pasta do pacote arquivado em CMakeLists.txt
set_target_properties(test PROPERTIES LINK_FLAGS "--bind --preload-file shaders")
-
Os arquivos/pastas empacotados precisam ser colocados no diretório cmake do projeto
-
Após a compilação, o programa poderá chamar os shaders no arquivo empacotado.
Memória
definir memória
Se a memória for insuficiente quando o Em for compilado, podemos modificar o valor em settings.js no diretório emsdk/upstream/emscripten/src INITIAL_MEMORY
para definir uma memória maior.
Erros de memória insuficiente também podem ocorrer durante o tempo de execução no lado da Web, mas o tamanho dos dados flutua enormemente durante o tempo de execução e seria um desperdício especificar uma memória grande durante a compilação inicial. O parâmetro de memória variável pode ser especificado para expandir a capacidade de memória em tempo de execução.
memória variável
Usando -s ALLOW_MEMORY_GROWTH
o modo, a memória variável pode afetar o desempenho ao compilar para asm.js. Mas quando o destino da compilação é wasm, usar o modo de memória mutável é muito eficiente e não afetará o desempenho.
depuração
compilar projeto
Projeto CMake
o código
- Defina o modo de renderização para OpenGLES3
SGERenderer::setCurrentRenderer(“OpenGLES3”);
- emscripten_set_main_loop define o loop de mensagem 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;
}
- versão específica do shader
No topo (primeira linha) do arquivo shader, a versão deve ser declarada:
# version 300 es
dependências da biblioteca
Durante a compilação do projeto cmake, as bibliotecas dependentes precisam ser adicionadas ao arquivo CMakelists.txt. Se a biblioteca A depender da biblioteca B e a classe ou método da biblioteca B for usada no código, a biblioteca B precisa ser adicionada como uma dependência:
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")
Após a compilação, restam apenas arquivos .wasm e .js, que encapsulam o código no projeto e não incluem pacotes de bibliotecas de terceiros, portanto, o tamanho do arquivo não será muito grande.
Parâmetro Em
A seguir estão alguns parâmetros de compilação emscripten, que podem ser especificados sob demanda.
--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
Execute a compilação de biblioteca estática para cada biblioteca da qual o programa depende.
janelas:
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
Finalmente, três arquivos: smartgis.3dexample.js, smartgis.3dexample.wasm e smartgis.3dexample.data são compilados.
iniciar web
Crie um novo arquivo index.html no diretório em execução, importe smartgis.3dexample.js, especifique a conexão da tela e execute as seguintes operações:
emrun --no_browser --port 8080 .
Abra a saída da URL pela janela de comando no navegador para visualizar o efeito no navegador. Se não houver conexão, altere 0.0.0.0 no endereço para localhost para atualizar.
Além do emrun, você também pode usar os seguintes métodos para iniciar o serviço.
python -m http.server 8080
//如果是python2,则使用下面代码
//python -m SimpleHTTPServer 8080
Efeito de renderização OpenGL
No momento, foi verificado preliminarmente que o mecanismo autônomo permite que o modo de renderização OpenGLES3 seja renderizado no lado da Web.
desenhar triângulo
O seguinte é o efeito exibido no lado da Web depois de desenhar o triângulo. Shaders são carregados em packfiles.
Carregar IFC
A seguir está a exibição do modelo ifc renderizado pelo mecanismo autônomo na web. Afetado pela impressão contínua de informações, o número de quadros na captura de tela é pequeno. Mas o número de quadros é de fato um problema que o motor precisa otimizar no futuro.