AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
HeadsUpDirectionIndicator.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 using UnityEngine;
4 
5 namespace HoloToolkit.Unity
6 {
7  // The easiest way to use this script is to drop in the HeadsUpDirectionIndicator prefab
8  // from the HoloToolKit. If you're having issues with the prefab or can't find it,
9  // you can simply create an empty GameObject and attach this script. You'll need to
10  // create your own pointer object which can by any 3D game object. You'll need to adjust
11  // the depth, margin and pivot variables to affect the right appearance. After that you
12  // simply need to specify the "targetObject" and then you should be set.
13  //
14  // This script assumes your point object "aims" along its local up axis and orients the
15  // object according to that assumption.
16  public class HeadsUpDirectionIndicator : MonoBehaviour
17  {
18  // Use as a named indexer for Unity's frustum planes. The order follows that laid
19  // out in the API documentation. DO NOT CHANGE ORDER unless a corresponding change
20  // has been made in the Unity API.
21  private enum FrustumPlanes
22  {
23  Left = 0,
24  Right,
25  Bottom,
26  Top,
27  Near,
28  Far
29  }
30 
31  [Tooltip("The object the direction indicator will point to.")]
32  public GameObject TargetObject;
33 
34  [Tooltip("The camera depth at which the indicator rests.")]
35  public float Depth;
36 
37  [Tooltip("The point around which the indicator pivots. Should be placed at the model's 'tip'.")]
38  public Vector3 Pivot;
39 
40  [Tooltip("The object used to 'point' at the target.")]
41  public GameObject PointerPrefab;
42 
43  [Tooltip("Determines what percentage of the visible field should be margin.")]
44  [Range(0.0f, 1.0f)]
45  public float IndicatorMarginPercent;
46 
47  [Tooltip("Debug draw the planes used to calculate the pointer lock location.")]
49 
50  private GameObject pointer;
51 
52  private static int frustumLastUpdated = -1;
53 
54  private static Plane[] frustumPlanes;
55  private static Vector3 cameraForward;
56  private static Vector3 cameraPosition;
57  private static Vector3 cameraRight;
58  private static Vector3 cameraUp;
59 
60  private Plane[] indicatorVolume;
61 
62  private void Start()
63  {
64  Depth = Mathf.Clamp(Depth, CameraCache.Main.nearClipPlane, CameraCache.Main.farClipPlane);
65 
66  if (PointerPrefab == null)
67  {
68  this.gameObject.SetActive(false);
69  return;
70  }
71 
72  pointer = GameObject.Instantiate(PointerPrefab);
73 
74  // We create the effect of pivoting rotations by parenting the pointer and
75  // offsetting its position.
76  pointer.transform.parent = transform;
77  pointer.transform.position = -Pivot;
78 
79  // Allocate the space to hold the indicator volume planes. Later portions of the algorithm take for
80  // granted that these objects have been initialized.
81  indicatorVolume = new Plane[]
82  {
83  new Plane(),
84  new Plane(),
85  new Plane(),
86  new Plane(),
87  new Plane(),
88  new Plane()
89  };
90  }
91 
92  // Update the direction indicator's position and orientation every frame.
93  private void Update()
94  {
95  if (!HasObjectsToTrack()) { return; }
96 
97  int currentFrameCount = Time.frameCount;
98  if (currentFrameCount != frustumLastUpdated)
99  {
100  // Collect the updated camera information for the current frame
101  CacheCameraTransform(CameraCache.Main);
102 
103  frustumLastUpdated = currentFrameCount;
104  }
105 
106  UpdatePointerTransform(CameraCache.Main, indicatorVolume, TargetObject.transform.position);
107  }
108 
109  private bool HasObjectsToTrack()
110  {
111  return TargetObject != null && pointer != null;
112  }
113 
114  // Cache data from the camera state that are costly to retrieve.
115  private void CacheCameraTransform(Camera camera)
116  {
117  cameraForward = camera.transform.forward;
118  cameraPosition = camera.transform.position;
119  cameraRight = camera.transform.right;
120  cameraUp = camera.transform.up;
121  frustumPlanes = GeometryUtility.CalculateFrustumPlanes(camera);
122  }
123 
124  // Assuming the target object is outside the view which of the four "wall" planes should
125  // the pointer snap to.
126  private FrustumPlanes GetExitPlane(Vector3 targetPosition, Camera camera)
127  {
128  // To do this we first create two planes that diagonally bisect the frustum
129  // These panes create four quadrants. We then infer the exit plane based on
130  // which quadrant the target position is in.
131 
132  // Calculate a set of vectors that can be used to build the frustum corners in world
133  // space.
134  float aspect = camera.aspect;
135  float fovy = 0.5f * camera.fieldOfView;
136  float near = camera.nearClipPlane;
137  float far = camera.farClipPlane;
138 
139  float tanFovy = Mathf.Tan(Mathf.Deg2Rad * fovy);
140  float tanFovx = aspect * tanFovy;
141 
142  // Calculate the edges of the frustum as world space offsets from the middle of the
143  // frustum in world space.
144  Vector3 nearTop = near * tanFovy * cameraUp;
145  Vector3 nearRight = near * tanFovx * cameraRight;
146  Vector3 nearBottom = -nearTop;
147  Vector3 nearLeft = -nearRight;
148  Vector3 farTop = far * tanFovy * cameraUp;
149  Vector3 farRight = far * tanFovx * cameraRight;
150  Vector3 farLeft = -farRight;
151 
152  // Calculate the center point of the near plane and the far plane as offsets from the
153  // camera in world space.
154  Vector3 nearBase = near * cameraForward;
155  Vector3 farBase = far * cameraForward;
156 
157  // Calculate the frustum corners needed to create 'd'
158  Vector3 nearUpperLeft = nearBase + nearTop + nearLeft;
159  Vector3 nearLowerRight = nearBase + nearBottom + nearRight;
160  Vector3 farUpperLeft = farBase + farTop + farLeft;
161 
162  Plane d = new Plane(nearUpperLeft, nearLowerRight, farUpperLeft);
163 
164  // Calculate the frustum corners needed to create 'e'
165  Vector3 nearUpperRight = nearBase + nearTop + nearRight;
166  Vector3 nearLowerLeft = nearBase + nearBottom + nearLeft;
167  Vector3 farUpperRight = farBase + farTop + farRight;
168 
169  Plane e = new Plane(nearUpperRight, nearLowerLeft, farUpperRight);
170 
171 #if UNITY_EDITOR
172  if (DebugDrawPointerOrientationPlanes)
173  {
174  // Debug draw a triangle coplanar with 'd'
175  Debug.DrawLine(nearUpperLeft, nearLowerRight);
176  Debug.DrawLine(nearLowerRight, farUpperLeft);
177  Debug.DrawLine(farUpperLeft, nearUpperLeft);
178 
179  // Debug draw a triangle coplanar with 'e'
180  Debug.DrawLine(nearUpperRight, nearLowerLeft);
181  Debug.DrawLine(nearLowerLeft, farUpperRight);
182  Debug.DrawLine(farUpperRight, nearUpperRight);
183  }
184 #endif
185 
186  // We're not actually interested in the "distance" to the planes. But the sign
187  // of the distance tells us which quadrant the target position is in.
188  float dDistance = d.GetDistanceToPoint(targetPosition);
189  float eDistance = e.GetDistanceToPoint(targetPosition);
190 
191  // d e
192  // +\- +/-
193  // \ -d +e /
194  // \ /
195  // \ /
196  // \ /
197  // \ /
198  // +d +e \/
199  // /\ -d -e
200  // / \
201  // / \
202  // / \
203  // / \
204  // / +d -e \
205  // +/- +\-
206 
207  if (dDistance > 0.0f)
208  {
209  if (eDistance > 0.0f)
210  {
211  return FrustumPlanes.Left;
212  } else
213  {
214  return FrustumPlanes.Bottom;
215  }
216  } else
217  {
218  if (eDistance > 0.0f)
219  {
220  return FrustumPlanes.Top;
221  } else
222  {
223  return FrustumPlanes.Right;
224  }
225  }
226  }
227 
228  // given a frustum wall we wish to snap the pointer to, this function returns a ray
229  // along which the pointer should be placed to appear at the appropriate point along
230  // the edge of the indicator field.
231  private bool TryGetIndicatorPosition(Vector3 targetPosition, Plane frustumWall, out Ray r)
232  {
233  // Think of the pointer as pointing the shortest rotation a user must make to see a
234  // target. The shortest rotation can be obtained by finding the great circle defined
235  // be the target, the camera position and the center position of the view. The tangent
236  // vector of the great circle points the direction of the shortest rotation. This
237  // great circle and thus any of it's tangent vectors are coplanar with the plane
238  // defined by these same three points.
239  Vector3 cameraToTarget = targetPosition - cameraPosition;
240  Vector3 normal = Vector3.Cross(cameraToTarget.normalized, cameraForward);
241 
242  // In the case that the three points are colinear we cannot form a plane but we'll
243  // assume the target is directly behind us and we'll use a pre-chosen plane.
244  if (normal == Vector3.zero)
245  {
246  normal = -Vector3.right;
247  }
248 
249  Plane q = new Plane(normal, targetPosition);
250  return TryIntersectPlanes(frustumWall, q, out r);
251  }
252 
253  // Obtain the line of intersection of two planes. This is based on a method
254  // described in the GPU Gems series.
255  private bool TryIntersectPlanes(Plane p, Plane q, out Ray intersection)
256  {
257  Vector3 rNormal = Vector3.Cross(p.normal, q.normal);
258  float det = rNormal.sqrMagnitude;
259 
260  if (det != 0.0f)
261  {
262  Vector3 rPoint = ((Vector3.Cross(rNormal, q.normal) * p.distance) +
263  (Vector3.Cross(p.normal, rNormal) * q.distance)) / det;
264  intersection = new Ray(rPoint, rNormal);
265  return true;
266  } else
267  {
268  intersection = new Ray();
269  return false;
270  }
271  }
272 
273  // Modify the pointer location and orientation to point along the shortest rotation,
274  // toward tergetPosition, keeping the pointer confined inside the frustum defined by
275  // planes.
276  private void UpdatePointerTransform(Camera camera, Plane[] planes, Vector3 targetPosition)
277  {
278  // Use the camera information to create the new bounding volume
279  UpdateIndicatorVolume(camera);
280 
281  // Start by assuming the pointer should be placed at the target position.
282  Vector3 indicatorPosition = cameraPosition + Depth * (targetPosition - cameraPosition).normalized;
283 
284  // Test the target position with the frustum planes except the "far" plane since
285  // far away objects should be considered in view.
286  bool pointNotInsideIndicatorField = false;
287  for (int i = 0; i < 5; ++i)
288  {
289  float dot = Vector3.Dot(planes[i].normal, (targetPosition - cameraPosition).normalized);
290  if (dot <= 0.0f)
291  {
292  pointNotInsideIndicatorField = true;
293  break;
294  }
295  }
296 
297  // if the target object appears outside the indicator area...
298  if (pointNotInsideIndicatorField)
299  {
300  // ...then we need to do some geometry calculations to lock it to the edge.
301 
302  // used to determine which edge of the screen the indicator vector
303  // would exit through.
304  FrustumPlanes exitPlane = GetExitPlane(targetPosition, camera);
305 
306  Ray r;
307  if (TryGetIndicatorPosition(targetPosition, planes[(int)exitPlane], out r))
308  {
309  indicatorPosition = cameraPosition + Depth * r.direction.normalized;
310  }
311  }
312 
313  this.transform.position = indicatorPosition;
314 
315  // The pointer's direction should always appear pointing away from the user's center
316  // of view. Thus we find the center point of the user's view in world space.
317 
318  // But the pointer should also appear perpendicular to the viewer so we find the
319  // center position of the view that is on the same plane as the pointer position.
320  // We do this by projecting the vector from the pointer to the camera onto the
321  // the camera's forward vector.
322  Vector3 indicatorFieldOffset = indicatorPosition - cameraPosition;
323  indicatorFieldOffset = Vector3.Dot(indicatorFieldOffset, cameraForward) * cameraForward;
324 
325  Vector3 indicatorFieldCenter = cameraPosition + indicatorFieldOffset;
326  Vector3 pointerDirection = (indicatorPosition - indicatorFieldCenter).normalized;
327 
328  // Align this object's up vector with the pointerDirection
329  this.transform.rotation = Quaternion.LookRotation(cameraForward, pointerDirection);
330  }
331 
332  // Here we adjust the Camera's frustum planes to place the cursor in a smaller
333  // volume, thus creating the effect of a "margin"
334  private void UpdateIndicatorVolume(Camera camera)
335  {
336  // The top, bottom and side frustum planes are used to restrict the movement
337  // of the pointer. These reside at indices 0-3;
338  for (int i = 0; i < 4; ++i)
339  {
340  // We can make the frustum smaller by rotating the walls "in" toward the
341  // camera's forward vector.
342 
343  // First find the angle between the Camera's forward and the plane's normal
344  float angle = Mathf.Acos(Vector3.Dot(frustumPlanes[i].normal.normalized, cameraForward));
345 
346  // Then we calculate how much we should rotate the plane in based on the
347  // user's setting. 90 degrees is our maximum as at that point we no longer
348  // have a valid frustum.
349  float angleStep = IndicatorMarginPercent * (0.5f * Mathf.PI - angle);
350 
351  // Because the frustum plane normals face in we must actually rotate away from the forward vector
352  // to narrow the frustum.
353  Vector3 normal = Vector3.RotateTowards(frustumPlanes[i].normal, cameraForward, -angleStep, 0.0f);
354 
355  indicatorVolume[i].normal = normal.normalized;
356  indicatorVolume[i].distance = frustumPlanes[i].distance;
357  }
358 
359  indicatorVolume[4] = frustumPlanes[4];
360  indicatorVolume[5] = frustumPlanes[5];
361  }
362  }
363 }
The purpose of this class is to provide a cached reference to the main camera. Calling Camera...
Definition: CameraCache.cs:12
static Camera Main
Returns a cached reference to the main camera and uses Camera.main if it hasn&#39;t been cached yet...
Definition: CameraCache.cs:20