static void MergeBonesSimple(Transform source, Transform rootBone, Transform[] bones, Matrix4x4[] bindPoses, ExportContext context) { context.meshToSkelRoot.Add(source, rootBone); context.meshToBones.Add(source, bones); Matrix4x4 existingMatrix; for (int i = 0; i < bones.Length; i++) { Transform bone = bones[i]; if (bone == null) { var srcPath = UnityTypeConverter.GetPath(source); Debug.LogWarning("Null bone at in bones list at position (" + i + ") " + srcPath); continue; } var path = UnityTypeConverter.GetPath(bone); context.pathToBone[path] = bone; context.boneToRoot[bone] = rootBone; context.bindPoses[bone] = bindPoses[i]; if (context.bindPoses.TryGetValue(bone, out existingMatrix) && existingMatrix != bindPoses[i]) { Debug.LogWarning("Duplicate bone with different bind poses: " + path + "\n" + existingMatrix.ToString() + "\n" + bindPoses[i].ToString()); } } }
public static void ExportSkeleton(ObjectContext objContext, ExportContext exportContext) { var scene = exportContext.scene; var sample = (SkeletonSample)objContext.sample; var boneNames = exportContext.skelSortedMap[objContext.gameObject.transform]; sample.joints = new string[boneNames.Count]; sample.bindTransforms = new Matrix4x4[boneNames.Count]; sample.restTransforms = new Matrix4x4[boneNames.Count]; string rootPath = UnityTypeConverter.GetPath(objContext.gameObject.transform); sample.transform = XformExporter.GetLocalTransformMatrix( objContext.gameObject.transform, scene.UpAxis == Scene.UpAxes.Z, new pxr.SdfPath(rootPath).IsRootPrimPath(), exportContext.basisTransform); int i = 0; foreach (string bonePath in boneNames) { if (string.IsNullOrEmpty(bonePath)) { sample.joints[i] = ""; i++; continue; } var bone = exportContext.pathToBone[bonePath]; if (bonePath == rootPath) { sample.joints[i] = "/"; } else { sample.joints[i] = bonePath.Replace(rootPath + "/", ""); } // TODO: When the bone bind transform contains the geomBindTransform from USD import, it // will be mixed into each bone. This transform should be saved in some way and removed // when exported as a skeleton. sample.bindTransforms[i] = exportContext.bindPoses[bone].inverse; sample.restTransforms[i] = XformExporter.GetLocalTransformMatrix( bone, false, false, exportContext.basisTransform); if (exportContext.basisTransform == BasisTransformation.SlowAndSafe) { sample.bindTransforms[i] = UnityTypeConverter.ChangeBasis(sample.bindTransforms[i]); // The restTransforms will get a change of basis from GetLocalTransformMatrix(). } i++; } scene.Write(objContext.path, sample); // Stop Skeleton from rendering bones in usdview by default. var im = new pxr.UsdGeomImageable(scene.GetPrimAtPath(objContext.path)); im.CreatePurposeAttr().Set(pxr.UsdGeomTokens.guide); }
static void CreateExportPlan(GameObject go, SampleBase sample, ExportFunction exportFunc, ExportContext context, string pathSuffix = null, object data = null, bool insertFirst = true) { // This is an exportable object. Transform expRoot = context.exportRoot; string path = UnityTypeConverter.GetPath(go.transform, expRoot); if (!string.IsNullOrEmpty(pathSuffix)) { path += pathSuffix; } if (!context.plans.ContainsKey(go)) { context.plans.Add(go, new ExportPlan()); } var exp = new Exporter { exportFunc = exportFunc, sample = sample, path = path, data = data }; if (insertFirst) { context.plans[go].exporters.Insert(0, exp); } else { context.plans[go].exporters.Add(exp); } // Include the parent xform hierarchy. // Note that the parent hierarchy is memoised, so despite looking expensive, the time // complexity is linear. Transform xf = go.transform.parent; if (xf != context.exportRoot && !context.plans.ContainsKey(xf.gameObject)) { // Since all GameObjects have a Transform, export all un-exported parents as transform. CreateExportPlan(xf.gameObject, CreateSample<XformSample>(context), XformExporter.ExportXform, context); CreateExportPlan(xf.gameObject, CreateSample<XformSample>(context), NativeExporter.ExportObject, context, insertFirst: false); } }
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()) { tmpPath = tmpPath.GetParentPath(); } // 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) { foreach (var mat in smr.sharedMaterials) { 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.ExportSkinnedMesh, context); CreateExportPlan(go, CreateSample<MeshSample>(context), NativeExporter.ExportObject, context, insertFirst: false); if (smr.rootBone == 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 { 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); } } 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); } }
public static void Export(GameObject root, ExportContext context, bool zeroRootTransform) { // Remove parent transform effects while exporting. // This must be restored before returning from this function. var parent = root.transform.parent; if (zeroRootTransform) { root.transform.SetParent(null, worldPositionStays: false); } // Also zero out and restore local rotations on the root. var localPos = root.transform.localPosition; var localRot = root.transform.localRotation; var localScale = root.transform.localScale; if (zeroRootTransform) { root.transform.localPosition = Vector3.zero; root.transform.localRotation = Quaternion.identity; root.transform.localScale = Vector3.one; } // Scale overall scene for export (e.g. USDZ export needs scale 100) root.transform.localScale *= context.scale; UnityEngine.Profiling.Profiler.BeginSample("USD: Export"); try { ExportImpl(root, context); var path = new pxr.SdfPath(UnityTypeConverter.GetPath(root.transform)); var prim = context.scene.Stage.GetPrimAtPath(path); if (prim) { context.scene.Stage.SetDefaultPrim(prim); } } finally { if (zeroRootTransform) { root.transform.localPosition = localPos; root.transform.localRotation = localRot; root.transform.localScale = localScale; root.transform.SetParent(parent, worldPositionStays: false); } else { root.transform.localScale = localScale; } UnityEngine.Profiling.Profiler.EndSample(); } }
/// <summary> /// Attempts to build a valid per-vertex UV set from the given object. If the type T is not /// the held type of the object "uv" argument, null is returned. If there is an error such /// that the type is correct, but the uv values are somehow incompatible, error messages /// will be generated and an empty array will be returned. /// </summary> /// <returns> /// An array of size > 0 on succes, an array of size 0 on failure, or null if the given object /// is not of the desired type T. /// </returns> private static T[] TryGetUVSet <T>(object uv, int[] uvIndices, int[] faceVertexCounts, int[] faceVertexIndices, int vertexCount, GameObject go) { if (uv.GetType() != typeof(T[])) { return(null); } var uvVec = (T[])uv; if (uvVec.Length == 0) { return(uvVec); } // Unroll UV indices if specified. if (uvIndices != null && uvIndices.Length > 0) { var newUvs = new T[uvIndices.Length]; for (int i = 0; i < uvIndices.Length; i++) { newUvs[i] = uvVec[uvIndices[i]]; } uvVec = newUvs; } // If there are more UVs than verts, the UVs must be face varying, e.g. each vertex for // each face has a unique UV value. These values must be collapsed such that verts shared // between faces also share a single UV value. if (uvVec.Length > vertexCount) { uvVec = UnrollFaceVarying(vertexCount, uvVec, faceVertexCounts, faceVertexIndices); if (uvVec == null) { return(new T[0]); } Debug.Assert(uvVec.Length == vertexCount); } // If there are fewer values, these must be "varying" / one value per face. // This is not yet supported. if (uvVec.Length < vertexCount) { Debug.LogWarning("Mesh UVs are constant or uniform, ignored " + UnityTypeConverter.GetPath(go.transform)); return(new T[0]); } return(uvVec); }
/// <summary> /// Loads or unloads the given payload object. Throws an exception if game object deos not have /// a UsdPrimSource behaviour. /// </summary> public void SetPayloadState(GameObject go, bool isLoaded) { var primSrc = go.GetComponent <UsdPrimSource>(); if (!primSrc) { throw new Exception("UsdPrimSource not found: " + UnityTypeConverter.GetPath(go.transform)); } var usdPrimPath = primSrc.m_usdPrimPath; InitUsd.Initialize(); var scene = GetScene(); if (scene == null) { throw new Exception("Failed to open: " + usdFullPath); } var prim = scene.GetPrimAtPath(usdPrimPath); if (prim == null || !prim) { throw new Exception("Prim not found: " + usdPrimPath); } foreach (var child in go.transform.GetComponentsInChildren <UsdPrimSource>().ToList()) { if (!child || child.gameObject == go) { continue; } GameObject.DestroyImmediate(child.gameObject); } if (!isLoaded) { prim.Unload(); return; } else { prim.Load(); } SceneImportOptions importOptions = new SceneImportOptions(); this.StateToOptions(ref importOptions); importOptions.usdRootPath = prim.GetPath(); SceneImporter.ImportUsd(go, scene, new PrimMap(), true, importOptions); }
public static void ExportSkinnedMesh(ObjectContext objContext, ExportContext exportContext) { var smr = objContext.gameObject.GetComponent <SkinnedMeshRenderer>(); UnityEngine.Profiling.Profiler.BeginSample("USD: Skinned Mesh"); ExportMesh(objContext, exportContext, smr.sharedMesh, smr.sharedMaterial, smr.sharedMaterials, exportMeshPose: exportContext.scene.Time == null); UnityEngine.Profiling.Profiler.EndSample(); if (exportContext.scene.Time != null) { return; } // Note that the baked mesh no longer has the bone weights, so here we switch back to the // shared SkinnedMeshRenderer mesh. Transform rootBone = null; if (smr.rootBone != null && !exportContext.boneToRoot.TryGetValue(smr.rootBone, out rootBone)) { Debug.LogWarning("Root bone not found in export context for " + UnityTypeConverter.GetPath(smr.rootBone)); return; } UnityEngine.Profiling.Profiler.BeginSample("USD: Skinning Weights"); // Skeleton path is stored in additionalData via the SceneExporter SyncExportContext(). It // would be nice to formalize this, rather than passing it as blind data. var skeletonPath = (string)objContext.additionalData; ExportSkelWeights(exportContext.scene, objContext.path, smr.sharedMesh, rootBone, smr.bones, skeletonPath); UnityEngine.Profiling.Profiler.EndSample(); }
bool InitExportableParents(GameObject go) { if (m_primMap.ContainsKey(go)) { // Stop processing parents, this keeps the performance of the traversal linear. return(false); } // Any object we add will only be exported as an Xform. string path = UnityTypeConverter.GetPath(go.transform); SampleBase sample = new XformSample(); m_primMap.Add(go, new ExportPlan { path = path, sample = sample, exportFunc = ExportXform }); Debug.Log(path + " " + sample.GetType().Name); // Continue processing parents. return(true); }
public static void ExportSkinnedMesh(ObjectContext objContext, ExportContext exportContext) { var smr = objContext.gameObject.GetComponent <SkinnedMeshRenderer>(); UnityEngine.Profiling.Profiler.BeginSample("USD: Skinned Mesh"); ExportMesh(objContext, exportContext, smr.sharedMesh, smr.sharedMaterial, smr.sharedMaterials, exportMeshPose: exportContext.scene.Time == null); UnityEngine.Profiling.Profiler.EndSample(); if (exportContext.scene.Time != null) { return; } // Note that the baked mesh no longer has the bone weights, so here we switch back to the // shared SkinnedMeshRenderer mesh. Transform rootBone = null; if (smr.rootBone != null && !exportContext.boneToRoot.TryGetValue(smr.rootBone, out rootBone)) { Debug.LogWarning("Root bone not found in export context for " + UnityTypeConverter.GetPath(smr.rootBone)); return; } UnityEngine.Profiling.Profiler.BeginSample("USD: Skinning Weights"); ExportSkelWeights(exportContext.scene, objContext.path, smr.sharedMesh, rootBone, smr.bones); UnityEngine.Profiling.Profiler.EndSample(); }
/// <summary> /// Applies the contents of this USD file to a foreign root object. /// </summary> /// <remarks> /// The idea here is that one may have many animation clips, but only a single GameObject in /// the Unity scenegraph. /// </remarks> public void SetTime(double time, UsdAsset foreignRoot, bool saveMeshUpdates) { var scene = GetScene(); if (scene == null) { Debug.LogWarning("Null scene from GetScene() at " + UnityTypeConverter.GetPath(transform)); return; } // Careful not to update any local members here, if this data is driven from a prefab, we // dont want those changes to be baked back into the asset. time += foreignRoot.m_usdTimeOffset; float usdTime = (float)(scene.StartTime + time * scene.Stage.GetTimeCodesPerSecond()); if (usdTime > scene.EndTime) { return; } if (usdTime < scene.StartTime) { return; } scene.Time = usdTime; var options = new SceneImportOptions(); foreignRoot.StateToOptions(ref options); PrepOptionsForTimeChange(ref options); if (foreignRoot.m_lastPrimMap == null) { foreignRoot.m_lastPrimMap = new PrimMap(); options.importHierarchy = true; } if (m_usdVariabilityCache) { if (m_lastAccessMask == null) { m_lastAccessMask = new AccessMask(); scene.IsPopulatingAccessMask = true; } } else { m_lastAccessMask = null; } if (m_debugPrintVariabilityCache && m_lastAccessMask != null && !scene.IsPopulatingAccessMask) { var sb = new System.Text.StringBuilder(); foreach (var kvp in m_lastAccessMask.Included) { sb.AppendLine(kvp.Key); foreach (var member in kvp.Value) { sb.AppendLine(" ." + member.Name); } sb.AppendLine(); } Debug.Log(sb.ToString()); } scene.AccessMask = m_lastAccessMask; SceneImporter.ImportUsd(foreignRoot.gameObject, scene, foreignRoot.m_lastPrimMap, options); scene.AccessMask = null; if (m_lastAccessMask != null) { scene.IsPopulatingAccessMask = false; } }
static void ExportSkelWeights(Scene scene, string path, Mesh unityMesh, Transform rootBone, Transform[] bones, string skeletonPath) { var sample = new SkelBindingSample(); sample.geomBindTransform.value = Matrix4x4.identity; sample.joints = new string[bones.Length]; if (!string.IsNullOrEmpty(skeletonPath)) { sample.skeleton.targetPaths = new string[] { skeletonPath }; } int b = 0; var rootPath = UnityTypeConverter.GetPath(rootBone); foreach (Transform bone in bones) { var bonePath = UnityTypeConverter.GetPath(bone); if (bonePath == rootPath) { sample.joints[b++] = "/"; } else { sample.joints[b++] = bonePath.Replace(rootPath + "/", ""); } } int i = 0; int w = 0; b = 0; #if UNITY_2019 var bonesPerVertex = unityMesh.GetBonesPerVertex(); var unityBoneWeights = unityMesh.GetAllBoneWeights(); byte maxWeightCount = 0; foreach (var c in bonesPerVertex) { maxWeightCount = maxWeightCount > c ? maxWeightCount : c; } sample.jointIndices.value = new int[bonesPerVertex.Length * maxWeightCount]; sample.jointIndices.elementSize = maxWeightCount; sample.jointIndices.interpolation = PrimvarInterpolation.Vertex; sample.jointWeights.value = new float[bonesPerVertex.Length * maxWeightCount]; sample.jointWeights.elementSize = maxWeightCount; sample.jointWeights.interpolation = PrimvarInterpolation.Vertex; foreach (var weightCount in bonesPerVertex) { for (int j = 0; j < weightCount; j++) { var bw = unityBoneWeights[b++]; sample.jointIndices.value[i++] = bw.boneIndex; sample.jointWeights.value[w++] = bw.weight; } // Unity allows a variable number of weights per bone, but we've made the array square, // which means we may need to skip a few indicies, if this vert doesn't use the max number // of weights. i += maxWeightCount - weightCount; w += maxWeightCount - weightCount; } #else var unityBoneWeights = unityMesh.boneWeights; if (unityBoneWeights.Length == 0) { Debug.LogWarning("Found zero bone weights at: " + path); return; } sample.jointIndices.value = new int[unityBoneWeights.Length * 4]; sample.jointIndices.elementSize = 4; sample.jointIndices.interpolation = PrimvarInterpolation.Vertex; sample.jointWeights.value = new float[unityBoneWeights.Length * 4]; sample.jointWeights.elementSize = 4; sample.jointWeights.interpolation = PrimvarInterpolation.Vertex; foreach (var bone in unityBoneWeights) { sample.jointIndices.value[i++] = bone.boneIndex0; sample.jointIndices.value[i++] = bone.boneIndex1; sample.jointIndices.value[i++] = bone.boneIndex2; sample.jointIndices.value[i++] = bone.boneIndex3; sample.jointWeights.value[w++] = bone.weight0; sample.jointWeights.value[w++] = bone.weight1; sample.jointWeights.value[w++] = bone.weight2; sample.jointWeights.value[w++] = bone.weight3; } #endif scene.Write(path, sample); }
static Transform MergeBonesBelowAnimator(Transform animator, ExportContext context) { var toRemove = new Dictionary <Transform, Transform>(); Transform commonRoot = null; foreach (var sourceAndRoot in context.meshToSkelRoot) { var meshXf = sourceAndRoot.Key; var meshRootBone = sourceAndRoot.Value; if (!meshRootBone.IsChildOf(animator)) { continue; } toRemove.Add(meshXf, meshRootBone); if (commonRoot == null) { // We use the parent because the root bone is part of the skeleton and we're establishing // the skeleton root here. If the root bone is used as the skeleton root, its transform // will get applied twice after export to USD: once for the UsdPrim which is the skeleton // root and once for the bone which is in the skeleton itself. The root bone could be // excluded from the skeleton, but this seems simpler. commonRoot = meshRootBone.parent; } else if (meshRootBone.IsChildOf(commonRoot)) { // Nothing to do. } else if (commonRoot.IsChildOf(meshRootBone)) { // The new root is a parent of the current common root, use it as the root instead. commonRoot = meshRootBone.parent; } else { // We have an animator which is a common parent of two disjoint skeletons, this is not // desirable because it requires that the animator be the common root, however this // root will be tagged as a guide, which will cuase the geometry not to render, which // will be confusing. Another option would be to construct a new common parent in USD, // but this will cause the asset namespace to change, which is almost never a good idea. commonRoot = animator; } } if (toRemove.Count == 0) { return(null); } // At this point, some number of root bones have been aggregated under some potentially new // common root. Next, we need to merge all these root bones and preserve the requirement that // the bones are in "parent first" order. var allBones = new List <Transform>(); foreach (var kvp in toRemove) { Transform curMeshXf = kvp.Key; Transform rootBone = kvp.Value; allBones.AddRange(context.meshToBones[curMeshXf]); // Downstream code will have a root bone and need to know how to make bone paths relative // to the new, arbitrary, common root which we have chosen. context.boneToRoot[rootBone] = commonRoot; context.meshToSkelRoot.Remove(curMeshXf); context.meshToBones.Remove(curMeshXf); } // Maintain a sorted list of bone names to ensure "parent first" ordering for UsdSkel. var allNames = allBones.Select(boneXf => UnityTypeConverter.GetPath(boneXf)) .OrderBy(str => str) .Distinct() .ToList(); context.skelSortedMap[commonRoot] = allNames; return(commonRoot); }
private static void BuildMesh_(string path, MeshSample usdMesh, Mesh unityMesh, GeometrySubsets geomSubsets, GameObject go, Renderer renderer, SceneImportOptions options) { // TODO: Because this method operates on a GameObject, it must be single threaded. For this // reason, it should be extremely light weight. All computation should be completed prior to // this step, allowing heavy computations to happen in parallel. This is not currently the // case, triangulation and change of basis are non-trivial operations. Computing the mesh // bounds, normals and tangents should similarly be moved out of this function and should not // rely on the UnityEngine.Mesh API. Material mat = renderer.sharedMaterial; bool changeHandedness = options.changeHandedness == BasisTransformation.SlowAndSafe; // // Points. // if (options.meshOptions.points == ImportMode.Import && usdMesh.points != null) { if (changeHandedness) { for (int i = 0; i < usdMesh.points.Length; i++) { usdMesh.points[i] = UnityTypeConverter.ChangeBasis(usdMesh.points[i]); } } if (usdMesh.faceVertexIndices != null) { // Annoyingly, there is a circular dependency between vertices and triangles, which makes // it impossible to have a fixed update order in this function. As a result, we must clear // the triangles before setting the points, to break that dependency. unityMesh.SetTriangles(new int[0] { }, 0); } unityMesh.vertices = usdMesh.points; } // // Purpose. // // Deactivate non-geometry prims (e.g. guides, render, etc). if (usdMesh.purpose != Purpose.Default) { go.SetActive(false); } // // Mesh Topology. // // TODO: indices should not be accessed if topology is not requested, however it may be // needed for facevarying primvars; that special case should throw a warning, rather than // reading the value. int[] originalIndices = new int[usdMesh.faceVertexIndices == null ? 0 : usdMesh.faceVertexIndices.Length]; // Optimization: only do this when there are face varying primvars. if (usdMesh.faceVertexIndices != null) { Array.Copy(usdMesh.faceVertexIndices, originalIndices, originalIndices.Length); } if (options.meshOptions.topology == ImportMode.Import && usdMesh.faceVertexIndices != null) { Profiler.BeginSample("Triangulate Mesh"); if (options.meshOptions.triangulateMesh) { // Triangulate n-gons. // For best performance, triangulate off-line and skip conversion. if (usdMesh.faceVertexIndices == null) { Debug.LogWarning("Mesh had no face indices: " + UnityTypeConverter.GetPath(go.transform)); return; } if (usdMesh.faceVertexCounts == null) { Debug.LogWarning("Mesh had no face counts: " + UnityTypeConverter.GetPath(go.transform)); return; } var indices = UnityTypeConverter.ToVtArray(usdMesh.faceVertexIndices); var counts = UnityTypeConverter.ToVtArray(usdMesh.faceVertexCounts); UsdGeomMesh.Triangulate(indices, counts); UnityTypeConverter.FromVtArray(indices, ref usdMesh.faceVertexIndices); } Profiler.EndSample(); Profiler.BeginSample("Convert LeftHanded"); bool isLeftHanded = usdMesh.orientation == Orientation.LeftHanded; if (changeHandedness && !isLeftHanded || !changeHandedness && isLeftHanded) { // USD is right-handed, so the mesh needs to be flipped. // Unity is left-handed, but that doesn't matter here. for (int i = 0; i < usdMesh.faceVertexIndices.Length; i += 3) { int tmp = usdMesh.faceVertexIndices[i]; usdMesh.faceVertexIndices[i] = usdMesh.faceVertexIndices[i + 1]; usdMesh.faceVertexIndices[i + 1] = tmp; } } Profiler.EndSample(); if (usdMesh.faceVertexIndices.Length > 65535) { unityMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; } Profiler.BeginSample("Breakdown triangles for Mesh Subsets"); if (geomSubsets.Subsets.Count == 0) { unityMesh.triangles = usdMesh.faceVertexIndices; } else { unityMesh.subMeshCount = geomSubsets.Subsets.Count; int subsetIndex = 0; foreach (var kvp in geomSubsets.Subsets) { int[] faceIndices = kvp.Value; int[] triangleIndices = new int[faceIndices.Length * 3]; for (int i = 0; i < faceIndices.Length; i++) { triangleIndices[i * 3 + 0] = usdMesh.faceVertexIndices[faceIndices[i] * 3 + 0]; triangleIndices[i * 3 + 1] = usdMesh.faceVertexIndices[faceIndices[i] * 3 + 1]; triangleIndices[i * 3 + 2] = usdMesh.faceVertexIndices[faceIndices[i] * 3 + 2]; } unityMesh.SetTriangles(triangleIndices, subsetIndex); subsetIndex++; } } Profiler.EndSample(); } // // Extent / Bounds. // bool hasBounds = usdMesh.extent.size.x > 0 || usdMesh.extent.size.y > 0 || usdMesh.extent.size.z > 0; if (ShouldImport(options.meshOptions.boundingBox) && hasBounds) { Profiler.BeginSample("Import Bounds"); if (changeHandedness) { usdMesh.extent.center = UnityTypeConverter.ChangeBasis(usdMesh.extent.center); usdMesh.extent.extents = UnityTypeConverter.ChangeBasis(usdMesh.extent.extents); } unityMesh.bounds = usdMesh.extent; Profiler.EndSample(); } else if (ShouldCompute(options.meshOptions.boundingBox)) { Profiler.BeginSample("Calculate Bounds"); unityMesh.RecalculateBounds(); Profiler.EndSample(); } // // Normals. // if (usdMesh.normals != null && ShouldImport(options.meshOptions.normals)) { Profiler.BeginSample("Import Normals"); if (changeHandedness) { for (int i = 0; i < usdMesh.points.Length; i++) { usdMesh.normals[i] = UnityTypeConverter.ChangeBasis(usdMesh.normals[i]); } } // If more normals than verts, assume face-varying. if (usdMesh.normals.Length > usdMesh.points.Length) { usdMesh.normals = UnrollFaceVarying(usdMesh.points.Length, usdMesh.normals, usdMesh.faceVertexCounts, originalIndices); } unityMesh.normals = usdMesh.normals; Profiler.EndSample(); } else if (ShouldCompute(options.meshOptions.normals)) { Profiler.BeginSample("Calculate Normals"); unityMesh.RecalculateNormals(); Profiler.EndSample(); } // // Tangents. // if (usdMesh.tangents != null && ShouldImport(options.meshOptions.tangents)) { Profiler.BeginSample("Import Tangents"); if (changeHandedness) { for (int i = 0; i < usdMesh.points.Length; i++) { var w = usdMesh.tangents[i].w; var t = UnityTypeConverter.ChangeBasis(usdMesh.tangents[i]); usdMesh.tangents[i] = new Vector4(t.x, t.y, t.z, w); } } unityMesh.tangents = usdMesh.tangents; Profiler.EndSample(); } else if (ShouldCompute(options.meshOptions.tangents)) { Profiler.BeginSample("Calculate Tangents"); unityMesh.RecalculateTangents(); Profiler.EndSample(); } // // Display Color. // if (ShouldImport(options.meshOptions.color) && usdMesh.colors != null && usdMesh.colors.Length > 0) { Profiler.BeginSample("Import Display Color"); // NOTE: The following color conversion assumes PlayerSettings.ColorSpace == Linear. // For best performance, convert color space to linear off-line and skip conversion. if (usdMesh.colors.Length == 1) { // Constant color can just be set on the material. if (options.useDisplayColorAsFallbackMaterial && options.materialImportMode != MaterialImportMode.None) { mat = options.materialMap.InstantiateSolidColor(usdMesh.colors[0].gamma); } } else if (usdMesh.colors.Length == usdMesh.points.Length) { // Vertex colors map on to verts. // TODO: move the conversion to C++ and use the color management API. for (int i = 0; i < usdMesh.colors.Length; i++) { usdMesh.colors[i] = usdMesh.colors[i]; } unityMesh.colors = usdMesh.colors; } else if (usdMesh.colors.Length == usdMesh.faceVertexCounts.Length) { // Uniform colors, one per face. // Unroll face colors into vertex colors. This is not strictly correct, but it's much faster // than the fully correct solution. var colors = new Color[unityMesh.vertexCount]; int idx = 0; try { for (int faceIndex = 0; faceIndex < usdMesh.colors.Length; faceIndex++) { var faceColor = usdMesh.colors[faceIndex]; for (int f = 0; f < usdMesh.faceVertexCounts[faceIndex]; f++) { int vertexInFaceIdx = originalIndices[idx++]; colors[vertexInFaceIdx] = faceColor; } } unityMesh.colors = colors; } catch (Exception ex) { Debug.LogException(new Exception("Failed loading uniform/per-face colors at " + path, ex)); } } else if (usdMesh.colors.Length > usdMesh.points.Length) { try { usdMesh.colors = UnrollFaceVarying(unityMesh.vertexCount, usdMesh.colors, usdMesh.faceVertexCounts, originalIndices); for (int i = 0; i < usdMesh.colors.Length; i++) { usdMesh.colors[i] = usdMesh.colors[i]; } unityMesh.colors = usdMesh.colors; } catch (Exception ex) { Debug.LogException( new Exception("Error unrolling Face-Varying colors at <" + path + ">", ex)); } } else { Debug.LogWarning("Uniform (color per face) display color not supported"); } Profiler.EndSample(); } // should import color // // UVs / Texture Coordinates. // // TODO: these should also be driven by the UV privmars required by the bound shader. Profiler.BeginSample("Import UV Sets"); ImportUv(path, unityMesh, 0, usdMesh.st, usdMesh.indices, usdMesh.faceVertexCounts, originalIndices, options.meshOptions.texcoord0, go); ImportUv(path, unityMesh, 0, usdMesh.uv, null, usdMesh.faceVertexCounts, originalIndices, options.meshOptions.texcoord0, go); ImportUv(path, unityMesh, 1, usdMesh.uv2, null, usdMesh.faceVertexCounts, originalIndices, options.meshOptions.texcoord1, go); ImportUv(path, unityMesh, 2, usdMesh.uv3, null, usdMesh.faceVertexCounts, originalIndices, options.meshOptions.texcoord2, go); ImportUv(path, unityMesh, 3, usdMesh.uv4, null, usdMesh.faceVertexCounts, originalIndices, options.meshOptions.texcoord3, go); Profiler.EndSample(); Profiler.BeginSample("Request Material Bindings"); // // Materials. // if (options.materialImportMode != MaterialImportMode.None) { if (mat == null) { mat = options.materialMap.InstantiateSolidColor(Color.white); } if (unityMesh.subMeshCount == 1) { renderer.sharedMaterial = mat; if (options.ShouldBindMaterials) { options.materialMap.RequestBinding(path, (scene, boundMat, primvars) => BindMat(scene, unityMesh, boundMat, renderer, path, primvars, usdMesh.faceVertexCounts, originalIndices)); } } else { var mats = new Material[unityMesh.subMeshCount]; for (int i = 0; i < mats.Length; i++) { mats[i] = mat; } renderer.sharedMaterials = mats; if (options.ShouldBindMaterials) { Debug.Assert(geomSubsets.Subsets.Count == unityMesh.subMeshCount); var subIndex = 0; foreach (var kvp in geomSubsets.Subsets) { int idx = subIndex++; options.materialMap.RequestBinding(kvp.Key, (scene, boundMat, primvars) => BindMat(scene, unityMesh, boundMat, renderer, idx, path, primvars, usdMesh.faceVertexCounts, originalIndices)); } } } } Profiler.EndSample(); // // Lightmap UV Unwrapping. // #if UNITY_EDITOR if (options.meshOptions.generateLightmapUVs) { #if !UNITY_2018_3_OR_NEWER if (unityMesh.indexFormat == UnityEngine.Rendering.IndexFormat.UInt32) { Debug.LogWarning("Skipping prim " + path + " due to large IndexFormat (UInt32) bug in older vesrsions of Unity"); return; } #endif Profiler.BeginSample("Unwrap Lightmap UVs"); var unwrapSettings = new UnityEditor.UnwrapParam(); unwrapSettings.angleError = options.meshOptions.unwrapAngleError; unwrapSettings.areaError = options.meshOptions.unwrapAngleError; unwrapSettings.hardAngle = options.meshOptions.unwrapHardAngle; // Convert pixels to unitless UV space, which is what unwrapSettings uses internally. unwrapSettings.packMargin = options.meshOptions.unwrapPackMargin / 1024.0f; UnityEditor.Unwrapping.GenerateSecondaryUVSet(unityMesh, unwrapSettings); Profiler.EndSample(); } #else if (options.meshOptions.generateLightmapUVs) { Debug.LogWarning("Lightmap UVs were requested to be generated, but cannot be generated outside of the editor"); } #endif }
public static void SyncExportContext(GameObject exportRoot, ExportContext context) { context.exportRoot = exportRoot.transform.parent; Traverse(exportRoot, InitExportableObjects, context); Transform expRoot = context.exportRoot; var foundAnimators = new List<Transform>(); foreach (var rootBoneXf in context.meshToSkelRoot.Values.ToArray()) { bool alreadyProcessed = false; foreach (var xf in foundAnimators) { if (rootBoneXf.IsChildOf(xf)) { alreadyProcessed = true; break; } } if (alreadyProcessed) { continue; } var animatorXf = rootBoneXf; while (animatorXf != null) { // If there is an animator, assume this is the root of the rig. // This feels very ad hoc, it would be nice to not use a heuristic. var anim = animatorXf.GetComponent<Animator>(); if (anim != null) { // Any root bones under this animator will be merged into their most common ancestor, // which is returned here and becomes the skeleton root. Transform skeletonRoot = MergeBonesBelowAnimator(animatorXf, context); if (skeletonRoot == null) { animatorXf = animatorXf.parent; Debug.LogWarning("No children found under animator: " + UnityTypeConverter.GetPath(animatorXf) + " Root bone XF: " + UnityTypeConverter.GetPath(rootBoneXf)); continue; } foundAnimators.Add(anim.transform); // The skeleton is exported at the skeleton root and UsdSkelAnimation is nested under // this prim as a new prim called "_anim". SkelRootSample rootSample = CreateSample<SkelRootSample>(context); string skelPath = UnityTypeConverter.GetPath(skeletonRoot, expRoot); rootSample.skeleton = skelPath; rootSample.animationSource = skelPath + "/_anim"; CreateExportPlan( animatorXf.gameObject, rootSample, SkeletonExporter.ExportSkelRoot, context, insertFirst: true); CreateExportPlan( animatorXf.gameObject, rootSample, NativeExporter.ExportObject, context, insertFirst: false); CreateExportPlan( skeletonRoot.gameObject, CreateSample<SkeletonSample>(context), SkeletonExporter.ExportSkeleton, context, insertFirst: true); CreateExportPlan( skeletonRoot.gameObject, CreateSample<SkeletonSample>(context), NativeExporter.ExportObject, context, insertFirst: false); CreateExportPlan( skeletonRoot.gameObject, CreateSample<SkelAnimationSample>(context), SkeletonExporter.ExportSkelAnimation, context, insertFirst: true, pathSuffix: "/_anim"); // Exporting animation is only possible while in-editor (in 2018 and earlier). #if UNITY_EDITOR #if false // Currently disabled, future work. if (anim.layerCount > 0) { for (int l = 0; l < anim.layerCount; l++) { int clipCount = anim.GetCurrentAnimatorClipInfoCount(l); var clipInfos = anim.GetCurrentAnimatorClipInfo(l); foreach (var clipInfo in clipInfos) { var bindings = UnityEditor.AnimationUtility.GetCurveBindings(clipInfo.clip); // Properties are expressed as individual values, for transforms this is: // m_LocalPosition.x,y,z // m_LocalScale.x,y,z // m_LocalRotation.x,y,z,w // Which means they must be reaggregated into matrices. foreach (var binding in bindings) { if (binding.type != typeof(Transform)) { continue; } Debug.Log(binding.path + "." + binding.propertyName); var knot = UnityEditor.AnimationUtility.GetEditorCurve(clipInfo.clip, binding); } } } } #endif // disabled. #endif // Editor only. break; } animatorXf = animatorXf.parent; } } }
void OnEnable() { InitUsd.Initialize(); if (string.IsNullOrEmpty(m_usdMeshPath)) { m_usdMeshPath = UnityTypeConverter.GetPath(transform); } var scene = GetScene(); var binding = ReadUsdWeights(scene); string skelRootPath; var skeleton = ReadUsdSkeleton(scene, out skelRootPath); if (binding == null) { binding = new SkelBindingSample(); } var mesh = GetComponent <SkinnedMeshRenderer>().sharedMesh; // Process classic four-bone weights first. var sb = new System.Text.StringBuilder(); #if UNITY_2019_1_OR_NEWER var bonesPerVert = mesh.GetBonesPerVertex(); int weightsPerBone = 0; foreach (int count in bonesPerVert) { weightsPerBone = weightsPerBone > count ? weightsPerBone : count; } var boneWeights = mesh.GetAllBoneWeights(); sb.AppendLine("Many-bone indices: (" + boneWeights.Length + " * 4)"); int bone = 0; int bi = 0; int wi = 0; foreach (var weight in boneWeights) { if (wi == 0) { sb.Append("i: " + bone + " ["); } sb.Append(weight.boneIndex + GetUsdBoneData(bi, wi, binding.jointIndices) + ","); wi++; if (wi == weightsPerBone) { sb.Append("]\n"); bi++; wi = 0; } if (bonesPerVert[bi] != weightsPerBone) { // TODO: Unity supports a variable number of weights per bone, but USD does not. // Therefore, the number of weights may be greater in USD than in Unity. Currently // the way this works does not correctly handle that case. Debug.LogWarning("Unity bone count issue, see code comment for details."); } bone++; } Debug.Log(sb.ToString()); bone = 0; bi = 0; wi = 0; sb = new System.Text.StringBuilder(); sb.AppendLine("Many-bone weights: (" + boneWeights.Length + " * 4)"); foreach (var weight in boneWeights) { if (wi == 0) { sb.Append("i: " + bone + " ["); } sb.Append(weight.weight + GetUsdBoneData(bi, wi, binding.jointWeights) + ","); wi++; if (wi == weightsPerBone) { sb.Append("]\n"); bi++; wi = 0; } bone++; } Debug.Log(sb.ToString()); #else sb.AppendLine("Legacy 4-bone indices: (" + mesh.boneWeights.Length + " * 4)"); int bone = 0; foreach (var weight in mesh.boneWeights) { sb.Append("["); sb.Append(weight.boneIndex0 + GetUsdBoneData(bone, 0, binding.jointIndices) + ","); sb.Append(weight.boneIndex1 + GetUsdBoneData(bone, 1, binding.jointIndices) + ","); sb.Append(weight.boneIndex2 + GetUsdBoneData(bone, 2, binding.jointIndices) + ","); sb.Append(weight.boneIndex3 + GetUsdBoneData(bone, 3, binding.jointIndices) + "]\n"); bone++; } Debug.Log(sb.ToString()); bone = 0; sb = new System.Text.StringBuilder(); sb.AppendLine("Legacy 4-bone weights: (" + mesh.boneWeights.Length + " * 4)"); foreach (var weight in mesh.boneWeights) { sb.Append("["); sb.Append(weight.weight0 + GetUsdBoneData(bone, 0, binding.jointWeights) + ","); sb.Append(weight.weight1 + GetUsdBoneData(bone, 1, binding.jointWeights) + ","); sb.Append(weight.weight2 + GetUsdBoneData(bone, 2, binding.jointWeights) + ","); sb.Append(weight.weight3 + GetUsdBoneData(bone, 3, binding.jointWeights) + "]\n"); bone++; } Debug.Log(sb.ToString()); #endif sb = new System.Text.StringBuilder(); var bones = GetComponent <SkinnedMeshRenderer>().bones; var rootBone = GetComponent <SkinnedMeshRenderer>().rootBone; var root = UnityTypeConverter.GetPath(rootBone); sb.AppendLine("Bones: (" + bones.Length + ")"); sb.AppendLine("Root Bone: " + root); int i = 0; foreach (var boneXf in bones) { sb.AppendLine(UnityTypeConverter.GetPath(boneXf)); if (binding.joints != null) { sb.AppendLine(root + "\\" + binding.joints[i++] + "\n"); } } Debug.Log(sb.ToString()); sb = new System.Text.StringBuilder(); sb.AppendLine("Bind Transforms: (" + mesh.bindposes.Length + ")"); i = -1; var options = new SceneImportOptions(); options.changeHandedness = m_basisTransform; foreach (var boneXf in bones) { i++; var bindPose = mesh.bindposes[i]; var bonePath = UnityTypeConverter.GetPath(boneXf); sb.AppendLine("Pose[" + i + "] " + bonePath); sb.AppendLine(bindPose.ToString()); if (skeleton.bindTransforms != null) { if (string.IsNullOrEmpty(skelRootPath)) { continue; } bonePath = bonePath.Substring(skelRootPath.Length); bonePath = bonePath.TrimStart('/'); foreach (var joint in skeleton.joints) { if (joint == bonePath) { var usdMat = skeleton.bindTransforms[i]; XformImporter.ImportXform(ref usdMat, options); sb.AppendLine(usdMat.ToString() + "\n"); bonePath = null; break; } } if (string.IsNullOrEmpty(bonePath)) { continue; } sb.Append("Bone not found in USD: " + bonePath + "\n\n"); } } Debug.Log(sb.ToString()); }
public static void ExportSkelAnimation(ObjectContext objContext, ExportContext exportContext) { var scene = exportContext.scene; var sample = (SkelAnimationSample)objContext.sample; var go = objContext.gameObject; var boneNames = exportContext.skelSortedMap[go.transform]; var skelRoot = go.transform; sample.joints = new string[boneNames.Count]; var worldXf = new Matrix4x4[boneNames.Count]; var worldXfInv = new Matrix4x4[boneNames.Count]; string rootPath = UnityTypeConverter.GetPath(go.transform); var basisChange = Matrix4x4.identity; basisChange[2, 2] = -1; for (int i = 0; i < boneNames.Count; i++) { var bonePath = boneNames[i]; if (!exportContext.pathToBone.ContainsKey(bonePath)) { sample.joints[i] = ""; continue; } var bone = exportContext.pathToBone[bonePath]; sample.joints[i] = bonePath.Replace(rootPath + "/", ""); worldXf[i] = bone.localToWorldMatrix; if (exportContext.basisTransform == BasisTransformation.SlowAndSafe) { worldXf[i] = UnityTypeConverter.ChangeBasis(worldXf[i]); } worldXfInv[i] = worldXf[i].inverse; } var rootXf = skelRoot.localToWorldMatrix.inverse; if (exportContext.basisTransform == BasisTransformation.SlowAndSafe) { rootXf = UnityTypeConverter.ChangeBasis(rootXf); } var skelWorldTransform = UnityTypeConverter.ToGfMatrix(rootXf); pxr.VtMatrix4dArray vtJointsLS = new pxr.VtMatrix4dArray((uint)boneNames.Count); pxr.VtMatrix4dArray vtJointsWS = UnityTypeConverter.ToVtArray(worldXf); pxr.VtMatrix4dArray vtJointsWSInv = UnityTypeConverter.ToVtArray(worldXfInv); var translations = new pxr.VtVec3fArray(); var rotations = new pxr.VtQuatfArray(); sample.scales = new pxr.VtVec3hArray(); var topo = new pxr.UsdSkelTopology(UnityTypeConverter.ToVtArray(sample.joints)); pxr.UsdCs.UsdSkelComputeJointLocalTransforms(topo, vtJointsWS, vtJointsWSInv, vtJointsLS, skelWorldTransform); pxr.UsdCs.UsdSkelDecomposeTransforms( vtJointsLS, translations, rotations, sample.scales); sample.translations = UnityTypeConverter.FromVtArray(translations); sample.rotations = UnityTypeConverter.FromVtArray(rotations); scene.Write(objContext.path, sample); }
public static void SyncExportContext(GameObject exportRoot, ExportContext context) { context.exportRoot = exportRoot.transform.parent; Traverse(exportRoot, InitExportableObjects, context); Transform expRoot = context.exportRoot; var foundAnimators = new List <Transform>(); foreach (var rootBoneXf in context.meshToSkelRoot.Values.ToArray()) { bool alreadyProcessed = false; foreach (var xf in foundAnimators) { if (rootBoneXf.IsChildOf(xf)) { alreadyProcessed = true; break; } } if (alreadyProcessed) { continue; } var animatorXf = rootBoneXf; while (animatorXf != null) { // If there is an animator, assume this is the root of the rig. // This feels very ad hoc, it would be nice to not use a heuristic. var anim = animatorXf.GetComponent <Animator>(); if (anim != null) { // Any root bones under this animator will be merged into their most common ancestor, // which is returned here and becomes the skeleton root. Transform skeletonRoot = MergeBonesBelowAnimator(animatorXf, context); if (skeletonRoot == null) { animatorXf = animatorXf.parent; Debug.LogWarning("No children found under animator: " + UnityTypeConverter.GetPath(animatorXf) + " Root bone XF: " + UnityTypeConverter.GetPath(rootBoneXf)); continue; } foundAnimators.Add(anim.transform); // The skeleton is exported at the skeleton root and UsdSkelAnimation is nested under // this prim as a new prim called "_anim". Unity.Formats.USD.SkelRootSample rootSample = CreateSample <Unity.Formats.USD.SkelRootSample>(context); string skelRootPath = UnityTypeConverter.GetPath(animatorXf.transform, expRoot); string skelPath = UnityTypeConverter.GetPath(skeletonRoot, expRoot); string skelPathSuffix = ""; string skelAnimSuffix = "/_anim"; // When there is a collision between the SkelRoot and the Skeleton, make a new USD Prim // for the Skeleton object. The reason this is safe is as follows: if the object was // imported from USD, then the structure should already be correct and this code path will // not be hit (and hence overrides, etc, will work correctly). If the object was created // in Unity and there happened to be a collision, then we can safely create a new prim // for the Skeleton prim because there will be no existing USD skeleton for which // the namespace must match, hence adding a new prim is still safe. if (skelPath == skelRootPath) { Debug.LogWarning("SkelRoot and Skeleton have the same path, renaming Skeleton"); skelPathSuffix = "/_skel"; } rootSample.animationSource = skelPath + skelAnimSuffix; // For any skinned mesh exported under this SkelRoot, pass along the skeleton path in // the "additional data" member of the exporter. Note that this feels very ad hoc and // should probably be formalized in some way (perhaps as a separate export event for // which the SkinnedMesh exporter can explicitly register). // // While it is possible to bind the skel:skeleton relationship at the SkelRoot and // have it inherit down namespace, the Apple importer did not respect this inheritance // and it sometimes causes issues with geometry embedded in the bone hierarchy. foreach (var p in context.plans) { if (p.Key.transform.IsChildOf(animatorXf.transform)) { foreach (var e in p.Value.exporters) { if (e.exportFunc == MeshExporter.ExportSkinnedMesh) { e.data = skelPath + skelPathSuffix; } } } } CreateExportPlan( animatorXf.gameObject, rootSample, SkeletonExporter.ExportSkelRoot, context, insertFirst: true); CreateExportPlan( animatorXf.gameObject, rootSample, NativeExporter.ExportObject, context, insertFirst: false); CreateExportPlan( skeletonRoot.gameObject, CreateSample <SkeletonSample>(context), SkeletonExporter.ExportSkeleton, context, insertFirst: true, pathSuffix: skelPathSuffix); CreateExportPlan( skeletonRoot.gameObject, CreateSample <SkeletonSample>(context), NativeExporter.ExportObject, context, insertFirst: false, pathSuffix: skelPathSuffix); CreateExportPlan( skeletonRoot.gameObject, CreateSample <SkelAnimationSample>(context), SkeletonExporter.ExportSkelAnimation, context, insertFirst: true, pathSuffix: skelAnimSuffix); // Exporting animation is only possible while in-editor (in 2018 and earlier). #if UNITY_EDITOR #if false // Currently disabled, future work. if (anim.layerCount > 0) { for (int l = 0; l < anim.layerCount; l++) { int clipCount = anim.GetCurrentAnimatorClipInfoCount(l); var clipInfos = anim.GetCurrentAnimatorClipInfo(l); foreach (var clipInfo in clipInfos) { var bindings = UnityEditor.AnimationUtility.GetCurveBindings(clipInfo.clip); // Properties are expressed as individual values, for transforms this is: // m_LocalPosition.x,y,z // m_LocalScale.x,y,z // m_LocalRotation.x,y,z,w // Which means they must be reaggregated into matrices. foreach (var binding in bindings) { if (binding.type != typeof(Transform)) { continue; } Debug.Log(binding.path + "." + binding.propertyName); var knot = UnityEditor.AnimationUtility.GetEditorCurve(clipInfo.clip, binding); } } } } #endif // disabled. #endif // Editor only. break; } animatorXf = animatorXf.parent; } } }