From 3bed3877b3d4c9c968fbd9315b3a7321e5beac9c Mon Sep 17 00:00:00 2001 From: Vicky Clark Date: Fri, 21 Apr 2023 15:59:36 +0100 Subject: [PATCH] First pass at adding relative path to UsdAsset. When Unity projects are moved or shared, the full path that is serialized for an imported asset causes lots of errors. This change attempts to, where possible, save only the relative path inside the Unity project folder, but still fallback to the full path when it's not in the project folder. We then also catch cases more cleanly when they don't exist at the stated path. --- .../Scripts/Behaviors/UsdAssetEditor.cs | 13 +- .../Scripts/Behaviors/UsdLayerStackEditor.cs | 7 + .../Scripts/Behaviors/UsdPrimSourceEditor.cs | 2 +- .../Runtime/Scripts/Behaviors/UsdAsset.cs | 124 +++++++++++++++++- .../Scripts/Behaviors/UsdLayerStack.cs | 8 +- 5 files changed, 143 insertions(+), 11 deletions(-) diff --git a/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdAssetEditor.cs b/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdAssetEditor.cs index 7d1ada3c7..b1d71dd62 100644 --- a/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdAssetEditor.cs +++ b/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdAssetEditor.cs @@ -229,7 +229,7 @@ private void DrawSimpleInspector(UsdAsset usdAsset) { string lastDir; if (string.IsNullOrEmpty(usdAsset.usdFullPath)) - lastDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); + lastDir = Application.dataPath; else lastDir = Path.GetDirectoryName(usdAsset.usdFullPath); string importFilepath = @@ -343,8 +343,15 @@ private void ReloadFromUsdAsCoroutine(UsdAsset stageRoot) stageRoot.StateToOptions(ref options); var parent = stageRoot.gameObject.transform.parent; var root = parent ? parent.gameObject : null; - stageRoot.ImportUsdAsCoroutine(root, stageRoot.usdFullPath, stageRoot.m_usdTimeOffset, options, - targetFrameMilliseconds: 5); + if (stageRoot.IsAssetPathValid()) + { + stageRoot.ImportUsdAsCoroutine(root, stageRoot.usdFullPath, stageRoot.m_usdTimeOffset, options, + targetFrameMilliseconds: 5); + } + else + { + Debug.LogWarning($"USD Asset path for `{stageRoot.name}` is invalid. Check that the path <{stageRoot.usdFullPath}> exists."); + } } } } diff --git a/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdLayerStackEditor.cs b/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdLayerStackEditor.cs index f5b70c279..4fbf901ba 100644 --- a/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdLayerStackEditor.cs +++ b/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdLayerStackEditor.cs @@ -43,6 +43,13 @@ public override void OnInspectorGUI() if (GUILayout.Button("Save Layer Stack")) { InitUsd.Initialize(); + var usdAsset = layerStack.GetComponent(); + if (!usdAsset.IsAssetPathValid()) + { + Debug.LogWarning($"USD Asset path for `{usdAsset.name}` is invalid."); + return; + } + Scene scene = Scene.Open(layerStack.GetComponent().usdFullPath); try { diff --git a/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdPrimSourceEditor.cs b/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdPrimSourceEditor.cs index 25c35d730..656da2aaf 100644 --- a/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdPrimSourceEditor.cs +++ b/package/com.unity.formats.usd/Editor/Scripts/Behaviors/UsdPrimSourceEditor.cs @@ -43,7 +43,7 @@ public override void OnInspectorGUI() } var scene = stageRoot.GetScene(); - if (scene == null) + if (scene == null || !stageRoot.IsAssetPathValid()) { Debug.LogError("Invalid scene: " + stageRoot.usdFullPath); return; diff --git a/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdAsset.cs b/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdAsset.cs index 5f3a9d4bf..3dbab2512 100644 --- a/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdAsset.cs +++ b/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdAsset.cs @@ -29,6 +29,13 @@ namespace Unity.Formats.USD [ExecuteInEditMode] public class UsdAsset : MonoBehaviour { + enum USDPathType + { + Unknown, + External, + Relative + } + /// /// The length of the USD playback time in seconds. /// @@ -39,24 +46,65 @@ public double Length /// /// The absolute file path to the USD file from which this asset was created. This path may - /// point to a location outside of the Unity project and may be any file type supported by - /// USD (e.g. usd, usda, usdc, abc, ...). Setting this path will not trigger the asset to be - /// reimported, Reload must be called explicitly. + /// be any file type supported by USD (e.g. usd, usda, usdc, abc, ...). Setting this path + /// will not trigger the asset to be reimported, Reload must be called explicitly. + /// While files external to the project folder are supported, they are not recommended as the + /// project will have errors when shared or moved. /// public string usdFullPath { - get { return string.IsNullOrEmpty(m_usdFile) ? string.Empty : (Path.GetFullPath(m_usdFile)); } - set { m_usdFile = value; } + get + { + switch (m_usdFilePathType) + { + case USDPathType.Relative: + // if we already have a relative path, return it + return Path.GetFullPath(m_relativeUSDPath); + case USDPathType.External: + // if we know the path is external, try to use it + return string.IsNullOrEmpty(m_usdFile) ? string.Empty : m_usdFile; + case USDPathType.Unknown: + // if we haven't inspected the path yet, do it now + AttemptUSDPathFixup(); + if (!string.IsNullOrEmpty(m_relativeUSDPath)) + return Path.GetFullPath(m_relativeUSDPath); + else + return string.IsNullOrEmpty(m_usdFile) ? string.Empty : m_usdFile; + default: + return string.Empty; + } + } + set + { + m_relativeUSDPath = DetermineRelativePathFromFullPath(value); + m_usdFilePathType = USDPathType.Relative; + + if (string.IsNullOrEmpty(m_relativeUSDPath)) + { + Debug.Log("USD file path provided is not within the Project folder. You may encounter errors if this project is later shared."); + m_relativeUSDPath = string.Empty; + m_usdFile = Path.IsPathRooted(value) ? value : Path.GetFullPath(value); + m_usdFilePathType = USDPathType.External; + } + } } // ----------------------------------------------------------------------------------------- // // Source Asset. // ----------------------------------------------------------------------------------------- // + // Wherever possible, use m_relativeUSDPath instead [Header("Source Asset")] [SerializeField] string m_usdFile; + [SerializeField] + string m_relativeUSDPath; + + // do not serialize so that we can try to fix paths on domain reload + [NonSerialized] + USDPathType m_usdFilePathType = USDPathType.Unknown; + [HideInInspector] [Tooltip("The Unity project path into which imported files (such as textures) will be placed.")] public string m_projectAssetPath = "Assets/"; @@ -209,6 +257,69 @@ private GameObject GetPrefabObject(GameObject root) } #endif + public bool IsAssetPathValid() + { + if (string.IsNullOrEmpty(usdFullPath)) + return false; + if (!File.Exists(usdFullPath)) + return false; + return true; + } + + /// + /// Converts inputPath to a Relative Path, if the inputPath is inside the Project folder. If the path is not inside the project folder, returns empty string. + /// + /// + /// The path to determine a path relative to the project folder from. We assume the inputPath is a full path, else this will fail. + /// + private string DetermineRelativePathFromFullPath(string inputPath) + { + string pathLower = inputPath.ToLower().Replace('\\', '/'); + string projectPath = System.IO.Directory.GetCurrentDirectory().ToLower().Replace('\\', '/'); // VRC: This might only be correct in editor- check this + + if (pathLower.StartsWith(projectPath)) + { + return inputPath.Substring(projectPath.Length + 1); // plus 1 for the trailing seperator + } + + return string.Empty; + } + + + /// + /// Attempt to fix an m_usdPath, which could previously be relative or external, to a relative path. + /// If the path is not inside the project folder, leave it. + /// + /// + /// This method does *not* determine whether a path exists or not. It is down to the user to make sure the path exists, + /// else it will error later. + /// + private void AttemptUSDPathFixup() + { + if (!string.IsNullOrEmpty(m_relativeUSDPath)) + { + m_usdFilePathType = USDPathType.Relative; + return; + } + else if (string.IsNullOrEmpty(m_usdFile)) + { + return; + } + + // set the m_usdFile var to contain full path in case it is external, and clear it later if it's not. + m_usdFile = Path.IsPathRooted(m_usdFile) ? m_usdFile : Path.GetFullPath(m_usdFile); + m_relativeUSDPath = DetermineRelativePathFromFullPath(m_usdFile); + + if (!string.IsNullOrEmpty(m_relativeUSDPath)) + { + m_usdFile = string.Empty; + m_usdFilePathType = USDPathType.Relative; + } + else + { + m_usdFilePathType = USDPathType.External; + } + } private void OnDestroy() { @@ -370,7 +481,7 @@ public Scene GetScene() if (m_lastScene?.Stage == null || SceneFileChanged()) { pxr.UsdStage stage = null; - if (string.IsNullOrEmpty(usdFullPath)) + if (!IsAssetPathValid()) { return null; } @@ -776,6 +887,7 @@ public void ImportUsdAsCoroutine(GameObject goRoot, SceneImportOptions importOptions, float targetFrameMilliseconds) { + // TODO: this would be much cleaner if we just used the path and time member variables, but that would be a breaking API change InitUsd.Initialize(); var scene = Scene.Open(usdFilePath); if (scene == null) diff --git a/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdLayerStack.cs b/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdLayerStack.cs index 1b6f13058..d9367fe5c 100644 --- a/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdLayerStack.cs +++ b/package/com.unity.formats.usd/Runtime/Scripts/Behaviors/UsdLayerStack.cs @@ -89,10 +89,16 @@ public void SaveToLayer() throw new NullReferenceException("Could not create layer: " + m_targetLayer); } + if (!stageRoot.IsAssetPathValid()) + { + Debug.LogWarning($"Asset path for {stageRoot.name} invalid."); + return; + } + Scene rootScene = Scene.Open(stageRoot.usdFullPath); if (rootScene == null) { - throw new NullReferenceException("Could not open base layer: " + stageRoot.usdFullPath); + throw new NullReferenceException($"Could not open base layer: <{stageRoot.usdFullPath}>"); } SetupNewSubLayer(rootScene, subLayerScene);