using System; using System.Runtime.CompilerServices; using Rukhanka.Toolbox; using Unity.Burst; using Unity.Burst.CompilerServices; using Unity.Burst.Intrinsics; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; //=================================================================================================================// [assembly: InternalsVisibleTo("Rukhanka.Tests")] namespace Rukhanka { partial struct AnimationProcessSystem { [BurstCompile] public struct ComputeBoneAnimationJob: IJobParallelForDefer { [NativeDisableParallelForRestriction] public NativeList animatedBonesBuffer; [NativeDisableParallelForRestriction] public NativeList boneTransformFlagsArr; [ReadOnly] public NativeList boneToEntityArr; [ReadOnly] public BufferLookup animationsToProcessLookup; [ReadOnly] public NativeList rigDefs; [ReadOnly] public NativeList entityArr; [NativeDisableParallelForRestriction] public BufferLookup rootMotionAnimStateBufferLookup; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// [SkipLocalsInit] public void Execute(int globalBoneIndex) { var boneToEntityIndex = boneToEntityArr[globalBoneIndex]; var (rigBoneIndex, entityIndex, rigBoneCount) = (boneToEntityIndex.y & 0xffff, boneToEntityIndex.x, boneToEntityIndex.y >> 16); var e = entityArr[entityIndex]; var rigDef = rigDefs[entityIndex]; var rigBlobAsset = rigDef.rigBlob; ref var rb = ref rigBlobAsset.Value.bones[rigBoneIndex]; var animationsToProcess = animationsToProcessLookup[e]; // Early exit if no animations if (animationsToProcess.IsEmpty) return; var transformFlags = RuntimeAnimationData.GetAnimationTransformFlagsRW(boneToEntityArr, boneTransformFlagsArr, globalBoneIndex, rigBoneCount); GetHumanRotationDataForSkeletonBone(out var humanBoneInfo, ref rigBlobAsset.Value.humanData, rigBoneIndex); // There are separate tracks for root motion var boneNameHash = rb.hash; if (rigDef.applyRootMotion && (rigBlobAsset.Value.rootBoneIndex == rigBoneIndex || rigBoneIndex == 0)) boneNameHash = ModifyBoneHashForRootMotion(boneNameHash); var rootMotionDeltaBone = rigDef.applyRootMotion && rigBoneIndex == 0; PrepareRootMotionStateBuffers(e, animationsToProcess, out var curRootMotionState, out var newRootMotionState, rootMotionDeltaBone); // Reference pose for root motion delta should be identity var refPose = Hint.Unlikely(rootMotionDeltaBone) ? BoneTransform.Identity() : rb.refPose; var blendedBonePose = refPose; var layerPose = new BoneTransform(); var weightSum = 0.0f; float3 layerFlags = 0; float3 totalFlags = 0; LayerInfo layerInfo = default; for (int ai = 0; ai < animationsToProcess.Length; ++ai) { var atp = animationsToProcess[ai]; // Root bone should be always included in animation computation var inAvatarMask = IsBoneInAvatarMask(rigBoneIndex, rb.humanBodyPart, atp.avatarMask) || rootMotionDeltaBone; if (atp.animation == BlobAssetReference.Null || atp.weight == 0 || atp.layerWeight == 0 || !inAvatarMask) continue; var curLayerInfo = GetLayerInfoFromAnimation(atp); // Apply layer animations if (layerInfo.index != curLayerInfo.index) { blendedBonePose = BlendLayerPose(blendedBonePose, layerPose, refPose, layerInfo, weightSum, layerFlags); weightSum = 0; layerFlags = 0; layerPose = new BoneTransform(); } layerInfo = curLayerInfo; var animTime = NormalizeAnimationTime(atp.time, ref atp.animation.Value); ref var clipTracks = ref atp.animation.Value.clipTracks; var boneHasAnimation = SampleAnimation ( ref atp.animation.Value, animTime, rigBoneIndex, boneNameHash, atp.blendMode, humanBoneInfo, out var bonePose, out var flags, out var trackRange ); if (boneHasAnimation) { if (Hint.Unlikely(rootMotionDeltaBone)) ProcessRootMotionDeltas(ref bonePose, ref clipTracks, trackRange, atp, curRootMotionState, ref newRootMotionState); weightSum += atp.weight; layerFlags += flags; totalFlags += flags; layerPose = AppendScaledPose(layerPose, bonePose, atp.weight); } } // Apply top layer pose blendedBonePose = BlendLayerPose(blendedBonePose, layerPose, refPose, layerInfo, weightSum, layerFlags); SetTransformFlags(totalFlags, transformFlags, rigBoneIndex); animatedBonesBuffer[globalBoneIndex] = blendedBonePose; if (rootMotionDeltaBone) SetRootMotionStateToComponentBuffer(newRootMotionState, e); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BoneTransform BlendLayerPose(in BoneTransform curPose, BoneTransform layerPose, in BoneTransform refPose, in LayerInfo layerInfo, float weightSum, float3 layerFlags) { BoneTransform rv = curPose; if (Hint.Likely(layerInfo.blendMode == AnimationBlendingMode.Override)) { // For override layers we need to apply reminder from the reference pose if layer total animation weight is not equal to one if (Hint.Unlikely(weightSum < 1)) layerPose = AppendScaledPose(layerPose, refPose, math.max(0, 1 - weightSum)); if (Hint.Likely(layerFlags.x > 0)) { rv.pos = math.lerp(curPose.pos, layerPose.pos, layerInfo.weight); } if (Hint.Likely(layerFlags.y > 0)) { layerPose.rot = math.normalizesafe(layerPose.rot); layerPose.rot = MathUtils.ShortestRotation(curPose.rot, layerPose.rot); rv.rot = math.nlerp(curPose.rot.value, layerPose.rot.value, layerInfo.weight); } if (Hint.Likely(layerFlags.z > 0)) { rv.scale = math.lerp(curPose.scale, layerPose.scale, layerInfo.weight); } } else { if (Hint.Likely(layerFlags.x > 0)) { rv.pos = curPose.pos + layerPose.pos * layerInfo.weight; } if (Hint.Likely(layerFlags.y > 0)) { quaternion layerRot = math.normalizesafe(new float4(layerPose.rot.value.xyz * layerInfo.weight, layerPose.rot.value.w)); layerRot = MathUtils.ShortestRotation(curPose.rot, layerRot); rv.rot = math.mul(curPose.rot, layerRot); } if (Hint.Likely(layerFlags.z > 0)) { rv.scale = curPose.scale * math.lerp(1, layerPose.scale, layerInfo.weight); } } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BoneTransform AppendScaledPose(in BoneTransform curPose, BoneTransform addedPose, float weight) { addedPose.rot = MathUtils.ShortestRotation(curPose.rot, addedPose.rot); BoneTransform rv = new BoneTransform() { pos = curPose.pos + addedPose.pos * weight, rot = new quaternion(curPose.rot.value + addedPose.rot.value * weight), scale = curPose.scale + addedPose.scale * weight }; return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public static uint ModifyBoneHashForRootMotion(uint h) { var rv = math.hash(new uint2(h, h * 2)); return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void PrepareRootMotionStateBuffers ( Entity e, in DynamicBuffer atps, out NativeArray curRootMotionState, out NativeList newRootMotionState, bool isRootMotionBone ) { curRootMotionState = default; newRootMotionState = default; if (Hint.Likely(!isRootMotionBone)) return; if (rootMotionAnimStateBufferLookup.HasBuffer(e)) curRootMotionState = rootMotionAnimStateBufferLookup[e].AsNativeArray(); newRootMotionState = new NativeList(atps.Length, Allocator.Temp); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ProcessRootMotionDeltas ( ref BoneTransform bonePose, ref TrackSet trackSet, int2 trackRange, in AnimationToProcessComponent atp, in NativeArray curRootMotionState, ref NativeList newRootMotionState ) { // Special care for root motion animation loops HandleRootMotionLoops(ref bonePose, ref trackSet, trackRange, atp); BoneTransform rootMotionPrevPose = bonePose; // Find animation history in history buffer var historyBufferIndex = 0; for (; curRootMotionState.IsCreated && historyBufferIndex < curRootMotionState.Length && curRootMotionState[historyBufferIndex].uniqueMotionId != atp.motionId; ++historyBufferIndex){ } var initialFrame = historyBufferIndex >= curRootMotionState.Length; if (Hint.Unlikely(!initialFrame)) { rootMotionPrevPose = curRootMotionState[historyBufferIndex].animationState; } var rmasc = new RootMotionAnimationStateComponent() { uniqueMotionId = atp.motionId, animationState = bonePose }; newRootMotionState.Add(rmasc); var invPrevPose = BoneTransform.Inverse(rootMotionPrevPose); var deltaPose = BoneTransform.Multiply(invPrevPose, bonePose); bonePose = deltaPose; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void SetRootMotionStateToComponentBuffer(in NativeList newRootMotionData, Entity e) { rootMotionAnimStateBufferLookup[e].CopyFrom(newRootMotionData.AsArray()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void SetTransformFlags(float3 flags, in AnimationTransformFlags flagArr, int boneIndex) { if (flags.x > 0) flagArr.SetTranslationFlag(boneIndex); if (flags.y > 0) flagArr.SetRotationFlag(boneIndex); if (flags.z > 0) flagArr.SetScaleFlag(boneIndex); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void GetHumanRotationDataForSkeletonBone(out HumanRotationData rv, ref BlobPtr hd, int rigBoneIndex) { rv = default; if (hd.IsValid) { rv = hd.Value.humanRotData[rigBoneIndex]; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// internal static float3 MuscleRangeToRadians(float3 minA, float3 maxA, float3 muscle) { // Map [-1; +1] range into [minRot; maxRot] var negativeRange = math.min(muscle, 0); var positiveRange = math.max(0, muscle); var negativeRot = math.lerp(0, minA, -negativeRange); var positiveRot = math.lerp(0, maxA, +positiveRange); var rv = negativeRot + positiveRot; return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void MuscleValuesToQuaternion(in HumanRotationData humanBoneInfo, ref BoneTransform bt) { var r = MuscleRangeToRadians(humanBoneInfo.minMuscleAngles, humanBoneInfo.maxMuscleAngles, bt.rot.value.xyz); r *= humanBoneInfo.sign; var qx = quaternion.AxisAngle(math.right(), r.x); var qy = quaternion.AxisAngle(math.up(), r.y); var qz = quaternion.AxisAngle(math.forward(), r.z); var qzy = math.mul(qz, qy); qzy.value.x = 0; bt.rot = math.mul(math.normalize(qzy), qx); ApplyHumanoidPostTransform(humanBoneInfo, ref bt); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public static float2 NormalizeAnimationTime(float at, ref AnimationClipBlob ac) { at += ac.cycleOffset; if (at < 0) at = 1 + at; var normalizedTime = ac.looped ? math.frac(at) : math.saturate(at); var rv = normalizedTime * ac.length; return new (rv, normalizedTime); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void CalculateLoopPose(ref TrackSet trackSet, int2 trackRange, ref BoneTransform bonePose, in HumanRotationData hrd, float normalizedTime) { var lerpFactor = normalizedTime; var rootPoseStart = GetTransformFrame(ref trackSet, trackRange, hrd, TrackFrame.First); var rootPoseEnd = GetTransformFrame(ref trackSet, trackRange, hrd, TrackFrame.Last); var dPos = rootPoseEnd.pos - rootPoseStart.pos; var dRot = math.mul(math.conjugate(rootPoseEnd.rot), rootPoseStart.rot); bonePose.pos -= dPos * lerpFactor; bonePose.rot = math.mul(bonePose.rot, math.slerp(quaternion.identity, dRot, lerpFactor)); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void HandleRootMotionLoops(ref BoneTransform bonePose, ref TrackSet ts, int2 trackRange, in AnimationToProcessComponent atp) { ref var animBlob = ref atp.animation.Value; if (!animBlob.looped) return; var numLoopCycles = (int)math.floor(atp.time + atp.animation.Value.cycleOffset); var cycleSign = math.sign(numLoopCycles); numLoopCycles = math.abs(numLoopCycles); if (numLoopCycles < 1) return; var endFramePose = GetTransformFrame(ref ts, trackRange, default, TrackFrame.Last); var startFramePose = GetTransformFrame(ref ts, trackRange, default, TrackFrame.First); var deltaPose = BoneTransform.Multiply(endFramePose, BoneTransform.Inverse(startFramePose)); if (cycleSign < 0) deltaPose = BoneTransform.Inverse(deltaPose); BoneTransform accumCyclePose = BoneTransform.Identity(); for (var c = numLoopCycles; c > 0; c >>= 1) { if ((c & 1) == 1) accumCyclePose = BoneTransform.Multiply(accumCyclePose, deltaPose); deltaPose = BoneTransform.Multiply(deltaPose, deltaPose); } bonePose = BoneTransform.Multiply(accumCyclePose, bonePose); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BoneTransform MixPoses(in BoneTransform curPose, BoneTransform inPose, float3 flags, float weight, AnimationBlendingMode blendMode) { BoneTransform rv = curPose; if (Hint.Likely(blendMode == AnimationBlendingMode.Override)) { if (Hint.Likely(flags.x > 0)) { rv.pos = math.lerp(curPose.pos, inPose.pos, weight); } if (Hint.Likely(flags.y > 0)) { inPose.rot = MathUtils.ShortestRotation(curPose.rot, inPose.rot); rv.rot = math.nlerp(curPose.rot.value, inPose.rot.value, weight); } if (Hint.Likely(flags.z > 0)) { rv.scale = math.lerp(curPose.scale, inPose.scale, weight); } } else { if (Hint.Likely(flags.x > 0)) { rv.pos = curPose.pos + inPose.pos * weight; } if (Hint.Likely(flags.y > 0)) { quaternion layerRot = math.normalizesafe(new float4(inPose.rot.value.xyz * weight, inPose.rot.value.w)); layerRot = MathUtils.ShortestRotation(curPose.rot, layerRot); rv.rot = math.mul(curPose.rot, layerRot); } if (Hint.Likely(flags.z > 0)) { rv.scale = curPose.scale * math.lerp(1, inPose.scale, weight); } } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ApplyHumanoidPostTransform(HumanRotationData hrd, ref BoneTransform bt) { bt.rot = math.mul(math.mul(hrd.preRot, bt.rot), hrd.postRot); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public static bool IsBoneInAvatarMask(int boneIndex, AvatarMaskBodyPart humanAvatarMaskBodyPart, BlobAssetReference am) { // If no avatar mask defined or bone hash is all zeroes assume that bone included if (!am.IsCreated || boneIndex < 0) return true; return (int)humanAvatarMaskBodyPart >= 0 ? IsBoneInHumanAvatarMask(humanAvatarMaskBodyPart, am) : am.Value.IsBoneIncluded(boneIndex); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public static bool IsBoneInHumanAvatarMask(AvatarMaskBodyPart humanBoneAvatarMaskIndex, BlobAssetReference am) { var rv = (am.Value.humanBodyPartsAvatarMask & 1 << (int)humanBoneAvatarMaskIndex) != 0; return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool SampleAnimation ( ref AnimationClipBlob acb, float2 animTime, int rigBoneIndex, uint boneHash, AnimationBlendingMode blendMode, in HumanRotationData hrd, out BoneTransform animatedPose, out float3 flags, out int2 clipTrackRange ) { animatedPose = BoneTransform.Identity(); flags = 0; clipTrackRange = 0; var trackIndex = acb.clipTracks.GetTrackGroupIndex(boneHash); if (Hint.Unlikely(trackIndex < 0)) return false; var time = animTime.x; var timeNrm = animTime.y; clipTrackRange = new int2(acb.clipTracks.trackGroups[trackIndex], acb.clipTracks.trackGroups[trackIndex + 1]); // This is safe because of trailing trackGroup (animatedPose, flags) = ProcessAnimationCurves(ref acb.clipTracks, clipTrackRange, hrd, time); // Make additive animation if requested var isAnimationAdditive = blendMode == AnimationBlendingMode.Additive; if (Hint.Unlikely(isAnimationAdditive)) { // Get separate additive frame if requested ref var additiveTrackSet = ref Hint.Unlikely(acb.additiveReferencePoseFrame.keyframes.Length > 0) ? ref acb.additiveReferencePoseFrame : ref acb.clipTracks; var additiveTrackIndex = additiveTrackSet.trackGroupPHT.Query(boneHash); if (additiveTrackIndex >= 0) { var additivePoseTrackRange = new int2(additiveTrackSet.trackGroups[additiveTrackIndex], additiveTrackSet.trackGroups[additiveTrackIndex + 1]); var additiveFramePose = GetTransformFrame(ref additiveTrackSet, additivePoseTrackRange, hrd, TrackFrame.First); MakeAdditiveAnimation(ref animatedPose, additiveFramePose); } } // Loop Pose calculus for all bones except root motion var calculateLoopPose = acb.loopPoseBlend && rigBoneIndex != 0; if (Hint.Unlikely(calculateLoopPose)) { CalculateLoopPose(ref acb.clipTracks, clipTrackRange, ref animatedPose, hrd, timeNrm); } return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void MakeAdditiveAnimation(ref BoneTransform rv, in BoneTransform zeroFramePose) { // If additive layer make difference between reference pose and current animated pose rv.pos = rv.pos - zeroFramePose.pos; var conjugateZFRot = math.normalizesafe(math.conjugate(zeroFramePose.rot)); conjugateZFRot = MathUtils.ShortestRotation(rv.rot, conjugateZFRot); rv.rot = math.mul(conjugateZFRot, rv.rot); rv.scale = rv.scale / zeroFramePose.scale; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BoneTransform GetTransformFrame(ref TrackSet ts, int2 trackRange, HumanRotationData hrd, TrackFrame tf) { var rv = BoneTransform.Identity(); bool eulerToQuaternion = false; var isHumanMuscle = false; for (int i = trackRange.x; i < trackRange.y; ++i) { var track = ts.tracks[i]; var kfIndex = track.keyFrameRange.x; if (tf == TrackFrame.Last) kfIndex += track.keyFrameRange.y - 1; var trackValue = ts.keyframes[kfIndex].v; var ci = (int)track.channelIndex; switch (track.bindingType) { case BindingType.Translation: rv.pos[ci] = trackValue; break; case BindingType.Quaternion: rv.rot.value[ci] = trackValue; break; case BindingType.EulerAngles: rv.rot.value[ci] = trackValue; eulerToQuaternion = true; break; case BindingType.HumanMuscle: rv.rot.value[ci] = trackValue; isHumanMuscle = true; break; case BindingType.Scale: rv.scale[ci] = trackValue; break; } } // If we have got Euler angles instead of quaternion, convert them here if (eulerToQuaternion) { rv.rot = quaternion.EulerZXY(math.radians(rv.rot.value.xyz)); } if (isHumanMuscle) { MuscleValuesToQuaternion(hrd, ref rv); } return rv; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// (BoneTransform, float3) ProcessAnimationCurves(ref TrackSet ts, int2 trackRange, HumanRotationData hrd, float time) { var rv = BoneTransform.Identity(); bool eulerToQuaternion = false; float3 flags = 0; var isHumanMuscle = false; for (int i = trackRange.x; i < trackRange.y; ++i) { var track = ts.tracks[i]; var interpolatedCurveValue = BlobCurve.SampleAnimationCurve(ref ts, i, time); var ci = (int)track.channelIndex; switch (track.bindingType) { case BindingType.Translation: rv.pos[ci] = interpolatedCurveValue; flags.x = 1; break; case BindingType.Quaternion: rv.rot.value[ci] = interpolatedCurveValue; flags.y = 1; break; case BindingType.EulerAngles: eulerToQuaternion = true; rv.rot.value[ci] = interpolatedCurveValue; flags.y = 1; break; case BindingType.HumanMuscle: rv.rot.value[ci] = interpolatedCurveValue; isHumanMuscle = true; flags.y = 1; break; case BindingType.Scale: rv.scale[ci] = interpolatedCurveValue; flags.z = 1; break; } } // If we have got Euler angles instead of quaternion, convert them here if (eulerToQuaternion) { rv.rot = quaternion.EulerZXY(math.radians(rv.rot.value.xyz)); } if (isHumanMuscle) { MuscleValuesToQuaternion(hrd, ref rv); } return (rv, flags); } } //=================================================================================================================// [BurstCompile] [WithNone(typeof(GPUAnimationEngineTag))] partial struct ProcessAnimatorParameterCurveJob: IJobEntity { void Execute(in DynamicBuffer animationsToProcess, ref DynamicBuffer acpc) { if (animationsToProcess.IsEmpty) return; for (var i = 0; i < acpc.Length; ++i) { ref var p = ref acpc.ElementAt(i); var parameterNameHash = Track.CalculateHash(p.hash); p.value.floatValue = ComputeAnimatedProperty(p.value.floatValue, animationsToProcess.AsNativeArray(), SpecialBones.AnimatorTypeNameHash, parameterNameHash); } } } //=================================================================================================================// [BurstCompile] [WithNone(typeof(GPUAnimationEngineTag))] partial struct AnimateBlendShapeWeightsJob: IJobEntity { [ReadOnly] public BufferLookup animationToProcessLookup; [ReadOnly] public ComponentLookup cullAnimationsLookup; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void Execute(DynamicBuffer blendShapeWeights, in SkinnedMeshRendererComponent asm) { if (IsAnimationsCulled(asm.animatedRigEntity) || !animationToProcessLookup.TryGetBuffer(asm.animatedRigEntity, out var animationsToProcess)) return; if (animationsToProcess.IsEmpty) return; for (var i = 0; i < blendShapeWeights.Length; ++i) { ref var p = ref blendShapeWeights.ElementAt(i); var bsi = asm.smrInfoBlob.Value.blendShapes[i].hash; var parameterNameHash = Track.CalculateHash(bsi); p.Value = ComputeAnimatedProperty(0, animationsToProcess.AsNativeArray(), asm.nameHash, parameterNameHash); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool IsAnimationsCulled(Entity animatedEntity) { return cullAnimationsLookup.HasComponent(animatedEntity) && cullAnimationsLookup.IsComponentEnabled(animatedEntity); } } //=================================================================================================================// [BurstCompile] struct CalculateBoneOffsetsJob: IJobChunk { [ReadOnly] public ComponentTypeHandle rigDefinitionTypeHandle; [ReadOnly] public ComponentLookup cullAnimationsTagLookup; [ReadOnly] public NativeArray chunkBaseEntityIndices; [ReadOnly] public NativeList entities; [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeList bonePosesOffsets; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { var rigDefAccessor = chunk.GetNativeArray(ref rigDefinitionTypeHandle); int baseEntityIndex = chunkBaseEntityIndices[unfilteredChunkIndex]; int validEntitiesInChunk = 0; var cee = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count); bonePosesOffsets[0] = 0; while (cee.NextEntityIndex(out var i)) { var rigDef = rigDefAccessor[i]; int entityInQueryIndex = baseEntityIndex + validEntitiesInChunk; ++validEntitiesInChunk; var e = entities[entityInQueryIndex]; var boneCount = GetRigBoneCountWithRespectToCulling(e, rigDef, cullAnimationsTagLookup); var v = new int2 ( // Bone count boneCount, // Number of ulong values that can hold bone transform flags (boneCount * 4 >> 6) + 1 ); bonePosesOffsets[entityInQueryIndex + 1] = v; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int GetRigBoneCountWithRespectToCulling(Entity e, in RigDefinitionComponent rdc, in ComponentLookup cullAnimationsTagLookup) { if (!cullAnimationsTagLookup.HasComponent(e) || !cullAnimationsTagLookup.IsComponentEnabled(e)) return rdc.rigBlob.Value.bones.Length; return 1; } } //=================================================================================================================// [BurstCompile] struct CalculatePerBoneInfoJob: IJobChunk { [ReadOnly] public NativeArray chunkBaseEntityIndices; [ReadOnly] public NativeList bonePosesOffsets; [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeList boneToEntityIndices; public ComponentTypeHandle rigDefinitionTypeHandle; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { var rigDefArray = chunk.GetNativeArray(ref rigDefinitionTypeHandle); int baseEntityIndex = chunkBaseEntityIndices[unfilteredChunkIndex]; int validEntitiesInChunk = 0; var cee = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count); while (cee.NextEntityIndex(out var i)) { int entityInQueryIndex = baseEntityIndex + validEntitiesInChunk; ++validEntitiesInChunk; var offset = bonePosesOffsets[entityInQueryIndex]; var nextOffset = bonePosesOffsets[entityInQueryIndex + 1]; // This is always valid because we have entities count + 1 array var boneCount = nextOffset.x - offset.x; var boneCountHighWORD = boneCount << 16; for (int k = 0; k < boneCount; ++k) { var boneIndexAndBoneCount = k | boneCountHighWORD; boneToEntityIndices[k + offset.x] = new int3(entityInQueryIndex, boneIndexAndBoneCount, offset.y); } var rigFrameData = new DynamicFrameData() { bonePoseOffset = offset.x, boneFlagsOffset = offset.y, rigBoneCount = boneCount, }; var rigDef = rigDefArray[i]; rigDef.dynamicFrameData = rigFrameData; rigDefArray[i] = rigDef; } } } //=================================================================================================================// [BurstCompile] struct DoPrefixSumJob: IJob { public NativeList boneOffsets; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void Execute() { var sum = new int2(0); for (int i = 0; i < boneOffsets.Length; ++i) { var v = boneOffsets[i]; sum += v; boneOffsets[i] = sum; } } } //=================================================================================================================// [BurstCompile] struct ResizeDataBuffersJob: IJob { [ReadOnly] public NativeList boneOffsets; public RuntimeAnimationData runtimeData; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void Execute() { var boneBufferLen = boneOffsets[^1]; runtimeData.animatedBonesBuffer.Resize(boneBufferLen.x, NativeArrayOptions.UninitializedMemory); runtimeData.worldSpaceBonesBuffer.Resize(boneBufferLen.x, NativeArrayOptions.UninitializedMemory); runtimeData.boneToEntityArr.Resize(boneBufferLen.x, NativeArrayOptions.UninitializedMemory); // Clear flags by two resizes runtimeData.boneTransformFlagsHolderArr.Resize(0, NativeArrayOptions.UninitializedMemory); runtimeData.boneTransformFlagsHolderArr.Resize(boneBufferLen.y, NativeArrayOptions.ClearMemory); } } //=================================================================================================================// [BurstCompile] partial struct CopyEntityBoneTransformsToAnimationBuffer: IJobEntity { [NativeDisableContainerSafetyRestriction] public NativeList animatedBoneTransforms; [ReadOnly] public ComponentLookup rigDefComponentLookup; [ReadOnly] public ComponentLookup gpuAnimationEngineTagLookup; [ReadOnly] public ComponentLookup parentComponentLookup; [ReadOnly] public ComponentLookup ptmComponentLookup; [NativeDisableContainerSafetyRestriction] public NativeList boneTransformFlags; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void Execute(Entity e, in AnimatorEntityRefComponent aer, in LocalTransform lt) { if (!rigDefComponentLookup.TryGetComponent(aer.animatorEntity, out var rdc)) return; if (gpuAnimationEngineTagLookup.IsComponentEnabled(aer.animatorEntity)) return; var entityBoneData = rdc.dynamicFrameData; if (entityBoneData.bonePoseOffset < 0) return; // If animation calculation was culled, we need operate only on valid bone range if (aer.boneIndexInAnimationRig >= entityBoneData.rigBoneCount) return; var bonePoses = RuntimeAnimationData.GetAnimationDataForRigRW(animatedBoneTransforms, entityBoneData.bonePoseOffset, entityBoneData.rigBoneCount); var transformFlags = AnimationTransformFlags.CreateFromBufferRW(boneTransformFlags, entityBoneData.boneFlagsOffset, entityBoneData.rigBoneCount); var boneFlags = new bool3 ( transformFlags.IsTranslationSet(aer.boneIndexInAnimationRig), transformFlags.IsRotationSet(aer.boneIndexInAnimationRig), transformFlags.IsScaleSet(aer.boneIndexInAnimationRig) ); if (!math.any(boneFlags)) { var entityPose = new BoneTransform(lt); if (ptmComponentLookup.TryGetComponent(e, out var ptm)) entityPose = new BoneTransform(lt, ptm); // Root motion delta should be zero if (rdc.applyRootMotion && aer.boneIndexInAnimationRig == 0) entityPose = BoneTransform.Identity(); ref var bonePose = ref bonePoses[aer.boneIndexInAnimationRig]; if (!boneFlags.x) bonePose.pos = entityPose.pos; if (!boneFlags.y) bonePose.rot = entityPose.rot; if (!boneFlags.z) bonePose.scale = entityPose.scale; // For entities without parent we indicate that bone pose is in world space if (!parentComponentLookup.HasComponent(e)) transformFlags.SetAbsoluteTransformFlag(aer.boneIndexInAnimationRig); } } } //=================================================================================================================// [BurstCompile] struct MakeAbsoluteTransformsJob: IJobChunk { [ReadOnly] public ComponentTypeHandle rigDefTypeHandle; [NativeDisableContainerSafetyRestriction] public NativeList localBoneTransforms; [NativeDisableContainerSafetyRestriction] public NativeList worldBoneTransforms; [ReadOnly] public NativeList boneTransformFlags; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { var rigDefArray = chunk.GetNativeArray(ref rigDefTypeHandle); var cee = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count); var flagsHolder = new NativeBitArray(0xff, Allocator.Temp, NativeArrayOptions.UninitializedMemory); while (cee.NextEntityIndex(out var i)) { var rigDef = rigDefArray[i]; ref var rigBones = ref rigDef.rigBlob.Value.bones; var rigFrameData = rigDef.dynamicFrameData; var rigBonesCount = rigFrameData.rigBoneCount; flagsHolder.Resize(rigBonesCount); flagsHolder.Clear(); var localBoneTransformsForRig = localBoneTransforms.GetSpan(rigFrameData.bonePoseOffset, rigBonesCount); var worldBoneTransformsForRig = worldBoneTransforms.GetSpan(rigFrameData.bonePoseOffset, rigBonesCount); var boneFlags = AnimationTransformFlags.CreateFromBufferRO(boneTransformFlags, rigFrameData.boneFlagsOffset, rigBonesCount); // Iterate over all animated bones and calculate absolute transform in-place for (int animationBoneIndex = 0; animationBoneIndex < rigBonesCount; ++animationBoneIndex) { if (boneFlags.IsAbsoluteTransform(animationBoneIndex)) { flagsHolder.Set(animationBoneIndex, true); worldBoneTransformsForRig[animationBoneIndex] = localBoneTransformsForRig[animationBoneIndex]; } MakeAbsoluteTransform(flagsHolder, animationBoneIndex, localBoneTransformsForRig, worldBoneTransformsForRig, rigDef.rigBlob); } // For all initially absolute bones calculate local transforms for (int animationBoneIndex = 0; animationBoneIndex < rigBonesCount; ++animationBoneIndex) { var parentBoneIndex = rigBones[animationBoneIndex].parentBoneIndex; if (!boneFlags.IsAbsoluteTransform(animationBoneIndex) || parentBoneIndex < 0) continue; var parentWorldTransform = worldBoneTransformsForRig[parentBoneIndex]; var worldTransform = worldBoneTransformsForRig[animationBoneIndex]; var localTransform = BoneTransform.Multiply(BoneTransform.Inverse(parentWorldTransform), worldTransform); localBoneTransformsForRig[animationBoneIndex] = localTransform; } } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void MakeAbsoluteTransform ( NativeBitArray absTransformFlags, int boneIndex, Span localBoneTransformsForRig, Span worldBoneTransformsForRig, in BlobAssetReference rigBlob ) { var resultBoneTransform = BoneTransform.Identity(); var myBoneIndex = boneIndex; ref var rigBones = ref rigBlob.Value.bones; bool absTransformFlag; do { absTransformFlag = absTransformFlags.IsSet(boneIndex); var animatedBoneTransform = absTransformFlag ? worldBoneTransformsForRig[boneIndex] : localBoneTransformsForRig[boneIndex]; resultBoneTransform = BoneTransform.Multiply(animatedBoneTransform, resultBoneTransform); boneIndex = rigBones[boneIndex].parentBoneIndex; } while (boneIndex >= 0 && !absTransformFlag); worldBoneTransformsForRig[myBoneIndex] = resultBoneTransform; absTransformFlags.Set(myBoneIndex, true); } } //=================================================================================================================// [BurstCompile] [WithDisabled(typeof(GPUAnimationEngineTag))] partial struct ComputeRootMotionJob: IJobEntity { [NativeDisableContainerSafetyRestriction] public NativeList animatedBonePoses; [ReadOnly] public ComponentLookup parentLookup; [ReadOnly] public ComponentLookup ptmLookup; [ReadOnly] public ComponentLookup localTransformLookup; public float deltaTime; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void Execute(Entity e, in RigDefinitionComponent rdc, ref RootMotionVelocityComponent rmvc, in LocalTransform lt) { if (!rdc.applyRootMotion) return; var boneData = RuntimeAnimationData.GetAnimationDataForRigRW(animatedBonePoses, rdc); if (boneData.IsEmpty) return; var motionDeltaPose = boneData[0]; var curEntityTransform = new BoneTransform(lt); if (rmvc.removeBuiltinEntityMovement) { boneData[0] = curEntityTransform; } else { var newEntityPose = BoneTransform.Multiply(curEntityTransform, motionDeltaPose); boneData[0] = newEntityPose; } rmvc.deltaPos = motionDeltaPose.pos; rmvc.deltaRot = motionDeltaPose.rot; rmvc.worldVelocity = motionDeltaPose.pos / deltaTime; TransformHelpers.ComputeWorldTransformMatrix(e, out var localTransformMatrix, ref localTransformLookup, ref parentLookup, ref ptmLookup); rmvc.worldVelocity = math.rotate(localTransformMatrix, rmvc.worldVelocity); } } } }