/// <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> /// Create a prefab from the imported hierarchy of game objects. /// This is the final output of an Editor glTF import. /// </summary> protected IEnumerator <GameObject> CreatePrefabEnum() { string basename = "scene.prefab"; if (!String.IsNullOrEmpty(_imported.Scene.name)) { basename = String.Format("{0}.prefab", GLTFUtils.cleanName(_imported.Scene.name)); } string dir = UnityPathUtil.GetProjectPath(_importPath); string path = Path.Combine(dir, basename); GameObject prefab = PrefabUtility.SaveAsPrefabAsset(_imported.Scene, path); // Make the prefab visible. // // Note: The model hierarchy is kept hidden during the glTF import // so that the user never sees the partially reconstructed // model. prefab.SetActive(true); // Note: base.Clear() removes imported game objects from // the scene and from memory, but does not remove imported // asset files from disk. base.Clear(); yield return(prefab); }
/// <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> /// Remove any imported game objects from scene and from memory, /// and remove any asset files that were generated. /// </summary> protected override void Clear() { // remove imported game objects from scene and from memory base.Clear(); // remove Unity asset files that were created during import UnityPathUtil.RemoveProjectDir(_importPath); }
/// <summary> /// Constructor /// </summary> public EditorGltfImporter(string gltfPath, string importPath, ProgressCallback progressCallback = null) : base(new Uri(gltfPath), null, new EditorGltfImportCache(UnityPathUtil.GetProjectPath(importPath)), progressCallback) { _importPath = importPath; }
/// <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> /// Create a prefab from the imported hierarchy of game objects. /// This is the final output of an Editor glTF import. /// </summary> protected IEnumerator <GameObject> CreatePrefabEnum() { string basename = "scene.prefab"; if (!String.IsNullOrEmpty(_imported.Scene.name)) { basename = String.Format("{0}.prefab", GLTFUtils.cleanName(_imported.Scene.name)); } string dir = UnityPathUtil.GetProjectPath(_importPath); string path = Path.Combine(dir, basename); GameObject prefab = PrefabUtility.SaveAsPrefabAsset(_imported.Scene, path); // Note: base.Clear() removes imported game objects from // the scene and from memory, but does not remove imported // asset files from disk. base.Clear(); yield return(prefab); }
/// <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); } }
/// <summary> /// Callback that is invoked when external file(s) are /// dragged-and-dropped into the Project Browser. /// </summary> private static void HandleDragAndDrop(string targetPath, string[] droppedPaths) { // Hold the Control key or Command key while // dragging-and-dropping a .gltf/.glb/.zip to // copy the file into the project without // performing an automatic glTF import. if (Event.current.control || Event.current.command) { return; } // Read current import options from Piglet Options window. // // Note: The SaveAssets/Refresh calls ensure that any changes // made in the Piglet Options window are saved out to disk // first. AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); _pigletOptions = Resources.Load <PigletOptions>("PigletOptions"); // Do nothing if drag-and-drop glTF import has been disabled by the user if (!_pigletOptions.EnableDragAndDropImport) { return; } // If `targetPath` is a regular file and not a directory, use // the parent directory as the import directory. string importDir = Directory.Exists(targetPath) ? targetPath : Path.GetDirectoryName(targetPath); importDir = UnityPathUtil.NormalizePathSeparators(importDir); // Exclude files that don't have .gltf/.glb extension. // // Note: I would prefer to pass skipped files through to Unity // for default drag-and-drop handling, but that does not seem // to be possible because `DragAndDrop.paths` is read-only. List <string> acceptedPaths = new List <string>(); foreach (string path in DragAndDrop.paths) { // Don't trigger automatic glTF import when we are dragging // a .gltf/.glb/.zip file from within the Unity project folder. // // When the source file is inside the Unity project folder, // the Unity drag-and-drop machinery will report a relative path // starting with "Assets/". if (path.StartsWith("Assets/")) { continue; } string _path = path.ToLower(); if (_path.EndsWith(".gltf") || _path.EndsWith(".glb")) { acceptedPaths.Add(path); } else if (_path.EndsWith(".zip") && ZipUtil.ContainsGltfFile(path)) { acceptedPaths.Add(path); } } if (acceptedPaths.Count > 0) { // Run GLTF import(s) in the background. StartImport(acceptedPaths, importDir); // Consume the `DragPerform` event, so that Unity's // default drag-and-drop handling, which // simply copies the file(s) into the target Assets // folder, is not performed. Event.current.Use(); } }
/// <summary> /// Coroutine to import GLTF files with Piglet's EditorGltfImporter. /// The string value returned via the IEnumerator is the target directory /// for the current import, so that files from an aborted/canceled import /// can be easily cleaned up. /// </summary> private static IEnumerator <string> ImportCoroutine(List <string> gltfPaths, string baseImportDir) { foreach (string gltfPath in gltfPaths) { string gltfBasename = Path.GetFileName(gltfPath); string gltfBasenameNoExt = Path.GetFileNameWithoutExtension(gltfPath); bool abortImport = false; // callback for updating progress during glTF import void OnProgress(GltfImportStep type, int count, int total) { ProgressLog.Instance.OnImportProgress(type, count, total); abortImport = EditorUtility.DisplayCancelableProgressBar( $"Importing {gltfBasename}...", ProgressLog.Instance.GetProgressMessage(), (float)count / total); } string importPath = UnityPathUtil.NormalizePathSeparators( Path.Combine(baseImportDir, gltfBasenameNoExt)); string importProjectPath = UnityPathUtil.GetProjectPath(importPath); if ((Directory.Exists(importPath) || File.Exists(importPath)) && _pigletOptions.PromptBeforeOverwritingFiles) { if (!EditorUtility.DisplayDialog( "Warning!", $"Overwrite \"{importProjectPath}\"?", "OK", "Cancel")) { yield break; } FileUtil.DeleteFileOrDirectory(importPath); AssetDatabase.Refresh(); } GltfImportTask importTask = EditorGltfImporter.GetImportTask(gltfPath, importPath, _pigletOptions.ImportOptions); importTask.OnProgress = OnProgress; GameObject importedPrefab = null; importTask.OnCompleted = (prefab) => importedPrefab = prefab; // restart import timer at zero ProgressLog.Instance.StartImport(); while (true) { if (abortImport) { importTask.Abort(); EditorUtility.ClearProgressBar(); yield break; } try { if (!importTask.MoveNext()) { break; } } catch (Exception e) { Debug.LogException(e); EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("Import Failed", String.Format("Import of {0} failed. " + "See Unity console log for details.", gltfBasename), "OK"); yield break; } yield return(importPath); } // Before modifying the selection, store a handle to // the transform of the currently selected game object (if any). Transform selectedTransform = Selection.activeTransform; // Select the prefab file in the Project Browser. if (_pigletOptions.SelectPrefabAfterImport) { Selection.activeObject = importedPrefab; yield return(importPath); } if (_pigletOptions.AddPrefabToScene) { // If we are currently in Prefab Mode, exit // back to the main scene hierarchy view. // // Note: Prefab Mode was introduced in Unity 2018.3. #if UNITY_2018_3_OR_NEWER if (StageUtility.GetCurrentStageHandle() != StageUtility.GetMainStageHandle()) { StageUtility.GoToMainStage(); } #endif GameObject instance = (GameObject)PrefabUtility .InstantiatePrefab(importedPrefab); // parent the prefab instance to the currently // selected GameObject (if any) if (selectedTransform != null) { instance.transform.parent = selectedTransform; } if (_pigletOptions.SelectPrefabInScene) { Selection.activeGameObject = instance; yield return(importPath); } } if (_pigletOptions.OpenPrefabAfterImport) { AssetDatabase.OpenAsset(importedPrefab); // Note: This is the best method I could find // for automatically centering the prefab in // the scene view. For further info, see // https://answers.unity.com/questions/813814/framing-objects-via-script-in-the-unity-editor.html SceneView.FrameLastActiveSceneView(); } EditorUtility.ClearProgressBar(); } }