Visualization Library 2.0.0

A lightweight C++ OpenGL middleware for 2D/3D graphics

VL     Star     Watch     Fork     Issue

[Download] [Tutorials] [All Classes] [Grouped Classes]
Portal-Based Culling Scene Manager Tutorial

This tutorial shows how to use the powerful vl::SceneManagerPortals class to render indoor or highly occluded scenes order of magnitudes more quickly than simply using frustum culling methods (even if using KdTrees or other accelation structures).

There are cases in which you want to render indoor or highly occluded but very complex scenes. Using frustum culling in this cases is not enough because the objects that remain in the view frustum are still too many. In such situations the solution is to avoid drawing the objects that are occluded by other objects. The vl::SceneManagerPortals class allows you to do so using the so called portal-based hidden surface removal algorithm. The basic concept is very simple, in a typical indoor environment most of the objects are hidden by the walls, the furniture etc. and the content of each room is visible either because the camera is in that room or through a door or window. We will call the rooms Sectors and the doors and windows Portals. If a door is not in our view frustum then also the room behind that door is not visible (unless we can see it through another door or window). Similarly, when a sector is visible the algorithm looks for all the Portals in that sector and checks if the Sectors behind those Portals are visible as well. This process is recursive and the algorithm might end up (but is definitely something you want to avoid!) traversing the whole scene going from sector to sector but usually only a small subset is traversed. When a new sector is discovered the Actors that contains are checked against not only the view frustum but also against the view frustum passing through the portals. This mechanism ensures a much higher probability of culling the Actors in the scene than using plain frustum culling, and the best part is that this is done traversing only a minimal part of the whole data set.

Using the vl::SceneManagerPortals class is extremely simple once you have defined your Portals, Sectors and the Actors that are in each sector. The challenging aspect is defining a good set of Portals and Sectors. This can be relatively simple for highly occluded environments or very difficult for complex or more open scenes. Furthermore the process of defining appropriate Portals and Sectors is very difficult to automate and usually requires human intervention. For example many FPS and even racing games use it. In this case the level editors and artists create appropriate portals in the right places and define by hand the Sectors. Portal and sector automatic generation is possible, for example it has been implemented already in the well-known game Quake I from Id Software, but usually severe limitations apply to the kinds of geometry that can be used and special tools are required.

On the other hand portal-based hidden surface removal not only can be tremendously efficient for the right kind of scenarios but it does not require any special OpenGL extension and thus works the same on any hardware and OpenGL implementation (unlike the various OpenGL extensions to perform occlusion queries like GL_ARB_occlusion_query which suffer other kinds of limitations).

Note that the vl::SceneManagerPortals can be used in combination or side by side with other scene managers and demonstrates how flexible and versatile Visualization Library's architecture is.

For more information on how to use the vl::SceneManagerPortals class in combination with other scene managers see:

For more information about portal-based hidden surface removal see:

View of the normal scene. View of the detected portals. Geometry rendered using only view frustum culling. Note how much geometry is rendered in the background that will never be visible. Geometry rendered using portal-based hidden surface removal. As we see portal-based hsr can take advantage of highly occluded scenes and can greatly boost the rendering process.

In the following example we will procedurally create a dungeon with many rooms and will automatically create all the Portals and Sectors. Much of the code is taken by the geometry, portal and sector generation. As you will see the actual code that uses the vl::SceneManagerPortals class is very few. If we had the portals and sectors ready we would just load them into a vl::SceneManagerPortals and install the portal scene manager in a few lines of code!

Note that to outline even more the benefits of the portal-based hidden surface removal algorithm we will also add several highly detailed spheres to each sector. Pressing <F8> will enable/disable the portal-based culling.

[From App_PortalCulling.cpp]

