using UnityEngine; namespace RootMotion.FinalIK { public partial class IKSolverVR : IKSolver { public partial class Locomotion { [Tooltip("Start moving (horizontal distance to HMD + HMD velocity) threshold.")] /// /// Start moving (horizontal distance to HMD + HMD velocity) threshold. /// [ShowIf("mode", Mode.Animated)] public float moveThreshold = 0.3f; // ANIMATION [ShowLargeHeaderIf("Animation", "mode", Mode.Animated)] [SerializeField] byte animationHeader; [Tooltip("Minimum locomotion animation speed.")] /// /// Minimum locomotion animation speed. /// [ShowRangeIf(0.1f, 1f, "mode", Mode.Animated)] public float minAnimationSpeed = 0.2f; [Tooltip("Maximum locomotion animation speed.")] /// /// Maximum locomotion animation speed. /// [ShowRangeIf(1f, 10f, "mode", Mode.Animated)] public float maxAnimationSpeed = 3f; [Tooltip("Smoothing time for Vector3.SmoothDamping 'VRIK_Horizontal' and 'VRIK_Vertical' parameters. Larger values make animation smoother, but less responsive.")] /// /// Smoothing time for Vector3.SmoothDamping 'VRIK_Horizontal' and 'VRIK_Vertical' parameters. Larger values make animation smoother, but less responsive. /// [ShowRangeIf(0.05f, 0.2f, "mode", Mode.Animated)] public float animationSmoothTime = 0.1f; [ShowLargeHeaderIf("Root Position", "mode", Mode.Animated)] [SerializeField] byte rootPositionHeader; [Tooltip("X and Z standing offset from the horizontal position of the HMD.")] /// /// X and Z standing offset from the horizontal position of the HMD. /// [ShowIf("mode", Mode.Animated)] public Vector2 standOffset; [Tooltip("Lerp root towards the horizontal position of the HMD with this speed while moving.")] /// /// Lerp root towards the horizontal position of the HMD with this speed while moving. /// [ShowRangeIf(0f, 50f, "mode", Mode.Animated)] public float rootLerpSpeedWhileMoving = 30f; [Tooltip("Lerp root towards the horizontal position of the HMD with this speed while in transition from locomotion to idle state.")] /// /// Lerp root towards the horizontal position of the HMD with this speed while in transition from locomotion to idle state. /// [ShowRangeIf(0f, 50f, "mode", Mode.Animated)] public float rootLerpSpeedWhileStopping = 10f; [Tooltip("Lerp root towards the horizontal position of the HMD with this speed while turning on spot.")] /// /// Lerp root towards the horizontal position of the HMD with this speed while turning on spot. /// [ShowRangeIf(0f, 50f, "mode", Mode.Animated)] public float rootLerpSpeedWhileTurning = 10f; [Tooltip("Max horizontal distance from the root to the HMD.")] /// /// Max horizontal distance from the root to the HMD. /// [ShowIf("mode", Mode.Animated)] public float maxRootOffset = 0.5f; [ShowLargeHeaderIf("Root Rotation", "mode", Mode.Animated)] [SerializeField] byte rootRotationHeader; [Tooltip("Max root angle from head forward while moving (ik.solver.spine.maxRootAngle).")] /// /// Max root angle from head forward while moving (ik.solver.spine.maxRootAngle). /// [ShowRangeIf(0f, 180f, "mode", Mode.Animated)] public float maxRootAngleMoving = 10f; [Tooltip("Max root angle from head forward while standing (ik.solver.spine.maxRootAngle.")] /// /// Max root angle from head forward while standing (ik.solver.spine.maxRootAngle. /// [ShowRangeIf(0f, 180f, "mode", Mode.Animated)] public float maxRootAngleStanding = 90f; /// /// Multiplies "VRIK_Horizontal" and "VRIK_Vertical" parameters. Larger values make steps longer and animation slower. /// [HideInInspector][SerializeField] public float stepLengthMlp = 1f; private Animator animator; private Vector3 velocityLocal, velocityLocalV; private Vector3 lastCorrection; private Vector3 lastHeadTargetPos; private Vector3 lastSpeedRootPos; private Vector3 lastEndRootPos; private float rootLerpSpeed, rootVelocityV; private float animSpeed = 1f; private float animSpeedV; private float stopMoveTimer; private float turn; private float maxRootAngleV; private float currentAnimationSmoothTime = 0.05f; private bool isMoving; private bool firstFrame = true; private static int VRIK_Horizontal; private static int VRIK_Vertical; private static int VRIK_IsMoving; private static int VRIK_Speed; private static int VRIK_Turn; private static bool isHashed; public void Initiate_Animated(Animator animator, Vector3[] positions) { this.animator = animator; if (animator == null && mode == Mode.Animated) { Debug.LogError("VRIK is in Animated locomotion mode, but cannot find Animator on the VRIK root gameobject."); } ResetParams(positions); } private void ResetParams(Vector3[] positions) { lastHeadTargetPos = positions[5]; lastSpeedRootPos = positions[0]; lastEndRootPos = lastSpeedRootPos; lastCorrection = Vector3.zero; isMoving = false; currentAnimationSmoothTime = 0.05f; stopMoveTimer = 1f; } public void Reset_Animated(Vector3[] positions) { ResetParams(positions); if (animator == null) return; if (!isHashed) { VRIK_Horizontal = Animator.StringToHash("VRIK_Horizontal"); VRIK_Vertical = Animator.StringToHash("VRIK_Vertical"); VRIK_IsMoving = Animator.StringToHash("VRIK_IsMoving"); VRIK_Speed = Animator.StringToHash("VRIK_Speed"); VRIK_Turn = Animator.StringToHash("VRIK_Turn"); isHashed = true; } if (!firstFrame) { animator.SetFloat(VRIK_Horizontal, 0f); animator.SetFloat(VRIK_Vertical, 0f); animator.SetBool(VRIK_IsMoving, false); animator.SetFloat(VRIK_Speed, 1f); animator.SetFloat(VRIK_Turn, 0f); } } private void AddDeltaRotation_Animated(Quaternion delta, Vector3 pivot) { Vector3 toLastEndRootPos = lastEndRootPos - pivot; lastEndRootPos = pivot + delta * toLastEndRootPos; Vector3 toLastSpeedRootPos = lastSpeedRootPos - pivot; lastSpeedRootPos = pivot + delta * toLastSpeedRootPos; Vector3 toLastHeadTargetPos = lastHeadTargetPos - pivot; lastHeadTargetPos = pivot + delta * toLastHeadTargetPos; } private void AddDeltaPosition_Animated(Vector3 delta) { lastEndRootPos += delta; lastSpeedRootPos += delta; lastHeadTargetPos += delta; } private float lastVelLocalMag; public void Solve_Animated(IKSolverVR solver, float scale, float deltaTime) { if (animator == null) { Debug.LogError("VRIK cannot find Animator on the VRIK root gameobject.", solver.root); return; } if (deltaTime <= 0f) return; // Root up vector Vector3 rootUp = solver.rootBone.solverRotation * Vector3.up; // Substract any motion from parent transforms Vector3 externalDelta = solver.rootBone.solverPosition - lastEndRootPos; externalDelta -= animator.deltaPosition; // Head target position Vector3 headTargetPos = solver.spine.headPosition; Vector3 standOffsetWorld = solver.rootBone.solverRotation * new Vector3(standOffset.x, 0f, standOffset.y) * scale; headTargetPos += standOffsetWorld; if (firstFrame) { lastHeadTargetPos = headTargetPos; firstFrame = false; } // Head target velocity Vector3 headTargetVelocity = (headTargetPos - lastHeadTargetPos) / deltaTime; lastHeadTargetPos = headTargetPos; headTargetVelocity = V3Tools.Flatten(headTargetVelocity, rootUp); // Head target offset Vector3 offset = headTargetPos - solver.rootBone.solverPosition; offset -= externalDelta; offset -= lastCorrection; offset = V3Tools.Flatten(offset, rootUp); // Turning Vector3 headForward = (solver.spine.IKRotationHead * solver.spine.anchorRelativeToHead) * Vector3.forward; headForward.y = 0f; Vector3 headForwardLocal = Quaternion.Inverse(solver.rootBone.solverRotation) * headForward; float angle = Mathf.Atan2(headForwardLocal.x, headForwardLocal.z) * Mathf.Rad2Deg; angle += solver.spine.rootHeadingOffset; float turnTarget = angle / 90f; bool isTurning = true; if (Mathf.Abs(turnTarget) < 0.2f) { turnTarget = 0f; isTurning = false; } turn = Mathf.Lerp(turn, turnTarget, Time.deltaTime * 3f); animator.SetFloat(VRIK_Turn, turn * 2f); // Local Velocity, animation smoothing Vector3 velocityLocalTarget = Quaternion.Inverse(solver.readRotations[0]) * (headTargetVelocity + offset); velocityLocalTarget *= weight * stepLengthMlp; float animationSmoothTimeTarget = isTurning && !isMoving ? 0.2f : animationSmoothTime; currentAnimationSmoothTime = Mathf.Lerp(currentAnimationSmoothTime, animationSmoothTimeTarget, deltaTime * 20f); velocityLocal = Vector3.SmoothDamp(velocityLocal, velocityLocalTarget, ref velocityLocalV, currentAnimationSmoothTime, Mathf.Infinity, deltaTime); float velLocalMag = velocityLocal.magnitude / stepLengthMlp; //animator.SetBool("VRIK_StartWithRightFoot", velocityLocal.x >= 0f); animator.SetFloat(VRIK_Horizontal, velocityLocal.x / scale); animator.SetFloat(VRIK_Vertical, velocityLocal.z / scale); // Is Moving float m = moveThreshold * scale; if (isMoving) m *= 0.9f; bool isMovingRaw = velocityLocal.sqrMagnitude > m * m; if (isMovingRaw) stopMoveTimer = 0f; else stopMoveTimer += deltaTime; isMoving = stopMoveTimer < 0.05f; // Max root angle float maxRootAngleTarget = isMoving ? maxRootAngleMoving : maxRootAngleStanding; solver.spine.maxRootAngle = Mathf.SmoothDamp(solver.spine.maxRootAngle, maxRootAngleTarget, ref maxRootAngleV, 0.2f, Mathf.Infinity, deltaTime); animator.SetBool(VRIK_IsMoving, isMoving); // Animation speed Vector3 currentRootPos = solver.rootBone.solverPosition; currentRootPos -= externalDelta; currentRootPos -= lastCorrection; Vector3 rootVelocity = (currentRootPos - lastSpeedRootPos) / deltaTime; lastSpeedRootPos = solver.rootBone.solverPosition; float rootVelocityMag = rootVelocity.magnitude; float animSpeedTarget = minAnimationSpeed; if (rootVelocityMag > 0f && isMovingRaw) { animSpeedTarget = animSpeed * (velLocalMag / rootVelocityMag); } animSpeedTarget = Mathf.Clamp(animSpeedTarget, minAnimationSpeed, maxAnimationSpeed); animSpeed = Mathf.SmoothDamp(animSpeed, animSpeedTarget, ref animSpeedV, 0.05f, Mathf.Infinity, deltaTime); animSpeed = Mathf.Lerp(1f, animSpeed, weight); animator.SetFloat(VRIK_Speed, animSpeed); // Is Stopping AnimatorTransitionInfo transInfo = animator.GetAnimatorTransitionInfo(0); bool isStopping = transInfo.IsUserName("VRIK_Stop"); // Root lerp speed float rootLerpSpeedTarget = 0; if (isMoving) rootLerpSpeedTarget = rootLerpSpeedWhileMoving; if (isStopping) rootLerpSpeedTarget = rootLerpSpeedWhileStopping; if (isTurning) rootLerpSpeedTarget = rootLerpSpeedWhileTurning; rootLerpSpeedTarget *= Mathf.Max(headTargetVelocity.magnitude, 0.2f); rootLerpSpeed = Mathf.Lerp(rootLerpSpeed, rootLerpSpeedTarget, deltaTime * 20f); // Root lerp and limits headTargetPos += V3Tools.ExtractVertical(solver.rootBone.solverPosition - headTargetPos, rootUp, 1f); if (maxRootOffset > 0f) { // Lerp towards head target position Vector3 p = solver.rootBone.solverPosition; if (rootLerpSpeed > 0f) { solver.rootBone.solverPosition = Vector3.Lerp(solver.rootBone.solverPosition, headTargetPos, rootLerpSpeed * deltaTime * weight); } lastCorrection = solver.rootBone.solverPosition - p; // Max offset offset = headTargetPos - solver.rootBone.solverPosition; offset = V3Tools.Flatten(offset, rootUp); float offsetMag = offset.magnitude; if (offsetMag > maxRootOffset) { lastCorrection += (offset - (offset / offsetMag) * maxRootOffset) * weight; solver.rootBone.solverPosition += lastCorrection; } } else { // Snap to head target position lastCorrection = (headTargetPos - solver.rootBone.solverPosition) * weight; solver.rootBone.solverPosition += lastCorrection; } lastEndRootPos = solver.rootBone.solverPosition; } } } }