Importing Models Directly to Memory
UNIGINE Import System allows you to import models and scenes in various external formats (FBX, DAE, OBJ, etc.). By default (i.e., when DefaultProcessor is used) for each imported scene or model a set of corresponding files in UNIGINE file formats (.mesh, .texture, .node, etc.) is created on disk.
However, in some cases it might be necessary to import models directly into your current scene on-the-fly, without creating any unnecessary files on disk (e.g. loading models of buildings for a smart city application, etc.). Moreover, this approach can speed up the whole importing process due to a reduced number of slow disk I/O operations.
For this purpose we should create a custom import processor.
This example demonstrates how to:
- Create your own custom import processor for the FbxImporter plugin.
- Use your custom import processor to bring a scene stored in an FBX file to the currently loaded UNIGINE scene.
Before we get started, a little bit of theory (as key points).
- UNIGINE API offers us a set of classes for implementing customized model import.
- All meta information about your imported scene should be stored in an instance of the ImportScene class. Basically a scene may contain meshes (ImportMesh), lights (ImportLight), cameras (ImportCamera), and other elements (see the complete list here).
- A custom import processor should be inherited from the ImportProcessor class.
Creating a Custom Import Processor#
So, let's start with the processor. We call it MemoryProcessor and implement creation of objects in the current UNIGINE world from the contents of the FBX-file skipping unnecessary saving of data to disk. So we shall simply run through the components of the imported FBX scene (cameras, lights and meshes), build the corresponding node hierarchy to be added to the world and create all necessary materials and textures. The code below is supplemented with comments explaining the basic aspects of the process.
Direct Memory Import Processor
// including necessary libraries
#include <UnigineImport.h>
#include <UnigineMaterials.h>
#include <UnigineFileSystem.h>
using namespace Unigine;
// our custom ImportProcessor to be used for direct memory import of scenes
class MemoryProcessor : public ImportProcessor
{
public:
void convertNode(const NodePtr &node_parent, const ImportNodePtr &import_node_parent,
NodePtr &node, const ImportNodePtr &import_node)
{
using namespace Unigine::Math;
if (ImportCameraPtr import_camera = import_node->getCamera())
{
node = getImporter()->importCamera(getImportProcessor(), import_camera);
if (node)
node->setWorldTransform(Mat4(import_node->getTransform()));
} else if (ImportLightPtr import_light = import_node->getLight())
{
node = getImporter()->importLight(getImportProcessor(), import_light);
if (node)
node->setWorldTransform(Mat4(import_node->getTransform()));
} else if (ImportMeshPtr import_mesh = import_node->getMesh())
{
ObjectPtr object;
if (import_mesh->isHasAnimations())
{
float fps = getImporter()->getParameterFloat("fps");
auto mesh_skinned = ObjectMeshSkinned::create();
mesh_skinned->setMeshProceduralMode(true);
mesh_skinned->applyMeshProcedural(meshes[import_mesh->getFilepath()]);
if (!String::isEmpty(import_mesh->getAnimationFilepath()))
mesh_skinned->setAnimPath(import_mesh->getAnimationFilepath());
mesh_skinned->setSpeed(fps);
object = mesh_skinned;
} else
{
ObjectMeshStaticPtr mesh_static = ObjectMeshStatic::create();
mesh_static->setMeshProceduralMode(true);
mesh_static->applyMeshProcedural(meshes[import_mesh->getFilepath()]);
object = mesh_static;
object->setWorldTransform(Mat4(import_node->getTransform()));
}
int num_geometries = import_mesh->getNumGeometries();
for (int i = 0; i < num_geometries; ++i)
{
ImportGeometryPtr geometry = import_mesh->getGeometry(i);
int num_surfaces = geometry->getNumSurfaces();
for (int s = 0; s < num_surfaces; ++s)
{
ImportSurfacePtr surface = geometry->getSurface(s);
const int surface_index = surface->getTargetSurface();
if (surface_index == -1 || surface_index >= object->getNumSurfaces())
{
Log::error("MemoryProcessor: can't find surface \"%s\".\n", surface->getName());
continue;
}
if (!compare(surface->getMinVisibleDistance(), -Consts::INF))
{
object->setMinVisibleDistance(surface->getMinVisibleDistance(),
surface_index);
}
if (!compare(surface->getMaxVisibleDistance(), Consts::INF))
{
object->setMaxVisibleDistance(surface->getMaxVisibleDistance(),
surface_index);
}
object->setMinFadeDistance(surface->getMinFadeDistance(), surface_index);
object->setMaxFadeDistance(surface->getMaxFadeDistance(), surface_index);
if (ImportMaterialPtr import_material = surface->getMaterial())
{
object->setMaterial(materials[import_material->getFilepath()],
surface_index);
}
}
}
node = object;
} else
{
node = NodeDummy::create();
node->setWorldTransform(Mat4(import_node->getTransform()));
}
node->setName(import_node->getName());
getImporter()->importNodeChild(getImportProcessor(), node_parent, import_node_parent,
node, import_node);
int num_children = import_node->getNumChildren();
for (int i = 0; i < num_children; ++i)
{
NodePtr child;
convertNode(node, import_node, child, import_node->getChild(i));
node->addWorldChild(child);
}
}
protected:
// method to be called on mesh processing
bool onProcessMesh(const MeshPtr &mesh, const ImportMeshPtr &import_mesh) override
{
UGUID guid = generate_unique_guid();
import_mesh->setFilepath(guid.makeString().get());
meshes.append(import_mesh->getFilepath(), mesh);
return true;
}
// method to be called on mesh animation processing
bool onProcessAnimation(const MeshAnimationPtr &animation, const ImportMeshPtr &import_mesh,
const ImportAnimationPtr &import_animation) override
{
StringStack<> animation_file_path = "import_animation_temp_blob_";
animation_file_path += generate_unique_guid().makeString();
animation_file_path += ".anim";
const bool added = FileSystem::addBlobFile(animation_file_path);
if (!added)
{
Log::error("Can't add a blob file: %s\n", animation_file_path.get());
return false;
}
import_animation->setFilepath(animation_file_path);
if (!animation->save(import_animation->getFilepath()))
return false;
import_mesh->setAnimationFilepath(import_animation->getFilepath());
return true;
}
// method to be called on texture processing
bool onProcessTexture(const ImportTexturePtr &import_texture) override
{
import_texture->setFilepath(import_texture->getOriginalFilepath());
return true;
}
// method to be called on material processing
bool onProcessMaterial(const MaterialPtr &material,
const ImportMaterialPtr &import_material) override
{
UGUID guid = generate_unique_guid();
import_material->setFilepath(guid.makeString().get());
materials.append(guid.makeString().get(), material);
return true;
}
private:
UGUID generate_unique_guid()
{
UGUID guid;
guid.generate();
int attempt = 100;
while (guids.contains(guid) && attempt-- > 0)
guid.generate();
guids.append(guid);
return guid;
}
private:
HashSet<UGUID> guids;
// HashMaps to be used for imported meshes and materials
HashMap<String, MeshPtr> meshes;
HashMap<String, MaterialPtr> materials;
};
Using Our Import Processor#
We have got our custom import processor, let's write a function using it to import the contents of the specified FBX to the currently loaded UNIGINE world.
Import Function
/// function importing the contents of the specified FBX to the currently loaded scene
NodePtr import(const char *filepath)
{
// creating an importer for our fbx file
ImporterPtr importer = Import::createImporterByFileName(filepath);
if (!importer)
return nullptr;
if (!importer->init(filepath))
return nullptr;
// creating our custom MemoryProcessor and using it to import meshes, textures, materials, and nodes without saving data to files on disk
MemoryProcessor memory_processor;
ImportScenePtr scene = importer->getScene();
int num_meshes = scene->getNumMeshes();
for (int mesh_index = 0; mesh_index < num_meshes; ++mesh_index)
{
MeshPtr m = Mesh::create();
importer->importMesh(memory_processor.getImportProcessor(), m, scene->getMesh(mesh_index));
if (m->getNumBones())
{
int num_animations = scene->getNumAnimations();
for (int anim_index = 0; anim_index < num_animations; ++anim_index)
{
MeshAnimationPtr animation_mesh = MeshAnimation::create();
importer->importAnimation(memory_processor.getImportProcessor(), animation_mesh,
scene->getMesh(mesh_index), scene->getAnimation(anim_index));
}
}
}
int num_textures = scene->getNumTextures();
for (int i = 0; i < num_textures; ++i)
{
importer->importTexture(memory_processor.getImportProcessor(), scene->getTexture(i));
}
MaterialPtr mesh_base = Materials::findManualMaterial("Unigine::mesh_base");
int num_materials = scene->getNumMaterials();
for (int i = 0; i < num_materials; ++i)
{
MaterialPtr m = mesh_base->inherit();
importer->importMaterial(memory_processor.getImportProcessor(), m, scene->getMaterial(i));
}
int num_nodes = scene->getNumNodes();
ImportNodePtr root_node;
for (int i = 0; i < num_nodes; ++i)
{
const ImportNodePtr &node = scene->getNode(i);
if (node->getParent() == nullptr)
{
root_node = node;
break;
}
}
if (root_node)
{
NodePtr node;
memory_processor.setImporter(importer);
memory_processor.convertNode(nullptr, nullptr, node, root_node);
return node;
}
return nullptr;
}
Now we can just load the FbxImporter plugin and use our import() function to add the desired model.
// including necessary libraries
#include <UnigineConsole.h>
// ...
// loading the FbxImporter plugin
Console::run("plugin_load FbxImporter");
Console::flush();
// importing an FBX-model directly to the scene
NodePtr imported_node = import("material_ball.fbx");
Example Code#
To test our custom import processor, we can create a new C++ project in SDK Browser and put all the code listed below in the AppWorldLogic.h and AppWorldLogic.cpp files.
AppWorldLogic.h
#ifndef __APP_WORLD_LOGIC_H__
#define __APP_WORLD_LOGIC_H__
#include <UnigineLogic.h>
#include <UnigineStreams.h>
class AppWorldLogic : public Unigine::WorldLogic
{
public:
AppWorldLogic();
virtual ~AppWorldLogic();
int init() override;
int update() override;
int postUpdate() override;
int updatePhysics() override;
int shutdown() override;
int save(const Unigine::StreamPtr &stream) override;
int restore(const Unigine::StreamPtr &stream) override;
private:
void run_animation(const Unigine::NodePtr &node);
void setup_gui();
void setup_player(const Unigine::NodePtr &node);
void import_file(const Unigine::String &filepath);
void dialog_file_ok_clicked(Unigine::WidgetDialogFilePtr dialog);
void dialog_file_cancel_clicked(Unigine::WidgetDialogFilePtr dialog);
void request_import(const Unigine::WidgetPtr &sender);
Unigine::NodePtr node_;
Unigine::String default_dialog_path{"./"};
};
#endif // __APP_WORLD_LOGIC_H__
AppWorldLogic.cpp
#include "AppWorldLogic.h"
#include <UnigineMathLib.h>
// including necessary libraries
#include <UnigineImport.h>
#include <UnigineMaterials.h>
#include <UnigineFileSystem.h>
#include <UnigineGame.h>
#include <UnigineWindowManager.h>
using namespace Unigine;
// our custom ImportProcessor to be used for direct memory import of scenes
class MemoryProcessor : public ImportProcessor
{
public:
void convertNode(const NodePtr &node_parent, const ImportNodePtr &import_node_parent,
NodePtr &node, const ImportNodePtr &import_node)
{
using namespace Unigine::Math;
if (ImportCameraPtr import_camera = import_node->getCamera())
{
node = getImporter()->importCamera(getImportProcessor(), import_camera);
if (node)
node->setWorldTransform(Mat4(import_node->getTransform()));
} else if (ImportLightPtr import_light = import_node->getLight())
{
node = getImporter()->importLight(getImportProcessor(), import_light);
if (node)
node->setWorldTransform(Mat4(import_node->getTransform()));
} else if (ImportMeshPtr import_mesh = import_node->getMesh())
{
ObjectPtr object;
if (import_mesh->isHasAnimations())
{
float fps = getImporter()->getParameterFloat("fps");
auto mesh_skinned = ObjectMeshSkinned::create();
mesh_skinned->setMeshProceduralMode(true);
mesh_skinned->applyMeshProcedural(meshes[import_mesh->getFilepath()]);
if (!String::isEmpty(import_mesh->getAnimationFilepath()))
mesh_skinned->setAnimPath(import_mesh->getAnimationFilepath());
mesh_skinned->setSpeed(fps);
object = mesh_skinned;
} else
{
ObjectMeshStaticPtr mesh_static = ObjectMeshStatic::create();
mesh_static->setMeshProceduralMode(true);
mesh_static->applyMeshProcedural(meshes[import_mesh->getFilepath()]);
object = mesh_static;
object->setWorldTransform(Mat4(import_node->getTransform()));
}
int num_geometries = import_mesh->getNumGeometries();
for (int i = 0; i < num_geometries; ++i)
{
ImportGeometryPtr geometry = import_mesh->getGeometry(i);
int num_surfaces = geometry->getNumSurfaces();
for (int s = 0; s < num_surfaces; ++s)
{
ImportSurfacePtr surface = geometry->getSurface(s);
const int surface_index = surface->getTargetSurface();
if (surface_index == -1 || surface_index >= object->getNumSurfaces())
{
Log::error("MemoryProcessor: can't find surface \"%s\".\n", surface->getName());
continue;
}
if (!compare(surface->getMinVisibleDistance(), -Consts::INF))
{
object->setMinVisibleDistance(surface->getMinVisibleDistance(),
surface_index);
}
if (!compare(surface->getMaxVisibleDistance(), Consts::INF))
{
object->setMaxVisibleDistance(surface->getMaxVisibleDistance(),
surface_index);
}
object->setMinFadeDistance(surface->getMinFadeDistance(), surface_index);
object->setMaxFadeDistance(surface->getMaxFadeDistance(), surface_index);
if (ImportMaterialPtr import_material = surface->getMaterial())
{
object->setMaterial(materials[import_material->getFilepath()],
surface_index);
}
}
}
node = object;
} else
{
node = NodeDummy::create();
node->setWorldTransform(Mat4(import_node->getTransform()));
}
node->setName(import_node->getName());
getImporter()->importNodeChild(getImportProcessor(), node_parent, import_node_parent,
node, import_node);
int num_children = import_node->getNumChildren();
for (int i = 0; i < num_children; ++i)
{
NodePtr child;
convertNode(node, import_node, child, import_node->getChild(i));
node->addWorldChild(child);
}
}
protected:
// method to be called on mesh processing
bool onProcessMesh(const MeshPtr &mesh, const ImportMeshPtr &import_mesh) override
{
UGUID guid = generate_unique_guid();
import_mesh->setFilepath(guid.makeString().get());
meshes.append(import_mesh->getFilepath(), mesh);
return true;
}
// method to be called on mesh animation processing
bool onProcessAnimation(const MeshAnimationPtr &animation, const ImportMeshPtr &import_mesh,
const ImportAnimationPtr &import_animation) override
{
StringStack<> animation_file_path = "import_animation_temp_blob_";
animation_file_path += generate_unique_guid().makeString();
animation_file_path += ".anim";
const bool added = FileSystem::addBlobFile(animation_file_path);
if (!added)
{
Log::error("Can't add a blob file: %s\n", animation_file_path.get());
return false;
}
import_animation->setFilepath(animation_file_path);
if (!animation->save(import_animation->getFilepath()))
return false;
import_mesh->setAnimationFilepath(import_animation->getFilepath());
return true;
}
// method to be called on texture processing
bool onProcessTexture(const ImportTexturePtr &import_texture) override
{
import_texture->setFilepath(import_texture->getOriginalFilepath());
return true;
}
// method to be called on material processing
bool onProcessMaterial(const MaterialPtr &material,
const ImportMaterialPtr &import_material) override
{
UGUID guid = generate_unique_guid();
import_material->setFilepath(guid.makeString().get());
materials.append(guid.makeString().get(), material);
return true;
}
private:
UGUID generate_unique_guid()
{
UGUID guid;
guid.generate();
int attempt = 100;
while (guids.contains(guid) && attempt-- > 0)
guid.generate();
guids.append(guid);
return guid;
}
private:
HashSet<UGUID> guids;
// HashMaps to be used for imported meshes and materials
HashMap<String, MeshPtr> meshes;
HashMap<String, MaterialPtr> materials;
};
/// function importing the contents of the specified FBX to the currently loaded scene
NodePtr import(const char *filepath)
{
// creating an importer for our fbx file
ImporterPtr importer = Import::createImporterByFileName(filepath);
if (!importer)
return nullptr;
if (!importer->init(filepath))
return nullptr;
// creating our custom MemoryProcessor and using it to import meshes, textures, materials, and nodes without saving data to files on disk
MemoryProcessor memory_processor;
ImportScenePtr scene = importer->getScene();
int num_meshes = scene->getNumMeshes();
for (int mesh_index = 0; mesh_index < num_meshes; ++mesh_index)
{
MeshPtr m = Mesh::create();
importer->importMesh(memory_processor.getImportProcessor(), m, scene->getMesh(mesh_index));
if (m->getNumBones())
{
int num_animations = scene->getNumAnimations();
for (int anim_index = 0; anim_index < num_animations; ++anim_index)
{
MeshAnimationPtr animation_mesh = MeshAnimation::create();
importer->importAnimation(memory_processor.getImportProcessor(), animation_mesh,
scene->getMesh(mesh_index), scene->getAnimation(anim_index));
}
}
}
int num_textures = scene->getNumTextures();
for (int i = 0; i < num_textures; ++i)
{
importer->importTexture(memory_processor.getImportProcessor(), scene->getTexture(i));
}
MaterialPtr mesh_base = Materials::findManualMaterial("Unigine::mesh_base");
int num_materials = scene->getNumMaterials();
for (int i = 0; i < num_materials; ++i)
{
MaterialPtr m = mesh_base->inherit();
importer->importMaterial(memory_processor.getImportProcessor(), m, scene->getMaterial(i));
}
int num_nodes = scene->getNumNodes();
ImportNodePtr root_node;
for (int i = 0; i < num_nodes; ++i)
{
const ImportNodePtr &node = scene->getNode(i);
if (node->getParent() == nullptr)
{
root_node = node;
break;
}
}
if (root_node)
{
NodePtr node;
memory_processor.setImporter(importer);
memory_processor.convertNode(nullptr, nullptr, node, root_node);
return node;
}
return nullptr;
}
AppWorldLogic::AppWorldLogic(){}
AppWorldLogic::~AppWorldLogic(){}
int AppWorldLogic::init()
{
setup_gui();
return 1;
}
void AppWorldLogic::run_animation(const NodePtr &node)
{
if (ObjectMeshSkinnedPtr skinned = checked_ptr_cast<ObjectMeshSkinned>(node))
{
skinned->setLoop(1);
skinned->play();
}
int num_children = node->getNumChildren();
for (int i = 0; i < num_children; ++i)
run_animation(node->getChild(i));
}
void AppWorldLogic::setup_gui()
{
auto gui = Gui::getCurrent();
auto window = WidgetWindow::create(gui, "Import File", 4, 4);
auto import_button = WidgetButton::create(gui, "Import");
import_button->getEventClicked().connect(this, &AppWorldLogic::request_import);
window->addChild(import_button, Gui::ALIGN_EXPAND);
window->arrange();
gui->addChild(window, Gui::ALIGN_OVERLAP | Gui::ALIGN_LEFT | Gui::ALIGN_TOP);
}
void AppWorldLogic::setup_player(const NodePtr &node)
{
static const float CAMERA_PHI = 75.0f;
static const float CAMERA_THETA = 140.0f;
static const float CAMERA_DISTANCE = 2.0f;
static const float FOV = 60.0f;
using namespace Unigine::Math;
ivec2 window_size = WindowManager::getMainWindow()->getClientSize();
WorldBoundSphere bound_sphere = node->getHierarchyBoundSphere();
Vec3 center = bound_sphere.center;
Scalar radius = bound_sphere.radius * CAMERA_DISTANCE *
max(1.0f, float(window_size.y) / window_size.y);
quat rotation= quat(1.0f, 0.0f, 0.0f, -CAMERA_PHI) * quat(0.0f, 0.0f, 1.0f, CAMERA_THETA);
Mat4 modelview{translate(Scalar(0.0f), Scalar(0.0f), -radius) *
Mat4(rotation) * translate(-center)};
mat4 projection = perspective(FOV, 1.0f, radius * 0.01f, radius * 2.0f);
if (PlayerPtr player = Game::getPlayer())
{
player->setWorldTransform(inverse(modelview));
player->setProjection(projection);
} else
Log::error("Main player not found.\n");
}
void AppWorldLogic::import_file(const String &filepath)
{
if (node_)
{
node_.deleteLater();
node_ = nullptr;
}
node_ = import(filepath);
if (node_)
{
Log::message("Node loaded \"%s\" from filepath \"%s\".\n",
node_->getName(), filepath.get());
setup_player(node_);
run_animation(node_);
}
else
Log::error("Can't import file from filepath %s\n", filepath.get());
}
void AppWorldLogic::dialog_file_ok_clicked(WidgetDialogFilePtr dialog)
{
const String filepath = dialog->getFile();
default_dialog_path = filepath.pathname();
import_file(filepath);
dialog.deleteLater();
}
void AppWorldLogic::dialog_file_cancel_clicked(WidgetDialogFilePtr dialog)
{
dialog.deleteLater();
}
void AppWorldLogic::request_import(const WidgetPtr &sender)
{
const Vector<String> &supported_extensions = Import::getSupportedExtensions();
const String extensions_filter = "." + String::join(supported_extensions, ".");
auto dialog_file = WidgetDialogFile::create(Gui::getCurrent(), "DialogFile");
dialog_file->setPath(default_dialog_path);
dialog_file->setFilter(extensions_filter);
dialog_file->getOkButton()->getEventClicked().connect(this, &AppWorldLogic::dialog_file_ok_clicked, dialog_file);
dialog_file->getCancelButton()->getEventClicked().connect(this, &AppWorldLogic::dialog_file_cancel_clicked, dialog_file);
Gui::getCurrent()->addChild(dialog_file, Gui::ALIGN_OVERLAP | Gui::ALIGN_CENTER);
dialog_file->setPermanentFocus();
}
////////////////////////////////////////////////////////////////////////////////
// start of the main loop
////////////////////////////////////////////////////////////////////////////////
int AppWorldLogic::update()
{
// Write here code to be called before updating each render frame: specify all graphics-related functions you want to be called every frame while your application executes.
return 1;
}
int AppWorldLogic::postUpdate()
{
// The engine calls this function after updating each render frame: correct behavior after the state of the node has been updated.
return 1;
}
int AppWorldLogic::updatePhysics()
{
// Write here code to be called before updating each physics frame: control physics in your application and put non-rendering calculations.
// The engine calls updatePhysics() with the fixed rate (60 times per second by default) regardless of the FPS value.
// WARNING: do not create, delete or change transformations of nodes here, because rendering is already in progress.
return 1;
}
////////////////////////////////////////////////////////////////////////////////
// end of the main loop
////////////////////////////////////////////////////////////////////////////////
int AppWorldLogic::shutdown()
{
// Write here code to be called on world shutdown: delete resources that were created during world script execution to avoid memory leaks.
return 1;
}
int AppWorldLogic::save(const Unigine::StreamPtr &stream)
{
// Write here code to be called when the world is saving its state (i.e. state_save is called): save custom user data to a file.
UNIGINE_UNUSED(stream);
return 1;
}
int AppWorldLogic::restore(const Unigine::StreamPtr &stream)
{
// Write here code to be called when the world is restoring its state (i.e. state_restore is called): restore custom user data to a file here.
UNIGINE_UNUSED(stream);
return 1;
}