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