Fix Export Transform Overrides (#USDU-216) (#279)

* Fix the XForm override export and add the option to the recorder

* Remove sublayers, remove ref from root, ass tests

* Rename exportXformOverride to exportTransformOverride

* Update changelog

* Fix UI Labels to workaround layout issues

* Fixes from PR review.

- Fix for null ref exception when the source GO is not a UsdAsset
- Fix for unclear UI

* XFormable schema type should not override the actual schema type.

* Removes the reference to the original file from exported override files written out by Recorder.

* Switch transform export back to Xform from Xformable, because using base UsdGeomXformable prim types is unusual

---------

Co-authored-by: Vicky Clark <vickycl@unity3d.com>
Co-authored-by: Andrew Lee <119626628+lee-aandrew@users.noreply.github.com>
Co-authored-by: Vicky Clark <45685026+vickycl@users.noreply.github.com>
This commit is contained in:
Julien Dubuisson 2023-04-13 05:05:36 -04:00 коммит произвёл GitHub
Родитель 5f70448108
Коммит fd48735070
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 253 добавлений и 88 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -72,4 +72,5 @@ TestProject/**/Logs
TestProject/**/*.csproj
TestProject/**/*.sln
TestProject/Usd-Development/Library
TestProject/Usd-Development/.vs
build_usdcs

Просмотреть файл

@ -124,6 +124,34 @@ When compatibility with runtime is required (i.e for a standalone build), the re
> **Note:** This feature has no dependency to and is not based on the Recorder package.
### Exporting Transform Overrides
Modifications to transforms in USD files can be exported as overrides to the original files.
Overrides can be exported from the USD menu with the GameObject containing the UsdAsset component selected, which will export transform overrides for the entire hierarchy. Alternatively, you can also export just the overrides when exporting from the Recorder window by changing the 'Override Setting' to 'Export Transform Overrides Only'.
For full asset pipeline flexibility these override files do not include a reference to the original file, but this can be added by manually adding a sublayer in the header of the resulting USDA file:
```
#usda 1.0
(
defaultPrim = "myCube"
endTimeCode = 0
startTimeCode = 0
upAxis = "Y"
subLayers = [
@c:/path/to/original/myCube.usda@
]
)
over "myCube"
{
...
}
```
Note: Modifications to the transform of the Root GameObject is not currently reflected in the override, as Unity assumes the root in all USD files is at the origin.
# License
The USD Unity SDK is licensed under the terms of the Apache

Просмотреть файл

@ -1,7 +1,11 @@
# Changes in usd-unitysdk for Unity
# Changes in usd-unity-sdk for Unity
## Unreleased
### Features
- The USD Recorder now has an option to export transform overrides.
### Bug Fixes
- "Export Transform Override" now properly exports modified transforms only.
- Fixed a bug where importing materials exported from USD version >= 21.11 would fail.
- Fixed loading of meshes with arbitrary primvars.
- Fixed regression in animated mesh properties.
@ -17,7 +21,7 @@
- Fixed an import bug causing instanced primitives not to be sanitized to fit Unity formats, including converting basis, triangulating and unwinding meshes, and unrolling primvars.
### Changed
- GC allocs reduced by half for Scene.GetAttributeAtPath and Scene.GetRelationshipAtPath
- GC allocs reduced by half for Scene.GetAttributeAtPath and Scene.GetRelationshipAtPath.
- Disabled plugins on unsupported platforms.
- Optimized triangulation to reduce the chance of a "Timed out while waiting for thread" error on importing a complex mesh.
- Optimized PointInstance importing.

Просмотреть файл

@ -1,5 +1,6 @@
#if RECORDER_AVAILABLE
using UnityEditor.Recorder;
using UnityEngine;
namespace UnityEditor.Formats.USD.Recorder
{
@ -12,7 +13,8 @@ namespace UnityEditor.Formats.USD.Recorder
EditorGUILayout.PropertyField(serializedObject.FindProperty("interpolationType"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("coordinateConversion"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("activePolicy"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("exportMaterials"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("exportTransformOverrides"), new GUIContent("Override Setting"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("exportMaterials"), new GUIContent("Materials"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("scale"));
}
}

Просмотреть файл

@ -70,6 +70,7 @@ namespace UnityEditor.Formats.USD.Recorder
context.basisTransform = Settings.BasisTransformation;
context.activePolicy = Settings.ActivePolicy;
context.exportMaterials = Settings.ExportMaterials;
context.exportTransformOverrides = Settings.ExportTransformOverrides;
context.scale = Settings.Scale;

Просмотреть файл

@ -1,6 +1,7 @@
#if RECORDER_AVAILABLE
using Unity.Formats.USD;
using UnityEditor.Recorder;
using USD.NET;
namespace UnityEditor.Formats.USD.Recorder
{
@ -11,10 +12,28 @@ namespace UnityEditor.Formats.USD.Recorder
protected override void BeginRecording(RecordingSession session)
{
if (Context.exportTransformOverrides)
{
// Settings.
UsdAsset usdAsset = Settings.GameObject.GetComponentInParent<UsdAsset>(); // Get the UsdAsset component in this GameObject or its nearest parent
if (usdAsset != null)
{
Context.scene.AddSubLayer(usdAsset.GetScene());
Context.scene.WriteMode = Scene.WriteModes.Over;
}
else
UnityEngine.Debug.LogError($"Unable to perform a 'transform overrides only' recording as <{Settings.GameObject.name}> is not a UsdAsset.");
}
SceneExporter.SyncExportContext(Settings.GameObject, Context);
SceneExporter.Export(Settings.GameObject,
Context,
zeroRootTransform: false);
if (Context.exportTransformOverrides)
{
// this is very brittle- if we have the chance of other sublayers in future we should store the index it was added at and only erase that one.
Context.scene.Stage.GetRootLayer().GetSubLayerPaths().Erase(0);
}
}
protected override void NewFrameReady(RecordingSession session)

Просмотреть файл

@ -18,6 +18,7 @@ namespace UnityEditor.Formats.USD.Recorder
[SerializeField] ActiveExportPolicy activePolicy = ActiveExportPolicy.ExportAsVisibility;
[SerializeField] bool exportMaterials = true;
[SerializeField] float scale = 1;
[SerializeField] ExportOverridesSetting exportTransformOverrides;
public UsdRecorderSettings()
{
@ -31,6 +32,12 @@ namespace UnityEditor.Formats.USD.Recorder
USDZ,
}
public enum ExportOverridesSetting
{
ExportInFull,
ExportTransformOverridesOnly
}
public UsdInterpolationType InterpolationType
{
get => interpolationType;
@ -49,6 +56,12 @@ namespace UnityEditor.Formats.USD.Recorder
set => exportMaterials = value;
}
public bool ExportTransformOverrides
{
get => exportTransformOverrides == ExportOverridesSetting.ExportTransformOverridesOnly ? true : false;
set => exportTransformOverrides = value ? ExportOverridesSetting.ExportTransformOverridesOnly : ExportOverridesSetting.ExportInFull;
}
public ActiveExportPolicy ActivePolicy
{
get => activePolicy;

Просмотреть файл

@ -82,6 +82,7 @@ namespace UnityEditor.Formats.USD.Recorder
public Unity.Formats.USD.BasisTransformation BasisTransformation { get; set; }
public UnityEditor.Formats.USD.Recorder.UsdRecorderSettings.Format ExportFormat { get; }
public bool ExportMaterials { get; set; }
public bool ExportTransformOverrides { get; set; }
protected virtual string Extension { get; }
public virtual System.Collections.Generic.IEnumerable<UnityEditor.Recorder.RecorderInputSettings> InputsSettings { get; }
public pxr.UsdInterpolationType InterpolationType { get; set; }

Просмотреть файл

@ -600,6 +600,7 @@ namespace Unity.Formats.USD
{
throw new Exception("Could not open base layer: " + sceneToReference.usdFullPath);
}
overs.AddSubLayer(baseLayer);
overs.Time = baseLayer.Time;
overs.StartTime = baseLayer.StartTime;
@ -614,20 +615,20 @@ namespace Unity.Formats.USD
overs,
BasisTransformation.SlowAndSafe,
exportUnvarying: false,
zeroRootTransform: true);
var rel = ImporterBase.MakeRelativePath(overs.FilePath, sceneToReference.usdFullPath);
GetFirstPrim(overs).GetReferences().AddReference(rel, GetFirstPrim(baseLayer).GetPath());
zeroRootTransform: true,
exportOverrides: true);
}
catch (System.Exception ex)
catch (Exception ex)
{
Debug.LogException(ex);
return;
}
finally
{
if (overs != null)
{
// Remove the reference to the original USD from the override file for flexibility in an asset pipeline
// TODO: Make this an optional setting
overs.Stage.GetRootLayer().GetSubLayerPaths().Erase(0);
overs.Save();
overs.Close();
}

Просмотреть файл

@ -25,8 +25,6 @@ namespace Unity.Formats.USD
UnityEngine.Profiling.Profiler.BeginSample("USD: Xform Conversion");
XformSample sample = (XformSample)objContext.sample;
var localRot = objContext.gameObject.transform.localRotation;
var localScale = objContext.gameObject.transform.localScale;
var path = new pxr.SdfPath(objContext.path);
// If exporting for Z-Up, rotate the world.
@ -37,9 +35,32 @@ namespace Unity.Formats.USD
correctZUp,
path.IsRootPrimPath(),
exportContext.basisTransform);
UnityEngine.Profiling.Profiler.EndSample();
// If exporting overrides, only export what changed
if (exportContext.exportTransformOverrides)
{
UnityEngine.Profiling.Profiler.BeginSample("USD: Xform override check");
var sourceSample = new XformSample();
float tolerance = 0.0001f;
exportContext.scene.Read(path, sourceSample);
bool areClose = true;
for (int i = 0; i < 16; i++)
{
if (Mathf.Abs(sample.transform[i] - sourceSample.transform[i]) > tolerance)
{
areClose = false;
break;
}
}
UnityEngine.Profiling.Profiler.EndSample();
if (areClose)
{
return;
}
}
UnityEngine.Profiling.Profiler.BeginSample("USD: Xform Write");
exportContext.scene.Write(objContext.path, sample);
UnityEngine.Profiling.Profiler.EndSample();
@ -55,7 +76,7 @@ namespace Unity.Formats.USD
try
{
foreach (var path in scene.Find<XformableSample>())
foreach (var path in scene.Find<XformSample>())
{
GameObject go;
if (!primMap.TryGetValue(path, out go))

Просмотреть файл

@ -61,6 +61,7 @@ namespace Unity.Formats.USD
public bool exportMaterials = true;
public bool exportNative = false;
public float scale = 1.0f;
public bool exportTransformOverrides = false;
public BasisTransformation basisTransform = BasisTransformation.FastWithNegativeScale;
public ActiveExportPolicy activePolicy = ActiveExportPolicy.ExportAsVisibility;
@ -121,12 +122,14 @@ namespace Unity.Formats.USD
bool exportUnvarying,
bool zeroRootTransform,
bool exportMaterials = false,
bool exportMonoBehaviours = false)
bool exportMonoBehaviours = false,
bool exportOverrides = false)
{
var context = new ExportContext();
context.scene = scene;
context.basisTransform = basisTransform;
context.exportRoot = root.transform.parent;
context.exportTransformOverrides = exportOverrides;
SyncExportContext(root, context);
// Since this is a one-shot convenience function, we will automatically split the export
@ -537,91 +540,99 @@ namespace Unity.Formats.USD
static void InitExportableObjects(GameObject go,
ExportContext context)
{
var smr = go.GetComponent<SkinnedMeshRenderer>();
var mr = go.GetComponent<MeshRenderer>();
var mf = go.GetComponent<MeshFilter>();
var cam = go.GetComponent<Camera>();
Transform expRoot = context.exportRoot;
var tmpPath = new pxr.SdfPath(UnityTypeConverter.GetPath(go.transform, expRoot));
while (!tmpPath.IsRootPrimPath())
if (context.exportTransformOverrides)
{
tmpPath = tmpPath.GetParentPath();
CreateExportPlan(go, CreateSample<XformSample>(context), XformExporter.ExportXform, context);
}
// TODO: What if this path is in use?
string materialBasePath = tmpPath.ToString() + "/Materials/";
// Ensure the "Materials" prim is defined with a valid prim type.
context.scene.Write(materialBasePath.TrimEnd('/'), new ScopeSample());
if (smr != null)
else
{
foreach (var mat in smr.sharedMaterials)
var smr = go.GetComponent<SkinnedMeshRenderer>();
var mr = go.GetComponent<MeshRenderer>();
var mf = go.GetComponent<MeshFilter>();
var cam = go.GetComponent<Camera>();
Transform expRoot = context.exportRoot;
var tmpPath = new pxr.SdfPath(UnityTypeConverter.GetPath(go.transform, expRoot));
while (!tmpPath.IsRootPrimPath())
{
if (!context.matMap.ContainsKey(mat))
{
string usdPath = materialBasePath +
pxr.UsdCs.TfMakeValidIdentifier(
mat.name + "_" + mat.GetInstanceID().ToString());
context.matMap.Add(mat, usdPath);
}
tmpPath = tmpPath.GetParentPath();
}
CreateExportPlan(go, CreateSample<MeshSample>(context), MeshExporter.ExportSkinnedMesh, context);
CreateExportPlan(go, CreateSample<MeshSample>(context), NativeExporter.ExportObject, context,
insertFirst: false);
if (smr.rootBone == null)
// TODO: What if this path is in use?
string materialBasePath = tmpPath.ToString() + "/Materials/";
// Ensure the "Materials" prim is defined with a valid prim type.
context.scene.Write(materialBasePath.TrimEnd('/'), new ScopeSample());
if (smr != null)
{
Debug.LogWarning("No root bone at: " + UnityTypeConverter.GetPath(go.transform, expRoot));
}
else if (smr.bones == null || smr.bones.Length == 0)
{
Debug.LogWarning("No bones at: " + UnityTypeConverter.GetPath(go.transform, expRoot));
}
else
{
// Each mesh in a model may have a different root bone, which now must be merged into a
// single skeleton for export to USD.
try
foreach (var mat in smr.sharedMaterials)
{
MergeBonesSimple(smr.transform, smr.rootBone, smr.bones, smr.sharedMesh.bindposes, context);
}
catch (Exception ex)
{
Debug.LogException(
new Exception("Failed to merge bones for " + UnityTypeConverter.GetPath(smr.transform),
ex));
}
}
}
else if (mf != null && mr != null)
{
foreach (var mat in mr.sharedMaterials)
{
if (mat == null)
{
continue;
if (!context.matMap.ContainsKey(mat))
{
string usdPath = materialBasePath +
pxr.UsdCs.TfMakeValidIdentifier(
mat.name + "_" + mat.GetInstanceID().ToString());
context.matMap.Add(mat, usdPath);
}
}
if (!context.matMap.ContainsKey(mat))
CreateExportPlan(go, CreateSample<MeshSample>(context), MeshExporter.ExportSkinnedMesh, context);
CreateExportPlan(go, CreateSample<MeshSample>(context), NativeExporter.ExportObject, context,
insertFirst: false);
if (smr.rootBone == null)
{
string usdPath = materialBasePath +
pxr.UsdCs.TfMakeValidIdentifier(
mat.name + "_" + mat.GetInstanceID().ToString());
context.matMap.Add(mat, usdPath);
Debug.LogWarning("No root bone at: " + UnityTypeConverter.GetPath(go.transform, expRoot));
}
else if (smr.bones == null || smr.bones.Length == 0)
{
Debug.LogWarning("No bones at: " + UnityTypeConverter.GetPath(go.transform, expRoot));
}
else
{
// Each mesh in a model may have a different root bone, which now must be merged into a
// single skeleton for export to USD.
try
{
MergeBonesSimple(smr.transform, smr.rootBone, smr.bones, smr.sharedMesh.bindposes, context);
}
catch (Exception ex)
{
Debug.LogException(
new Exception("Failed to merge bones for " + UnityTypeConverter.GetPath(smr.transform),
ex));
}
}
}
else if (mf != null && mr != null)
{
foreach (var mat in mr.sharedMaterials)
{
if (mat == null)
{
continue;
}
CreateExportPlan(go, CreateSample<MeshSample>(context), MeshExporter.ExportMesh, context);
CreateExportPlan(go, CreateSample<MeshSample>(context), NativeExporter.ExportObject, context,
insertFirst: false);
}
else if (cam)
{
CreateExportPlan(go, CreateSample<CameraSample>(context), CameraExporter.ExportCamera, context);
CreateExportPlan(go, CreateSample<CameraSample>(context), NativeExporter.ExportObject, context,
insertFirst: false);
if (!context.matMap.ContainsKey(mat))
{
string usdPath = materialBasePath +
pxr.UsdCs.TfMakeValidIdentifier(
mat.name + "_" + mat.GetInstanceID().ToString());
context.matMap.Add(mat, usdPath);
}
}
CreateExportPlan(go, CreateSample<MeshSample>(context), MeshExporter.ExportMesh, context);
CreateExportPlan(go, CreateSample<MeshSample>(context), NativeExporter.ExportObject, context,
insertFirst: false);
}
else if (cam)
{
CreateExportPlan(go, CreateSample<CameraSample>(context), CameraExporter.ExportCamera, context);
CreateExportPlan(go, CreateSample<CameraSample>(context), NativeExporter.ExportObject, context,
insertFirst: false);
}
}
}

Просмотреть файл

@ -52,6 +52,7 @@ namespace Unity.Formats.USD
public bool exportMaterials;
public bool exportNative;
public UnityEngine.Transform exportRoot;
public bool exportTransformOverrides;
public System.Collections.Generic.Dictionary<UnityEngine.Material, string> matMap;
public System.Collections.Generic.Dictionary<UnityEngine.Transform, UnityEngine.Transform[]> meshToBones;
public System.Collections.Generic.Dictionary<UnityEngine.Transform, UnityEngine.Transform> meshToSkelRoot;
@ -380,7 +381,7 @@ namespace Unity.Formats.USD
public static class SceneExporter
{
public static void Export(UnityEngine.GameObject root, Unity.Formats.USD.ExportContext context, bool zeroRootTransform);
public static void Export(UnityEngine.GameObject root, USD.NET.Scene scene, Unity.Formats.USD.BasisTransformation basisTransform, bool exportUnvarying, bool zeroRootTransform, bool exportMaterials = False, bool exportMonoBehaviours = False);
public static void Export(UnityEngine.GameObject root, USD.NET.Scene scene, Unity.Formats.USD.BasisTransformation basisTransform, bool exportUnvarying, bool zeroRootTransform, bool exportMaterials = False, bool exportMonoBehaviours = False, bool exportOverrides = False);
public static void SyncExportContext(UnityEngine.GameObject exportRoot, Unity.Formats.USD.ExportContext context);
}

Просмотреть файл

@ -6,6 +6,7 @@ using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using USD.NET;
using USD.NET.Unity;
using Assert = UnityEngine.Assertions.Assert;
namespace Unity.Formats.USD.Tests
@ -209,4 +210,65 @@ namespace Unity.Formats.USD.Tests
CollectionAssert.AreEqual(colours, mesh.colors);
}
}
class ExportXFormOverride
{
static readonly string SourceFilePath = Path.ChangeExtension(Path.GetTempFileName(), "usda");
static readonly string OverFilePath = Path.ChangeExtension(Path.GetTempFileName(), "usda");
[OneTimeSetUp]
public void CreateLoadExport()
{
// Create a new Stage
var scene = ExportHelpers.InitForSave(SourceFilePath);
var xformToken = new TfToken("Xform");
scene.Stage.DefinePrim(new SdfPath("/root/A"), xformToken);
scene.Stage.DefinePrim(new SdfPath("/root/B"), xformToken);
scene.Save();
// Load the stage and modify /root/A transform
var root = ImportHelpers.ImportSceneAsGameObject(scene);
scene.Close();
var primA = root.transform.Find("A");
primA.transform.localPosition = new Vector3(10.0f, 10.0f, 10.0f);
// Export overrides
var usdAsset = root.GetComponentInParent<UsdAsset>();
var overs = ExportHelpers.InitForSave(OverFilePath);
usdAsset.ExportOverrides(overs);
}
[Test]
public void ExportXFormOverride_OnlyExportChanges_Success()
{
var outScene = Scene.Open(OverFilePath);
NUnit.Framework.Assert.IsTrue(outScene.Stage.GetPrimAtPath(new SdfPath("/root/A")).IsValid());
NUnit.Framework.Assert.IsFalse(outScene.Stage.GetPrimAtPath(new SdfPath("/root/B")).IsValid());
}
[Test]
public void ExportXFormOverride_NoSublayers_True()
{
var outScene = Scene.Open(OverFilePath);
NUnit.Framework.Assert.Zero(outScene.Stage.GetRootLayer().GetNumSubLayerPaths());
}
[Test]
public void ExportXFormOverride_NoPrimDefined_True()
{
var outScene = Scene.Open(OverFilePath);
foreach (var prim in outScene.Stage.GetAllPrims())
{
Debug.Log(prim.GetPath().ToString());
NUnit.Framework.Assert.AreEqual(SdfSpecifier.SdfSpecifierOver, prim.GetSpecifier());
}
}
[OneTimeTearDown]
public void DeleteTestFiles()
{
File.Delete(OverFilePath);
File.Delete(SourceFilePath);
}
}
}

Просмотреть файл

@ -171,12 +171,12 @@ namespace Unity.Formats.USD.Tests
var skeletalRootInUsd = m_USDScene.Stage.GetPrimAtPath(new pxr.SdfPath(UnityTypeConverter.GetPath(skeletalRoot.transform)));
var skeletalRootInUsdType = skeletalRootInUsd.GetTypeName();
var expectedSkeletalRootInUsdType = new pxr.TfToken("SkelRoot");
Assert.AreEqual(skeletalRootInUsdType, expectedSkeletalRootInUsdType);
Assert.AreEqual(expectedSkeletalRootInUsdType, skeletalRootInUsdType);
var skeletonInUsd = m_USDScene.Stage.GetPrimAtPath(new pxr.SdfPath(UnityTypeConverter.GetPath(skeleton.transform)));
var skeletonInUsdType = skeletonInUsd.GetTypeName();
var expectedSkeletonInUsdType = new pxr.TfToken("Skeleton");
Assert.AreEqual(skeletonInUsdType, expectedSkeletonInUsdType);
Assert.AreEqual(expectedSkeletonInUsdType, skeletonInUsdType);
}
}
}