forked from microsoft/MixedRealityToolkit-Unity
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHeadsUpDirectionIndicator.cs
369 lines (313 loc) · 15.4 KB
/
HeadsUpDirectionIndicator.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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using UnityEngine;
namespace HoloToolkit.Unity
{
// The easiest way to use this script is to drop in the HeadsUpDirectionIndicator prefab
// from the HoloToolKit. If you're having issues with the prefab or can't find it,
// you can simply create an empty GameObject and attach this script. You'll need to
// create your own pointer object which can by any 3D game object. You'll need to adjust
// the depth, margin and pivot variables to affect the right appearance. After that you
// simply need to specify the "targetObject" and then you should be set.
//
// This script assumes your point object "aims" along its local up axis and orients the
// object according to that assumption.
public class HeadsUpDirectionIndicator : MonoBehaviour
{
// Use as a named indexer for Unity's frustum planes. The order follows that layed
// out in the API documentation. DO NOT CHANGE ORDER unless a corresponding change
// has been made in the Unity API.
private enum FrustumPlanes
{
Left = 0,
Right,
Bottom,
Top,
Near,
Far
}
[Tooltip("The object the direction indicator will point to.")]
public GameObject TargetObject;
[Tooltip("The camera depth at which the indicator rests.")]
public float Depth;
[Tooltip("The point around which the indicator pivots. Should be placed at the model's 'tip'.")]
public Vector3 Pivot;
[Tooltip("The object used to 'point' at the target.")]
public GameObject PointerPrefab;
[Tooltip("Determines what percentage of the visible field should be margin.")]
[Range(0.0f, 1.0f)]
public float IndicatorMarginPercent;
[Tooltip("Debug draw the planes used to calculate the pointer lock location.")]
public bool DebugDrawPointerOrientationPlanes;
private GameObject pointer;
private static int frustumLastUpdated = -1;
private static Plane[] frustumPlanes;
private static Vector3 cameraForward;
private static Vector3 cameraPosition;
private static Vector3 cameraRight;
private static Vector3 cameraUp;
private Plane[] indicatorVolume;
private void Start()
{
Depth = Mathf.Clamp(Depth, Camera.main.nearClipPlane, Camera.main.farClipPlane);
if (PointerPrefab == null)
{
this.gameObject.SetActive(false);
return;
}
pointer = GameObject.Instantiate(PointerPrefab);
// We create the effect of pivoting rotations by parenting the pointer and
// offsetting its position.
pointer.transform.parent = transform;
pointer.transform.position = -Pivot;
// Allocate the space to hold the indicator volume planes. Later portions of the algorithm take for
// granted that these objects have been initialized.
indicatorVolume = new Plane[]
{
new Plane(),
new Plane(),
new Plane(),
new Plane(),
new Plane(),
new Plane()
};
}
// Update the direction indicator's position and orientation every frame.
private void Update()
{
// No object to track?
if (TargetObject == null || pointer == null)
{
// bail out early.
return;
}
else
{
int currentFrameCount = UnityEngine.Time.frameCount;
if (currentFrameCount != frustumLastUpdated)
{
// Collect the updated camera information for the current frame
CacheCameraTransform(Camera.main);
frustumLastUpdated = currentFrameCount;
}
UpdatePointerTransform(Camera.main, indicatorVolume, TargetObject.transform.position);
}
}
// Cache data from the camera state that are costly to retrieve.
private void CacheCameraTransform(Camera camera)
{
cameraForward = camera.transform.forward;
cameraPosition = camera.transform.position;
cameraRight = camera.transform.right;
cameraUp = camera.transform.up;
frustumPlanes = GeometryUtility.CalculateFrustumPlanes(camera);
}
// Assuming the target object is outside the view which of the four "wall" planes should
// the pointer snap to.
private FrustumPlanes GetExitPlane(Vector3 targetPosition, Camera camera)
{
// To do this we first create two planes that diagonally bisect the frustum
// These panes create four quadrants. We then infer the exit plane based on
// which quadrant the target position is in.
// Calculate a set of vectors that can be used to build the frustum corners in world
// space.
float aspect = camera.aspect;
float fovy = 0.5f * camera.fieldOfView;
float near = camera.nearClipPlane;
float far = camera.farClipPlane;
float tanFovy = Mathf.Tan(Mathf.Deg2Rad * fovy);
float tanFovx = aspect * tanFovy;
// Calculate the edges of the frustum as world space offsets from the middle of the
// frustum in world space.
Vector3 nearTop = near * tanFovy * cameraUp;
Vector3 nearRight = near * tanFovx * cameraRight;
Vector3 nearBottom = -nearTop;
Vector3 nearLeft = -nearRight;
Vector3 farTop = far * tanFovy * cameraUp;
Vector3 farRight = far * tanFovx * cameraRight;
Vector3 farLeft = -farRight;
// Caclulate the center point of the near plane and the far plane as offsets from the
// camera in world space.
Vector3 nearBase = near * cameraForward;
Vector3 farBase = far * cameraForward;
// Calculate the frustum corners needed to create 'd'
Vector3 nearUpperLeft = nearBase + nearTop + nearLeft;
Vector3 nearLowerRight = nearBase + nearBottom + nearRight;
Vector3 farUpperLeft = farBase + farTop + farLeft;
Plane d = new Plane(nearUpperLeft, nearLowerRight, farUpperLeft);
// Calculate the frustum corners needed to create 'e'
Vector3 nearUpperRight = nearBase + nearTop + nearRight;
Vector3 nearLowerLeft = nearBase + nearBottom + nearLeft;
Vector3 farUpperRight = farBase + farTop + farRight;
Plane e = new Plane(nearUpperRight, nearLowerLeft, farUpperRight);
#if UNITY_EDITOR
if (DebugDrawPointerOrientationPlanes)
{
// Debug draw a tringale coplanar with 'd'
Debug.DrawLine(nearUpperLeft, nearLowerRight);
Debug.DrawLine(nearLowerRight, farUpperLeft);
Debug.DrawLine(farUpperLeft, nearUpperLeft);
// Debug draw a triangle coplanar with 'e'
Debug.DrawLine(nearUpperRight, nearLowerLeft);
Debug.DrawLine(nearLowerLeft, farUpperRight);
Debug.DrawLine(farUpperRight, nearUpperRight);
}
#endif
// We're not actually interested in the "distance" to the planes. But the sign
// of the distance tells us which quadrant the target position is in.
float dDistance = d.GetDistanceToPoint(targetPosition);
float eDistance = e.GetDistanceToPoint(targetPosition);
// d e
// +\- +/-
// \ -d +e /
// \ /
// \ /
// \ /
// \ /
// +d +e \/
// /\ -d -e
// / \
// / \
// / \
// / \
// / +d -e \
// +/- +\-
if (dDistance > 0.0f)
{
if (eDistance > 0.0f)
{
return FrustumPlanes.Left;
}
else
{
return FrustumPlanes.Bottom;
}
}
else
{
if (eDistance > 0.0f)
{
return FrustumPlanes.Top;
}
else
{
return FrustumPlanes.Right;
}
}
}
// given a frustum wall we wish to snap the pointer to, this function returns a ray
// along which the pointer should be placed to appear at the appropiate point along
// the edge of the indicator field.
private bool TryGetIndicatorPosition(Vector3 targetPosition, Plane frustumWall, out Ray r)
{
// Think of the pointer as pointing the shortest rotation a user must make to see a
// target. The shortest rotation can be obtained by finding the great circle defined
// be the target, the camera position and the center position of the view. The tangent
// vector of the great circle points the direction of the shortest rotation. This
// great circle and thus any of it's tangent vectors are coplanar with the plane
// defined by these same three points.
Vector3 cameraToTarget = targetPosition - cameraPosition;
Vector3 normal = Vector3.Cross(cameraToTarget.normalized, cameraForward);
// In the case that the three points are colinear we cannot form a plane but we'll
// assume the target is directly behind us and we'll use a prechosen plane.
if (normal == Vector3.zero)
{
normal = -Vector3.right;
}
Plane q = new Plane(normal, targetPosition);
return TryIntersectPlanes(frustumWall, q, out r);
}
// Obtain the line of intersection of two planes. This is based on a method
// described in the GPU Gems series.
private bool TryIntersectPlanes(Plane p, Plane q, out Ray intersection)
{
Vector3 rNormal = Vector3.Cross(p.normal, q.normal);
float det = rNormal.sqrMagnitude;
if (det != 0.0f)
{
Vector3 rPoint = ((Vector3.Cross(rNormal, q.normal) * p.distance) +
(Vector3.Cross(p.normal, rNormal) * q.distance)) / det;
intersection = new Ray(rPoint, rNormal);
return true;
}
else
{
intersection = new Ray();
return false;
}
}
// Modify the pointer location and orientation to point along the shortest rotation,
// toward tergetPosition, keeping the pointer confined inside the frustum defined by
// planes.
private void UpdatePointerTransform(Camera camera, Plane[] planes, Vector3 targetPosition)
{
// Use the camera information to create the new bounding volume
UpdateIndicatorVolume(camera);
// Start by assuming the pointer should be placed at the target position.
Vector3 indicatorPosition = cameraPosition + Depth * (targetPosition - cameraPosition).normalized;
// Test the target position with the frustum planes except the "far" plane since
// far away objects should be considered in view.
bool pointNotInsideIndicatorField = false;
for (int i = 0; i < 5; ++i)
{
float dot = Vector3.Dot(planes[i].normal, (targetPosition - cameraPosition).normalized);
if (dot <= 0.0f)
{
pointNotInsideIndicatorField = true;
break;
}
}
// if the target object appears outside the indicator area...
if (pointNotInsideIndicatorField)
{
// ...then we need to do some geometry calculations to lock it to the edge.
// used to determine which edge of the screen the indicator vector
// would exit through.
FrustumPlanes exitPlane = GetExitPlane(targetPosition, camera);
Ray r;
if (TryGetIndicatorPosition(targetPosition, planes[(int)exitPlane], out r))
{
indicatorPosition = cameraPosition + Depth * r.direction.normalized;
}
}
this.transform.position = indicatorPosition;
// The pointer's direction should always appear pointing away from the user's center
// of view. Thus we find the center point of the user's view in world space.
// But the pointer should also appear perpendicular to the viewer so we find the
// center position of the view that is on the same plane as the pointer position.
// We do this by projecting the vector from the pointer to the camera onto the
// the camera's forward vector.
Vector3 indicatorFieldOffset = indicatorPosition - cameraPosition;
indicatorFieldOffset = Vector3.Dot(indicatorFieldOffset, cameraForward) * cameraForward;
Vector3 indicatorFieldCenter = cameraPosition + indicatorFieldOffset;
Vector3 pointerDirection = (indicatorPosition - indicatorFieldCenter).normalized;
// allign this object's up vector with the pointerDirection
this.transform.rotation = Quaternion.LookRotation(cameraForward, pointerDirection);
}
// Here we adjust the Camera's frustum planes to place the cursor in a smaller
// volume, thus creating the effect of a "margin"
private void UpdateIndicatorVolume(Camera camera)
{
// The top, bottom and side frustum planes are used to restrict the movement
// of the pointer. These reside at indices 0-3;
for (int i = 0; i < 4; ++i)
{
// We can make the frustum smaller by rotating the walls "in" toward the
// camera's forward vector.
// First find the angle between the Camera's forward and the plane's normal
float angle = Mathf.Acos(Vector3.Dot(frustumPlanes[i].normal.normalized, cameraForward));
// Then we calculate how much we should rotate the plane in based on the
// user's setting. 90 degrees is our maximum as at that point we no longer
// have a valid frustum.
float angleStep = IndicatorMarginPercent * (0.5f * Mathf.PI - angle);
// Because the frustum plane normals face in we must actually rotate away from the forward vector
// to narrow the frustum.
Vector3 normal = Vector3.RotateTowards(frustumPlanes[i].normal, cameraForward, -angleStep, 0.0f);
indicatorVolume[i].normal = normal.normalized;
indicatorVolume[i].distance = frustumPlanes[i].distance;
}
indicatorVolume[4] = frustumPlanes[4];
indicatorVolume[5] = frustumPlanes[5];
}
}
}