Splitting geometry before optimization
Posted: Wed Dec 15, 2021 11:57 am
Sometime geometry come in a single mesh, one piece made of many disconnected sub-parts.
In term of optimization that would be better to provide each part in a different meshes instead for this potentially huge single mesh.
This is particularly true when you are using MagicCruncher:
A nice pipeline could be the following:
Instantiation is a point that makes this process a little more complex than it could be as the loaded scene might have several instance of a single huge mesh and that would be nice to explode the mesh and optimize it taking care of Instantiation. Saying that, if you have 2 instances of the same huge mesh, you only need to optimize the explode version of one of this mesh then replace the both original mesh with the result at the end of the process.
So now how to do that programatically?
In term of optimization that would be better to provide each part in a different meshes instead for this potentially huge single mesh.
This is particularly true when you are using MagicCruncher:
- MagicCruncher speed is not linear and looking for the best optimization is slower with a 1000000 polygon mesh than with 100 meshes of 10000 polygons.
- As MagicCruncher compute deviation based on the global bounding box, it will work less well if you provide a large bounding box with many small details. Imagine a house with a glass on a table. If you provide the whole house, the glass is considered to be insignificant at the house scale. But the glass might matter and if it can be disconnected from the whole house, MagicCruncher will optimize it taking into account its own scale.
A nice pipeline could be the following:
Code: Select all
Load a scene → Disconnect meshes it into separate parts → Optimize each parts → Merge back the part into the single mesh → save the scene
So now how to do that programatically?
Code: Select all
{
CSceneImportOptions options;
options.flags = SCENE_IMPORT_GENERATE_UV;
CXString infile_name = input_file_path.c_str();
C3DIo infile(infile_name, FILE_PARSER_LOADING);
C3DScene* scene = infile.Read(&options);
_tprintf(FormatString(_T("Scene %s has been readed (%.2f sec) \r\n"), (LPCTSTR)infile_name, (timeGetTick() - time) / 1000.0f));
// First, tidy up the scene
// 1. Collect the nodes
{
C3DNodeArray scenenodes;
C3DGeomObject* object;
unsigned int explodedObjects = 0, newObjects = 0;
C3DNodePos pos = scene->GetFirstNode(OBJECT_GEOM, NODEPOS_INSTANCE_ONCE);
while (pos)
{
C3DSceneNode* node = scene->GetNextNode(object, pos);
if (!object)
continue;
scenenodes.Add(node);
}
// 2. Explode nodes in isolated parts, this will give better optimisation
int explodeInstanceID = 0, i, scenesize = scenenodes.GetSize();
for (i = 0; i < scenesize; i++)
{
C3DSceneNode* node = scenenodes[i];
object = node->GetGeomObject();
C3DGeomObjectArray objects;
int objectsize = object->ExplodeIntoIsolatedParts(objects);
// Optional weld which is best to perform after ExplodeIntoIsolatedParts.
for (int k = 0; k < objectsize; k++)
objects[k]->Weld3DPoints(0.0f, true);
if (objectsize <= 1) // Node cannot be exploded.
continue;
explodeInstanceID++;
explodedObjects++;
newObjects += objectsize;
// All instance nodes has to been replaced by the exploded parts
#define EXPLODE_INSTANCE_ID MAKE_CUSTOM_ID('E', 'X', 'I', 'D')
C3DNodeArray nodes;
scene->GetInstancesByID(NULL, node, nodes, true);
int j, nodesize = nodes.GetSize();
for (j = 0; j < nodesize; j++)
{
C3DSceneNode* node = nodes[j];
CXString name = node->GetName();
node->SetObject(C3DGroup::Create(OBJECT_GROUP)); // This is not the same to call xNew(C3DGroup) and C3DGroup::Create. In the second call, object is created inside C3DDei memory heap and there is no problem to delete the object even if the plugin is destroyed)
// Give an ID to be able to reaggreate these objects
CCustomData& data = node->GetCustomData();
data.SetInt(EXPLODE_INSTANCE_ID, explodeInstanceID);
for (int k = 0; k < objectsize; k++)
{
C3DSceneNode* subnode = C3DSceneNode::Create();
subnode->SetName(FormatString(_T("%s"), (LPCTSTR)name));
subnode->SetObject(j == 0 ? objects[k] : scene->CreateObjectInstance(objects[k]));
scene->AddNode(node, subnode);
}
}
}
_tprintf(FormatString(_T("Scene has been cleaned (%d object exploded into %d isolated objects\r\n"), explodedObjects, newObjects));
}
// Now, convert scene and use C3DExtObject. This type is required for optimizing.
// If ommitted, no optimization is performed
C3DPolygonCruncherObjectCreator cruncherCreator(DEFAULT_OPTIMIZATION_OBJECT);
scene->ConvertToType(NULL, &cruncherCreator);
// Create the optimiser and attach scene
CSceneOptimizer* optimiser = xNew(CSceneOptimizer);
// Progressive gives better resultswhen there are multiple objects.
optimiser->SetFlag(SCENEOPTIMIZER_PROGRESSIVE_RATIO, true);
optimiser->SetFlag(SCENEOPTIMIZER_TRACK_CHANGES, true);
optimiser->SetScene(*scene, true);
// Define some optimization settings
optimiser->LockInitialization();
optimiser->SetOptimizeMode(DEFAULT_OPTIMIZATION_MODE, true);
optimiser->UnlockInitialization();
// Compute the dynamic mesh
bool cancel;
optimiser->Optimize(0.0f, cancel);
// Get the dynamic mesh and compute the multiresolution scene
C3DScene* optimisescene = optimiser->GetScene(OPTIMIZED_MULTIRESOLUTION_SCENE);
// We no longer need the initial scene so delete it to save memory.
optimiser->DeleteScene();
// A few settings, currently using the defaults from Mootools.
double magicThreshold[3] = { 0.05f, 0.01f, 0.001f };
unsigned int magicDichoLevel[3] = {
CSceneOptimizer::MAGICCRUNCHER_DRAFT_DICHO,
CSceneOptimizer::MAGICCRUNCHER_MEDIUM_DICHO,
CSceneOptimizer::MAGICCRUNCHER_ACCURATE_DICHO
};
int magicThresholdIndex = 2;
int magicDichoIndex = 1;
CSceneOptimizer::MagicCruncherMode dichoLevel = CSceneOptimizer::MAGICCRUNCHER_MEDIUM_DICHO;
bool magicExternal = false;
longuint magictime = timeGetTick();
// MagicCruncherOptimization defines the best ratio for each object in the
// scene meaning that each object might have a different ratio.
_tprintf(
FormatString(
_T("Looking for magic ratio (similarity : %s, dichoLevel : %s) (%s)\r\n"),
magicThresholdIndex == 0
? _T("Low similarity")
: magicThresholdIndex == 1
? _T("Middle similarity")
: _T("High similarity"),
magicDichoIndex == 0
? _T("Close to optimal ratio")
: magicDichoIndex == 1
? _T("Medium speed")
: _T("Low speed"),
magicExternal
? _T("using external process")
: _T("using internal process")
)
);
// Do the actual work!
// This automatically sets the best ratio for each object.
double ratio = optimiser->MagicCruncherOptimization(
CSceneOptimizer::MAGICCRUNCHER_DEFAULT | magicDichoLevel[magicDichoIndex],
magicThreshold[magicThresholdIndex],
CSceneOptimizer::APPLY_TO_SCENE,
magicExternal
);
_tprintf(
FormatString(
_T("Magic Ratio found: %.2f in %.2f seconds\r\n"),
100.0f - ratio * 100.0f,
(timeGetTick() - magictime) / 1000.0f
)
);
// Get some feedback informatiom
int ptnbr, maxptnbr, facenbr, maxfacenbr;
maxfacenbr = optimiser->GetMultiresolutionInfo(CSceneOptimizer::MULTIRES_MAX_FACES);
maxptnbr = optimiser->GetMultiresolutionInfo(CSceneOptimizer::MULTIRES_MAX_POINTS);
facenbr = optimiser->GetMultiresolutionInfo(CSceneOptimizer::MULTIRES_CUR_FACES);
ptnbr = optimiser->GetMultiresolutionInfo(CSceneOptimizer::MULTIRES_CUR_POINTS);
_tprintf(FormatString(_T("Number of faces : %d / %d\r\n"), facenbr, maxfacenbr));
_tprintf(FormatString(_T("Number of points : %d / %d\r\n\r\n"), ptnbr, maxptnbr));
// Output the crunched file.
C3DScene* sceneToSave = optimiser->GetScene(
OPTIMIZED_CLEANED_MULTIRESOLUTION_SCENE | OPTIMIZED_COPY_SCENE
);
// Reaggreate
{
CHashMap<int, int, C3DObject*> instanceObjects;
int explodeInstanceID = 0;
C3DBaseObject* object;
C3DNodePos prevpos, pos = sceneToSave->GetFirstNode(sceneToSave->GetRoot(), NODEPOS_INSTANCE_ONCE);
while (pos)
{
prevpos = pos;
C3DSceneNode* node = sceneToSave->GetNextNode(object, pos);
if (!object)
continue;
// Give an ID to be able to reaggreate these objects
C3DNodeArray childs;
C3DObject* concatenatedObject = NULL;
CCustomData& data = node->GetCustomData();
if (data.GetInt(EXPLODE_INSTANCE_ID, explodeInstanceID, 0))
{
data.RemoveCustom(EXPLODE_INSTANCE_ID); // Remove tag, so we've process
sceneToSave->GetNodesByType(node, OBJECT_GEOM, childs, true, true);
int childsize = childs.GetSize();
if (!childsize)
continue;
// If the instance is found for the first time, concatenate the childs and keep the object in a map.
if (!instanceObjects.Find(explodeInstanceID, concatenatedObject))
{
C3DGeomObjectArray objects;
objects.SetSize(childsize);
for (int i = 0; i < childsize; i++)
objects.SetAtGrow(i, childs[i]->GetGeomObject());
concatenatedObject = xNewParams(C3DObject, xNew(C3DPointList), xNew(C3DFaceList));
if (concatenatedObject->Concatenate(sceneToSave, objects))
instanceObjects.Insert(explodeInstanceID, concatenatedObject);
}
else
concatenatedObject = (C3DObject*)sceneToSave->CreateObjectInstance(concatenatedObject); // Already created. Create an instance to the object
if (concatenatedObject)
{
node->SetObject(concatenatedObject);
for (int i = 0; i < childsize; i++)
sceneToSave->RemoveNode(childs[i]);
pos = prevpos; // we removed node's childs, so pos is corrupted. Go back to prevpos.
}
}
}
}
{
// Sort by material
C3DGeomObject* object;
C3DNodePos pos = sceneToSave->GetFirstNode(OBJECT_GEOM, NODEPOS_INSTANCE_ONCE);
while (pos)
{
C3DSceneNode* node = sceneToSave->GetNextNode(object, pos);
if (!object)
continue;
node->GetGeomObject()->SortFaces(SORT_BY_MATERIAL);
}
}
// Ensure normals are in the file.
sceneToSave->SetSmoothAngle(DEG2RADF(5));
sceneToSave->GenerateNormals(
NULL,
GENNORMAL_SPEC_CHANNEL | GENNORMAL_ALL_FACES | GENNORMAL_PER_VERTEX,
NORMAL_AREAWEIGHTED_MODE
);
CXString outfile_name = output_file_path.c_str();
C3DIo outfile(outfile_name, FILE_PARSER_SAVING);
outfile.Save(sceneToSave);
sceneToSave->Delete();
_tprintf(FormatString(_T("Total process done in %.2f sec\r\n"), (timeGetTick() - time) / 1000.0f));
// optimiser deletes the scene.
xDelete(optimiser);
return 0;
}