VR Development Framework
v 1.0.0
 All Classes Namespaces Functions Variables Enumerations Enumerator Properties Events Pages
AVR_PoseProvider.cs
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4 using Unity.Netcode;
5 #if UNITY_EDITOR
6 using UnityEditor;
7 #endif
8 
9 using AVR.Core;
10 
11 namespace AVR.Avatar {
12  /// <summary>
13  /// Estimates the pose of a player from the locations of VR controllers and HMD
14  /// </summary>
15  [AVR.Core.Attributes.DocumentationUrl("class_a_v_r_1_1_avatar_1_1_a_v_r___pose_provider.html")]
16  public class AVR_PoseProvider : AVR.Core.AVR_Component
17  {
18  /// <summary>
19  /// Endpoint of the players view vector
20  /// </summary>
21  public Vector3 lookAtPos =>
22  #if AVR_NET
23  !IsOwner ? m_ReplicatedState.Value.lookAtPos :
24  #endif
25  eyeTransform.localPosition + transform.InverseTransformDirection(eyeTransform.forward);
26 
27  /// <summary>
28  /// Left hand rotation
29  /// </summary>
30  public Quaternion leftHandRot =>
31  #if AVR_NET
32  !IsOwner ? m_ReplicatedState.Value.leftHandRot :
33  #endif
34  leftHandTarget.rotation;
35 
36  /// <summary>
37  /// Right hand rotation
38  /// </summary>
39  public Quaternion rightHandRot =>
40  #if AVR_NET
41  !IsOwner ? m_ReplicatedState.Value.rightHandRot :
42  #endif
43  rightHandTarget.rotation;
44 
45  /// <summary>
46  /// Left foot position
47  /// </summary>
48  public Vector3 leftFootPos =>
49  #if AVR_NET
50  !IsOwner ? m_ReplicatedState.Value.leftFootPos :
51  #endif
52  leftFootTarget.localPosition;
53 
54  /// <summary>
55  /// Left foot rotation
56  /// </summary>
57  public Quaternion leftFootRot =>
58  #if AVR_NET
59  !IsOwner ? m_ReplicatedState.Value.leftFootRot :
60  #endif
61  leftFootTarget.rotation;
62 
63  /// <summary>
64  /// Right foot position
65  /// </summary>
66  public Vector3 rightFootPos =>
67  #if AVR_NET
68  !IsOwner ? m_ReplicatedState.Value.rightFootPos :
69  #endif
70  rightFootTarget.localPosition;
71 
72  /// <summary>
73  /// Right foot rotation
74  /// </summary>
75  public Quaternion rightFootRot =>
76  #if AVR_NET
77  !IsOwner ? m_ReplicatedState.Value.rightFootRot :
78  #endif
79  rightFootTarget.rotation;
80 
81  /// <summary>
82  /// Position of the rig's pivot (Point on the ground directly underneath the HMD)
83  /// </summary>
84  public Vector3 pivotPos =>
85  #if AVR_NET
86  !IsOwner ? m_ReplicatedState.Value.pivotPos :
87  #endif
88  pivotTransform.localPosition;
89 
90  /// <summary>
91  /// Rotation of the pivot
92  /// </summary>
93  public Quaternion pivotRot =>
94  #if AVR_NET
95  !IsOwner ? m_ReplicatedState.Value.pivotRot :
96  #endif
97  pivotTransform.rotation;
98 
99  /// <summary>
100  /// Position of the body
101  /// </summary>
102  public Vector3 bodyPos =>
103  #if AVR_NET
104  !IsOwner ? m_ReplicatedState.Value.bodyPos :
105  #endif
106  bodyTransform.localPosition;
107 
108  /// <summary>
109  /// Rotation of the body
110  /// </summary>
111  public Quaternion bodyRot =>
112  #if AVR_NET
113  !IsOwner ? m_ReplicatedState.Value.bodyRot :
114  #endif
115  bodyTransform.rotation;
116 
117  /// <summary>
118  /// Position of the head
119  /// </summary>
120  public Vector3 eyePos =>
121  #if AVR_NET
122  !IsOwner ? m_ReplicatedState.Value.eyePos :
123  #endif
124  eyeTransform.localPosition;
125 
126  /// <summary>
127  /// Rotation of the head
128  /// </summary>
129  public Quaternion eyeRot =>
130  #if AVR_NET
131  !IsOwner ? m_ReplicatedState.Value.eyeRot :
132 #endif
133  eyeTransform.rotation;
134 
135  /// <summary>
136  /// Left hand position
137  /// </summary>
138  public Vector3 leftHandPos {
139  get
140  {
141  #if AVR_NET
142  if(!IsOwner)
143  {
144  return m_ReplicatedState.Value.leftHandPos;
145  }
146  #endif
147 
148  if (leftHandFilter) return transform.InverseTransformPoint(leftHandFilter.naturalize_point(leftHandTarget.position));
149  return transform.InverseTransformPoint(leftHandTarget.position);
150  }
151  }
152 
153  /// <summary>
154  /// Right hand position
155  /// </summary>
156  public Vector3 rightHandPos {
157  get
158  {
159  #if AVR_NET
160  if (!IsOwner)
161  {
162  return m_ReplicatedState.Value.rightHandPos;
163  }
164  #endif
165 
166  if (rightHandFilter) return transform.InverseTransformPoint(rightHandFilter.naturalize_point(rightHandTarget.position));
167  return transform.InverseTransformPoint(rightHandTarget.position);
168  }
169  }
170 
171  /// <summary>
172  /// Left hand naturalization filter
173  /// </summary>
175  public AVR_PoseNaturalizationFilter leftHandFilter;
176 
177  /// <summary>
178  /// Right hand naturalization filter
179  /// </summary>
181  public AVR_PoseNaturalizationFilter rightHandFilter;
182 
183  protected Transform leftHandTarget => playerRig.leftHandController ? playerRig.leftHandController.transform : pivotTransform;
184  protected Transform rightHandTarget => playerRig.rightHandController ? playerRig.rightHandController.transform : pivotTransform;
185  protected Transform leftFootTarget;
186  protected Transform rightFootTarget;
187  protected Transform pivotTransform;
188  protected Transform bodyTransform;
189  protected Transform eyeTransform;
190 
191  /// <summary>
192  /// Inertia of the body. (Increasing this will help with micromovements and stutters of the body)
193  /// </summary>
194  [Range(0.0001f, 1.0f)]
195  public float body_inertia = 0.3f;
196 
197  /// <summary>
198  /// Blending speed between a leaning and standing position
199  /// </summary>
200  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
201  public float lean_blend_speed = 3.5f;
202 
203  /// <summary>
204  /// Maximum yaw angle of the head in relation of the body
205  /// </summary>
206  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
207  public float max_head_yaw = 30f;
208 
209  /// <summary>
210  /// Maximum pitch angle of the head in relation of the body
211  /// </summary>
212  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
213  public float max_head_pitch = 30f;
214 
215  /// <summary>
216  /// Maximum roll angle of the head in relation of the body
217  /// </summary>
218  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
219  public float max_head_roll = 30f;
220 
221  /// <summary>
222  /// Default height of the body/torso
223  /// </summary>
224  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
225  public float default_body_height = 0.9f;
226 
227  /// <summary>
228  /// Maximum height of the body/torso fromt he ground
229  /// </summary>
230  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
231  public float max_body_height = 1.0f;
232 
233  /// <summary>
234  /// Minimum height of the body/torso fromt he ground
235  /// </summary>
236  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
237  public float min_body_height = 0.5f;
238 
239  /// <summary>
240  /// Local offset vector from eyes to neck
241  /// </summary>
242  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
243  public Vector3 local_eye_to_neck_offset = new Vector3(0.0f, -0.1f, -0.05f);
244 
245  /// <summary>
246  /// Distance between neck and body/torso
247  /// </summary>
248  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
249  public float neck_body_distance = 0.4f;
250 
251  /// <summary>
252  /// Distance between the right foot and the pivot
253  /// </summary>
254  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
255  public Vector3 foot_offset_from_pivot = new Vector3(0.2f, 0.05f, 0.0f);
256 
257  /// <summary>
258  /// Speed at which feet move to their target position
259  /// </summary>
260  [AVR.Core.Attributes.FoldoutGroup("Calibration")]
261  public float foot_follow_speed = 3.0f;
262 
263  /// <summary>
264  /// Collision mask of the ground
265  /// </summary>
266  public LayerMask groundCollisionMask;
267 
268  private float lean_factor = 0.0f;
269  private float lean_conf = 0.0f;
270  private float last_yaw = 0.0f;
271 
272  protected override void Awake()
273  {
274  base.Awake();
275  leftFootTarget = AVR.Core.Utils.Misc.CreateEmptyGameObject("leftFootTarget", transform);
276  rightFootTarget = AVR.Core.Utils.Misc.CreateEmptyGameObject("rightFootTarget", transform);
277  pivotTransform = AVR.Core.Utils.Misc.CreateEmptyGameObject("pivotTarget", transform);
278  bodyTransform = AVR.Core.Utils.Misc.CreateEmptyGameObject("bodyTarget", transform);
279  eyeTransform = AVR.Core.Utils.Misc.CreateEmptyGameObject("eyeTransform", transform);
280  }
281 
282  Vector3 ApplyInertia(Vector3 current, Vector3 target) {
283  float dist = Vector3.Distance(current, target);
284  float move_mult = Mathf.SmoothStep(0f, 1f, dist / body_inertia);
285  return Vector3.MoveTowards(current, target, dist*move_mult);
286  }
287 
288  void SetBody() {
289  // Pre-processing
290  {
291  // Calculation of lean_conf, which indicates our confidence of whether the palyer is leaning forwards or standing upright.
292  if (playerRig.isLeaningForwardsConfidence() > 0.5f)
293  {
294  lean_factor = Mathf.Lerp(lean_factor, 1.0f, Time.deltaTime * lean_blend_speed);
295  }
296  else
297  {
298  lean_factor = Mathf.Lerp(lean_factor, 0.0f, Time.deltaTime * lean_blend_speed);
299  }
300  lean_conf = lean_factor * playerRig.isLeaningForwardsConfidence();
301 
302 
303  // We apply inertia to the main cameras position, to avoid unnecessary micro-movement. Note: We do not apply this to the lookat-target, so the head rotation will be fully responsive.
304  eyeTransform.position = ApplyInertia(eyeTransform.position, playerRig.MainCamera.transform.position);
305 
306  // Here we apply clamping bounds on the pitch and roll angles of the head. We deal with yaw in a separate function.
307  //NOTE / TODO: This could lead to problems, as we use localRotation of the camera here. If, say, the camera was in a child object of the GenericXRDevice object, the local rotation would be zero.
308  Quaternion r = playerRig.MainCamera.transform.localRotation;
309  r = AVR.Core.Utils.Geom.ClampQuaternionRotation(r, new Vector3(max_head_pitch, 360, max_head_roll));
310  eyeTransform.localRotation = r;
311  }
312 
313 
314 
315  //No we get to the actual body transform. First we reset its rotation:
316  bodyTransform.up = Vector3.up;
317 
318  // Get the rotation and position if the player is standing upright:
319  GetStandingTransform(out Quaternion stout, out Vector3 stpos);
320  // Get the rotation and position if the player is leaning forwards:
321  GetLeanTransform(out Quaternion bdout, out Vector3 bdpos);
322 
323  // We designate an "unsafe position/rotation", which corresponds to our leaning transform, depending on our leaning-confidence value.
324  Vector3 unsafe_pos = Vector3.Lerp(stpos, bdpos, lean_conf);
325  Quaternion unsafe_rot = Quaternion.Lerp(stout, bdout, lean_conf);
326 
327  // The unsafe position corresponds to the position/rotation we believe the body has *based on our lean_conf value*.
328  // PROBLEM: we may end up with the players feet hovering in the air. Here we correct for this.
329  // We interpolate between our "unsafe" (the higher) position and the regular, standing-upright (lower) position, based on the distance of the body to the ground (pivot)
330  {
331  float sh = stpos.y - pivotTransform.position.y; // safe height
332  float uh = unsafe_pos.y - pivotTransform.position.y;// unsafe height
333 
334  float lamb = Mathf.Clamp((default_body_height - sh) / (uh - sh), 0.0f, 1.0f);
335 
336  unsafe_pos = Vector3.Lerp(stpos, unsafe_pos, lamb);
337  unsafe_rot = Quaternion.Lerp(stout, unsafe_rot, lamb);
338  }
339 
340  // Apply pos and rot to transform
341  bodyTransform.position = unsafe_pos;
342  bodyTransform.rotation = unsafe_rot;
343 
344  // Update pivot based on new body position
345  UpdatePivot();
346 
347  // Clamp the distance top the ground to be sure
348  bodyTransform.position = new Vector3(
349  bodyTransform.position.x,
350  pivotTransform.position.y + Mathf.Clamp(bodyTransform.position.y - pivotTransform.position.y, min_body_height, max_body_height),
351  bodyTransform.position.z
352  );
353  }
354 
355  void Update()
356  {
357  #if AVR_NET
358  if(synchronizePose) sync();
359  #endif
360 
361  SetBody();
362 
363  // This is something we need in "CorrectBodyYawAngle".
364  last_yaw = Mathf.MoveTowardsAngle(last_yaw, bodyTransform.rotation.eulerAngles.y, 9999.0f);
365 
366  UpdateLeftFoot();
367  UpdateRightFoot();
368  }
369 
370  void GetStandingTransform(out Quaternion rot, out Vector3 pos) {
371  // Standing upright position is fairly simple. Just go to neck and straight down from there.
372  Vector3 NeckPos = eyeTransform.position + eyeTransform.TransformVector(local_eye_to_neck_offset);
373 
374  pos = NeckPos + neck_body_distance * -Vector3.up;
375 
376  rot = CorrectBodyYawAngle(Quaternion.LookRotation(bodyTransform.forward, Vector3.up));
377  }
378 
379  void GetLeanTransform(out Quaternion rot, out Vector3 pos) {
380  //Lean position is the same as standing upright, but instead of straight down we go downwards by MainCamera.transform.down
381  Vector3 NeckPos = eyeTransform.position + eyeTransform.TransformVector(local_eye_to_neck_offset);
382 
383  pos = NeckPos + neck_body_distance * -playerRig.MainCamera.transform.up;
384 
385  rot = CorrectBodyYawAngle(Quaternion.LookRotation(eyeTransform.forward, NeckPos - pos));
386  }
387 
388  void OnDrawGizmos() {
389  if(Application.isPlaying) {
390  #if AVR_NET
391  if (!IsOwner) return;
392  #endif
393 
394  Gizmos.color = Color.green;
395  Gizmos.DrawSphere(eyeTransform.position, 0.1f);
396 
397  Gizmos.DrawRay(eyeTransform.position, eyeTransform.forward);
398 
399  Gizmos.color = Color.white;
400 
401  Vector3 local_eye_to_neck_offset = new Vector3(0.0f, -0.1f, -0.1f);
402  Vector3 NeckPos = eyeTransform.position + eyeTransform.TransformVector(local_eye_to_neck_offset);
403  Gizmos.DrawLine(eyeTransform.position, NeckPos);
404  Gizmos.DrawCube(NeckPos, new Vector3(0.05f, 0.05f, 0.05f));
405 
406  Gizmos.DrawLine(NeckPos, bodyTransform.position);
407  Gizmos.DrawCube(bodyTransform.position, new Vector3(0.05f, 0.05f, 0.05f));
408 
409  Gizmos.DrawCube(bodyTransform.position, new Vector3(0.2f, 0.2f, 0.2f));
410  Gizmos.DrawCube(pivotTransform.position, new Vector3(0.05f, 0.05f, 0.05f));
411  Gizmos.DrawRay(pivotTransform.position, pivotTransform.forward);
412 
413  Gizmos.color = Color.red;
414  Gizmos.DrawLine(bodyTransform.position, leftFootTarget.position);
415  Gizmos.DrawCube(leftFootTarget.position, new Vector3(0.05f, 0.05f, 0.05f));
416  Gizmos.DrawLine(bodyTransform.position, rightFootTarget.position);
417  Gizmos.DrawCube(rightFootTarget.position, new Vector3(0.05f, 0.05f, 0.05f));
418 
419  Gizmos.color = Color.yellow;
420  Gizmos.DrawLine(NeckPos, leftHandPos);
421  Gizmos.DrawLine(NeckPos, rightHandPos);
422  }
423  }
424 
425  Quaternion CorrectBodyYawAngle(Quaternion rot) {
426  float head_yaw = eyeTransform.localRotation.eulerAngles.y;
427  float body_yaw = last_yaw;
428 
429  // Get the difference in yaw between body and eyes/head
430  float yaw_diff = Mathf.DeltaAngle(head_yaw, body_yaw);
431 
432  // Account for max_head_yaw
433  yaw_diff = Mathf.Sign(yaw_diff) * Mathf.Max(0, Mathf.Abs(yaw_diff) - max_head_yaw);
434 
435  // Calcualte the speed at which we'll adapt.
436  float yaw_adapt_speed = Mathf.Abs(yaw_diff) * Time.deltaTime * 2.0f;
437 
438  // Adapt the rotation to the resulting yaw
439  return Quaternion.Euler(
440  bodyTransform.localRotation.eulerAngles.x,
441  Mathf.MoveTowardsAngle(body_yaw, head_yaw, yaw_adapt_speed),
442  bodyTransform.localRotation.eulerAngles.z
443  );
444  }
445 
446  void UpdateLeftFoot() {
447  Vector3 thPos = pivotTransform.position + new Vector3(-foot_offset_from_pivot.x, foot_offset_from_pivot.y, foot_offset_from_pivot.z);
448 
449  leftFootTarget.forward = Vector3.Lerp(leftFootTarget.forward, pivotTransform.forward - 0.5f * pivotTransform.right, foot_follow_speed * Time.deltaTime);
450 
451  leftFootTarget.position = Vector3.Lerp(leftFootTarget.position, thPos, foot_follow_speed * Time.deltaTime);
452  }
453 
455  Vector3 thPos = pivotTransform.position + foot_offset_from_pivot;
456 
457  rightFootTarget.forward = Vector3.Lerp(rightFootTarget.forward, pivotTransform.forward + 0.5f * pivotTransform.right, foot_follow_speed * Time.deltaTime);
458 
459  rightFootTarget.position = Vector3.Lerp(rightFootTarget.position, thPos, foot_follow_speed * Time.deltaTime);
460  }
461 
462  void UpdatePivot() {
463  Vector3 r_origin = bodyTransform.position;
464 
465  if (Physics.Raycast(r_origin, Vector3.down, out RaycastHit hit, 2.0f, groundCollisionMask))
466  {
467  pivotTransform.position = hit.point;
468  }
469 
470  pivotTransform.forward = playerRig.XZPlaneFacingDirection;
471  }
472 
473 #if AVR_NET
474  [HideInInspector]
476  public bool synchronizePose = false;
477 
478  [ServerRpc(RequireOwnership = false)]
479  private void syncServerRpc(InternalState state)
480  {
481  m_ReplicatedState.Value = state;
482  }
483 
484  private void sync()
485  {
486  if (IsOwner)
487  {
488  InternalState state = new InternalState();
489  state.FromReference(this);
490  }
491  else
492  {
493  m_ReplicatedState.Value.ApplyState(this);
494  }
495  }
496 
497  private readonly NetworkVariable<InternalState> m_ReplicatedState = new NetworkVariable<InternalState>(NetworkVariableReadPermission.Everyone, new InternalState());
498 
499  private struct InternalState : IInternalState<AVR_PoseProvider>
500  {
501  public Vector3 lookAtPos;
502  public Quaternion leftHandRot;
503  public Quaternion rightHandRot;
504  public Vector3 leftFootPos;
505  public Quaternion leftFootRot;
506  public Vector3 rightFootPos;
507  public Quaternion rightFootRot;
508  public Vector3 pivotPos;
509  public Quaternion pivotRot;
510  public Vector3 bodyPos;
511  public Quaternion bodyRot;
512  public Vector3 eyePos;
513  public Quaternion eyeRot;
514  public Vector3 leftHandPos;
515  public Vector3 rightHandPos;
516 
517  public void FromReference(AVR_PoseProvider reference)
518  {
519  this.lookAtPos = reference.lookAtPos;
520  this.leftHandRot = reference.leftHandRot;
521  this.rightHandRot = reference.rightHandRot;
522  this.leftFootPos = reference.leftFootPos;
523  this.leftFootRot = reference.leftFootRot;
524  this.rightFootPos = reference.rightFootPos;
525  this.rightFootRot = reference.rightFootRot;
526  this.pivotPos = reference.pivotPos;
527  this.pivotRot = reference.pivotRot;
528  this.bodyPos = reference.bodyPos;
529  this.bodyRot = reference.bodyRot;
530  this.eyePos = reference.eyePos;
531  this.eyeRot = reference.eyeRot;
532  this.leftHandPos = reference.leftHandPos;
533  this.rightHandPos = reference.rightHandPos;
534  }
535 
536  public void ApplyState(AVR_PoseProvider reference)
537  {
538  // Nothing to do here
539  }
540 
541  public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
542  {
543  serializer.SerializeValue(ref lookAtPos);
544  serializer.SerializeValue(ref leftHandRot);
545  serializer.SerializeValue(ref rightHandRot);
546  serializer.SerializeValue(ref leftFootPos);
547  serializer.SerializeValue(ref leftFootRot);
548  serializer.SerializeValue(ref rightFootPos);
549  serializer.SerializeValue(ref rightFootRot);
550  serializer.SerializeValue(ref pivotPos);
551  serializer.SerializeValue(ref pivotRot);
552  serializer.SerializeValue(ref bodyPos);
553  serializer.SerializeValue(ref bodyRot);
554  serializer.SerializeValue(ref eyePos);
555  serializer.SerializeValue(ref eyeRot);
556  serializer.SerializeValue(ref leftHandPos);
557  serializer.SerializeValue(ref rightHandPos);
558  }
559  }
560 #endif
561  }
562 }
Estimates the pose of a player from the locations of VR controllers and HMD
Vector3 lookAtPos
Endpoint of the players view vector
A PoseNaturalizationFilter "naturalizes" a pose. For instance, when a player places the controllers o...
Makes a property of an object only show in the Network-behaviour window. Also works for private/prote...
Quaternion CorrectBodyYawAngle(Quaternion rot)
Sets the documentation html file inside of Packages/com.avr.core/Documentation/html of a given class...
void GetLeanTransform(out Quaternion rot, out Vector3 pos)
Vector3 ApplyInertia(Vector3 current, Vector3 target)
Assigns given attributes to a foldout group in the inspector. The way these are drawn is determined b...
LayerMask groundCollisionMask
Collision mask of the ground
void GetStandingTransform(out Quaternion rot, out Vector3 pos)