Search Unity

Clone and subtract from substance or split substance help

Discussion in 'Scripting' started by Krileon, Aug 24, 2013.

  1. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    I'm trying to write an editor script to select a substance and split the materials in it into multiple copies of the substance. Basically lets say you've 60 materials in a substance. The script would allow you to split that 1 substance into 2 substances with 30 materials each. It would need to maintain the material maps as well if possible.

    Does anyone know if this is even possible? I know you can manually duplicate with Ctrl + D, but it does not handle the splitting nor does it maintain material mapping.
     
  2. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    I was able to successfully and uniquely duplicate the substance (with materials intact, but mapping lost of course) using the below editor script, which basically is no different than pressing Ctrl + D.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEditor;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6.  
    7. public class SplitSubstance : EditorWindow {
    8.  
    9.     private SubstanceArchive substance;
    10.  
    11.     [MenuItem ("Window/Split Substance")]
    12.     static void Init() {
    13.         EditorWindow.GetWindow( typeof( SplitSubstance ) );
    14.     }
    15.  
    16.     void OnGUI() {
    17.         substance = EditorGUILayout.ObjectField( "Substance", substance, typeof( SubstanceArchive ), false ) as SubstanceArchive;
    18.        
    19.         if ( GUILayout.Button( "Split" ) ) {
    20.             string fromPath = AssetDatabase.GetAssetPath( substance.GetInstanceID() );
    21.             string toPath = AssetDatabase.GenerateUniqueAssetPath( fromPath );
    22.  
    23.             AssetDatabase.CopyAsset( fromPath, toPath );
    24.             AssetDatabase.Refresh();
    25.         }
    26.     }
    27. }
    28.  
    Next step is to somehow edit the clone, count the number of materials, determine how many splits we need to make, clone multiple times and edit out the materials from each clone incrementally. So for example if we need to split 3 times into sets of 5 then it should be cloned 3 times and remove all but 1-5 for clone 1, all but 6-11 for clone 2, and remove all but 12-17 from clone 3.

    Question is, is it even possible to count the number of materials in a substance and delete all but a specific number of them? It looks like SubstanceImporter can help with this, but I've yet to figure out quite how to use it.
     
    Last edited: Aug 24, 2013
  3. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    Any suggestions? Not finding much information on how to parse the child procedural materials from SubstanceArchive or how to use SubstanceImporter, which seams to have the functions to count the materials in a substance, etc..
     
  4. EricBatut

    EricBatut

    Joined:
    Feb 14, 2012
    Posts:
    8
  5. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    The problem is there is no documentation on how to actually use SubstanceImporter. As mentioned above it looks like the correct class, but I can't figure out how to use it. There's no example in the reference manual. I've successfully cloned the substance, including its materials, but I now need to use SubstanceImporter on the clone to count and remove anything beyond XYZ count, which should be easy if I could figure out how to use SubstanceImporter. Will be glad to share the script if I can get this completed. Above will at least successfully clone the substance and its materials.
     
  6. EricBatut

    EricBatut

    Joined:
    Feb 14, 2012
    Posts:
    8
    Yes, the SubstanceImporter class is documented, but no proper example of what to do with it is ever given :(

    As for all asset importers, one way to access an asset's importer is via AssetImporter.GetAtPath(string) (doc here: http://docs.unity3d.com/Documentation/ScriptReference/AssetImporter.GetAtPath.html), like this:

    // You can get a reference to your ProceduralMaterial any way you want, this is just an example
    ProceduralMaterial sbs = renderer.sharedMaterial as ProceduralMaterial;
    string path = AssetDatabase.GetAssetPath(sbs);
    SubstanceImporter importer = AssetImporter.GetAtPath(path) as SubstanceImporter;

    "importer" is now your reference to the SubstanceImporter, from which you can rename/delete/create/clone/count material instances. If you clone SBSARs in order to split your original "all instances attached to a single SBSAR" importer, then you will have to clone the SBSAR (which you already did), then get its importer, get the original SBSAR's importer, and then juggle with the two importers to transfer material instances from the former to the latter.

    Hopefully that makes sense :)

    Best Regards,
    Eric
     
  7. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    Awesome! Thank you! Will see what I can come up with.
     
  8. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    Ok, got it working. The below script will duplicate and properly split up a substance. It does not maintain material references (meaning you'll need to re-apply them to your models from the split up substances) as I'm not sure that's even possible. It also does not touch the original substance to prevent any and all possible data loss. It will incrementally name the cloned substances

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEditor;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6.  
    7. public class SplitSubstance : EditorWindow {
    8.  
    9.     private SubstanceArchive substance;
    10.     private int batch;
    11.  
    12.     private string fromPath;
    13.     private SubstanceImporter fromImporter;
    14.     private int fromMaterialCount;
    15.     ProceduralMaterial[] fromMaterials;
    16.  
    17.     private string toPath;
    18.     SubstanceImporter toImporter;
    19.     ProceduralMaterial[] toMaterials;
    20.  
    21.     private int begin;
    22.     private int end;
    23.     private int index;
    24.  
    25.     [MenuItem ("Window/Split Substance")]
    26.     static void Init() {
    27.         EditorWindow.GetWindow( typeof( SplitSubstance ) );
    28.     }
    29.  
    30.     void OnGUI() {
    31.         substance = EditorGUILayout.ObjectField( "Substance", substance, typeof( SubstanceArchive ), false ) as SubstanceArchive;
    32.         batch = EditorGUILayout.IntSlider( "Batch", batch, 1, 25 );
    33.  
    34.         if ( GUILayout.Button( "Split" ) ) {
    35.             fromPath = AssetDatabase.GetAssetPath( substance.GetInstanceID() );
    36.             fromImporter = AssetImporter.GetAtPath( fromPath ) as SubstanceImporter;
    37.             fromMaterialCount = fromImporter.GetMaterialCount();
    38.             fromMaterials = fromImporter.GetMaterials();
    39.             index = 1;
    40.             begin = 1;
    41.             end = batch;
    42.  
    43.             parseSubstance();
    44.         }
    45.     }
    46.  
    47.     void parseSubstance() {
    48.         if ( ( fromMaterialCount > 0 )  ( end <= fromMaterialCount ) ) {
    49.             toPath = AssetDatabase.GenerateUniqueAssetPath( fromPath );
    50.  
    51.             AssetDatabase.CopyAsset( fromPath, toPath );
    52.  
    53.             toImporter = AssetImporter.GetAtPath( toPath ) as SubstanceImporter;
    54.             toMaterials = toImporter.GetMaterials();
    55.  
    56.             foreach ( ProceduralMaterial fromMaterial in fromMaterials ) {
    57.                 if ( ( index < begin ) || ( index > end ) ) {
    58.                     foreach ( ProceduralMaterial toMaterial in toMaterials ) {
    59.                         if ( toMaterial.name == fromMaterial.name ) {
    60.                             toImporter.DestroyMaterial( toMaterial );
    61.                         }
    62.                     }
    63.                 }
    64.  
    65.                 index++;
    66.             }
    67.            
    68.             index = 1;
    69.             begin = ( end + 1 );
    70.             end = ( end + batch );
    71.            
    72.             if ( ( end > fromMaterialCount )  ( begin <= fromMaterialCount ) ) {
    73.                 end = fromMaterialCount;
    74.             }
    75.  
    76.             parseSubstance();
    77.         } else {
    78.             AssetDatabase.Refresh();
    79.         }
    80.     }
    81. }
    82.  
    To use it simply add the script to your project then go to Window > Split Substance. Next drag and drop or use the selector to select your substance. Now using the "Batch" parameter select how many materials you want per substance (limited between 1 and 25). Next click "Split" and it'll begin the split. Once it's done it'll refresh so the import can begin. You'll now notice the new split up substances with all your material settings still intact.

    Note: Be carefully with crazy large substances as it'll likely cause Unity to run out of memory.
     
    Last edited: Aug 29, 2013
  9. Krileon

    Krileon

    Joined:
    Oct 30, 2012
    Posts:
    642
    Ok, I've updated it to do a refresh after every batch. This causes GC to clear the memory so large substances won't crash Unity. I tested a 100 material substance on a 4GB RAM laptop and didn't run out of memory this time as well as monitored memory to see if GC was clearing properly (it was). So if you've super large substances to split up then the below should do fine. It still may take awhile though.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEditor;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6.  
    7. public class SplitSubstance : EditorWindow {
    8.  
    9.     private SubstanceArchive substance;
    10.     private int batch;
    11.  
    12.     private string fromPath;
    13.     private SubstanceImporter fromImporter;
    14.     private int fromMaterialCount;
    15.     ProceduralMaterial[] fromMaterials;
    16.  
    17.     private string toPath;
    18.     SubstanceImporter toImporter;
    19.     ProceduralMaterial[] toMaterials;
    20.  
    21.     private int begin;
    22.     private int end;
    23.     private int index;
    24.  
    25.     [MenuItem ("Window/Split Substance")]
    26.     static void Init() {
    27.         EditorWindow.GetWindow( typeof( SplitSubstance ) );
    28.     }
    29.  
    30.     void OnGUI() {
    31.         substance = EditorGUILayout.ObjectField( "Substance", substance, typeof( SubstanceArchive ), false ) as SubstanceArchive;
    32.         batch = EditorGUILayout.IntSlider( "Batch", batch, 1, 25 );
    33.  
    34.         if ( GUILayout.Button( "Split" ) ) {
    35.             fromPath = AssetDatabase.GetAssetPath( substance.GetInstanceID() );
    36.             fromImporter = AssetImporter.GetAtPath( fromPath ) as SubstanceImporter;
    37.             fromMaterialCount = fromImporter.GetMaterialCount();
    38.             fromMaterials = fromImporter.GetMaterials();
    39.             index = 1;
    40.             begin = 1;
    41.             end = batch;
    42.  
    43.             parseSubstance();
    44.         }
    45.     }
    46.  
    47.     void parseSubstance() {
    48.         if ( ( fromMaterialCount > 0 )  ( end <= fromMaterialCount ) ) {
    49.             toPath = AssetDatabase.GenerateUniqueAssetPath( fromPath );
    50.  
    51.             AssetDatabase.CopyAsset( fromPath, toPath );
    52.  
    53.             toImporter = AssetImporter.GetAtPath( toPath ) as SubstanceImporter;
    54.             toMaterials = toImporter.GetMaterials();
    55.  
    56.             foreach ( ProceduralMaterial fromMaterial in fromMaterials ) {
    57.                 if ( ( index < begin ) || ( index > end ) ) {
    58.                     foreach ( ProceduralMaterial toMaterial in toMaterials ) {
    59.                         if ( toMaterial.name == fromMaterial.name ) {
    60.                             toImporter.DestroyMaterial( toMaterial );
    61.                         }
    62.                     }
    63.                 }
    64.  
    65.                 index++;
    66.             }
    67.  
    68.             index = 1;
    69.             begin = ( end + 1 );
    70.             end = ( end + batch );
    71.  
    72.             if ( ( end > fromMaterialCount )  ( begin <= fromMaterialCount ) ) {
    73.                 end = fromMaterialCount;
    74.             }
    75.  
    76.             AssetDatabase.Refresh();
    77.  
    78.             parseSubstance();
    79.         } else {
    80.             AssetDatabase.Refresh();
    81.         }
    82.     }
    83. }
    84.