#if UNITY_EDITOR using System; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEditor; using UnityEngine; using System.Linq; using Rukhanka.Hybrid.RTP; using Rukhanka.Toolbox; using Unity.Assertions; using FixedStringName = Unity.Collections.FixedString512Bytes; using Unity.Mathematics; using Unity.Burst; using Unity.Entities; using AnimationClip = UnityEngine.AnimationClip; using Hash128 = Unity.Entities.Hash128; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// namespace Rukhanka.Hybrid { [BurstCompile] public partial class AnimationClipBaker { // To reduce huge memory consumption during baking of many animations, I will reuse temporarty buffers NativeList keyFramesList = new (Allocator.Temp); NativeList trackList = new (Allocator.Temp); NativeList trackGroupHashes = new (Allocator.Temp); NativeList trackGroupOffsets = new (Allocator.Temp); #if RUKHANKA_DEBUG_INFO NativeList trackGroupNames = new (Allocator.Temp); #endif ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// struct ParsedCurveBinding { public BindingType bindingType; public uint channelIndex; public string boneName; public string channelName; public bool IsValid() => boneName.Length > 0; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// (string, string) SplitPath(string path) { var arr = path.Split('/'); Assert.IsTrue(arr.Length > 0); var rv = (arr.Last(), arr.Length > 1 ? arr[^2] : ""); return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BindingType PickGenericBindingTypeByString(string bindingString) => bindingString switch { "m_LocalPosition" => BindingType.Translation, "m_LocalRotation" => BindingType.Quaternion, "localEulerAngles" => BindingType.EulerAngles, "localEulerAnglesRaw" => BindingType.EulerAngles, "m_LocalScale" => BindingType.Scale, _ => BindingType.Unknown }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint ChannelIndexFromString(string c) => c switch { "x" => 0, "y" => 1, "z" => 2, "w" => 3, _ => 0 }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// string ConstructBoneClipName(string name, string path, Type animatedObjectType) { if (animatedObjectType == typeof(UnityEngine.Animator)) return SpecialBones.AnimatorTypeName; var rv = name; if (animatedObjectType != typeof(UnityEngine.SkinnedMeshRenderer)) { // Empty name string is unnamed root bone if (name.Length == 0 && path.Length == 0) { rv = SpecialBones.UnnamedRootBoneName; } } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int2 CopyKeyFrames(Keyframe[] keysArr, ref NativeList outKeyFrames) { var rv = new int2(outKeyFrames.Length, keysArr.Length); foreach (var k in keysArr) { var kf = new KeyFrame() { time = k.time, inTan = math.select(0, k.inTangent, math.isfinite(k.inTangent)), outTan = math.select(0, k.outTangent, math.isfinite(k.outTangent)), v = k.value }; outKeyFrames.Add(kf); } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Track CreateTrackData(int2 keyFrameRange, ParsedCurveBinding pb) { var rv = new Track(); rv.keyFrameRange = keyFrameRange; #if RUKHANKA_DEBUG_INFO rv.name = $"{pb.boneName}.{pb.channelName}"; #endif // For unknown binding types treat props as channel name hash if (pb.bindingType == BindingType.Unknown) { rv.props = Track.CalculateHash(pb.channelName); } else { rv.bindingType = pb.bindingType; rv.channelIndex = pb.channelIndex; } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void InsertTrackIntoGroup(Track t, string trackName) { // Tracks with same path hash will go into same track group. This is needed to speedup access of bone data // (no need to query each attribute hash) var nameHash = trackName.CalculateHash32(); // Search for name in already existing groups var i = trackGroupHashes.IndexOf(nameHash); if (i < 0) { // Create new track group and add track into it trackGroupHashes.Add(nameHash); trackGroupOffsets.Add((uint)trackList.Length); trackList.Add(t); #if RUKHANKA_DEBUG_INFO trackGroupNames.Add(trackName); #endif } else { // Insert track into existing group var startIndex = (int)trackGroupOffsets[i]; trackList.InsertRange(startIndex, 1); trackList[startIndex] = t; // Shift all other groups to the right for (var l = 0; l < trackGroupOffsets.Length; ++l) { ref var tgo = ref trackGroupOffsets.ElementAt(l); if (tgo > startIndex) tgo += 1; } } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ParsedCurveBinding ParseGenericCurveBinding(EditorCurveBinding b) { var rv = new ParsedCurveBinding(); var t = b.propertyName.Split('.'); var propName = t[0]; var channel = t.Length > 1 ? t[1] : ""; rv.channelIndex = ChannelIndexFromString(channel); rv.bindingType = PickGenericBindingTypeByString(propName); rv.channelName = b.propertyName; var nameAndPath = SplitPath(b.path); rv.boneName = ConstructBoneClipName(nameAndPath.Item1, nameAndPath.Item2, b.type); return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int GetHumanBoneIndexForHumanName(in HumanDescription hd, FixedStringName humanBoneName) { var humanBoneIndexInAvatar = Array.FindIndex(hd.human, x => x.humanName == humanBoneName); return humanBoneIndexInAvatar; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ParsedCurveBinding ParseHumanoidCurveBinding(EditorCurveBinding b, Avatar avatar) { if (!humanoidMappingTable.TryGetValue(b.propertyName, out var rv)) return ParseGenericCurveBinding(b); var hd = avatar.humanDescription; var humanBoneIndexInAvatar = GetHumanBoneIndexForHumanName(hd, rv.boneName); if (humanBoneIndexInAvatar < 0) return rv; if (rv.bindingType == BindingType.HumanMuscle) { var humanBoneDef = hd.human[humanBoneIndexInAvatar]; rv.boneName = humanBoneDef.boneName; } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ParsedCurveBinding ParseCurveBinding(AnimationClip ac, EditorCurveBinding b, Avatar avatar) { var rv = ac.isHumanMotion ? ParseHumanoidCurveBinding(b, avatar) : ParseGenericCurveBinding(b); return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// KeyFrame AddKeyFrameFromFloatValue(float2 key, float v) { var kf = new KeyFrame() { time = key.x, inTan = key.y, outTan = key.y, v = v }; return kf; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// [BurstCompile] void ComputeTangents(in Span keyFrames) { for (int i = 0; i < keyFrames.Length; ++i) { var p0 = i == 0 ? keyFrames[0] : keyFrames[i - 1]; var p1 = keyFrames[i]; var p2 = i == keyFrames.Length - 1 ? keyFrames[i] : keyFrames[i + 1]; var outV = math.normalizesafe(new float2(p2.time, p2.v) - new float2(p1.time, p1.v)); var outTan = outV.x > 0.0001f ? outV.y / outV.x : 0; var inV = math.normalizesafe(new float2(p1.time, p1.v) - new float2(p0.time, p0.v)); var inTan = inV.x > 0.0001f ? inV.y / inV.x : 0; var dt = math.abs(inTan) + math.abs(outTan); var f = dt > 0 ? math.abs(inTan) / dt : 0; var avgTan = math.lerp(inTan, outTan, f); var k = keyFrames[i]; k.outTan = avgTan; k.inTan = avgTan; keyFrames[i] = k; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// NativeList CreateKeyframeTimes(float animationLength, float dt, float frameTime) { var numFrames = (int)math.ceil(animationLength / dt) + 1; var rv = new NativeList(numFrames, Allocator.Temp); if (frameTime >= 0) { rv.Add(frameTime); return rv; } var curTime = 0.0f; for (var i = 0; i < numFrames; ++i) { rv.Add(curTime); curTime += dt; } if (rv.Length > 0) rv[^1] = math.min(animationLength, rv[^1]); return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ReadCurvesFromTransform(Transform tr, in NativeArray trackSpan, int keyIndex, float time) { quaternion q = tr.localRotation; float3 t = tr.localPosition; Span vArr = stackalloc float[7]; vArr[0] = t.x; vArr[1] = t.y; vArr[2] = t.z; vArr[3] = q.value.x; vArr[4] = q.value.y; vArr[5] = q.value.z; vArr[6] = q.value.w; for (int l = 0; l < vArr.Length; ++l) { var kfIndex = trackSpan[l].keyFrameRange.x + keyIndex; var kf = AddKeyFrameFromFloatValue(time, vArr[l]); keyFramesList[kfIndex] = kf; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// unsafe void SampleUnityAnimation(AnimationClip ac, Animator anm, ValueTuple[] trs, bool applyRootMotion, float frameTime) { if (trs.Length == 0) return; var sampleAnimationFrameTime = 1 / 60.0f; var keysList = CreateKeyframeTimes(ac.length, sampleAnimationFrameTime, frameTime); var tracks = new [] { new Track(BindingType.Translation, 0), new Track(BindingType.Translation, 1), new Track(BindingType.Translation, 2), new Track(BindingType.Quaternion, 0), new Track(BindingType.Quaternion, 1), new Track(BindingType.Quaternion, 2), new Track(BindingType.Quaternion, 3), }; var rac = anm.runtimeAnimatorController; var origPos = anm.transform.position; var origRot = anm.transform.rotation; var origRootMotion = anm.applyRootMotion; var prevAnmCulling = anm.cullingMode; anm.runtimeAnimatorController = null; anm.cullingMode = AnimatorCullingMode.AlwaysAnimate; anm.applyRootMotion = true; anm.transform.position = Vector3.zero; anm.transform.rotation = quaternion.identity; var newTracks = new NativeArray(tracks.Length * trs.Length, Allocator.Temp); for (int k = 0; k < newTracks.Length; ++k) { var nt = tracks[k % tracks.Length]; nt.keyFrameRange = new int2(keyFramesList.Length, keysList.Length); #if RUKHANKA_DEBUG_INFO nt.name = $"{trs[k / tracks.Length].Item1.name}.{nt.bindingType}.{nt.channelIndex}"; #endif keyFramesList.Resize(keyFramesList.Length + keysList.Length, NativeArrayOptions.ClearMemory); newTracks[k] = nt; } for (int i = 0; i < keysList.Length; ++i) { var time = keysList[i]; ac.SampleAnimation(anm.gameObject, time); for (int l = 0; l < trs.Length; ++l) { var tr = trs[l].Item1; var trackSpan = newTracks.GetSubArray(l * tracks.Length, tracks.Length); ReadCurvesFromTransform(tr, trackSpan, i, time); } } for (int l = 0; l < trs.Length; ++l) { var transformHash = trs[l].Item2; var idx = trackGroupHashes.IndexOf(transformHash); if (idx < 0) { trackGroupHashes.Add(transformHash); trackGroupOffsets.Add((uint)trackList.Length); #if RUKHANKA_DEBUG_INFO trackGroupNames.Add(trs[l].Item1.name); #endif } else { trackGroupOffsets[idx] = (uint)trackList.Length; } trackList.AddRange(newTracks.GetSubArray(l * tracks.Length, tracks.Length)); } for (int m = 0; m < newTracks.Length; ++m) { var nt = newTracks[m]; var trackKeyFrames = new Span(keyFramesList.GetUnsafePtr() + nt.keyFrameRange.x, nt.keyFrameRange.y); ComputeTangents(trackKeyFrames); } anm.cullingMode = prevAnmCulling; anm.runtimeAnimatorController = rac; anm.transform.position = origPos; anm.transform.rotation = origRot; anm.applyRootMotion = origRootMotion; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// (Transform, uint) GetRootBoneTransform(Animator anm) { if (anm.avatar.isHuman) { var hipsTransform = anm.GetBoneTransform(HumanBodyBones.Hips); var hd = anm.avatar.humanDescription; var humanBoneIndexInDesc = GetHumanBoneIndexForHumanName(hd, "Hips"); var rigHipsBoneName = new FixedStringName(hd.human[humanBoneIndexInDesc].boneName).CalculateHash32(); return (hipsTransform, rigHipsBoneName); } var rootBoneName = anm.avatar.GetRootMotionNodeName(); var rootBoneNameHash = new FixedStringName(rootBoneName).CalculateHash32(); var rootBoneTransform = Rukhanka.Toolbox.TransformUtils.FindChildRecursively(anm.transform, rootBoneName); return (rootBoneTransform, rootBoneNameHash); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void SampleMissingCurves(AnimationClip ac, Animator anm, float frameTime) { var trs = new List>(); var entityRootTransform = anm.transform; var rootBoneTransformData = GetRootBoneTransform(anm); if (anm.isHuman) trs.Add(rootBoneTransformData); // Sample curves for non-rootmotion animations SampleUnityAnimation(ac, anm, trs.ToArray(), false, frameTime); // Sample root motion curves trs.Clear(); var entityRootHash = SpecialBones.UnnamedRootBoneName.CalculateHash32(); entityRootHash = AnimationProcessSystem.ComputeBoneAnimationJob.ModifyBoneHashForRootMotion(entityRootHash); trs.Add((entityRootTransform, entityRootHash)); // Modify bone hash to separate root motion tracks and ordinary tracks rootBoneTransformData.Item2 = AnimationProcessSystem.ComputeBoneAnimationJob.ModifyBoneHashForRootMotion(rootBoneTransformData.Item2); trs.Add(rootBoneTransformData); SampleUnityAnimation(ac, anm, trs.ToArray(), true, frameTime); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void BakeAnimationEvents(BlobBuilder bb, ref AnimationClipBlob acb, AnimationClip ac) { if (ac.events.Length == 0) return; var eventsArr = bb.Allocate(ref acb.events, ac.events.Length); for (var i = 0; i < eventsArr.Length; ++i) { var ae = ac.events[i]; ref var bakedEvent = ref eventsArr[i]; #if RUKHANKA_DEBUG_INFO if (ae.functionName.Length > 0) bb.AllocateString(ref bakedEvent.name, ae.functionName); if (ae.stringParameter.Length > 0) bb.AllocateString(ref bakedEvent.stringParam, ae.stringParameter); #endif bakedEvent.nameHash = new FixedStringName(ae.functionName).CalculateHash32(); bakedEvent.time = ae.time / ac.length; bakedEvent.floatParam = ae.floatParameter; bakedEvent.intParam = ae.intParameter; bakedEvent.stringParamHash = new FixedStringName(ae.stringParameter).CalculateHash32(); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// AnimationClip[] Deduplicate(AnimationClip[] animationClips) { var dedupList = new List(); var dupSet = new NativeHashSet(animationClips.Length, Allocator.Temp); foreach (var a in animationClips) { if (a != null && #if UNITY_6000_4_OR_NEWER !dupSet.Add(a.GetEntityId().GetRawData()) #else !dupSet.Add((ulong)a.GetInstanceID()) #endif ) continue; dedupList.Add(a); } return dedupList.ToArray(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int BuildAnimationBakeList(IBaker baker, AnimationClip[] animationClips, Avatar avatar, out NativeArray> alreadyBakedList) { alreadyBakedList = new (animationClips.Length, Allocator.Temp); var rv = 0; for (var i = 0; i < animationClips.Length; ++i) { var ac = animationClips[i]; if (ac == null) continue; // Check for blob asset store first var animationHash = BakingUtils.ComputeAnimationHash(ac, avatar); var isAnimationExists = baker.TryGetBlobAssetReference(animationHash, out var acb); if (!isAnimationExists) { // Try cached baked animation acb = BlobCache.LoadBakedAnimationFromCache(ac, avatar); if (acb == BlobAssetReference.Null) { rv += 1; } else { // Don't forget to add loaded animation to blob asset store baker.AddBlobAssetWithCustomHash(ref acb, animationHash); } } alreadyBakedList[i] = acb; } // Return count of animations need to perform full bake return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public NativeArray> BakeAnimations(IBaker baker, AnimationClip[] animationClips, Avatar avatar, GameObject animatedObjectRoot) { if (animationClips == null || animationClips.Length == 0) return default; animationClips = Deduplicate(animationClips); // Firstly create list of animations that need to be baked (not present in cache and in not in blob asset store) var numClipsToBake = BuildAnimationBakeList(baker, animationClips, avatar, out var alreadyBakedAnimations); // If nothing to bake, just return already baked list if (numClipsToBake == 0) return alreadyBakedAnimations; // Now bake animations that require full rebake // Need to make instance of object because when we will sample animations object placement can be modified. // Also prefabs will not update its transforms GameObject objectCopy = null; Animator animatorCopy = null; if (avatar != null) { objectCopy = GameObject.Instantiate(animatedObjectRoot); objectCopy.hideFlags = HideFlags.HideAndDontSave; animatorCopy = objectCopy.GetComponent(); if (animatorCopy == null) animatorCopy = objectCopy.AddComponent(); animatorCopy.avatar = avatar; } for (var i = 0; i < animationClips.Length; ++i) { var clipBlob = alreadyBakedAnimations[i]; var a = animationClips[i]; if (clipBlob != BlobAssetReference.Null) continue; var animationHash = BakingUtils.ComputeAnimationHash(a, avatar); var isAnimationExists = baker.TryGetBlobAssetReference(animationHash, out clipBlob); if (!isAnimationExists) { clipBlob = CreateAnimationBlobAsset(a, animatorCopy, animationHash); baker.AddBlobAssetWithCustomHash(ref clipBlob, animationHash); } else { Debug.Log($"Animation '{a.name}' is duplicate!"); } alreadyBakedAnimations[i] = clipBlob; baker.DependsOn(a); } if (objectCopy != null) GameObject.DestroyImmediate(objectCopy); return alreadyBakedAnimations; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public BlobAssetReference CreateAnimationBlobAsset(AnimationClip ac, Animator animator, Hash128 animationHash) { var avatar = animator?.avatar; var acSettings = AnimationUtility.GetAnimationClipSettings(ac); var bb = new BlobBuilder(Allocator.Temp); ref var rv = ref bb.ConstructRoot(); #if RUKHANKA_DEBUG_INFO var startTimeMarker = Time.realtimeSinceStartupAsDouble; if (ac.name.Length > 0) bb.AllocateString(ref rv.name, ac.name); #endif rv.length = ac.length; rv.looped = ac.isLooping; rv.hash = animationHash; rv.loopPoseBlend = acSettings.loopBlend; rv.cycleOffset = acSettings.cycleOffset; BakeAnimationEvents(bb, ref rv, ac); BakeTrackSet(bb, ref rv.clipTracks, out var maxTrackKeyframeLength, -1, ac, animator, avatar); if (acSettings.additiveReferencePoseClip != null) BakeTrackSet(bb, ref rv.additiveReferencePoseFrame, out _, acSettings.additiveReferencePoseTime, acSettings.additiveReferencePoseClip, animator, avatar); #if RUKHANKA_DEBUG_INFO var dt = Time.realtimeSinceStartupAsDouble - startTimeMarker; rv.bakingTime = (float)dt; #endif rv.maxTrackKeyframeLength = maxTrackKeyframeLength; var bar = bb.CreateBlobAssetReference(Allocator.Persistent); // Save baked animation into cache BlobCache.SaveBakedAnimationToCache(ac, avatar, bar); return bar; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Keyframe[] GetKeyFramesArray(UnityEngine.AnimationCurve animationCurve, float frameTime) { if (frameTime < 0) return animationCurve.keys; var oneFrameAnimation = new Keyframe[] { new (0, animationCurve.Evaluate(frameTime)) }; return oneFrameAnimation; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void BakeTrackSet(BlobBuilder bb, ref TrackSet outTrackSet, out uint maxTrackKeyframeLength, float frameTime, AnimationClip ac, Animator animator, Avatar avatar) { var bindings = AnimationUtility.GetCurveBindings(ac); keyFramesList.Clear(); trackList.Clear(); trackGroupHashes.Clear(); trackGroupOffsets.Clear(); #if RUKHANKA_DEBUG_INFO trackGroupNames.Clear(); #endif maxTrackKeyframeLength = 0u; foreach (var b in bindings) { var ec = AnimationUtility.GetEditorCurve(ac, b); var pb = ParseCurveBinding(ac, b, animator?.avatar); var inKeyframes = GetKeyFramesArray(ec, frameTime); var keyFramesRange = CopyKeyFrames(inKeyframes, ref keyFramesList); var trackData = CreateTrackData(keyFramesRange, pb); InsertTrackIntoGroup(trackData, pb.boneName); maxTrackKeyframeLength = math.max(maxTrackKeyframeLength, (uint)keyFramesRange.y); } if (avatar != null) { // Sample root and hips curves and from unity animations. Maybe sometime I will figure out all RootT/RootQ and body pose generation formulas and this step could be replaced with generation. SampleMissingCurves(ac, animator, frameTime); } // Create special end empty track group because track group tracks count is calculated as trackGroup[i + 1].trackCount - trackGroup[i].trackCount trackGroupHashes.Add(0xffffffff); trackGroupOffsets.Add((uint)trackList.Length); #if RUKHANKA_DEBUG_INFO trackGroupNames.Add("RUKHANKA_TRAILING_TRACK"); #endif CreateTrackSetBlob(ref bb, ref outTrackSet, ac.name); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// unsafe void CreateTrackSetBlob(ref BlobBuilder bb, ref TrackSet outTrackSet, string animationName) { var keyFramesBlobArray = bb.Allocate(ref outTrackSet.keyframes, keyFramesList.Length); UnsafeUtility.MemCpy(keyFramesBlobArray.GetUnsafePtr(), keyFramesList.GetUnsafeReadOnlyPtr(), keyFramesList.Length * UnsafeUtility.SizeOf()); var tracksBlobArray = bb.Allocate(ref outTrackSet.tracks, trackList.Length); UnsafeUtility.MemCpy(tracksBlobArray.GetUnsafePtr(), trackList.GetUnsafeReadOnlyPtr(), trackList.Length * UnsafeUtility.SizeOf()); var trackGroupsBlobArray = bb.Allocate(ref outTrackSet.trackGroups, trackGroupOffsets.Length); UnsafeUtility.MemCpy(trackGroupsBlobArray.GetUnsafePtr(), trackGroupOffsets.GetUnsafeReadOnlyPtr(), trackGroupOffsets.Length * UnsafeUtility.SizeOf()); // Make a hash table from track group hashes var phtIsCreated = Perfect2HashTable.Build(trackGroupHashes.AsArray(), out var pht, out var seed); Assert.IsTrue(phtIsCreated, $"Cannot create track perfect hash table for animation {animationName}"); var phtBlobArray = bb.Allocate(ref outTrackSet.trackGroupPHT.pht, pht.Length); UnsafeUtility.MemCpy(phtBlobArray.GetUnsafePtr(), pht.GetUnsafeReadOnlyPtr(), pht.Length * UnsafeUtility.SizeOf()); #if RUKHANKA_DEBUG_INFO var trackGroupDebugInfoArr = bb.Allocate(ref outTrackSet.trackGroupDebugInfo, trackGroupHashes.Length); for (var i = 0; i < trackGroupHashes.Length; ++i) { var trackGroupInfoBlob = new TrackGroupInfo() { hash = trackGroupHashes[i], name = trackGroupNames[i] }; trackGroupDebugInfoArr[i] = trackGroupInfoBlob; } #endif outTrackSet.trackGroupPHT.seed = seed; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void MakeAdditiveReferencePoseFrame(ref NativeList bc, AnimationClipSettings acs, Avatar avatar) { /* if (acs.additiveReferencePoseClip == null) return; for (var k = 0; k < bc.Length; ++k) { var bcc = bc[k]; for (var m = 0; m < bcc.animationCurves.Length; ++m) { ref var ac = ref bcc.animationCurves.ElementAt(m); } } var clipPose = SampleAnimation(acs.additiveReferencePoseClip, avatar, acs.additiveReferencePoseTime); foreach (var c in clipPose) { for (var k = 0; k < bc.Length; ++k) { var bcc = bc[k]; if (c.pb.boneName != bcc.name) continue; for (var m = 0; m < bcc.animationCurves.Length; ++m) { ref var ac = ref bcc.animationCurves.ElementAt(m); if (ac.bindingType == BindingType.Scale || (ac.bindingType == BindingType.Quaternion && ac.channelIndex == 3)) ac.additiveReferenceValue = 1; if (ac.bindingType == c.pb.bindingType && ac.channelIndex == c.pb.channelIndex) { ac.additiveReferenceValue = c.value; break; } } } } */ } } } #endif