/// <summary> /// Constructor. /// </summary> /// <param name="importBaseDir"> /// The base project directory for saving the imported /// Unity assets (e.g. "Assets/Imported/MyModel"). /// </param> public EditorGltfImportCache(string importBaseDir) { Textures = new SerializedAssetList <Texture2D>(SerializeTexture); Materials = new SerializedAssetList <Material>(SerializeMaterial); Meshes = new SerializedAssetList <List <KeyValuePair <Mesh, Material> > > (SerializeMesh); // create directory structure for imported assets _importBaseDir = importBaseDir; Directory.CreateDirectory( UnityPathUtil.GetAbsolutePath(_importBaseDir)); _importTexturesDir = Path.Combine(_importBaseDir, "Textures"); Directory.CreateDirectory( UnityPathUtil.GetAbsolutePath(_importTexturesDir)); _importMaterialsDir = Path.Combine(_importBaseDir, "Materials"); Directory.CreateDirectory( UnityPathUtil.GetAbsolutePath(_importMaterialsDir)); _importMeshesDir = Path.Combine(_importBaseDir, "Meshes"); Directory.CreateDirectory( UnityPathUtil.GetAbsolutePath(_importMeshesDir)); }
/// <summary> /// Save the input mesh to disk as a set of Unity .asset /// files and return a new mesh. The input mesh is a list /// mesh primitives, where each primitive is a KeyValuePair /// of a Mesh and a Material. The returned mesh (i.e. list of primitives) /// is the same as the input list, except that the Mesh /// for each primitive has been replaced by one that is backed /// by a Unity .asset file. These Mesh objects know about /// their backing .asset file and will automatically sync /// in-memory changes to the Mesh to disk. (For further /// info, see the Unity documentation for AssetDatabase.) /// </summary> /// <param name="mesh"> /// The mesh (list of mesh primitives) to be serialized to disk. /// </param> /// <returns> /// A new mesh (list of mesh primitives) that is backed by a /// set of .asset files (one per mesh primitive). /// </returns> protected List <KeyValuePair <Mesh, Material> > SerializeMesh( int index, List <KeyValuePair <Mesh, Material> > mesh) { Directory.CreateDirectory(UnityPathUtil.GetAbsolutePath(_importMeshesDir)); for (int i = 0; i < mesh.Count; ++i) { Mesh primitiveMesh = mesh[i].Key; Material primitiveMaterial = mesh[i].Value; string basename = String.Format("{0}.asset", primitiveMesh.name); string path = Path.Combine(_importMeshesDir, basename); // Serialize the mesh to disk as a Unity asset. // // Note: The primitiveMaterial does not need // to be serialized here, since that has already // been done during the earlier material-importing // step. AssetDatabase.CreateAsset(primitiveMesh, path); AssetDatabase.Refresh(); primitiveMesh = (Mesh)AssetDatabase.LoadAssetAtPath( path, typeof(Mesh)); mesh[i] = new KeyValuePair <Mesh, Material>( primitiveMesh, primitiveMaterial); } return(mesh); }
/// <summary> /// Save the given AnimationClip to disk as a Unity asset /// and return a new AnimationClip. The returned AnimationClip /// is the same as the original, except that it knows /// about the .anim file that backs it and will automatically /// synchronize in-memory changes to disk. (For further /// info, see the Unity documentation for AssetDatabase.) /// </summary> /// <param name="clip"> /// The AnimationClip to be serialized to disk /// </param> /// <returns> /// A new AnimationClip that is backed by a .anim file /// </returns> private AnimationClip SerializeAnimationClip(int index, AnimationClip clip) { Directory.CreateDirectory(UnityPathUtil.GetAbsolutePath(_importAnimationsDir)); string basename = string.Format("{0}.anim", clip.name); string path = Path.Combine(_importAnimationsDir, basename); AssetDatabase.CreateAsset(clip, path); AssetDatabase.Refresh(); clip = (AnimationClip)AssetDatabase.LoadAssetAtPath( path, typeof(AnimationClip)); return(clip); }
/// <summary> /// Save the given material to disk as a Unity asset /// and return a new Material. The returned Material /// is the same as the original, except that it knows /// about the .mat file that backs it and will automatically /// synchronize in-memory changes to disk. (For further /// info, see the Unity documentation for AssetDatabase.) /// </summary> /// <param name="material"> /// The material to be serialized to disk /// </param> /// <returns> /// A new Material that is backed by a .mat file /// </returns> protected Material SerializeMaterial(int index, Material material) { Directory.CreateDirectory(UnityPathUtil.GetAbsolutePath(_importMaterialsDir)); string basename = String.Format("{0}.mat", material.name); string path = Path.Combine(_importMaterialsDir, basename); AssetDatabase.CreateAsset(material, path); AssetDatabase.Refresh(); material = (Material)AssetDatabase.LoadAssetAtPath( path, typeof(Material)); return(material); }
/// <summary> /// Save the given texture to disk as a Unity asset and /// return a new Texture2D. The returned Texture2D /// is the same as the original, except that it /// knows about the asset file that backs it and will /// automatically synchronize in-memory changes to disk. /// (For further info, see the Unity documentation for /// AssetDatabase.) /// </summary> /// <param name="texture"> /// The texture to be serialized to disk. /// </param> /// <returns> /// A new Texture2D that is backed by an asset file. /// </returns> protected Texture2D SerializeTexture(int index, Texture2D texture) { // Unity's Texture2D.LoadImage() method imports // .png/.jpg images upside down, so flip it // right side up again. texture = TextureUtil.FlipTexture(texture); string basename = String.Format("texture_{0}.png", index); string pngPath = Path.Combine(_importTexturesDir, basename); byte[] pngData = texture.EncodeToPNG(); File.WriteAllBytes(UnityPathUtil.GetAbsolutePath(pngPath), pngData); AssetDatabase.Refresh(); texture = (Texture2D)AssetDatabase.LoadAssetAtPath( pngPath, typeof(Texture2D)); return(texture); }
/// <summary> /// Callback that is invoked for each item (file or folder) that /// is currently visible in the left/right panes of the Project Browser. /// </summary> /// <param name="guid"></param> /// <param name="selectionRect"></param> private static void ProjectItemOnGUI(string guid, Rect selectionRect) { Event @event = Event.current; // Get a reference to the Project Browser window that the user last // interacted with. (It is possible to have multiple Project Browsers // open at the same time.) // // Note: There is no Unity API for doing this, so we have to use // reflection to access private/internal members. var projectBrowser = ProjectBrowserExtensions .GetLastInteractedProjectBrowser(); // Get the position of the Project Browser window, in screen coordinates. Vector2 projectBrowserPosition = ((EditorWindow)projectBrowser).position.position; // For each event, we build a mapping of items -> rects. This mapping // is needed to determine if our drop target is located inside a tree // area or a list area, and to determine if the drop target is outside // of any items (e.g. dropping into an empty area of the files pane). // // I use `@event.rawType` here instead of `@event.type` because // `@event.type` is set to `EventType.Ignore` when the mouse is located // outside of the tree/list area containing the current item // (identified by `guid`). if (@event.rawType != _currentEventType) { _currentEventType = @event.rawType; List <ItemRect> temp = _itemRectsLastFrame; _itemRectsLastFrame = _itemRectsThisFrame; _itemRectsThisFrame = temp; _itemRectsThisFrame.Clear(); } Vector2 localMousePosition = Event.current.mousePosition; Vector2 globalMousePosition = GUIUtility.GUIToScreenPoint(localMousePosition); // Record guid => screen rect mapping for the current item. Rect globalRect = new Rect( GUIUtility.GUIToScreenPoint(selectionRect.position), selectionRect.size); // Note: Non-assets in the Project Browser (e.g. "Favorites", "Packages") // have empty GUIDs. if (guid.Length > 0) { _itemRectsThisFrame.Add(new ItemRect { guid = guid, rect = globalRect }); } // Note: When file(s) are dragged onto the right pane, the left pane // will have `@event.type == EventType.Ignore` and `@event.rawType == // EventType.DragPerform`. (And likewise for items in the right pane // when file(s) are dragged onto the left pane.) I'm not 100% sure why // this happens, but I think it because the mouse events are outside // the active GUI clip area (e.g. `GUI.BeginClip()`, // `GUI.BeginScrollRect()`). if (@event.type == EventType.DragPerform || @event.rawType == EventType.DragPerform) { // Determine the target directory for the GLTF import (i.e. // directory where the model prefab and associated assets will be // created), based on the drag-and-drop target in the Project // Browser. // // If the GLTF file(s) are dragged onto a directory, import into // that directory. If the GLTF file(s) are dragged onto a file, // import into the parent directory of that file. If the GLTF // file(s) are dragged onto an empty area, import into the // directory that is currently selected in the left pane. string dragTargetGuid = null; DropPosition dropPosition = DropPosition.UponItem; for (int i = 0; i < _itemRectsLastFrame.Count; ++i) { Rect itemRect = _itemRectsLastFrame[i].rect; string itemGuid = _itemRectsLastFrame[i].guid; if (itemRect.Contains(globalMousePosition)) { ProjectBrowserExtensions.ItemType itemType = ProjectBrowserExtensions.GetItemType(itemRect); string itemPath = UnityPathUtil.NormalizePathSeparators( AssetDatabase.GUIDToAssetPath(itemGuid)); // TreeItem areas support dropping files between // vertically adjacent folders/files (as indicated by a // horizontal blue line in the UI), whereas // list areas do not. if (itemType == ProjectBrowserExtensions.ItemType.TreeItem) { // The height of the target region at the top/bottom of // an item rect that corresponds to dropping files // between items. // // This value is hardcoded to match Unity's internal // value for `TreeViewGUI.k_HalfDropBetweenHeight`. For // Unity's own implementation of this logic, see // `TreeViewDragging.TryGetDropPosition`. const float halfDropBetweenHeight = 4f; if (globalMousePosition.y <= itemRect.yMin + halfDropBetweenHeight) { dropPosition = DropPosition.AboveItem; } else if (globalMousePosition.y >= itemRect.yMax - halfDropBetweenHeight) { dropPosition = DropPosition.BelowItem; } else { dropPosition = DropPosition.UponItem; } } // The easy case: we are dropping file(s) directly onto a file/folder // (rather than between two vertically adjacent files/folders). if (dropPosition == DropPosition.UponItem) { dragTargetGuid = itemGuid; break; } if (dropPosition == DropPosition.BelowItem) { // Special case: If we are dropping below the last item // in a tree/list, then the drop target should be the // parent folder of the target item. // // Note: In "Two Column Layout", `_itemRectsLastFrame` // stores both tree items and list items in the same // list. As a result, the current item (i) may be in // the left pane (i.e. folder tree) while the next item // (i + 1) is in the right pane (i.e. files list). In // this case, we are not really dropping between items, // but we are rather dropping after the last item in // the folder tree. ProjectBrowserExtensions.ItemType nextItemType = ProjectBrowserExtensions.ItemType.TreeItem; if (i + 1 < _itemRectsLastFrame.Count) { Rect nextItemRect = _itemRectsLastFrame[i + 1].rect; nextItemType = ProjectBrowserExtensions .GetItemType(nextItemRect); } if (i + 1 >= _itemRectsLastFrame.Count || nextItemType != itemType) { // Special case: user is not allowed to drop into the // parent folder of "Assets", so do nothing. if (itemPath == "Assets") { return; } string itemParentPath = Path.GetDirectoryName(itemPath); dragTargetGuid = AssetDatabase.AssetPathToGUID(itemParentPath); break; } // If the next item is a child file/folder of the // current item, then the current item should be the // drag target. Otherwise, the current and next // items are siblings and the drag target should // be their shared parent folder. string nextItemGuid = _itemRectsLastFrame[i + 1].guid; string nextItemPath = UnityPathUtil.NormalizePathSeparators( AssetDatabase.GUIDToAssetPath(nextItemGuid)); if (UnityPathUtil.GetParentDir(nextItemPath) == itemPath) { dragTargetGuid = itemGuid; break; } else { dragTargetGuid = AssetDatabase.AssetPathToGUID( UnityPathUtil.GetParentDir(itemPath)); break; } } if (dropPosition == DropPosition.AboveItem) { // If we are dropping above the topmost item in a // tree/list. // // Note: This should never happen except when the user // is drops files above the root "Assets" folder. In // the case that a subset of items is currently being // shown in a scroll window, the tree/list window will // automatically scroll up when the user hovers the // mouse above the top visible item. if (i == 0) { return; } // If the prev item is parent folder of the // current item, then the previous item should be the // drag target. Otherwise, the current and previous // items are siblings and the drag target should // be their shared parent folder. string prevItemGuid = _itemRectsLastFrame[i - 1].guid; string prevItemPath = UnityPathUtil.NormalizePathSeparators( AssetDatabase.GUIDToAssetPath(prevItemGuid)); if (UnityPathUtil.GetParentDir(itemPath) == prevItemPath) { dragTargetGuid = prevItemGuid; break; } else { dragTargetGuid = AssetDatabase.AssetPathToGUID( UnityPathUtil.GetParentDir(itemPath)); break; } } } } if (dragTargetGuid == null) { // The user dragged the GLTF file(s) onto an empty area of the // Project Browser. Use the currently selected folder in the // left pane (i.e. folder tree) as the import directory. dragTargetGuid = AssetDatabase.AssetPathToGUID( ProjectBrowserExtensions.GetSelectedProjectFolder()); } string dragTargetProjectPath = AssetDatabase.GUIDToAssetPath(dragTargetGuid); string dragTargetPath = UnityPathUtil.GetAbsolutePath(dragTargetProjectPath); // Invoke user-defined drag-and-drop callbacks OnDragAndDrop?.Invoke(dragTargetPath, DragAndDrop.paths); } }