Creating a custom asset with automatic import and drag&drop support (Unity)

Apr 30, 2023 · 4 mins read
Creating a custom asset with automatic import and drag&drop support (Unity)

Creating custom asset with automatic import and drag&drop support

In my volume rendering plugin I allow users to import various volumetric dataset formats, such as DICOM and NRRD. Recently I received a request for being able to save imported datasets as assets, so they can easily be used at runtime without needing to import the source file again.

TLDR: This can be done quite easily, by setting up a scripted importer.

I also wanted to add support for dragging and dropping these assets into the scene view or scene hierarchy. This should then spawn a GameObject with a VolumeRenderedObject component that references this dataset.

TLDR: This can be done by registering a handler using DragAndDrop.AddDropHandler.

Scripted importer

Scripted importers allow you to handle how an file with a certain extension (in my case “.raw”, “.nrrd”, etc.) will be imported when added to the Assets folder.

To do this, simply create a class that inherits from ScriptedImporter and implements the OnImportAsset method.

In this function you can import the file however you want. When you have an object (may be a ScriptableObject) you want to save as an asset, do the following:

ctx.AddObjectToAsset("main obj", yourImportedObject);
ctx.SetMainObject(yourImportedObject);

Full source code:

#if UNITY_2020_2_OR_NEWER
using UnityEngine;
using UnityEditor.AssetImporters;

namespace UnityVolumeRendering
{
    [ScriptedImporter(1, "raw")]
    public class RawScriptedImporter : ScriptedImporter
    {
        [SerializeField]
        private Vector3Int dimension = new Vector3Int(128, 256, 256);
        [SerializeField]
        private DataContentFormat dataFormat = DataContentFormat.Int16;
        [SerializeField]
        private Endianness endianness = Endianness.LittleEndian;
        [SerializeField]
        private int bytesToSkip = 0;

        public override void OnImportAsset(AssetImportContext ctx)
        {
            string fileToImport = ctx.assetPath;

            RawDatasetImporter importer = new RawDatasetImporter(fileToImport, dimension.x, dimension.y, dimension.z, dataFormat, endianness, bytesToSkip);
            VolumeDataset dataset = importer.Import();

            if (dataset)
            {
                ctx.AddObjectToAsset("main obj", dataset);
                ctx.SetMainObject(dataset);
            }
            else
            {
                Debug.LogError($"Failed to load dataset: {fileToImport}");
            }
        }
    }
}
#endif

Note: This feature is only available in Unity 2020.2 or newer, so we need to add a #if UNITY_2020_2_OR_NEWER check to avoid compilation errors on older versions.

Note: The serialised private member variables at the top are there because we want to display and potentially modify them in the scripted importer editor (read below). If a user modifies their values and clicks “Apply” we will re-import the dataset using the modified values. You may not need to do this, depending on your use case.

Scripted importer editor

If you wish to set up a custom inspector for your new asset, create a class that inherits from ScriptedImporterEditor and implements the OnInspectorGUI method.

#if UNITY_2020_2_OR_NEWER
using UnityEditor;
using UnityEditor.AssetImporters;

namespace UnityVolumeRendering
{
    [CustomEditor(typeof(RawScriptedImporter))]
    public class RawScriptedImporterEditor : ScriptedImporterEditor
    {
        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            RawScriptedImporter importer = (RawScriptedImporter)target;
            SerializedProperty dimension = serializedObject.FindProperty("dimension");
            SerializedProperty dataFormat = serializedObject.FindProperty("dataFormat");
            SerializedProperty endianness = serializedObject.FindProperty("endianness");
            SerializedProperty bytesToSkip = serializedObject.FindProperty("bytesToSkip");

            EditorGUILayout.PropertyField(dimension);
            EditorGUILayout.PropertyField(dataFormat);
            EditorGUILayout.PropertyField(endianness);
            EditorGUILayout.PropertyField(bytesToSkip);

            serializedObject.ApplyModifiedProperties();

            ApplyRevertGUI();
        }
    }
}
#endif

**Note: ** Remember to call ApplyRevertGUI at the end. This will add buttons for applying and reverting changes to the asset. Applying will cause our OnImportAsset method to be called again.

Adding support for drag&drop into scene view/hierarchy

Next, we want to allow users to drag and drop these imported dataset assets into the scene. This should spawn a GameObject with a VolumeRenderedObject component that references them.

To do this, we need to register some callbacks to be called once in the editor.

The delegates we need to register are:

Both of these can be registered through DragAndDrop.AddDropHandler.

We will register them in a special InitializeOnLoadMethod method, that will get called once on load.

In these callback methods, we can check the perform parameter, which will be

  • false while the user is dragging the object
  • true when the user tries to “drop” the object (on mouse button up)

When perform is false, we return DragAndDropVisualMode.Move. When it’s true, we spawn our object.

We can get a reference to the object being dragged by checking DragAndDrop.objectReferences[0].

Full source code:

#if UNITY_2021_2_OR_NEWER
using UnityEditor;
using UnityEngine;

namespace UnityVolumeRendering
{
    static class DragDropHandler
    {
        [InitializeOnLoadMethod]
        static void OnLoad()
        {
            DragAndDrop.AddDropHandler(OnSceneDrop);
            DragAndDrop.AddDropHandler(OnHierarchyDrop);
        }

        private static DragAndDropVisualMode OnSceneDrop(Object dropUpon, Vector3 worldPosition, Vector2 viewportPosition, Transform parentForDraggedObjects, bool perform)
        {
            if (perform && DragAndDrop.objectReferences[0] is VolumeDataset)
            {
                VolumeDataset datasetAsset = (VolumeDataset)DragAndDrop.objectReferences[0];
                VolumeObjectFactory.CreateObject(datasetAsset);
            }
            return DragAndDropVisualMode.Move;
        }

        private static DragAndDropVisualMode OnHierarchyDrop(int dropTargetInstanceID, HierarchyDropFlags dropMode, Transform parentForDraggedObjects, bool perform)
        {
            if (perform && DragAndDrop.objectReferences[0] is VolumeDataset)
            {
                VolumeDataset datasetAsset = (VolumeDataset)DragAndDrop.objectReferences[0];
                VolumeRenderedObject spawnedObject = VolumeObjectFactory.CreateObject(datasetAsset);
                GameObject parentObject = (GameObject)EditorUtility.InstanceIDToObject(dropTargetInstanceID);
                if (parentObject)
                {
                    spawnedObject.gameObject.transform.SetParent(parentObject.transform);
                }
            }

            return DragAndDropVisualMode.Move;
        }
    }
}
#endif

Note: This feature is only available in Unity 2021.2 or newer, so we need to add “#if UNITY_2021_2_OR_NEWER” at the top.

We can now drag and drop our dataset assets into the scene!

See full pull request here

Sharing is caring!


Comments

You can use your Mastodon account to reply to this post.

Reply