Search Unity

(Help) Script to remove small parts from a complex model

Discussion in 'Scripting' started by erikwp, Aug 8, 2019.

  1. erikwp

    erikwp

    Joined:
    Jan 16, 2018
    Posts:
    25
    Hi I have a script that removes small parts from a complex model that I imported into Unity. The model is a hyper detailed CAD model and too complex for visualizing it in Unity. So I made a script to remove the small parts from the model like this:
    Code (CSharp):
    1.     void IterateThroughChildren(Transform parent)
    2.     {
    3.         foreach (Transform child in parent)
    4.         {
    5.             if (child.GetComponent<MeshFilter>() != null){
    6.                 float mult = 1000;
    7.                 float sizeX = child.GetComponent<MeshFilter>().mesh.bounds.size.x * mult;
    8.                 float sizeY = child.GetComponent<MeshFilter>().mesh.bounds.size.y * mult;
    9.                 float sizeZ = child.GetComponent<MeshFilter>().mesh.bounds.size.z * mult;
    10.                 float meshSize = sizeX+sizeY+sizeZ;
    11.                 if(meshSize<50){
    12.                     Destroy(child.gameObject);
    13.                 Debug.Log("destroyed");
    14.                 }
    15.             }
    16.             if (child.childCount > 0)
    17.                 IterateThroughChildren(child); //recursively iterate through children of children
    18.         }
    19.  
    20.     }
    21. void Start()
    22. {
    23. Debug.Log("remove small parts");
    24. IterateThroughChildren(transform);
    25. }
    26.  
    But I can't figure out how to properly save the model once the parts have been removed. As you can see the script executes in Play mode. I also tried to run it in Edit mode using ExecuteInEditMode, but that did not really work either.

    I tried to simply create a prefab from my Gameobject after it was edited, but all the meshes were missing from the prefab and I couldn't figure out how to fix that.

    Is there any way to accomplish this properly?

    Much thanks
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    So you're not really going to be able to "save the model" (the FBX file will be unchanged), but you can make this run in edit mode, and the changes will remain in the object as shown in the scene. But [ExecuteInEditMode] is the wrong approach, since this is just something you want to run once, while [ExecuteInEditMode] only really works on Update(), LateUpdate, etc which run continually.

    Instead, use the attribute [ContextMenu("Click Here")]. This will create a context menu item that will run the function, which you can get to by right-clicking the top "bar" of the script in the Inspector.

    Aside from that, I have a few notes on improving the function itself (and things which will help you out in Unity generally). First, anytime you find yourself calling GetComponent<> on the same thing repeatedly, just store the reference:
    Code (csharp):
    1. var mf = GetComponent<MeshFilter>();
    2. float sizeX = mf.bounds.size.x;
    Second, you don't actually need that to be recursive. Instead, use GetComponentsInChildren<MeshFilter>() and loop through the result of that.
     
    erikwp likes this.
  3. erikwp

    erikwp

    Joined:
    Jan 16, 2018
    Posts:
    25
    @StarManta
    Thank you for this, I managed to get it working using the ContextMenu. Improved code here:

    Code (CSharp):
    1. public class RemoveSmallParts : MonoBehaviour
    2. {
    3. [SerializeField]
    4. float minimumPartSize = 100;
    5. [ContextMenu("Simplify Model")]
    6.     void IterateThroughChildren()
    7.     {
    8.         Debug.Log("removing small parts");
    9.         MeshFilter[] mfArray =  GetComponentsInChildren<MeshFilter>();
    10.         foreach (var element in mfArray)
    11.         {
    12.             Debug.Log(element.gameObject.name);
    13.                 float mult = 1000;
    14.                 float sizeX = element.mesh.bounds.size.x * mult;
    15.                 float sizeY = element.mesh.bounds.size.y * mult;
    16.                 float sizeZ = element.mesh.bounds.size.z * mult;
    17.                 float meshSize = sizeX + sizeY + sizeZ;
    18.                 if (meshSize < minimumPartSize)
    19.                 {
    20.                     DestroyImmediate(element.gameObject);
    21.                     Debug.Log("destroyed");
    22.                 }
    23.            
    24.         }
    25.  
    26.     }
    27. }
    28.  
    But I am having the problem creating a prefab from my modified GameObject. When I create a prefab from the GO, the prefab version is missing all of the meshes, the MeshFilter components are just empty. Any suggestions for fixing this problem?
    Thank you
     
  4. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
  5. erikwp

    erikwp

    Joined:
    Jan 16, 2018
    Posts:
    25
    Thanks @kdgalla , I have created a small script to save the GO as a prefab:
    Code (CSharp):
    1. public class SaveAsPrefab : MonoBehaviour
    2. {
    3.  
    4.     [ContextMenu("Save As Prefab (With Meshes)")]
    5.     static void SavePrefab()
    6.     {
    7.         // Create a simple material asset
    8.  
    9.         AssetDatabase.CreateAsset(gameObject, "Assets/Prefabs/MyPrefab.prefab");
    10.  
    11.     }
    12. }
    Only problem is that this does not compile, it does not like the gameObject variable in the CreateAsset argument:
    Code (CSharp):
    1. GameObject Component.gameObject
    2. The game object this component is attached to. A component is always attached to a game object.
    3.  
    4. An object reference is required for the non-static field, method, or property 'Component.gameObject' (CS0120) [Assembly-CSharp]
    How do I reference the gameObject attached to the script in a static function?
     
  6. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    You have to save just the meshes, not the whole GameObject.

    Edit: I see that you loop through the meshes and destroy the small ones. All of the meshes that you save, however, you can combine them into one mesh and it might make it easier to manage the results:

    https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html

    I suppose you can't do this if each of the meshes needs a separate material, though.
     
    Last edited: Aug 8, 2019
  7. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
  8. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Or yeah. If the PrefabUtility actually stores the mesh data then it would probably be a lot easier to just use that, rather than what I was saying.
     
  9. erikwp

    erikwp

    Joined:
    Jan 16, 2018
    Posts:
    25
    Hmm its not working with PrefabUtility. Its saving the prefab with the script but still the meshes are all missing. Heres the code:

    Code (CSharp):
    1. public class SaveAsPrefab : MonoBehaviour
    2. {
    3.  
    4.     [ContextMenu("Save As Prefab (With Meshes)")]
    5.      void SavePrefab()
    6.     {
    7.         // Create a simple material asset
    8.  
    9.             PrefabUtility.SaveAsPrefabAsset(gameObject,  "Assets/Prefabs/MyPrefab.prefab");
    10.     }
    11. }
     
  10. erikwp

    erikwp

    Joined:
    Jan 16, 2018
    Posts:
    25
    I ended up writing a script that repairs the prefab by re-adding all the missing meshes. For this to work each mesh component must be named the same as its corresponding gameobject. The script also is looking in the specific folder which contains my original model file. The script creates a context menu button the executes the repair function. Hopefully it is helpful for someone.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEditor;
    5. public class RepairMissingMeshes : MonoBehaviour
    6. {
    7.  
    8.     [ContextMenu("Repair Missing Meshes")]
    9.     void ReplaceMissingMeshes()
    10.     {
    11.         Transform[] childArray = GetComponentsInChildren<Transform>();
    12.  
    13.         foreach (Transform child in childArray)
    14.         {
    15.             if (child.GetComponent<MeshFilter>() != null)
    16.             {
    17.                 if (GameObject.Find(child.name) == null)
    18.                 {
    19.                     Debug.Log("GO not found");
    20.                 }
    21. //here we find the correct model that corresponds to this particular mesh in the prefab
    22.                 string[] guids = AssetDatabase.FindAssets(child.name, new[] { "Assets/Models" });
    23.                 if (guids.Length > 0)
    24.                 {
    25.  
    26.                     string guid = guids[0];
    27.                     string path = AssetDatabase.GUIDToAssetPath(guid);
    28.                     GameObject go = (GameObject)AssetDatabase.LoadAssetAtPath(path, typeof(GameObject));
    29. //now go through the model and find the desired mesh by name
    30.                     MeshFilter[] mfArray = go.GetComponentsInChildren<MeshFilter>();
    31.                     if (mfArray != null)
    32.                     {
    33.                         foreach (var element in mfArray)
    34.                         {
    35.                             if (element.name == child.name)
    36.                             {
    37.                                 child.GetComponent<MeshFilter>().sharedMesh = element.sharedMesh;
    38.                             }
    39.                         }
    40.                     }
    41.  
    42.                 }
    43.  
    44.             }
    45.         }
    46.         PrefabUtility.SaveAsPrefabAsset(gameObject,"Assets/Prefabs/"+gameObject.name+".prefab");
    47.     }
    48. }
    49.