DEV Community

Pratyush Mohanty
Pratyush Mohanty

Posted on

Plugin/Add-on Development to improve workflow in Unity

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.

Tools Menu plugin

[MenuItem("Tools/Filter Small Objects")]
public static void ShowWindow()
{
    GetWindow<MaxSizeFilterTool>("Filter Small Objects");
}

Enter fullscreen mode Exit fullscreen mode

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

Search feature

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);
}

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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

Bounding Box - Parent in green. child in orange

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;

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Codebase

Access the code for this here
Code for MaxSizeFilterTool

Top comments (0)