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);