Page 1 of 1

Splitting geometry before optimization

Posted: Wed Dec 15, 2021 11:57 am
by mootools
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:
  • 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.
So disconnecting isolated parts is an option that might have some interest to obtain gain in term of speed and accuracy.
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
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?

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;

}