In Unity, you can write C# scripts not just for game logic but also to automate tasks and improve your workflow. One powerful way to do this is by developing custom Editor tools that integrate directly into the Unity Editor itself.
Here’s a plugin I developed called MaxSizeFilterTool. This tool is particularly useful when importing complex 3D models that often include numerous small, unnecessary GameObjects — often invisible or insignificant to the final scene but still adding to poly count and clutter.
This plugin allows you to:
Select a parent GameObject (typically a model you imported),
View its children,
Filter child GameObjects based on their bounding box volume, and
Delete extremely small objects that contribute little to the visual output but increase complexity.
1. Make GUI for the plugin accessible from Tools menu , simple message displaying plugin
The plugin is an EditorWindow that appears in Unity’s Tools menu.
[MenuItem("Tools/Filter Small Objects")]
public static void ShowWindow()
{
GetWindow<MaxSizeFilterTool>("Filter Small Objects");
}
This line adds an option to the Unity toolbar.
In OnEnable() and OnDisable(), we hook and unhook into Unity’s Scene GUI event, allowing us to draw bounding boxes in the 3D view:
2. Code for searching and selecting objects
To make selection easier, the tool includes a search bar and scroll list. It collects all scene GameObjects via:
private void RefreshObjectList()
{
allObjects.Clear();
allObjects.AddRange(FindObjectsOfType<GameObject>());
filteredObjects = new List<GameObject>(allObjects);
}
This populates the list with all objects in the filtered list
filteredObjects = allObjects
.Where(obj => string.IsNullOrEmpty(searchQuery) || obj.name.ToLower().Contains(searchQuery.ToLower()))
.ToList();
Then a foreach loop generates clcikable buttons for each object in the filtered list
foreach (var obj in filteredObjects)
{
if (GUILayout.Button(obj.name))
{
selectedObject = obj;
GetObjectInfo(selectedObject);
}
}
When clicked, the plugin sets the object as selected and calls GetObjectInfo() to analyze it.
3. Code for generating bounding box of the 3d model and the child game objects
The GetObjectInfo() function gathers several pieces of data:
Total number of child objects
Number of empty children (no mesh, no children)
Total triangle and vertex count
Combined bounding box volume
I was stuck witht he volume of the objects not being calculated opproperly and not "sticking" to the object co-ordinates while moving.
To fix this, I had to use the TransformPoint() and TransformVector() to convert local bounds to world space.
MeshFilter[] meshes = obj.GetComponentsInChildren<MeshFilter>();
Bounds combinedBounds = new Bounds(obj.transform.position, Vector3.zero);
foreach (var meshFilter in meshes)
{
if (meshFilter.sharedMesh != null)
{
triangleCount += meshFilter.sharedMesh.triangles.Length / 3;
vertexCount += meshFilter.sharedMesh.vertexCount;
var bounds = meshFilter.sharedMesh.bounds;
bounds.center = meshFilter.transform.TransformPoint(bounds.center);
bounds.extents = meshFilter.transform.TransformVector(bounds.extents);
combinedBounds.Encapsulate(bounds);
}
}
objectVolume = combinedBounds.size.x * combinedBounds.size.y * combinedBounds.size.z;
4. Determining volume of the objects from the bounding box
The user can adjust a slider to define a minimum volume threshold. Child objects smaller than this will be displayed:
sliderValue = EditorGUILayout.Slider("Filter by Volume", sliderValue, 0f, objectVolume);
And then we can compute the volume for each child objects
and then list them
private float GetObjectVolume(GameObject obj)
{
MeshFilter[] meshes = obj.GetComponentsInChildren<MeshFilter>();
Bounds combinedBounds = new Bounds(obj.transform.position, Vector3.zero);
foreach (var meshFilter in meshes)
{
if (meshFilter.sharedMesh != null)
{
var bounds = meshFilter.sharedMesh.bounds;
bounds.center = meshFilter.transform.TransformPoint(bounds.center);
bounds.extents = meshFilter.transform.TransformVector(bounds.extents);
combinedBounds.Encapsulate(bounds);
}
}
return combinedBounds.size.x * combinedBounds.size.y * combinedBounds.size.z;
}
private void DisplayAllDescendantObjectsWithVolumeLessThanSlider(Transform parentTransform, ref int filteredObjectCount)
{
foreach (Transform child in parentTransform)
{
float childVolume = GetObjectVolume(child.gameObject);
if (childVolume < sliderValue)
{
EditorGUILayout.LabelField($"Child Object: {child.gameObject.name} - Volume: {childVolume:F6}");
filteredObjectCount++;
}
DisplayAllDescendantObjectsWithVolumeLessThanSlider(child, ref filteredObjectCount);
}
}
4. Finishing Up
Here is some more sections containing undo support and deletion logic .
Deletion and undo code
private void DeleteFilteredObjects()
{
lastDeletedObjects.Clear();
lastDeletedCount = 0;
foreach (Transform child in selectedObject.transform)
{
float childVolume = GetObjectVolume(child.gameObject);
if (childVolume < sliderValue)
{
lastDeletedObjects.Add(child.gameObject);
lastDeletedCount++;
}
}
Undo.RegisterCompleteObjectUndo(selectedObject, "Delete Filtered Children");
foreach (var obj in lastDeletedObjects)
{
Undo.DestroyObjectImmediate(obj);
}
GetObjectInfo(selectedObject);
Repaint();
}
private void UndoLastDelete()
{
if (lastDeletedCount > 0)
{
Undo.PerformUndo();
lastDeletedCount = 0;
lastDeletedObjects.Clear();
GetObjectInfo(selectedObject);
Repaint();
}
}
To help visualize the object bounds, this plugin draws wireframe boxes in the Scene view:
private void OnSceneGUI(SceneView sceneView)
{
if (selectedObject == null) return;
if (showBoundingBox)
{
DrawBounds(selectedObject, Color.green);
}
if (showAllChildBounds)
{
foreach (Transform child in selectedObject.GetComponentsInChildren<Transform>())
{
DrawBounds(child.gameObject, new Color(1, 0.5f, 0, 0.5f));
}
}
}
private void DrawBounds(GameObject obj, Color color)
{
MeshFilter[] meshes = obj.GetComponentsInChildren<MeshFilter>();
if (meshes.Length == 0) return;
Bounds combinedBounds = meshes[0].sharedMesh.bounds;
combinedBounds.center = meshes[0].transform.TransformPoint(combinedBounds.center);
combinedBounds.extents = meshes[0].transform.TransformVector(combinedBounds.extents);
for (int i = 1; i < meshes.Length; i++)
{
if (meshes[i].sharedMesh != null)
{
var bounds = meshes[i].sharedMesh.bounds;
bounds.center = meshes[i].transform.TransformPoint(bounds.center);
bounds.extents = meshes[i].transform.TransformVector(bounds.extents);
combinedBounds.Encapsulate(bounds);
}
}
Handles.color = color;
Handles.DrawWireCube(combinedBounds.center, combinedBounds.size);
}
Codebase
Access the code for this here
Code for MaxSizeFilterTool
Top comments (0)