using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.ProjectWindowCallback;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace UnityGLTF
{
public static class GLTFExportMenu
{
private const string MenuPrefix = "Assets/UnityGLTF/";
private const string MenuPrefixGameObject = "GameObject/UnityGLTF/";
private const string ExportGlb = "Export selected";
private const string ExportGlbBatch = "Export each as separate asset";
public static string RetrieveTexturePath(UnityEngine.Texture texture)
{
var path = AssetDatabase.GetAssetPath(texture);
// texture is a subasset
if (AssetDatabase.GetMainAssetTypeAtPath(path) != typeof(Texture2D))
{
var ext = Path.GetExtension(path);
if (string.IsNullOrWhiteSpace(ext)) return texture.name + ".png";
path = path.Replace(ext, "-" + texture.name + ext);
}
return path;
}
private struct ExportBatch
{
public string sceneName;
public Transform[] rootTransforms;
public Object[] rootResources;
public SceneAsset[] sceneAssets;
public TransformData?[] rootTransformOverride;
}
private struct TransformData
{
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
}
private static GLTFSettings _cachedSettings;
private static GLTFSettings settings
{
get {
if (_cachedSettings == null)
{
_cachedSettings = GLTFSettings.GetOrCreateSettings();
}
return _cachedSettings;
}
}
private static TransformData? GetOverride(Transform transform)
{
switch (settings.EditorExportTransformMode)
{
// Heuristic for correcting the placement of the object in the scene
// Keep world scale
// This is questionable; alternative: keep local UNIFORM scale, so when the parent is stretched we don't want to inherit that stretch.
// Otherwise, exporting a child of a stretched object will result in an exported object that looks visually different from the scene.
// Keep local rotation
// Adjust local position
// - easy case for now: set to 0
// - better heuristic might be: if current local position in any axis fits into the bounds: keep that axis; if it doesn't fit into the bounds: set to 0
case GLTFSettings.TransformMode.Auto:
return new TransformData
{
position = Vector3.zero,
rotation = transform.localRotation,
scale = transform.lossyScale
};
case GLTFSettings.TransformMode.WorldTransforms:
return new TransformData
{
position = transform.position,
rotation = transform.rotation,
scale = transform.lossyScale
};
case GLTFSettings.TransformMode.Reset:
var normalizedScale = transform.lossyScale;
var maxEdge = Math.Max(normalizedScale.x, Math.Max(normalizedScale.y, normalizedScale.z));
if (maxEdge < 0.00001f)
normalizedScale = Vector3.one;
else
normalizedScale /= maxEdge;
return new TransformData()
{
position = Vector3.zero,
rotation = Quaternion.identity,
scale = normalizedScale,
};
// This is the built-in case for GLTFExporter: it exports the local transforms of nodes into glTF.
case GLTFSettings.TransformMode.LocalTransforms:
default:
return null;
}
}
///
/// This method collects GameObjects into export jobs. It supports multi-selection exports in various useful ways.
/// 1. When multiple scene objects are selected, they are exported together as one file.
/// 2. When multiple prefabs are selected, each one gets exported as individual file.
/// 3. When multiple scenes are selected, each scene gets exported as individual file.
///
/// True if the current selection can be exported.
private static bool TryGetExportBatchesFromSelection(out List batches, bool separateBatches)
{
if (Selection.transforms.Length > 1)
{
if (!separateBatches)
{
// A better Auto heuristic for rootTransformOverride here is more complicated; we would need to determine what the shared root of those objects is, and
// decide if we want to keep world position or keep local position in the shared root.
batches = new List()
{
new()
{
sceneName = SceneManager.GetActiveScene().name,
rootTransforms = Selection.transforms,
rootTransformOverride = Selection.transforms.Select(GetOverride).ToArray(),
}
};
}
else
{
batches = new List();
foreach (var transform in Selection.transforms)
{
batches.Add(new ExportBatch()
{
sceneName = transform.name,
rootTransforms = new[] { transform },
rootTransformOverride = new[] { GetOverride(transform) },
});
}
}
return true;
}
// Special case for one selected object: use the object's name as the scene name
if (Selection.transforms.Length == 1)
{
var transform = Selection.transforms[0];
batches = new List()
{
new()
{
sceneName = Selection.activeGameObject.name,
rootTransforms = new[] { transform },
rootTransformOverride = new[] { GetOverride(transform) },
}
};
return true;
}
// Project object selection
if (Selection.objects.Any() && Selection.objects.All(x => x is GameObject))
{
if (!separateBatches)
{
batches = new List()
{
new()
{
sceneName = Selection.objects.First().name,
rootTransforms = Array.ConvertAll(Selection.objects, x => (Transform)x),
}
};
}
else
{
batches = new List();
foreach (var obj in Selection.objects)
{
var go = (GameObject) obj;
batches.Add(new ExportBatch()
{
sceneName = go.name,
rootTransforms = new[] { go.transform },
});
}
}
return true;
}
// Project material selection
if (Selection.objects.Any() && Selection.objects.All(x => x is Material))
{
if (!separateBatches)
{
batches = new List()
{
new() {
sceneName = "Material Library",
rootResources = Selection.objects,
}
};
}
else
{
batches = new List();
foreach (var obj in Selection.objects)
{
var material = (Material) obj;
batches.Add(new ExportBatch()
{
sceneName = material.name,
rootResources = new Object[] { material },
});
}
}
return true;
}
// Project scene selection
if (Selection.objects.Any() && Selection.objects.All(x => x is SceneAsset))
{
batches = new List();
if (!separateBatches)
{
batches.Add(new ExportBatch()
{
sceneName = "Scenes",
sceneAssets = Selection.objects.Cast().ToArray(),
});
}
else
{
foreach (var obj in Selection.objects)
{
var scene = (SceneAsset) obj;
batches.Add(new()
{
sceneName = scene.name,
sceneAssets = new[] { scene },
});
}
}
/*
sceneName = Selection.objects.First().name;
if (openSceneIfNeeded)
{
var firstScene = (SceneAsset) Selection.objects.First();
var stage = ScriptableObject.CreateInstance();
stage.Setup(AssetDatabase.GetAssetPath(firstScene));
StageUtility.GoToStage(stage, true);
var roots = stage.scene.GetRootGameObjects();
rootTransforms = Array.ConvertAll(roots, x => x.transform);
rootResources = null;
return true;
}
*/
return true;
}
batches = null;
return false;
}
private class ExportStage: PreviewSceneStage
{
private static MethodInfo _openPreviewScene;
protected override bool OnOpenStage() => true;
// public void Setup(string scenePath)
// {
// #if !UNITY_2023_1_OR_NEWER
// if (_openPreviewScene == null) _openPreviewScene = typeof(EditorSceneManager).GetMethod("OpenPreviewScene", (BindingFlags)(-1), null, new[] {typeof(string), typeof(bool)}, null);
// if (_openPreviewScene == null) return;
//
// scene = (Scene) _openPreviewScene.Invoke(null, new object[] { scenePath, false });
// #else
// scene = EditorSceneManager.OpenPreviewScene(scenePath, false);
// #endif
// }
protected override GUIContent CreateHeaderContent()
{
return new GUIContent("Export: " + scene.name);
}
}
private static bool ExportBinary => settings.EditorExportFileFormat == GLTFSettings.ExportFileFormat.Glb;
private const int Priority = 34;
[MenuItem(MenuPrefix + ExportGlb + " &SPACE", true, Priority)]
[MenuItem(MenuPrefixGameObject + ExportGlb, true, Priority)]
private static bool ExportSelectedValidate()
{
return TryGetExportBatchesFromSelection(out _, false);
}
[MenuItem(MenuPrefix + ExportGlb + " &SPACE", false, Priority)]
[MenuItem(MenuPrefixGameObject + ExportGlb, false, Priority)]
private static void ExportSelected(MenuCommand command)
{
// The exporter handles multi-selection. We don't want to call export multiple times here.
if (command.context && Selection.objects.Length > 1 && command.context != Selection.objects[0])
return;
_ExportSelected(false);
}
[MenuItem(MenuPrefix + ExportGlbBatch, true, Priority + 1)]
[MenuItem(MenuPrefixGameObject + ExportGlbBatch, true, Priority + 1)]
private static bool ExportSelectedBatchValidate()
{
if (Selection.objects.Length < 2) return false;
return TryGetExportBatchesFromSelection(out _, false);
}
[MenuItem(MenuPrefix + ExportGlbBatch, false, Priority + 1)]
[MenuItem(MenuPrefixGameObject + ExportGlbBatch, false, Priority + 1)]
private static void ExportSelectedBatch(MenuCommand command)
{
// The exporter handles multi-selection. We don't want to call export multiple times here.
if (command.context && Selection.objects.Length > 1 && command.context != Selection.objects[0])
return;
_ExportSelected(true);
}
private static void _ExportSelected(bool separateBatches)
{
if (!TryGetExportBatchesFromSelection(out var batches, separateBatches))
{
Debug.LogError("Can't export: selection is empty");
return;
}
for (var i = 0; i < batches.Count; i++)
{
var success = Export(batches[i], ExportBinary, i == 0);
if (!success) break;
}
}
[MenuItem(MenuPrefix + "Export active scene", true, Priority + 2)]
private static bool ExportSceneValidate()
{
var activeScene = SceneManager.GetActiveScene();
if (!activeScene.IsValid()) return false;
return true;
}
[MenuItem(MenuPrefix + "Export active scene", false, Priority + 2)]
private static void ExportScene()
{
var scene = SceneManager.GetActiveScene();
ExportScene(scene);
}
private static void ExportScene(Scene scene)
{
var gameObjects = scene.GetRootGameObjects();
var transforms = Array.ConvertAll(gameObjects, gameObject => gameObject.transform);
Export(new ExportBatch() {
rootTransforms = transforms,
sceneName = scene.name,
rootResources = null,
}, ExportBinary, true);
}
private static void ExportAllScenes()
{
var roots = new List();
var scenesThatWereNotLoaded = new List();
for (var i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!scene.IsValid()) continue;
var sceneWasLoaded = scene.isLoaded;
if (!sceneWasLoaded)
{
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Additive);
scenesThatWereNotLoaded.Add(scene);
}
roots.AddRange(Array.ConvertAll(scene.GetRootGameObjects(), gameObject => gameObject.transform));
}
Export(new ExportBatch() {
rootTransforms = roots.ToArray(),
sceneName = SceneManager.GetActiveScene().name,
rootResources = null,
}, ExportBinary, true);
foreach (var scene in scenesThatWereNotLoaded)
{
if (!scene.isLoaded) continue;
EditorSceneManager.CloseScene(scene, false);
}
}
private static void ExportAllScenesBatch()
{
for (var i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!scene.IsValid()) continue;
var sceneWasLoaded = scene.isLoaded;
if (!sceneWasLoaded) EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Additive);
var roots = Array.ConvertAll(scene.GetRootGameObjects(), gameObject => gameObject.transform);
var success = Export(new ExportBatch() {
rootTransforms = roots,
sceneName = scene.name,
rootResources = null,
}, ExportBinary, i == 0);
if (!sceneWasLoaded) EditorSceneManager.CloseScene(scene, false);
if (!success) break;
}
}
private static bool Export(ExportBatch batch, bool binary, bool askForLocation)
{
var currentlyOpenScene = SceneManager.GetActiveScene().path;
if (batch.sceneAssets != null)
{
if (batch.sceneAssets.Length == 0)
{
// Successful export of empty batch
return true;
}
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
var first = batch.sceneAssets.First();
EditorSceneManager.OpenScene(AssetDatabase.GetAssetPath(first), OpenSceneMode.Single);
foreach (var scene in batch.sceneAssets)
{
if (scene == first) continue;
EditorSceneManager.OpenScene(AssetDatabase.GetAssetPath(scene), OpenSceneMode.Additive);
}
var sceneRoots = new List();
for (var i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!scene.IsValid()) continue;
sceneRoots.AddRange(Array.ConvertAll(scene.GetRootGameObjects(), gameObject => gameObject.transform));
}
batch.rootTransforms = sceneRoots.ToArray();
}
}
var exportOptions = new ExportContext(settings) { TexturePathRetriever = RetrieveTexturePath };
var haveOverrides = batch.rootTransformOverride != null && batch.rootTransformOverride.Length > 0 && batch.rootTransforms != null;
var originalTransforms = batch.rootTransforms?.Select(x => (x.localPosition, x.localRotation, x.localScale)).ToArray();
if (haveOverrides)
{
var overrideLength = batch.rootTransformOverride.Length;
for (int i = 0; i < batch.rootTransforms.Length; i++)
{
var overrideData = batch.rootTransformOverride[i % overrideLength];
if (!overrideData.HasValue) continue;
batch.rootTransforms[i].localPosition = overrideData.Value.position;
batch.rootTransforms[i].localRotation = overrideData.Value.rotation;
batch.rootTransforms[i].localScale = overrideData.Value.scale;
}
}
var exporter = new GLTFSceneExporter(batch.rootTransforms, exportOptions);
if (batch.rootResources != null)
{
exportOptions.AfterSceneExport += (sceneExporter, _) =>
{
foreach (var resource in batch.rootResources)
{
if (resource is Material material)
sceneExporter.ExportMaterial(material);
if (resource is Texture2D texture)
sceneExporter.ExportTexture(texture, "unknown");
if (resource is Mesh mesh)
sceneExporter.ExportMesh(mesh);
}
};
}
var invokedByShortcut = Event.current?.type == EventType.KeyDown;
var path = settings.SaveFolderPath;
if ((askForLocation && !invokedByShortcut) || !Directory.Exists(path))
path = EditorUtility.SaveFolderPanel("glTF Export Path", settings.SaveFolderPath, "");
var havePath = !string.IsNullOrEmpty(path);
if (havePath)
{
var sceneName = batch.sceneName;
var ext = binary ? ".glb" : ".gltf";
var resultFile = GLTFSceneExporter.GetFileName(path, sceneName, ext);
settings.SaveFolderPath = path;
if (binary)
exporter.SaveGLB(path, sceneName);
else
exporter.SaveGLTFandBin(path, sceneName);
Debug.Log("Exported to " + resultFile);
EditorUtility.RevealInFinder(resultFile);
}
if (haveOverrides)
{
for (var i = 0; i < batch.rootTransforms.Length; i++)
{
var (position, rotation, scale) = originalTransforms[i];
batch.rootTransforms[i].localPosition = position;
batch.rootTransforms[i].localRotation = rotation;
batch.rootTransforms[i].localScale = scale;
}
}
if (currentlyOpenScene != SceneManager.GetActiveScene().path)
EditorSceneManager.OpenScene(currentlyOpenScene, OpenSceneMode.Single);
return havePath;
}
const string SettingsMenu = "Open Export Settings";
[MenuItem(MenuPrefixGameObject + SettingsMenu, true, 3000)]
private static bool ShowSettingsValidate()
{
return TryGetExportBatchesFromSelection(out _, false);
}
[InitializeOnLoadMethod]
private static void Hooks()
{
SceneHierarchyHooks.addItemsToSceneHeaderContextMenu += (menu, scene) =>
{
menu.AddItem(new GUIContent("UnityGLTF/Export selected scene"), false, () => ExportScene(scene));
if (SceneManager.sceneCount > 1)
{
menu.AddItem(new GUIContent("UnityGLTF/Export each scene as separate asset"), false, ExportAllScenesBatch);
menu.AddItem(new GUIContent("UnityGLTF/Export all scenes as one asset"), false, ExportAllScenes);
}
};
SceneHierarchyHooks.addItemsToGameObjectContextMenu += (menu, gameObject) =>
{
if (gameObject)
{
var current = settings.EditorExportFileFormat == GLTFSettings.ExportFileFormat.Glb;
menu.AddItem(new GUIContent("UnityGLTF/Export as binary (GLB)"), current, () =>
{
current = !current;
settings.EditorExportFileFormat = current ? GLTFSettings.ExportFileFormat.Glb : GLTFSettings.ExportFileFormat.Gltf;
});
}
else
{
menu.AddItem(new GUIContent("UnityGLTF/Export active scene"), false, ExportScene);
if (SceneManager.sceneCount > 1)
{
menu.AddItem(new GUIContent("UnityGLTF/Export each scene as separate asset"), false, ExportAllScenesBatch);
menu.AddItem(new GUIContent("UnityGLTF/Export all scenes as one asset"), false, ExportAllScenes);
}
}
};
}
[MenuItem(MenuPrefix + SettingsMenu, false, 3000)]
[MenuItem(MenuPrefixGameObject + SettingsMenu, false, 3000)]
private static void ShowSettings()
{
SettingsService.OpenProjectSettings("Project/UnityGLTF");
}
const string ExportAsBinary = MenuPrefix + "Export as binary (GLB)";
[MenuItem(ExportAsBinary, false, 3001)]
private static void ToggleExportAsGltf()
{
var current = settings.EditorExportFileFormat == GLTFSettings.ExportFileFormat.Glb;
current = !current;
settings.EditorExportFileFormat = current ? GLTFSettings.ExportFileFormat.Glb : GLTFSettings.ExportFileFormat.Gltf;
Menu.SetChecked(ExportAsBinary, current);
}
}
internal static class GLTFCreateMenu
{
[MenuItem("Assets/Create/UnityGLTF/Material", false)]
private static void CreateNewAsset()
{
var filename = "glTF Material Library.gltf";
var content = @"{
""asset"": {
""generator"": ""UnityGLTF"",
""version"": ""2.0""
},
""materials"": [
{
""name"": ""Material"",
""pbrMetallicRoughness"": {
""metallicFactor"": 0.0
}
}
]
}";
var importAction = ScriptableObject.CreateInstance();
importAction.fileContent = content;
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, importAction, filename, null, (string) null);
}
// Based on DoCreateAssetWithContent.cs
private class AdjustImporterAction : EndNameEditAction
{
public string fileContent;
public override void Action(int instanceId, string pathName, string resourceFile)
{
var templateContent = SetLineEndings(fileContent, EditorSettings.lineEndingsForNewScripts);
File.WriteAllText(Path.GetFullPath(pathName), templateContent);
AssetDatabase.ImportAsset(pathName);
// This is why we're not using ProjectWindowUtil.CreateAssetWithContent directly:
// We want glTF materials created with UnityGLTF to also use UnityGLTF for importing.
AssetDatabase.SetImporterOverride(pathName);
var asset = AssetDatabase.LoadAssetAtPath(pathName, typeof (UnityEngine.Object));
ProjectWindowUtil.ShowCreatedAsset(asset);
}
}
// Unmodified from ProjectWindowUtil.cs:SetLineEndings (internal)
private static string SetLineEndings(string content, LineEndingsMode lineEndingsMode)
{
string replacement;
switch (lineEndingsMode)
{
case LineEndingsMode.OSNative:
replacement = Application.platform != RuntimePlatform.WindowsEditor ? "\n" : "\r\n";
break;
case LineEndingsMode.Unix:
replacement = "\n";
break;
case LineEndingsMode.Windows:
replacement = "\r\n";
break;
default:
replacement = "\n";
break;
}
content = Regex.Replace(content, "\\r\\n?|\\n", replacement);
return content;
}
}
}