/
ZEDSkeletonAnimator.cs
464 lines (398 loc) · 21.4 KB
/
ZEDSkeletonAnimator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(HeightOffsetter))]
public class ZEDSkeletonAnimator : MonoBehaviour
{
protected Animator animator;
private HeightOffsetter heightOffsetter;
#region inspector vars
[Header("IK SETTINGS")]
[Tooltip("Distance (between sole and environment under it) under which a foot is considered fully on the floor for IK calculation. Full foot IK application ratio will be applied.")]
public float thresholdEnterGroundedState = .03f;
[Tooltip("Distance (between sole and environment under it) under which the IK will be gradually applied. Above this, no foot IK is applied.")]
public float thresholdLeaveGroundedState = .2f;
[Tooltip("Radius of movements filtered when the filterMovementsOnGround parameter is enabled.")]
public float thresholdFootLock = .05f;
[Tooltip("Layers detected as floor for the IK")]
public LayerMask raycastDetectionLayers;
[Tooltip("Coefficient of application of foot IK, position-wise.")]
[Range(0f, 1f)]
public float ikPositionApplicationRatio = 1f;
[Tooltip("Coefficient of application of foot IK, rotation-wise.")]
[Range(0f, 1f)]
public float ikRotationApplicationRatio = 1f;
// Coefficient of application of hint to bend legs in right direction.
private readonly float ikHintWeight = 1f;
[Header("RIG SETTINGS")]
public float ankleHeightOffset = 0.102f;
[Header("Keyboard controls")]
public KeyCode resetAutomaticOffset = KeyCode.R;
#endregion
#region vars
private SkeletonHandler skhandler = null;
public SkeletonHandler Skhandler { get => skhandler; set => skhandler = value; }
private Vector3 rootHeightOffset = Vector3.zero;
public Vector3 RootHeightOffset { get => rootHeightOffset; set => rootHeightOffset = value; }
private ZEDBodyTrackingManager bodyTrackingManager;
public ZEDBodyTrackingManager BodyTrackingManager { get => bodyTrackingManager; set => bodyTrackingManager = value; }
/// Foot locking variables
// If not on ground, no lock will be applied
private bool groundedL = false;
private bool groundedR = false;
// SDK position to compare agianst for foot locking
private Vector3 curPosAnkleL = Vector3.zero;
private Vector3 curPosAnkleR = Vector3.zero;
// If true, foot should not move
private bool lockFootL = false;
private bool lockFootR = false;
#endregion
/// --------------------------------------------
/// ------------MonoBehaviour methods-----------
/// --------------------------------------------
#region MonoBehaviour Methods
private void Awake()
{
bodyTrackingManager = FindObjectOfType<ZEDBodyTrackingManager>();
if (bodyTrackingManager == null)
{
Debug.LogError("ZEDManagerIK: No body tracking manager loaded!");
}
heightOffsetter = GetComponent<HeightOffsetter>();
}
void Start()
{
animator = GetComponent<Animator>();
}
private void Update()
{
// reset automatic height offset calibration
if (Input.GetKeyDown(resetAutomaticOffset))
{
heightOffsetter.CurrentheightOffset = 0;
}
}
#endregion
/// --------------------------------------------
/// -----------------Foot IK--------------------
/// --------------------------------------------
#region Foot IK
private Vector3 curIKTargetPosL = Vector3.zero;
private Vector3 curIKTargetPosR = Vector3.zero;
private Quaternion curIKTargetRotL = Quaternion.identity;
private Quaternion curIKTargetRotR = Quaternion.identity;
private Vector3 targetFootLockL = Vector3.zero;
private Vector3 targetFootLockR = Vector3.zero;
/// <summary>
/// Checks the foot locking state if the feature is enabled and returns the point on ground to use as effector for the IK.
/// </summary>
/// <param name="footLock">Foot locking state for this foot.</param>
/// <param name="hitPoint">Hit position of the ray from the foot.</param>
/// <param name="prevIKTargetPos">IK target on the previous frame. Should be replaced with the output of this function.</param>
/// <param name="grounded">If the foot is close enough to the ground to have full ik application.</param>
/// <param name="targetFootLock">Updated only if foot is unlocked. Target to lerp toward.</param>
/// <returns>The effector target position</returns>
private Vector3 FindIKTargetPosition(bool footLock, Vector3 hitPoint, Vector3 prevIKTargetPos, bool grounded, ref Vector3 targetFootLock)
{
float flsmooth = Mathf.Clamp(1 - bodyTrackingManager.footLockingSmoothingValue, 0, 1);
if (bodyTrackingManager.EnableFootLocking)
{
if (footLock && grounded)
{
return Vector3.Lerp(prevIKTargetPos,
new Vector3(targetFootLock.x, hitPoint.y + ankleHeightOffset, targetFootLock.z),
flsmooth);
}
else
{
targetFootLock = hitPoint + new Vector3(0, ankleHeightOffset, 0);
return Vector3.Lerp(
prevIKTargetPos,
hitPoint + new Vector3(0, ankleHeightOffset, 0),
flsmooth);
}
}
else
{
return Vector3.Lerp(
prevIKTargetPos,
hitPoint + new Vector3(0, ankleHeightOffset, 0),
flsmooth);
}
}
/// <summary>
/// Returns the target rotation for the IK. The goal it to have the foot flat depending on the normal found by the raycast.
/// </summary>
/// <param name="hitNormal">Normal at raycast hit.</param>
/// <param name="toePos">Toes position. Used to correctly orient the foot following its forward direction.</param>
/// <param name="anklePos">Ankle position. Used to correctly orient the foot following its forward direction.</param>
/// <returns></returns>
private Quaternion FindIKTargetRotation(Vector3 hitNormal, Vector3 toePos, Vector3 anklePos, Quaternion rootOrientation, Quaternion curFootRotation)
{
Vector3 forward = Vector3.ProjectOnPlane(rootOrientation * (toePos - anklePos), Vector3.up);
return Quaternion.Slerp(curFootRotation, Quaternion.LookRotation(forward, hitNormal), Mathf.Clamp(1 - bodyTrackingManager.footLockingSmoothingValue, 0, 1));
}
private Vector3 FindIKHintPosition(HumanBodyBones kneeBone)
{
Transform kneeT = animator.GetBoneTransform(kneeBone);
Vector3 hintPos = kneeT.position + 0.4f * kneeT.forward;
hintPos = animator.bodyRotation * hintPos;
hintPos += animator.bodyPosition;
return hintPos;
}
#endregion
/// --------------------------------------------
/// -----------------Foot Locking---------------
/// --------------------------------------------
#region Foot Locking
/// <summary>
/// Check if the SDK ankle keypoints have moved far enough from their previous position (on the horizontal plane).
/// If not, it'll be considered jittering, and should be taken into account in the foot IK.
/// This method should be called when the ZEDBodyTrackingManager updates the handler, as it only relies on the SDK positions.
/// </summary>
/// <param name="newPosAnkleL"></param>
/// <param name="newPosAnkleR"></param>
public void CheckFootLock(Vector3 newPosAnkleL, Vector3 newPosAnkleR)
{
lockFootL = HorizontalDist(curPosAnkleL, newPosAnkleL) < thresholdFootLock;
lockFootR = HorizontalDist(curPosAnkleR, newPosAnkleR) < thresholdFootLock;
if (!lockFootL) curPosAnkleL = newPosAnkleL;
if (!lockFootR) curPosAnkleR = newPosAnkleR;
}
#endregion
/// --------------------------------------------
/// -----------------Utility Methods------------
/// --------------------------------------------
#region Utility methods
/// <summary>
/// Computes and returns distance between the projections of <paramref name="vec1"/> and <paramref name="vec2"/> on an horizontal plane
/// </summary>
private float HorizontalDist(Vector3 vec1, Vector3 vec2)
{
return Vector2.Distance(new Vector2(vec1.x, vec1.z), new Vector2(vec2.x, vec2.z));
}
/// <summary>
/// Gets a linear interpolation of the application ratio for the IK depending on the distance to the floor.
/// Used for smoothing the transition between grounded and not.
/// </summary>
/// <param name="d">Distance between sole and floor (not ankle and floor).</param>
/// <param name="tMin">Threshold min: Distance above this value implies no application of IK.</param>
/// <param name="tMax">Threshold max: Distance under this value implies full (depending on setting) application of IK.</param>
/// <param name="rMax">Max ratio (default 1).</param>
/// <returns>Ratio of IK application between 0 and rmax, depending on d, tmin and tmax./returns>
private float GetLinearIKRatio(float d, float tMin, float tMax, float rMax = 1)
{
float ikr = Mathf.Min(1, Mathf.Max(0, rMax * (tMin - d) / (tMin - tMax)));
return ikr;
}
/// <summary>
/// Get position of bone after application of bodyPosition and bodyRotation.
/// </summary>
/// <param name="bone"></param>
private Vector3 GetWorldPosCurrentState(HumanBodyBones bone)
{
Vector3 ret = animator.GetBoneTransform(bone).position;
ret = animator.bodyPosition + (animator.bodyRotation * ret);
return ret;
}
/// <summary>
/// Utility function to compute and apply the height offset from the height offset manager to the body.
/// </summary>
private void ManageHeightOffset(Vector3 posAnkleL, Vector3 posAnkleR, Vector3 hitPointL, Vector3 hitPointR)
{
rootHeightOffset = heightOffsetter.ComputeRootHeightOffsetFromRaycastInfo(posAnkleL, posAnkleR, hitPointL, hitPointR, ankleHeightOffset);
if (animator != null)
{
animator.bodyPosition += rootHeightOffset;
}
else { transform.position = Skhandler.TargetBodyPositionWithHipOffset + rootHeightOffset; }
}
#endregion
#region Delayed Spawn
[HideInInspector]
public bool canSpawn = true;
public void TryShowAvatar(bool newVisibility)
{
gameObject.SetActive(canSpawn && newVisibility);
}
#endregion
/// --------------------------------------------
/// -----------------Main Pipeline--------------
/// --------------------------------------------
#region Main Pipeline
/// <summary>
/// Applies the current local rotations of the rig to the animator.
/// </summary>
void ApplyAllRigRotationsOnAnimator()
{
if(bodyTrackingManager.bodyMode == ZEDBodyTrackingManager.BODY_MODE.FULL_BODY)
{
skhandler.MoveAnimator(bodyTrackingManager.EnableSmoothing, 1 - Mathf.Clamp(bodyTrackingManager.smoothingValue, 0, 0.99f));
}
else if(bodyTrackingManager.bodyMode == ZEDBodyTrackingManager.BODY_MODE.UPPER_BODY)
{
skhandler.MoveAnimatorUpperBody(bodyTrackingManager.EnableSmoothing, Mathf.Clamp(1 - bodyTrackingManager.smoothingValue, 0, 1));
}
}
/// <summary>
/// Raycast based on <paramref name="ankleLastFramePosL"/> and <paramref name="ankleLastFramePosR"/>, the position of the feet at the previous frame.
/// If the ray does not hit, the hitPoint are set to the position of the feet and the hitNormal to Vector3.up.
/// </summary>
private void RaycastManagementAnimator(
out bool hitSuccessfulL, out bool hitSuccessfulR,
out Vector3 hitPointL, out Vector3 hitPointR,
out Vector3 hitNormalL, out Vector3 hitNormalR,
Vector3 ankleLastFramePosL, Vector3 ankleLastFramePosR)
{
// Initialize vars
hitPointL = ankleLastFramePosL; hitPointR = ankleLastFramePosR;
hitNormalL = Vector3.up; hitNormalR = Vector3.up;
Vector3 postStartRayL = GetWorldPosCurrentState(HumanBodyBones.LeftFoot);
Vector3 postStartRayR = GetWorldPosCurrentState(HumanBodyBones.RightFoot);
// Shoot a ray from 5m above the foot towards 5m under the foot
Ray rayL = new Ray(postStartRayL + (Vector3.up * 5), Vector3.down);
hitSuccessfulL = Physics.Raycast(rayL, out RaycastHit hitL, 10, raycastDetectionLayers);
if (hitSuccessfulL) { hitPointL = hitL.point; hitNormalL = hitL.normal; }
Ray rayR = new Ray(postStartRayR + (Vector3.up * 5), Vector3.down);
hitSuccessfulR = Physics.Raycast(rayR, out RaycastHit hitR, 10, raycastDetectionLayers);
if (hitSuccessfulR) { hitPointR = hitR.point; hitNormalR = hitR.normal; }
}
/// <summary>
/// Computes Foot IK.
/// 1) Apply bones rotations to animator 2) Apply root position and rotations. 3) Do Foot IK.
/// </summary>
void OnAnimatorIK()
{
if (skhandler)
{
// 1) Update target positions and rotations.
ApplyAllRigRotationsOnAnimator();
// 2) Set root position/rotation
animator.bodyPosition = bodyTrackingManager.bodyMode == ZEDBodyTrackingManager.BODY_MODE.FULL_BODY
? skhandler.TargetBodyPositionWithHipOffset
: new Vector3(skhandler.TargetBodyPositionWithHipOffset.x, animator.bodyPosition.y, skhandler.TargetBodyPositionWithHipOffset.z);
animator.bodyRotation = skhandler.TargetBodyOrientationSmoothed;
// Store raycast info data
bool hitSuccessfulL; bool hitSuccessfulR;
Vector3 hitPointL; Vector3 hitPointR;
Vector3 hitNormalL; Vector3 hitNormalR;
// Get raycast information from feet
RaycastManagementAnimator(out hitSuccessfulL, out hitSuccessfulR, out hitPointL, out hitPointR, out hitNormalL, out hitNormalR, GetWorldPosCurrentState(HumanBodyBones.LeftFoot), GetWorldPosCurrentState(HumanBodyBones.RightFoot));
// Manage Height Offset
ManageHeightOffset(GetWorldPosCurrentState(HumanBodyBones.LeftFoot),
GetWorldPosCurrentState(HumanBodyBones.RightFoot),
hitPointL, hitPointR);
if (bodyTrackingManager.EnableFootIK)
{
/// ----------------------------------------------------------------
/// Left Foot ------------------------------------------------------
/// ----------------------------------------------------------------
animator.SetIKHintPosition(AvatarIKHint.LeftKnee, FindIKHintPosition(HumanBodyBones.LeftLowerLeg));
animator.SetIKHintPositionWeight(AvatarIKHint.LeftKnee, ikHintWeight);
// Find the effector position & rotation for the IK
curIKTargetPosL = FindIKTargetPosition(lockFootL, hitPointL, curIKTargetPosL, groundedL, ref targetFootLockL);
curIKTargetRotL = FindIKTargetRotation(hitNormalL,
animator.GetBoneTransform(HumanBodyBones.LeftToes).position,
animator.GetBoneTransform(HumanBodyBones.LeftFoot).position,
Skhandler.TargetBodyOrientationSmoothed,
curIKTargetRotL);
// Set effectors and application ratios
if (hitSuccessfulL)
{
animator.SetIKPosition(AvatarIKGoal.LeftFoot, curIKTargetPosL);
animator.SetIKRotation(AvatarIKGoal.LeftFoot, curIKTargetRotL);
// distance from sole to floor
float dist = Mathf.Abs(hitPointL.y - GetWorldPosCurrentState(HumanBodyBones.LeftFoot).y + ankleHeightOffset);
groundedL = dist <= thresholdEnterGroundedState;
float ikRatioPos = GetLinearIKRatio(dist, thresholdLeaveGroundedState, thresholdEnterGroundedState, ikPositionApplicationRatio);
float ikRatioRot = GetLinearIKRatio(dist, thresholdLeaveGroundedState, thresholdEnterGroundedState, ikRotationApplicationRatio);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, ikRatioRot);
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, ikRatioPos);
}
else // set application ratios to 0
{
groundedL = false;
animator.SetIKPosition(AvatarIKGoal.LeftFoot, curIKTargetPosL);
animator.SetIKRotation(AvatarIKGoal.LeftFoot, curIKTargetRotL);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 0);
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 0);
}
/// ----------------------------------------------------------------
/// Right Foot -----------------------------------------------------
/// ----------------------------------------------------------------
animator.SetIKHintPosition(AvatarIKHint.RightKnee, FindIKHintPosition(HumanBodyBones.RightLowerLeg));
animator.SetIKHintPositionWeight(AvatarIKHint.RightKnee, ikHintWeight);
// Find the effector position & rotation for the IK
curIKTargetPosR = FindIKTargetPosition(lockFootR, hitPointR, curIKTargetPosR, groundedR, ref targetFootLockR);
curIKTargetRotR = FindIKTargetRotation(hitNormalR,
animator.GetBoneTransform(HumanBodyBones.RightToes).position,
animator.GetBoneTransform(HumanBodyBones.RightFoot).position,
Skhandler.TargetBodyOrientationSmoothed,
curIKTargetRotR);
// Set effectors and application ratios
if (hitSuccessfulR)
{
animator.SetIKPosition(AvatarIKGoal.RightFoot, curIKTargetPosR);
animator.SetIKRotation(AvatarIKGoal.RightFoot, curIKTargetRotR);
// distance from sole to floor
float dist = Mathf.Abs(hitPointR.y - GetWorldPosCurrentState(HumanBodyBones.RightFoot).y + ankleHeightOffset);
groundedR = dist <= thresholdEnterGroundedState;
float ikRatioPos = GetLinearIKRatio(dist, thresholdLeaveGroundedState, thresholdEnterGroundedState, ikPositionApplicationRatio);
float ikRatioRot = GetLinearIKRatio(dist, thresholdLeaveGroundedState, thresholdEnterGroundedState, ikRotationApplicationRatio);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, ikRatioRot);
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, ikRatioPos);
}
else // set application ratios to 0
{
groundedR = false;
animator.SetIKPosition(AvatarIKGoal.RightFoot, curIKTargetPosR);
animator.SetIKRotation(AvatarIKGoal.RightFoot, curIKTargetRotR);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 0);
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 0);
}
}
//if the IK is not active, disable ik hints/effectors
else
{
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 0);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 0);
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 0);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 0);
animator.SetIKHintPositionWeight(AvatarIKHint.RightKnee, 0);
animator.SetIKHintPositionWeight(AvatarIKHint.LeftKnee, 0);
}
}
}
#endregion
/// -----------------------------------------
/// ------------ UPPER BODY MODE ------------
/// -----------------------------------------
public void UpdateNavigationAndLegAnimationData()
{
// Stabilize Y position
RaycastHit raycastHit = new RaycastHit();
if (Physics.Raycast(skhandler.TargetBodyPositionWithHipOffset + new Vector3(0,1,0),
Vector3.down, out raycastHit, 3f))
{
//transform.position = new Vector3(skhandler.TargetBodyPositionWithHipOffset.x, raycastHit.point.y, skhandler.TargetBodyPositionWithHipOffset.z);
skhandler.TargetBodyPositionWithHipOffset = new Vector3(skhandler.TargetBodyPositionWithHipOffset.x, raycastHit.point.y, skhandler.TargetBodyPositionWithHipOffset.z);
} else
{
//transform.position = new Vector3 (skhandler.TargetBodyPositionWithHipOffset.x, 0, skhandler.TargetBodyPositionWithHipOffset.z);
skhandler.TargetBodyPositionWithHipOffset = new Vector3 (skhandler.TargetBodyPositionWithHipOffset.x, 0, skhandler.TargetBodyPositionWithHipOffset.z);
}
// Take only rotation on Y axis.
//transform.rotation = Quaternion.Euler(0,skhandler.TargetBodyOrientationSmoothed.eulerAngles.y,0);
Skhandler.TargetBodyOrientationSmoothed = Quaternion.Euler(0, skhandler.TargetBodyOrientationSmoothed.eulerAngles.y, 0);
transform.rotation = skhandler.TargetBodyOrientationSmoothed;
// Find anim speed depending on if going forward or backward
//float sc = Vector3.Dot(transform.forward, skhandler.rootVelocity);
float sc = Vector3.Dot(transform.forward, skhandler.rootVelocity);
float animSpeed = sc >= 0
? skhandler.rootVelocity.magnitude
: -1f * skhandler.rootVelocity.magnitude;
animator.SetFloat("Forward", animSpeed, 0.1f, Time.deltaTime);
}
}