Search Unity

Question Importing Custom Assets

Discussion in 'Asset Importing & Exporting' started by jpvanoosten, Jun 15, 2022.

  1. jpvanoosten

    jpvanoosten

    Joined:
    Nov 20, 2009
    Posts:
    50
    Maybe this question has been asked before, so sorry for that.

    I'm creating a tetrahedral mesh and saving it in MSH file format. I'm loading the data in Unity but I'm unsure how to create a custom asset importer for this.

    I've tried creating a custom component to hold the imported mesh:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. namespace Cradle.TetWild
    4. {
    5.     public class TetMeshComponent : MonoBehaviour
    6.     {
    7.         public TetMesh Mesh { get; set; }
    8.  
    9.         public TetMeshComponent(TetMesh mesh)
    10.         {
    11.             Mesh = mesh;
    12.         }
    13.     }
    14. }
    15.  
    And I've created a custom importer:
    Code (CSharp):
    1. using UnityEditor.AssetImporters;
    2.  
    3. namespace Cradle.TetWild
    4. {
    5.     /// <summary>
    6.     /// Import ".tet" assets in the editor.
    7.     /// </summary>
    8.     [ScriptedImporter(1, new[] { "tet", "msh" })]
    9.     public class TetMeshImporter : ScriptedImporter
    10.     {
    11.         public override void OnImportAsset(AssetImportContext ctx)
    12.         {
    13.             var tetMesh = TetMeshLoader.Load(ctx.assetPath);
    14.             TetMeshComponent component = new TetMeshComponent(tetMesh);
    15.             if (component != null) // TODO: Not sure how to import a custom mesh properly.
    16.             {
    17.                 ctx.AddObjectToAsset("tetrahedral mesh", component);
    18.                 ctx.SetMainObject(component);
    19.             }
    20.             else
    21.             {
    22.                 ctx.LogImportError("Could not create TetMeshComponent", component);
    23.             }
    24.         }
    25.     }
    26. }
    27.  
    But the
    component
    is always
    null
    and "Could not create TetMeshComponent" is printed in the debug console in Unity.

    I've tried several different things (
    TetMeshComponent
    was previously derived from
    Component
    , but I recently changed it to
    MonoBehaviour
    to see if I could get it to work, but no luck...)

    I looked at AlembicImporter and USDForUnity examples, but those are very complicated examples and I can't see how to apply them for my use case.

    I also considered just using a Unity
    Mesh
    for the import, but the tetrahedral mesh does not exactly map to a triangulated mesh correctly (each tetrahedral element contains 4 vertices and needs information about node neighbors etc...) so I'm skeptical about this approach.

    Does anyone have any advice on importing an asset type that does not map directly to one of the built-in asset types?

    Thanks in advance.
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,956
  3. jpvanoosten

    jpvanoosten

    Joined:
    Nov 20, 2009
    Posts:
    50
    Thanks for the quick reply.

    I'm looking into using
    ScriptableObject
    for this. My initial tests seem to work, but I'm not sure how this will behave when importing these assets at runtime (which is a requirement for my use case).
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,956
    At runtime? In that case you cannot use anything from the UnityEditor namespace. So none of the importers etc. You would have to roll your own functionality of:

    • pick a file to import (there is no asset import automatism at runtime)
    • read the file and convert its data to a mesh (this would be the same as in editor)
    • possibly serialize the mesh to a file - I think that should be possible using JsonUtility and having a class that holds all the info to recreate the mesh (if importing the mesh again would be unsuitable). Here‘s an example but this post is Unity 5-ish old, perhaps there are better ways in the current Unity: https://www.riccardostecca.net/articles/save_and_load_mesh_data_in_unity/
     
  5. vladala

    vladala

    Unity Technologies

    Joined:
    Mar 3, 2017
    Posts:
    189
    As it was said before you cannot "import" meshes at runtime.
    What you can do is "load it from a file at runtime".
    Be mindful that you need to implement your own Build post processor to copy the assets into the streamingAssets of your build and rewrite all the load paths to the relative to that.
     
  6. jpvanoosten

    jpvanoosten

    Joined:
    Nov 20, 2009
    Posts:
    50
    I realize I wasn't too clear from my original post. I have a "loader" that parses a tetrahedral mesh in MSH file format, so that's all working. But I'm creating a package for a client but I don't have access to the client's application.

    For testing my loader in the editor, I want to write an importer for the MSH files so I can just "reimport" the MSH file and check that it is working correctly.

    I now have this:
    Code (CSharp):
    1. using System.IO;
    2. using UnityEditor;
    3. using UnityEditor.AssetImporters;
    4. using UnityEngine;
    5.  
    6. namespace Cradle.TetWild
    7. {
    8.     /// <summary>
    9.     /// Import ".tet" assets in the editor.
    10.     /// </summary>
    11.     [ScriptedImporter(1, new[] { "tet", "msh" })]
    12.     public class TetMeshImporter : ScriptedImporter
    13.     {
    14.         [SerializeField] private int numNodes;
    15.         [SerializeField] private int numElements;
    16.  
    17.         public override void OnImportAsset(AssetImportContext ctx)
    18.         {
    19.             var tetMesh = TetMeshLoader.Load(ctx.assetPath);
    20.             if (tetMesh != null)
    21.             {
    22.                 numNodes = tetMesh.Nodes.Length;
    23.                 numElements = tetMesh.Elements.Length;
    24.  
    25.                 var fileName = Path.GetFileNameWithoutExtension(ctx.assetPath);
    26.  
    27.                 var go = new GameObject(fileName);
    28.                 var mesh = tetMesh.Mesh;
    29.                 mesh.name = $"{fileName}_mesh";
    30.                 tetMesh.name = $"{fileName}_tet";
    31.  
    32.                 go.AddComponent<MeshFilter>().mesh = mesh;
    33.                 go.AddComponent<TetWildComponent>().TetMesh = tetMesh;
    34.  
    35.                 Material mat = new Material(Shader.Find("Standard"));
    36.                 mat.name = $"{fileName}_mat";
    37.  
    38.                 go.AddComponent<MeshRenderer>().material = mat;
    39.  
    40.                 var icon = AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/nl.buas.cradle.tetwildunity/Editor/TetMesh Icon.png");
    41.  
    42.                 ctx.AddObjectToAsset(fileName, go, icon);
    43.                 ctx.AddObjectToAsset(tetMesh.name, tetMesh, icon);
    44.                 ctx.AddObjectToAsset(mesh.name, mesh);
    45.                 ctx.AddObjectToAsset(mat.name, mat);
    46.                 ctx.SetMainObject(go);
    47.             }
    48.             else
    49.             {
    50.                 ctx.LogImportError($"Error occurred while loading {ctx.assetPath}");
    51.             }
    52.         }
    53.     }
    54. }
    55.  
    Which is working pretty well now and I'm able to test the loader. The thing I was struggling with was that I didn't really want a
    GameObject
    on the imported mesh, but after a while, I realize this is the best approach to be able to "drag & drop" the imported mesh into the scene to visualize the tetrahedral mesh (it's a bit of a spaghetti mess, but I can see the tetrahedrons).

    In order to get this to work, the
    TetMesh
    class now extends from
    ScriptableObject
    and I use
    var tetMesh = ScriptableObject.CreateInstance<TetMesh>();
    to create an instance of the
    TetMesh
    . The documentation is a bit confusing about this:

    When you use the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting. In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development.​

    But does this mean that I can't instantiate a scriptable object at runtime??
     
  7. bastien_humeau

    bastien_humeau

    Unity Technologies

    Joined:
    Jun 14, 2017
    Posts:
    191
    That documentation is very confusing, I've openned a ticket asking it to be changed to something like:
    While in the UnityEngine Editor, you can use AssetDatabase methods to save ScriptableObjects content into your Assets folder, even while in playmode. In a deployed build, however, once a ScriptableObject is loaded it is considered read-only and cannot be used to save back the loaded data if you changed them anymore.​

    Yes, creating a GameObject and setting it as the main asset of the importer is definitely the best way to go.

    One side note on your ScriptedImporter code. Do not use dynamic names in ctx.AddObjectToAsset for the Ids. If the id changes, then all references in an existing scene will be broken. In your case, because the ids follow the file name and some part of the file content, that means changing the file will break any existing references to the mesh/gameobject in a scene.
    The best way to do that is to always use a constant string, the name itself doesn't really matter as it is only used internally for reference and not accessible to anyone.
    For example in your case, I'd use:
    Code (CSharp):
    1. ctx.AddObjectToAsset("root", go, icon);
    2. ctx.AddObjectToAsset("meshData", tetMesh, icon);
    3. ctx.AddObjectToAsset("mesh", mesh);
    4. ctx.AddObjectToAsset("mat", mat);
     
    jpvanoosten likes this.