using System; using System.IO; using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Threading.Tasks; using static BrainFailProductions.PolyFewRuntime.PolyfewRuntime; #if UNITY_EDITOR using UnityEditor; #endif namespace BrainFailProductions.PolyFew.AsImpL { /// /// Component that imports objects from a model, both at run-rime and as assets in Editor. /// /// public class ObjectImporter : MonoBehaviour { public static ReferencedNumeric downloadProgress; public static int activeDownloads; #pragma warning disable private static float objDownloadProgress; #pragma warning disable private static float textureDownloadProgress; private static float materialDownloadProgress; public static bool isException; public ObjectImporter() { isException = false; downloadProgress = new ReferencedNumeric(0); objDownloadProgress = 0; textureDownloadProgress = 0; materialDownloadProgress = 0; activeDownloads = 6; } #if UNITY_EDITOR /// /// Import the model as a set of assets. This should be set only by the Editor window. /// [HideInInspector] public bool importAssets = false; /// /// Set the import path for assets. This should be set only by the Editor window. /// [HideInInspector] public string importAssetPath = "_ImportedOBJ"; #endif protected int numTotalImports = 0; protected bool allLoaded = false; protected ImportOptions buildOptions; protected List loaderList; #if UNITY_EDITOR // raw subdivision in percentages of the import phases (empirically computed importing a large sample OBJ file) // TODO: refine or change this method private static float TEX_PHASE_PERC = 13f; private static float OBJ_PHASE_PERC = 76f; //private static float ASSET_PHASE_PERC = 11f; private string importMessage = string.Empty; #endif #pragma warning disable private ImportPhase importPhase = ImportPhase.Idle; /// /// Event triggered when starting to import. /// public event Action ImportingStart; /// /// Event triggered when finished importing. /// public event Action ImportingComplete; /// /// Event triggered when a single model has been created and before it is imported. /// public event Action CreatedModel; /// /// Event triggered when a single model has been successfully imported. /// public event Action ImportedModel; /// /// Event triggered when an error occurred importing a model. /// public event Action ImportError; private enum ImportPhase { Idle, TextureImport, ObjLoad, AssetBuild, Done } /// /// Number of pending import activities. /// public int NumImportRequests { get { return numTotalImports; } } #if UNITY_EDITOR public bool AllImported { get { return importAssets ? importPhase == ImportPhase.Done : allLoaded; } } /// /// Import progress percentage (0..100) /// public float ImportProgress { get { if (Loader.totalProgress.singleProgress.Count > 0) { if (importAssets) { switch (importPhase) { case ImportPhase.TextureImport: return 0f; case ImportPhase.ObjLoad: return TEX_PHASE_PERC + (OBJ_PHASE_PERC / 100f) * Loader.totalProgress.singleProgress[0].percentage; case ImportPhase.AssetBuild: return TEX_PHASE_PERC + OBJ_PHASE_PERC; case ImportPhase.Done: return 100f; } } else { return Loader.totalProgress.singleProgress[0].percentage; } } return 0f; } } /// /// Message updated while importing of objects /// public string ImportMessage { get { if (Loader.totalProgress.singleProgress.Count > 0) { if (importAssets) { switch (importPhase) { case ImportPhase.Idle: return string.Empty; case ImportPhase.ObjLoad: return Loader.totalProgress.singleProgress[0].message; case ImportPhase.Done: return "Finalizing...."; default: return importMessage; } } else { return Loader.totalProgress.singleProgress[0].message; } } return string.Empty; } } /// /// Start importing a new object /// /// /// Absolute path of the file to import /// Transform to which attach the new object (it can be null) /// Import options public void ImportFile(string absolutePath, Transform parentObject, ImportOptions options) { buildOptions = options; #pragma warning disable ImportFileAsync(absolutePath, parentObject); } /// /// Called as a coroutine by ImportFile() /// /// /// /// TODO: refactor this method, it is too long. private async Task ImportFileAsync(string absolutePath, Transform parentObject, string texturesFolderPath = "", string materialsFolderPath = "") { Loader loader = CreateLoader(absolutePath); if (loader == null) { return; } loader.buildOptions = buildOptions; Debug.Log("Loading: " + absolutePath); float startTotTime = Time.realtimeSinceStartup; float startTime = Time.realtimeSinceStartup; importPhase = ImportPhase.TextureImport; string dirName = Path.GetDirectoryName(absolutePath); string sourceBasePath = string.IsNullOrEmpty(dirName) ? "" : dirName; if (!sourceBasePath.EndsWith("/")) { sourceBasePath += "/"; } string newName = Path.GetFileNameWithoutExtension(absolutePath);//fileInfo.Name.Substring(0, fileInfo.Name.Length - 4); if (importAssets) { Debug.LogFormat("Importing assets from {0}...", absolutePath); importMessage = "Creating folders..."; FileInfo fileInfo = new FileInfo(absolutePath); string fileName = fileInfo.Name; if (!string.IsNullOrEmpty(importAssetPath)) { EditorUtil.CreateAssetFolder("Assets", importAssetPath); EditorUtil.CreateAssetFolder("Assets/" + importAssetPath, fileName); } else { EditorUtil.CreateAssetFolder("Assets", fileName); } string prefabRelPath = (!string.IsNullOrEmpty(importAssetPath)) ? importAssetPath + "/" + fileName : fileName; string prefabPath = "Assets/" + prefabRelPath; string prefabName = prefabPath + "/" + fileName.Replace('.', '_') + ".prefab"; string[] texturePaths = loader.ParseTexturePaths(absolutePath); EditorUtil.CreateAssetFolder(prefabPath, "Textures"); EditorUtil.CreateAssetFolder(prefabPath, "Materials"); string destBasePath = Application.dataPath + "/../" + prefabPath; foreach (string texPath in texturePaths) { string source = texPath; if (!Path.IsPathRooted(source)) { source = sourceBasePath + texPath; } FileInfo texFileInfo = new FileInfo(source); string dest = destBasePath + "/Textures/" + texFileInfo.Name; importMessage = "Copying texture " + source + "..."; File.Copy(source, dest, true); Debug.LogFormat("Texture {0} copied to {1}", source, dest); } AssetDatabase.Refresh(); AssetDatabase.StartAssetEditing(); foreach (string texPath in texturePaths) { FileInfo textFileInfo = new FileInfo(texPath); string texAssetPath = prefabPath + "/Textures/" + textFileInfo.Name; importMessage = "Importing texture " + texAssetPath + "..."; EditorUtil.SetTextureReadable(texAssetPath); } AssetDatabase.SaveAssets(); AssetDatabase.StopAssetEditing(); //File.Copy(basePath+mtlLibName, destBasePath+mtlLibName, true); //File.Copy(absolutePath, destBasePath+fileName, true); importMessage = "Updating assets..."; AssetDatabase.Refresh(); Debug.LogFormat("Texture files imported in {0} seconds", Time.realtimeSinceStartup - startTime); startTime = Time.realtimeSinceStartup; importMessage = "Loading OBJ file..."; importPhase = ImportPhase.ObjLoad; loader.altTexPath = prefabPath + "/Textures/"; loader.buildOptions = buildOptions; await loader.Load(newName, absolutePath, parentObject, texturesFolderPath, materialsFolderPath); importMessage = "Saving assets..."; AssetDatabase.SaveAssets(); importMessage = "Refreshing assets..."; AssetDatabase.Refresh(); Debug.LogFormat("OBJ files loaded in {0} seconds", Time.realtimeSinceStartup - startTime); startTime = Time.realtimeSinceStartup; GameObject loadedObj = Loader.GetModelByPath(absolutePath); importMessage = "Creating mesh assets..."; importPhase = ImportPhase.AssetBuild; // TODO: check if the prefab already exists MeshFilter[] meshFilters = loadedObj.GetComponentsInChildren(); foreach (var filter in meshFilters) { Mesh mesh = filter.sharedMesh; if (!AssetDatabase.Contains(mesh)) { EditorUtil.CreateAssetFolder(prefabPath, "Meshes"); AssetDatabase.CreateAsset(mesh, prefabPath + "/Meshes/" + mesh.name + ".asset"); } } importMessage = "Creating material assets..."; AssetDatabase.StartAssetEditing(); MeshRenderer[] meshRend = loadedObj.GetComponentsInChildren(); foreach (var rend in meshRend) { Material mtl = rend.sharedMaterial; if (!AssetDatabase.Contains(mtl)) { string mtlAssetPath = prefabPath + "/Materials/" + mtl.name + ".mat"; AssetDatabase.CreateAsset(mtl, mtlAssetPath); } } importMessage = "Saving assets..."; AssetDatabase.SaveAssets(); AssetDatabase.StopAssetEditing(); importMessage = "Updating assets..."; AssetDatabase.Refresh(); importMessage = "Creating prefab..."; #if UNITY_2018_3_OR_NEWER PrefabUtility.SaveAsPrefabAssetAndConnect(loadedObj, prefabName, InteractionMode.AutomatedAction); #else PrefabUtility.CreatePrefab(prefabName, loadedObj, ReplacePrefabOptions.ConnectToPrefab); #endif //GameObject. objObject.GetComponent(); Debug.LogFormat("Assets created in {0} seconds", Time.realtimeSinceStartup - startTime); importPhase = ImportPhase.Done; } else { importPhase = ImportPhase.ObjLoad; await loader.Load(newName, absolutePath, parentObject); } Debug.LogFormat("OBJ files imported in {0} seconds", Time.realtimeSinceStartup - startTotTime); } #endif /// /// Create the proper loader component according to the file extension. /// /// path of the model to be imported /// A proper loader or null if not available. private Loader CreateLoader(string absolutePath, bool isNetwork = false) { if(isNetwork) { Loader laoder = gameObject.AddComponent(); laoder.ModelCreated += OnModelCreated; laoder.ModelLoaded += OnImported; laoder.ModelError += OnImportError; return laoder; } string ext = Path.GetExtension(absolutePath); if (string.IsNullOrEmpty(ext)) { throw new System.InvalidOperationException("No extension defined, unable to detect file format. Please provide a full path to the file that ends with the file name including its extension."); //return null; } Loader loader = null; ext = ext.ToLower(); if (ext.StartsWith(".php")) { if (!ext.EndsWith(".obj")) { // TODO: other formats supported? Remark: often there are zip and rar archives without extension. throw new System.InvalidOperationException("Unable to detect file format in " + ext); //return null; } loader = gameObject.AddComponent(); } else { switch (ext) { case ".obj": loader = gameObject.AddComponent(); break; // TODO: add mode formats here... default: throw new System.InvalidOperationException($"File format not supported ({ext})"); //Debug.LogErrorFormat("File format not supported ({0})", ext); //return null; } } loader.ModelCreated += OnModelCreated; loader.ModelLoaded += OnImported; loader.ModelError += OnImportError; return loader; } /// /// Request to load a file asynchronously. /// /// /// /// /// public async Task ImportModelAsync(string objName, string filePath, Transform parentObj, ImportOptions options, string texturesFolderPath = "", string materialsFolderPath = "") { if (loaderList == null) { loaderList = new List(); } if (loaderList.Count == 0) { numTotalImports = 0;// files.Length; ImportingStart?.Invoke(); } string absolutePath = filePath.Contains("//") ? filePath : Path.GetFullPath(filePath); absolutePath = absolutePath.Replace('\\', '/'); Loader loader = CreateLoader(absolutePath); if (loader == null) { throw new SystemException("Failed to import obj."); //return null; } numTotalImports++; loaderList.Add(loader); loader.buildOptions = options; if (string.IsNullOrEmpty(objName)) { objName = Path.GetFileNameWithoutExtension(absolutePath); } allLoaded = false; GameObject loaded = await loader.Load(objName, absolutePath, parentObj, texturesFolderPath, materialsFolderPath); return loaded; } public async Task ImportModelFromNetwork(string objURL, string objName, string diffuseTexURL, string bumpTexURL, string specularTexURL, string opacityTexURL, string materialURL, ReferencedNumeric downloadProgress, ImportOptions options) { if (loaderList == null) { loaderList = new List(); } if (loaderList.Count == 0) { numTotalImports = 0;// files.Length; ImportingStart?.Invoke(); } Loader loader = CreateLoader("", true); if (loader == null) { throw new SystemException("Failed to import obj."); //return null; } numTotalImports++; loaderList.Add(loader); loader.buildOptions = options; allLoaded = false; if(string.IsNullOrWhiteSpace(objName)) { objName = ""; } ObjectImporter.downloadProgress = downloadProgress; //string objURL, string objName, string textureURL, string materialURL, ReferencedNumeric< float > downloadProgress, ImportOptions options GameObject loaded; try { loaded = await loader.LoadFromNetwork(objURL, diffuseTexURL, bumpTexURL, specularTexURL, opacityTexURL, materialURL, objName); } catch (Exception ex) { throw ex; } return loaded; } public void ImportModelFromNetworkWebGL(string objURL, string objName, string diffuseTexURL, string bumpTexURL, string specularTexURL, string opacityTexURL, string materialURL, ReferencedNumeric downloadProgress, ImportOptions options, Action OnSuccess, Action OnError) { if (loaderList == null) { loaderList = new List(); } if (loaderList.Count == 0) { numTotalImports = 0;// files.Length; ImportingStart?.Invoke(); } Loader loader = CreateLoader("", true); if (loader == null) { OnError(new SystemException("Loader initialization failed due to unknown reasons.")); } numTotalImports++; loaderList.Add(loader); loader.buildOptions = options; allLoaded = false; if (string.IsNullOrWhiteSpace(objName)) { objName = ""; } ObjectImporter.downloadProgress = downloadProgress; StartCoroutine(loader.LoadFromNetworkWebGL(objURL, diffuseTexURL, bumpTexURL, specularTexURL, opacityTexURL, materialURL, objName, OnSuccess, OnError)); } /// /// Update the loading/importing status /// public virtual void UpdateStatus() { if (allLoaded) return; int num_loaded_files = numTotalImports - Loader.totalProgress.singleProgress.Count; bool loading = num_loaded_files < numTotalImports; if (!loading) { allLoaded = true; if (loaderList != null) { foreach (var loader in loaderList) { Destroy(loader); } loaderList.Clear(); } OnImportingComplete(); } } protected virtual void Update() { UpdateStatus(); } /// /// Called when finished importing. It triggers ImportingComplete event, if it was set. /// protected virtual void OnImportingComplete() { if (ImportingComplete != null) { ImportingComplete(); } } /// /// Called when each model has been created and before it is imported. It triggers CreatedModel event, if it was set. /// protected virtual void OnModelCreated(GameObject obj, string absolutePath) { if (CreatedModel != null) { CreatedModel(obj, absolutePath); } } /// /// Called when each model has been imported. It triggers ImportedModel event, if it was set. /// protected virtual void OnImported(GameObject obj, string absolutePath) { if (ImportedModel != null) { ImportedModel(obj, absolutePath); } } /// /// Called when a model import fails. It triggers ImportError event, if it was set. /// protected virtual void OnImportError(string absolutePath) { if (ImportError != null) { ImportError(absolutePath); } } } }