AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
SolverSurfaceMagnetism.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 
4 using UnityEngine;
5 
6 namespace HoloToolkit.Unity
7 {
12  {
13  #region public enums
15  {
16  CameraFacing,
17  ToObject,
18  ToLinkedPosition
19  }
20  public enum RaycastModeEnum
21  {
22  Simple,
23  Box,
24  Sphere
25  }
26 
27  public enum OrientModeEnum
28  {
29  None,
30  Vertical,
31  Full,
32  Blended
33  }
34  #endregion
35 
36  #region public members
37  [Tooltip("LayerMask to apply Surface Magnetism to")]
38  public LayerMask MagneticSurface = 0;
39 
40  [Tooltip("Max distance to check for surfaces")]
41  public float MaxDistance = 3.0f;
42  [Tooltip("Closest distance to bring object")]
43  public float CloseDistance = 0.5f;
44 
45  [Tooltip("Offset from surface along surface normal")]
46  public float SurfaceNormalOffset = 0.5f;
47  [Tooltip("Offset from surface along ray cast direction")]
48  public float SurfaceRayOffset = 0;
49 
50  [Tooltip("Surface raycast mode. Simple = single raycast, Complex = bbox corners")]
51  public RaycastModeEnum raycastMode = RaycastModeEnum.Simple;
52 
53  [Tooltip("Number of rays per edge, should be odd. Total casts is n^2")]
54  public int BoxRaysPerEdge = 3;
55 
56  [Tooltip("If true, use orthographic casting for box lines instead of perspective")]
57  public bool OrthoBoxCast = false;
58 
59  [Tooltip("Align to ray cast direction if box cast hits many normals facing in varying directions")]
60  public float MaximumNormalVariance = 0.5f;
61 
62  [Tooltip("Radius to use for sphere cast")]
63  public float SphereSize = 1.0f;
64 
65  [Tooltip("When doing volume casts, use size override if non-zero instead of object's current scale")]
66  public float VolumeCastSizeOverride = 0;
67 
68  [Tooltip("When doing volume casts, use linked AltScale instead of object's current scale")]
69  public bool UseLinkedAltScaleOverride = false;
70 
71  // This is broken
72  [Tooltip("Instead of using mesh normal, extract normal from tex coord (SR is reported to put smoothed normals in there)")]
73  bool UseTexCoordNormals = false;
74 
75  [Tooltip("Raycast direction. Can cast from head in facing dir, or cast from head to object position")]
76  public RaycastDirectionEnum raycastDirection = RaycastDirectionEnum.ToLinkedPosition;
77 
78  [Tooltip("Orientation mode. None = no orienting, Vertical = Face head, but always oriented up/down, Full = Aligned to surface normal completely")]
79  public OrientModeEnum orientationMode = OrientModeEnum.Vertical;
80 
81  [Tooltip("Orientation Blend Value 0.0 = All head 1.0 = All surface")]
82  public float OrientBlend = 0.65f;
83 
84  [HideInInspector]
85  public bool OnSurface;
86  #endregion
87 
88  #region private members
89  private BoxCollider m_BoxCollider;
90  private const float maxDot = 0.97f;
91  #endregion
92 
93  protected void Start()
94  {
95  if (raycastMode == RaycastModeEnum.Box)
96  {
97  m_BoxCollider = GetComponent<BoxCollider>();
98  if (m_BoxCollider == null)
99  {
100  Debug.LogError("Box raycast mode requires a BoxCollider, but none was found! Defaulting to Simple raycast mode");
101  raycastMode = RaycastModeEnum.Simple;
102  }
103 
104  if (Application.isEditor)
105  {
107  }
108  }
109 
110  if (Application.isEditor && UseTexCoordNormals)
111  {
112  Debug.LogWarning("Disabling tex coord normals while in editor mode");
113  UseTexCoordNormals = false;
114  }
115  }
116 
125  private static bool DefaultRaycast(Vector3 origin, Vector3 direction, float distance, LayerMask surface, out RaycastResultHelper result)
126  {
127  return RaycastHelper.First(origin, direction, distance, surface, out result);
128  }
129 
130  private static bool DefaultSpherecast(Vector3 origin, Vector3 direction, float radius, float distance, LayerMask surface, out RaycastResultHelper result)
131  {
132  return RaycastHelper.SphereFirst(origin, direction, radius, distance, surface, out result);
133  }
134 
139  Vector3 GetRaycastOrigin()
140  {
141  if (solverHandler.TransformTarget == null)
142  {
143  return Vector3.zero;
144  }
145  return solverHandler.TransformTarget.position;
146  }
147 
154  Vector3 GetRaycastEndPoint()
155  {
156  Vector3 ret = Vector3.forward;
157  switch (raycastDirection)
158  {
159  case RaycastDirectionEnum.CameraFacing:
160  ret = solverHandler.TransformTarget.position + solverHandler.TransformTarget.forward;
161  break;
162 
163  case RaycastDirectionEnum.ToObject:
164  ret = transform.position;
165  break;
166 
167  case RaycastDirectionEnum.ToLinkedPosition:
168  ret = solverHandler.GoalPosition;
169  break;
170  }
171  return ret;
172  }
173 
178  Vector3 GetRaycastDirection()
179  {
180  Vector3 ret = Vector3.forward;
181  if (raycastDirection == RaycastDirectionEnum.CameraFacing)
182  {
183 
184  if (solverHandler.TransformTarget)
185  {
186  ret = solverHandler.TransformTarget.forward;
187  }
188  }
189  else
190  {
191  ret = (GetRaycastEndPoint() - GetRaycastOrigin()).normalized;
192  }
193  return ret;
194  }
195 
204  Quaternion CalculateMagnetismOrientation(Vector3 rayDir, Vector3 surfaceNormal)
205  {
206  // Calculate the surface rotation
207  Vector3 newDir = -surfaceNormal;
208  if (IsNormalVertical(newDir))
209  {
210  newDir = rayDir;
211  }
212 
213  newDir.y = 0;
214 
215  Quaternion surfaceRot = Quaternion.LookRotation(newDir, Vector3.up);
216 
217  switch (orientationMode)
218  {
219  case OrientModeEnum.None:
220  return solverHandler.GoalRotation;
221 
222  case OrientModeEnum.Vertical:
223  return surfaceRot;
224 
225  case OrientModeEnum.Full:
226  return Quaternion.LookRotation(-surfaceNormal, Vector3.up);
227 
228  case OrientModeEnum.Blended:
229  return Quaternion.Slerp(solverHandler.GoalRotation, surfaceRot, OrientBlend);
230  default:
231  return Quaternion.identity;
232  }
233  }
234 
240  bool IsNormalVertical(Vector3 normal)
241  {
242  return 1f - Mathf.Abs(normal.y) < 0.01f;
243  }
244 
249  float GetScaleOverride()
250  {
251  if (UseLinkedAltScaleOverride)
252  {
253  return solverHandler.AltScale.Current.magnitude;
254  }
255  return VolumeCastSizeOverride;
256  }
257 
258  public override void SolverUpdate()
259  {
260  // Pass-through by default
261  this.GoalPosition = WorkingPos;
262  this.GoalRotation = WorkingRot;
263 
264  // Determine raycast params
265  Ray ray = new Ray(GetRaycastOrigin(), GetRaycastDirection());
266 
267  // Skip if there's no valid direction
268  if (ray.direction == Vector3.zero)
269  {
270  return;
271  }
272 
273  float ScaleOverride = GetScaleOverride();
274  float len;
275  bool bHit;
276  RaycastResultHelper result;
277  Vector3 hitDelta;
278 
279  switch (raycastMode)
280  {
281  case RaycastModeEnum.Simple:
282  default:
283 
284  // Do the cast!
285  bHit = DefaultRaycast(ray.origin, ray.direction, MaxDistance, MagneticSurface, out result);
286 
287  OnSurface = bHit;
288 
289  if (UseTexCoordNormals)
290  {
291  result.OverrideNormalFromTextureCoord();
292  }
293 
294  // Enforce CloseDistance
295  hitDelta = result.Point - ray.origin;
296  len = hitDelta.magnitude;
297  if (len < CloseDistance)
298  {
299  result.OverridePoint(ray.origin + ray.direction * CloseDistance);
300  }
301 
302  // Apply results
303  if (bHit)
304  {
305  GoalPosition = result.Point + SurfaceNormalOffset * result.Normal + SurfaceRayOffset * ray.direction;
306  GoalRotation = CalculateMagnetismOrientation(ray.direction, result.Normal);
307  }
308  break;
309 
310  case RaycastModeEnum.Box:
311 
312  Vector3 scale = transform.lossyScale;
313  if (ScaleOverride > 0)
314  {
315  scale = scale.normalized * ScaleOverride;
316  }
317 
318  Quaternion orientation = orientationMode == OrientModeEnum.None ? Quaternion.LookRotation(ray.direction, Vector3.up) : CalculateMagnetismOrientation(ray.direction, Vector3.up);
319  Matrix4x4 targetMatrix = Matrix4x4.TRS(Vector3.zero, orientation, scale);
320 
321  if (m_BoxCollider == null)
322  {
323  m_BoxCollider = this.GetComponent<BoxCollider>();
324  }
325 
326  Vector3 extents = m_BoxCollider.size;
327 
328  Vector3[] positions;
329  Vector3[] normals;
330  bool[] hits;
331 
332  if (RaycastHelper.CastBoxExtents(extents, transform.position, targetMatrix, ray, MaxDistance, MagneticSurface, DefaultRaycast, BoxRaysPerEdge, OrthoBoxCast, out positions, out normals, out hits))
333  {
334  Plane plane;
335  float distance;
336 
337  // place an unconstrained plane down the ray. Never use vertical constrain.
338  FindPlacementPlane(ray.origin, ray.direction, positions, normals, hits, m_BoxCollider.size.x, MaximumNormalVariance, false, orientationMode == OrientModeEnum.None, out plane, out distance);
339 
340  // If placing on a horzizontal surface, need to adjust the calculated distance by half the app height
341  float verticalCorrectionOffset = 0;
342  if (IsNormalVertical(plane.normal) && !Mathf.Approximately(ray.direction.y, 0))
343  {
344  float boxSurfaceOffsetVert = targetMatrix.MultiplyVector(new Vector3(0, extents.y / 2f, 0)).magnitude;
345  Vector3 correctionVec = boxSurfaceOffsetVert * (ray.direction / ray.direction.y);
346  verticalCorrectionOffset = -correctionVec.magnitude;
347  }
348 
349  float boxSurfaceOffset = targetMatrix.MultiplyVector(new Vector3(0, 0, extents.z / 2f)).magnitude;
350 
351  // Apply boxSurfaceOffset to rayDir and not surfaceNormalDir to reduce sliding
352  GoalPosition = ray.origin + ray.direction * Mathf.Max(CloseDistance, distance + SurfaceRayOffset + boxSurfaceOffset + verticalCorrectionOffset) + plane.normal * (0 * boxSurfaceOffset + SurfaceNormalOffset);
353  GoalRotation = CalculateMagnetismOrientation(ray.direction, plane.normal);
354  OnSurface = true;
355  }
356  else
357  {
358  OnSurface = false;
359  }
360  break;
361 
362  case RaycastModeEnum.Sphere:
363 
364  // Do the cast!
365  float size = ScaleOverride > 0 ? ScaleOverride : transform.lossyScale.x * SphereSize;
366  bHit = DefaultSpherecast(ray.origin, ray.direction, size, MaxDistance, MagneticSurface, out result);
367  OnSurface = bHit;
368 
369  // Enforce CloseDistance
370  hitDelta = result.Point - ray.origin;
371  len = hitDelta.magnitude;
372  if (len < CloseDistance)
373  {
374  result.OverridePoint(ray.origin + ray.direction * CloseDistance);
375  }
376 
377  // Apply results
378  if (bHit)
379  {
380  GoalPosition = result.Point + SurfaceNormalOffset * result.Normal + SurfaceRayOffset * ray.direction;
381  GoalRotation = CalculateMagnetismOrientation(ray.direction, result.Normal);
382  }
383  break;
384  }
385 
386  // Do frame to frame updates of transform, smoothly toward the goal, if desired
387  UpdateWorkingPosToGoal();
388  UpdateWorkingRotToGoal();
389  }
390 
405  private static void FindPlacementPlane(Vector3 origin, Vector3 direction, Vector3[] positions, Vector3[] normals, bool[] hits, float assetWidth, float maxNormalVariance, bool constrainVertical, bool bUseClosestDistance, out Plane plane, out float closestDistance)
406  {
407  bool debugEnabled = RaycastHelper.DebugEnabled;
408 
409  int numRays = positions.Length;
410 
411  Vector3 originalDirection = direction;
412  if (constrainVertical)
413  {
414  direction.y = 0.0f;
415  direction = direction.normalized;
416  }
417 
418  // go through all the points and find the closest distance
419  int closestPoint = -1;
420  closestDistance = float.PositiveInfinity;
421  float farthestDistance = 0f;
422  int numHits = 0;
423  Vector3 averageNormal = Vector3.zero;
424 
425  for (int i = 0; i < numRays; i++)
426  {
427  if (hits[i] != false)
428  {
429  float dist = Vector3.Dot(direction, positions[i] - origin);
430 
431  if (dist < closestDistance)
432  {
433  closestPoint = i;
434  closestDistance = dist;
435  }
436  if (dist > farthestDistance)
437  {
438  farthestDistance = dist;
439  }
440 
441  averageNormal += normals[i];
442  ++numHits;
443  }
444  }
445  averageNormal /= numHits;
446 
447  // Calculate variance of all normals
448  float variance = 0;
449  for (int i = 0; i < numRays; ++i)
450  {
451  if (hits[i] != false)
452  {
453  variance += (normals[i] - averageNormal).magnitude;
454  }
455  }
456  variance /= numHits;
457 
458  // If variance is too high, I really don't want to deal with this surface
459  // And if we don't even have enough rays, I'm not confident about this at all
460  if (variance > maxNormalVariance || numHits < numRays / 4)
461  {
462  plane = new Plane(-direction, positions[closestPoint]);
463  return;
464  }
465 
466  // go through all the points and find the most orthagonal plane
467  float lowAngle = float.PositiveInfinity;
468  int lowIndex = -1;
469  float highAngle = float.NegativeInfinity;
470  int highIndex = -1;
471 
472  for (int i = 0; i < numRays; i++)
473  {
474  if (hits[i] == false || i == closestPoint)
475  {
476  continue;
477  }
478 
479  Vector3 diff = (positions[i] - positions[closestPoint]);
480  if (constrainVertical)
481  {
482  diff.y = 0.0f;
483  diff.Normalize();
484 
485  if (diff == Vector3.zero)
486  {
487  continue;
488  }
489  }
490  else
491  {
492  diff.Normalize();
493  }
494 
495  float angle = Vector3.Dot(direction, diff);
496 
497  if (angle < lowAngle)
498  {
499  lowAngle = angle;
500  lowIndex = i;
501  }
502  }
503 
504  if (!constrainVertical && lowIndex != -1)
505  {
506  for (int i = 0; i < numRays; i++)
507  {
508  if (hits[i] == false || i == closestPoint || i == lowIndex)
509  {
510  continue;
511  }
512 
513  float dot = Mathf.Abs(Vector3.Dot((positions[i] - positions[closestPoint]).normalized, (positions[lowIndex] - positions[closestPoint]).normalized));
514  if (dot > maxDot)
515  {
516  continue;
517  }
518 
519  Vector3 normal = Vector3.Cross(positions[lowIndex] - positions[closestPoint], positions[i] - positions[closestPoint]).normalized;
520 
521  float nextAngle = Mathf.Abs(Vector3.Dot(direction, normal));
522 
523  if (nextAngle > highAngle)
524  {
525  highAngle = nextAngle;
526  highIndex = i;
527  }
528  }
529  }
530 
531  Vector3 placementNormal;
532  if (lowIndex != -1)
533  {
534  if (debugEnabled)
535  {
536  Debug.DrawLine(positions[closestPoint], positions[lowIndex], Color.red);
537  }
538 
539  if (highIndex != -1)
540  {
541  if (debugEnabled)
542  {
543  Debug.DrawLine(positions[closestPoint], positions[highIndex], Color.green);
544  }
545  placementNormal = Vector3.Cross(positions[lowIndex] - positions[closestPoint], positions[highIndex] - positions[closestPoint]).normalized;
546  }
547  else
548  {
549  Vector3 planeUp = Vector3.Cross(positions[lowIndex] - positions[closestPoint], direction);
550  placementNormal = Vector3.Cross(positions[lowIndex] - positions[closestPoint], constrainVertical ? Vector3.up : planeUp).normalized;
551  }
552 
553  if (debugEnabled)
554  {
555  Debug.DrawLine(positions[closestPoint], positions[closestPoint] + placementNormal, Color.blue);
556  }
557  }
558  else
559  {
560  placementNormal = direction * -1.0f;
561  }
562 
563  if (Vector3.Dot(placementNormal, direction) > 0.0f)
564  {
565  placementNormal *= -1.0f;
566  }
567 
568  plane = new Plane(placementNormal, positions[closestPoint]);
569 
570  if (debugEnabled)
571  {
572  Debug.DrawRay(positions[closestPoint], placementNormal, Color.cyan);
573  }
574 
575  // Figure out how far the plane should be.
576  if (!bUseClosestDistance && closestPoint >= 0)
577  {
578  float centerPlaneDistance;
579  Ray centerPlaneRay = new Ray(origin, originalDirection);
580  if (plane.Raycast(centerPlaneRay, out centerPlaneDistance) || centerPlaneDistance != 0)
581  {
582  // When the plane is nearly parallel to the user, we need to clamp the distance to where the raycasts hit.
583  closestDistance = Mathf.Clamp(centerPlaneDistance, closestDistance, farthestDistance + assetWidth * 0.5f);
584  }
585  else
586  {
587  Debug.LogError("FindPlacementPlane: Not expected to have the center point not intersect the plane.");
588  }
589  }
590  }
591  }
592 }
static bool First(Vector3 origin, Vector3 direction, float distance, LayerMask layerMask, out RaycastResultHelper result)
static bool SphereFirst(Vector3 origin, Vector3 direction, float radius, float distance, LayerMask layerMask, out RaycastResultHelper result)
SolverBase is the base abstract class for all Solvers to derive from. It provides state tracking...
Definition: Solver.cs:15
SurfaceMagnetism casts rays to Surfaces in the world align the object to the surface.
static bool CastBoxExtents(Vector3 extents, Vector3 targetPosition, Matrix4x4 trs, Ray ray, float maxDistance, LayerMask surface, RaycastFunc raycastFunc, int raysPerEdge, bool ortho, out Vector3[] points, out Vector3[] normals, out bool[] hits)