#include "BaseDemo.hpp"
// Here we define our dungeon which will be the test platform for our portal scene manager.
// X = room, for each room a sector is generated, each room is filled with a couple of spheres.
// When an X is next to another X a passage (and relative Portal) is generated.
const int map_size = 7;
const char* map[map_size] =
{
" ",
" XXXXX ",
" XXXXX ",
" XXXXX ",
" XXXXX ",
" XXXXX ",
" "
};
const float room_h = 5.0f;
const float room_size_out = 20.0f;
const float room_size_in = 18.0f;
// When the demo starts press the "f" key to activate the "fly" or "ghost" camera mode.
class App_PortalCulling: public BaseDemo
{
public:
virtual vl::String appletInfo()
{
return BaseDemo::appletInfo() +
"- F6: toggles wireframe\n" +
"- F7: toggles show portals\n" +
"- F8: enable/disable portal-based culling\n" +
"\n";
}
void initEvent()
{
// Basic initialization
vl::Log::notify(appletInfo());
ghostCameraManipulator()->setMovementSpeed(5.0f);
generateDungeon();
}
// keyboard controls
// F6 = toggle wireframe
// F7 = toggle show portals
// F8 = enable/disable portal-based culling
void keyPressEvent(unsigned short ch, vl::EKey key)
{
BaseDemo::keyPressEvent(ch, key);
if (key == vl::Key_F6)
{
if (mPolygonMode->frontFace() == vl::PM_LINE)
mPolygonMode->set(vl::PM_FILL, vl::PM_FILL);
else
mPolygonMode->set(vl::PM_LINE, vl::PM_LINE);
}
else
if (key == vl::Key_F7)
mPortalSceneManager->setShowPortals( !mPortalSceneManager->showPortals() );
else
if (key == vl::Key_F8)
mPortalSceneManager->setCullingEnabled( !mPortalSceneManager->cullingEnabled() );
}
// simple method to keep the camera on the floor
void updateScene()
{
vl::mat4 im = trackball()->camera()->modelingMatrix();
vl::vec3 t = im.getT(); t.y() = 1.5f; im.setT(t);
trackball()->camera()->setModelingMatrix(im);
}
// procedurally generate the dungeon with sectors, geometry and portals
void generateDungeon()
{
// Initialize the Sectors.
vl::ref<vl::Sector> sectors[map_size][map_size] =
{
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector },
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector },
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector },
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector },
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector },
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector },
{ new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector, new vl::Sector }
};
// ############################################
// # Install and use our SceneManagerPortals! #
// ############################################
mPortalSceneManager = new vl::SceneManagerPortals;
mPortalSceneManager->setShowPortals(true);
// remove all the other scene managers
rendering()->as<vl::Rendering>()->sceneManagers()->clear();
// install our SceneManagerPortals!
rendering()->as<vl::Rendering>()->sceneManagers()->push_back(mPortalSceneManager.get());
// Skip the boring code below and just pay attention to how the Sector and Portal classes are used!
// generate the effects we will use for the walls, ceilings etc. of our dungeon
mPolygonMode = new vl::PolygonMode;
floor_fx->shader()->enable(vl::EN_LIGHTING);
floor_fx->shader()->gocLight(0);
floor_fx->shader()->gocLightModel()->setTwoSide(true);
floor_fx->shader()->gocMaterial()->setDiffuse(vl::crimson);
floor_fx->shader()->setRenderState(mPolygonMode.get());
vl::ref<vl::Effect> ceiling_fx = new vl::Effect;
ceiling_fx->shader()->enable(vl::EN_DEPTH_TEST);
ceiling_fx->shader()->enable(vl::EN_LIGHTING);
ceiling_fx->shader()->gocLight(0);
ceiling_fx->shader()->gocLightModel()->setTwoSide(true);
ceiling_fx->shader()->gocMaterial()->setDiffuse(vl::gray);
ceiling_fx->shader()->setRenderState(mPolygonMode.get());
wall_fx->shader()->gocLight(0)->setLinearAttenuation(0.025f);
wall_fx->shader()->gocLightModel()->setTwoSide(true);
wall_fx->shader()->gocMaterial()->setDiffuse(vl::gold);
wall_fx->shader()->setRenderState(mPolygonMode.get());
// boring code to generate the gometry of various kinds of walls, with out door, with door, with the passage and portal.
vl::ref<vl::Geometry> wall_1_a = vl::makeGrid(vl::vec3(room_h/2.0f, 0, 0), room_h, room_size_in, 20, 20);
wall_1_a->computeNormals();
wall_1_a->transform(vl::mat4::getRotation(90, 0, 0, 1));
vl::ref<vl::Geometry> wall_2_a = vl::makeGrid(vl::vec3(0, 0, room_h/2.0f), room_size_in, room_h, 20, 20);
wall_2_a->computeNormals();
wall_2_a->transform(vl::mat4::getRotation(-90, 1, 0, 0));
float w1 = room_size_in / 6.0f;
float h1 = room_h / 3.0f * 2.0f;
float w2 = room_size_in / 2.0f;
float h2 = room_h;
// wall 1 b
vert_array = new vl::ArrayFloat3;
wall_1_b->setVertexArray(vert_array.get());
vert_array->resize(8);
// 1-----------2
// | |
// | 6---5 |
// | | | |
// 0---7 4---3
vert_array->at(0) = vl::fvec3(0, 0, -w2);
vert_array->at(1) = vl::fvec3(0, +h2, -w2);
vert_array->at(2) = vl::fvec3(0, +h2, +w2);
vert_array->at(3) = vl::fvec3(0, 0, +w2);
vert_array->at(4) = vl::fvec3(0, 0, +w1);
vert_array->at(5) = vl::fvec3(0, h1, +w1);
vert_array->at(6) = vl::fvec3(0, h1, -w1);
vert_array->at(7) = vl::fvec3(0, 0, -w1);
wall_1_b->drawCalls().push_back(de.get());
de->indexBuffer()->resize(12);
de->indexBuffer()->at(0) = 1; // a
de->indexBuffer()->at(1) = 0;
de->indexBuffer()->at(2) = 7;
de->indexBuffer()->at(3) = 6;
de->indexBuffer()->at(4) = 1; // b
de->indexBuffer()->at(5) = 6;
de->indexBuffer()->at(6) = 5;
de->indexBuffer()->at(7) = 2;
de->indexBuffer()->at(8) = 5; // c
de->indexBuffer()->at(9) = 4;
de->indexBuffer()->at(10) = 3;
de->indexBuffer()->at(11) = 2;
wall_1_b->computeNormals();
// wall_1_c
vl::fvec3 v1c(room_size_out-room_size_in, 0, 0);
vert_array = new vl::ArrayFloat3;
wall_1_c->setVertexArray(vert_array.get());
vert_array->resize(8);
// 1-----------2
// | 6---5 |
// | | | |
// | 7---4 |
// 0-----------3
vert_array->at(0) = vl::fvec3(0, 0, -w1);
vert_array->at(1) = vl::fvec3(0, h1, -w1);
vert_array->at(2) = vl::fvec3(0, h1, +w1);
vert_array->at(3) = vl::fvec3(0, 0, +w1);
vert_array->at(4) = vl::fvec3(0, 0, +w1) + v1c;
vert_array->at(5) = vl::fvec3(0, h1, +w1) + v1c;
vert_array->at(6) = vl::fvec3(0, h1, -w1) + v1c;
vert_array->at(7) = vl::fvec3(0, 0, -w1) + v1c;
wall_1_c->drawCalls().push_back(de.get());
de->indexBuffer()->resize(16);
de->indexBuffer()->at(0) = 1; // a
de->indexBuffer()->at(1) = 0;
de->indexBuffer()->at(2) = 7;
de->indexBuffer()->at(3) = 6;
de->indexBuffer()->at(4) = 1; // b
de->indexBuffer()->at(5) = 6;
de->indexBuffer()->at(6) = 5;
de->indexBuffer()->at(7) = 2;
de->indexBuffer()->at(8) = 5; // c
de->indexBuffer()->at(9) = 4;
de->indexBuffer()->at(10) = 3;
de->indexBuffer()->at(11) = 2;
de->indexBuffer()->at(12) = 0; // d
de->indexBuffer()->at(13) = 3;
de->indexBuffer()->at(14) = 4;
de->indexBuffer()->at(15) = 7;
wall_1_c->computeNormals();
// wall 2 b
vert_array = new vl::ArrayFloat3;
wall_2_b->setVertexArray(vert_array.get());
vert_array->resize(8);
// 1-----------2
// | |
// | 6---5 |
// | | | |
// 0---7 4---3
vert_array->at(0) = vl::fvec3(-w2, 0, 0);
vert_array->at(1) = vl::fvec3(-w2, +h2, 0);
vert_array->at(2) = vl::fvec3(+w2, +h2, 0);
vert_array->at(3) = vl::fvec3(+w2, 0, 0);
vert_array->at(4) = vl::fvec3(+w1, 0, 0);
vert_array->at(5) = vl::fvec3(+w1, h1, 0);
vert_array->at(6) = vl::fvec3(-w1, h1, 0);
vert_array->at(7) = vl::fvec3(-w1, 0, 0);
wall_2_b->drawCalls().push_back(de.get());
de->indexBuffer()->resize(12);
de->indexBuffer()->at(0) = 1; // a
de->indexBuffer()->at(1) = 0;
de->indexBuffer()->at(2) = 7;
de->indexBuffer()->at(3) = 6;
de->indexBuffer()->at(4) = 1; // b
de->indexBuffer()->at(5) = 6;
de->indexBuffer()->at(6) = 5;
de->indexBuffer()->at(7) = 2;
de->indexBuffer()->at(8) = 5; // c
de->indexBuffer()->at(9) = 4;
de->indexBuffer()->at(10) = 3;
de->indexBuffer()->at(11) = 2;
wall_2_b->computeNormals();
// wall_2_c
vl::fvec3 v2c(0, 0, room_size_out-room_size_in);
vert_array = new vl::ArrayFloat3;
wall_2_c->setVertexArray(vert_array.get());
vert_array->resize(8);
// 1-----------2
// | 6---5 |
// | | | |
// | 7---4 |
// 0-----------3
vert_array->at(0) = vl::fvec3(-w1, 0, 0);
vert_array->at(1) = vl::fvec3(-w1, h1, 0);
vert_array->at(2) = vl::fvec3(+w1, h1, 0);
vert_array->at(3) = vl::fvec3(+w1, 0, 0);
vert_array->at(4) = vl::fvec3(+w1, 0, 0) + v2c;
vert_array->at(5) = vl::fvec3(+w1, h1, 0) + v2c;
vert_array->at(6) = vl::fvec3(-w1, h1, 0) + v2c;
vert_array->at(7) = vl::fvec3(-w1, 0, 0) + v2c;
wall_2_c->drawCalls().push_back(de.get());
de->indexBuffer()->resize(16);
de->indexBuffer()->at(0) = 1; // a
de->indexBuffer()->at(1) = 0;
de->indexBuffer()->at(2) = 7;
de->indexBuffer()->at(3) = 6;
de->indexBuffer()->at(4) = 1; // b
de->indexBuffer()->at(5) = 6;
de->indexBuffer()->at(6) = 5;
de->indexBuffer()->at(7) = 2;
de->indexBuffer()->at(8) = 5; // c
de->indexBuffer()->at(9) = 4;
de->indexBuffer()->at(10) = 3;
de->indexBuffer()->at(11) = 2;
de->indexBuffer()->at(12) = 0; // d
de->indexBuffer()->at(13) = 3;
de->indexBuffer()->at(14) = 4;
de->indexBuffer()->at(15) = 7;
wall_2_c->computeNormals();
// very heavy spheres to be added to the sectors to outline more clearly the advantages of portal-based culling.
vl::ref<vl::Geometry> sphere = vl::makeUVSphere(vl::vec3(0, 3, 0), 1, 200, 200);
sphere->computeNormals();
ball_fx->shader()->gocLight(0);
ball_fx->shader()->setRenderState(mPolygonMode.get());
// for each cell of the dungeon
for(int y=0; y<map_size; ++y)
{
for(int x=0; x<map_size; ++x)
{
if (map[y][x] == 'X')
{
// ###########################
// # allocate the new Sector #
// ###########################
vl::Sector* sector1 = sectors[y][x].get();
mPortalSceneManager->sectors().push_back(sector1);
// add floor to the sector/room
vl::ref<vl::Geometry> floor = vl::makeGrid(vl::vec3(x*room_size_out, 0, y*room_size_out), room_size_in, room_size_in, 20, 20);
floor->computeNormals();
sector1->actors()->push_back( new vl::Actor(floor.get(), floor_fx.get(), NULL) );
// add ceiling to the sector/room
vl::ref<vl::Geometry> ceiling = vl::makeGrid(vl::vec3(x*room_size_out, room_h, y*room_size_out), room_size_in, room_size_in, 20, 20);
ceiling->computeNormals();
sector1->actors()->push_back( new vl::Actor(ceiling.get(), ceiling_fx.get(), NULL) );
// generate walls, doors and portals:
if(map[y][x-1] == 'X') // west door: the two sectors comunicate
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(-room_size_in/2.0f, 0, 0));
// add to the sector
sector1->actors()->push_back( new vl::Actor(wall_1_b.get(), wall_fx.get(), tr.get()) );
}
else
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(-room_size_in/2.0f, 0, 0));
// add to the sector
sector1->actors()->push_back( new vl::Actor(wall_1_a.get(), wall_fx.get(), tr.get()) );
}
if(map[y][x+1] == 'X') // east door: the two sectors comunicate
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(+room_size_in/2.0f, 0, 0));
// add to the sector
sector1->actors()->push_back( new vl::Actor(wall_1_b.get(), wall_fx.get(), tr.get()) );
sector1->actors()->push_back( new vl::Actor(wall_1_c.get(), wall_fx.get(), tr.get()) );
// #####################################################
// # create the two Portals to connect the two Sectors #
// #####################################################
// portal1: sector1 -> sector2
// portal2: sector2 -> sector1
// This is very important: in order to connect two sectors A and B you have to create a
// portal in A whose target is B, and other portal in B whose target is A.
// This way each sector can see the other.
// new portal
vl::Sector* sector2 = sectors[y][x+1].get();
// link each sector to the other using their portals
sector1->portals().push_back(portal1.get()); portal1->setTargetSector( sector2 );
sector2->portals().push_back(portal2.get()); portal2->setTargetSector( sector1 );
// setup portal geometry - note: the portal geometry must be a planar, convex polygon, in world space coordinates.
portal1->geometry().resize(4);
portal1->geometry().at(0) = (vl::fmat4)m * (vl::fvec3(0, 0, +w1) + v1c);
portal1->geometry().at(1) = (vl::fmat4)m * (vl::fvec3(0, h1, +w1) + v1c);
portal1->geometry().at(2) = (vl::fmat4)m * (vl::fvec3(0, h1, -w1) + v1c);
portal1->geometry().at(3) = (vl::fmat4)m * (vl::fvec3(0, 0, -w1) + v1c);
portal2->geometry().resize(4);
portal2->geometry().at(0) = (vl::fmat4)m * (vl::fvec3(0, 0, +w1) + v1c);
portal2->geometry().at(1) = (vl::fmat4)m * (vl::fvec3(0, h1, +w1) + v1c);
portal2->geometry().at(2) = (vl::fmat4)m * (vl::fvec3(0, h1, -w1) + v1c);
portal2->geometry().at(3) = (vl::fmat4)m * (vl::fvec3(0, 0, -w1) + v1c);
}
else
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(+room_size_in/2.0f, 0, 0));
sector1->actors()->push_back( new vl::Actor(wall_1_a.get(), wall_fx.get(), tr.get()) );
}
if(map[y-1][x] == 'X') // south door: the two sectors comunicate
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(0, 0, -room_size_in/2.0f));
sector1->actors()->push_back( new vl::Actor(wall_2_b.get(), wall_fx.get(), tr.get()) );
}
else
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(0, 0, -room_size_in/2.0f));
sector1->actors()->push_back( new vl::Actor(wall_2_a.get(), wall_fx.get(), tr.get()) );
}
if(map[y+1][x] == 'X') // north door: the two sectors comunicate
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(0, 0, +room_size_in/2.0f));
sector1->actors()->push_back( new vl::Actor(wall_2_b.get(), wall_fx.get(), tr.get()) );
sector1->actors()->push_back( new vl::Actor(wall_2_c.get(), wall_fx.get(), tr.get()) );
// same as seen above...
// #####################################################
// # create the two Portals to connect the two Sectors #
// #####################################################
// portal1: sector1 -> sector2
// portal2: sector2 -> sector1
// This is very important: in order to connect two sectors A and B you have to create a
// portal in A whose target is B, and other portal in B whose target is A.
// This way each sector can see the other.
// new portal
vl::Sector* sector2 = sectors[y+1][x].get();
// link each sector to the other using their portals
sector1->portals().push_back(portal1.get()); portal1->setTargetSector( sector2 );
sector2->portals().push_back(portal2.get()); portal2->setTargetSector( sector1 );
// setup portal geometry - note: the portal geometry must be a planar, convex polygon, in world space coordinates.
portal1->geometry().resize(4);
portal1->geometry().at(0) = (vl::fmat4)m * (vl::fvec3(+w1, 0, 0) + v2c);
portal1->geometry().at(1) = (vl::fmat4)m * (vl::fvec3(+w1, h1, 0) + v2c);
portal1->geometry().at(2) = (vl::fmat4)m * (vl::fvec3(-w1, h1, 0) + v2c);
portal1->geometry().at(3) = (vl::fmat4)m * (vl::fvec3(-w1, 0, 0) + v2c);
portal2->geometry().resize(4);
portal2->geometry().at(0) = (vl::fmat4)m * (vl::fvec3(+w1, 0, 0) + v2c);
portal2->geometry().at(1) = (vl::fmat4)m * (vl::fvec3(+w1, h1, 0) + v2c);
portal2->geometry().at(2) = (vl::fmat4)m * (vl::fvec3(-w1, h1, 0) + v2c);
portal2->geometry().at(3) = (vl::fmat4)m * (vl::fvec3(-w1, 0, 0) + v2c);
}
else
{
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(0, 0, +room_size_in/2.0f));
sector1->actors()->push_back( new vl::Actor(wall_2_a.get(), wall_fx.get(), tr.get()) );
}
// add other actors to the scene
for(unsigned int i=0; i<25; ++i)
{
tr = new vl::Transform;
float tx = (rand()%100) / 100.0f * room_size_in - room_size_in / 2.0f;
float tz = (rand()%100) / 100.0f * room_size_in - room_size_in / 2.0f;
m = vl::mat4::getTranslation(vl::vec3(x*room_size_out, 0, y*room_size_out) + vl::vec3(tx, 0, tz));
sector1->actors()->push_back( new vl::Actor(sphere.get(), ball_fx.get(), tr.get()) );
}
// ############################################
// # simple way to define the Sector's volume #
// ############################################
// Each Sector must have a set of volumes that are checked at the beginning of each rendering
// to decide in which one the camera is. In our case we just use the bounding box of the
// Sector itself. Note that the volumes of a Sector must not intersect the volumes of the other
// Sectors, otherwise the algorithm won't be able to determine in which Sector the camera is in.
// I.e, it will randomly pick the first found!
sector1->volumes().push_back( sector1->computeBoundingBox() );
}
}
}
// #######################################
// # initialize the portal scene manager #
// #######################################
// Precomputes some internal data and performs some sanity checks.
mPortalSceneManager->initialize();
}
protected:
vl::ref<vl::SceneManagerPortals> mPortalSceneManager;
};
// Have fun!