- //
- // 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
- {
-
- /// <summary>
- /// SurfaceMagnetism casts rays to Surfaces in the world align the object to the surface.
- /// </summary>
- public class SolverSurfaceMagnetism : Solver
- {
- #region public enums
- public enum RaycastDirectionEnum
- {
- CameraFacing,
- ToObject,
- ToLinkedPosition
- }
- public enum RaycastModeEnum
- {
- Simple,
- Box,
- Sphere
- }
-
- public enum OrientModeEnum
- {
- None,
- Vertical,
- Full,
- Blended
- }
- #endregion
-
- #region public members
- [Tooltip("LayerMask to apply Surface Magnetism to")]
- public LayerMask MagneticSurface = 0;
-
- [Tooltip("Max distance to check for surfaces")]
- public float MaxDistance = 3.0f;
- [Tooltip("Closest distance to bring object")]
- public float CloseDistance = 0.5f;
-
- [Tooltip("Offset from surface along surface normal")]
- public float SurfaceNormalOffset = 0.5f;
- [Tooltip("Offset from surface along ray cast direction")]
- public float SurfaceRayOffset = 0;
-
- [Tooltip("Surface raycast mode. Simple = single raycast, Complex = bbox corners")]
- public RaycastModeEnum raycastMode = RaycastModeEnum.Simple;
-
- [Tooltip("Number of rays per edge, should be odd. Total casts is n^2")]
- public int BoxRaysPerEdge = 3;
-
- [Tooltip("If true, use orthographic casting for box lines instead of perspective")]
- public bool OrthoBoxCast = false;
-
- [Tooltip("Align to ray cast direction if box cast hits many normals facing in varying directions")]
- public float MaximumNormalVariance = 0.5f;
-
- [Tooltip("Radius to use for sphere cast")]
- public float SphereSize = 1.0f;
-
- [Tooltip("When doing volume casts, use size override if non-zero instead of object's current scale")]
- public float VolumeCastSizeOverride = 0;
-
- [Tooltip("When doing volume casts, use linked AltScale instead of object's current scale")]
- public bool UseLinkedAltScaleOverride = false;
-
- // This is broken
- [Tooltip("Instead of using mesh normal, extract normal from tex coord (SR is reported to put smoothed normals in there)")]
- bool UseTexCoordNormals = false;
-
- [Tooltip("Raycast direction. Can cast from head in facing dir, or cast from head to object position")]
- public RaycastDirectionEnum raycastDirection = RaycastDirectionEnum.ToLinkedPosition;
-
- [Tooltip("Orientation mode. None = no orienting, Vertical = Face head, but always oriented up/down, Full = Aligned to surface normal completely")]
- public OrientModeEnum orientationMode = OrientModeEnum.Vertical;
-
- [Tooltip("Orientation Blend Value 0.0 = All head 1.0 = All surface")]
- public float OrientBlend = 0.65f;
-
- [HideInInspector]
- public bool OnSurface;
- #endregion
-
- #region private members
- private BoxCollider m_BoxCollider;
- private const float maxDot = 0.97f;
- #endregion
-
- protected override void Start()
- {
- base.Start();
-
- if (raycastMode == RaycastModeEnum.Box)
- {
- m_BoxCollider = GetComponent<BoxCollider>();
- if (m_BoxCollider == null)
- {
- Debug.LogError("Box raycast mode requires a BoxCollider, but none was found! Defaulting to Simple raycast mode");
- raycastMode = RaycastModeEnum.Simple;
- }
-
- if (Application.isEditor)
- {
- RaycastHelper.DebugEnabled = true;
- }
- }
-
- if (Application.isEditor && UseTexCoordNormals)
- {
- Debug.LogWarning("Disabling tex coord normals while in editor mode");
- UseTexCoordNormals = false;
- }
- }
-
- /// <summary>
- /// Wraps the raycast call in one spot.
- /// </summary>
- /// <param name="origin"></param>
- /// <param name="direction"></param>
- /// <param name="distance"></param>
- /// <param name="result"></param>
- /// <returns>bool, true if a surface was hit</returns>
- private static bool DefaultRaycast(Vector3 origin, Vector3 direction, float distance, LayerMask surface, out RaycastResultHelper result)
- {
- return RaycastHelper.First(origin, direction, distance, surface, out result);
- }
-
- private static bool DefaultSpherecast(Vector3 origin, Vector3 direction, float radius, float distance, LayerMask surface, out RaycastResultHelper result)
- {
- return RaycastHelper.SphereFirst(origin, direction, radius, distance, surface, out result);
- }
-
- /// <summary>
- /// Where should rays originate from?
- /// </summary>
- /// <returns>Vector3</returns>
- Vector3 GetRaycastOrigin()
- {
- if (solverHandler.TransformTarget == null)
- {
- return Vector3.zero;
- }
- return solverHandler.TransformTarget.position;
- }
-
- /// <summary>
- /// Which point should the ray cast toward? Not really the 'end' of the ray. The ray may be cast along
- /// the head facing direction, from the eye to the object, or to the solver's linked position (working from
- /// the previous solvers)
- /// </summary>
- /// <returns>Vector3, a point on the ray besides the origin</returns>
- Vector3 GetRaycastEndPoint()
- {
- Vector3 ret = Vector3.forward;
- switch (raycastDirection)
- {
- case RaycastDirectionEnum.CameraFacing:
- ret = solverHandler.TransformTarget.position + solverHandler.TransformTarget.forward;
- break;
-
- case RaycastDirectionEnum.ToObject:
- ret = transform.position;
- break;
-
- case RaycastDirectionEnum.ToLinkedPosition:
- ret = solverHandler.GoalPosition;
- break;
- }
- return ret;
- }
-
- /// <summary>
- /// Calculate the raycast direction based on the two ray points
- /// </summary>
- /// <returns>Vector3, the direction of the raycast</returns>
- Vector3 GetRaycastDirection()
- {
- Vector3 ret = Vector3.forward;
- if (raycastDirection == RaycastDirectionEnum.CameraFacing)
- {
-
- if (solverHandler.TransformTarget)
- {
- ret = solverHandler.TransformTarget.forward;
- }
- }
- else
- {
- ret = (GetRaycastEndPoint() - GetRaycastOrigin()).normalized;
- }
- return ret;
- }
-
- /// <summary>
- /// Calculates how the object should orient to the surface. May be none to pass shared orientation through,
- /// oriented to the surface but fully vertical, fully oriented to the surface normal, or a slerped blend
- /// of the vertial orientation and the pass-through rotation.
- /// </summary>
- /// <param name="rayDir"></param>
- /// <param name="surfaceNormal"></param>
- /// <returns>Quaternion, the orientation to use for the object</returns>
- Quaternion CalculateMagnetismOrientation(Vector3 rayDir, Vector3 surfaceNormal)
- {
- // Calculate the surface rotation
- Vector3 newDir = -surfaceNormal;
- if (IsNormalVertical(newDir))
- {
- newDir = rayDir;
- }
-
- newDir.y = 0;
-
- Quaternion surfaceRot = Quaternion.LookRotation(newDir, Vector3.up);
-
- switch (orientationMode)
- {
- case OrientModeEnum.None:
- return solverHandler.GoalRotation;
-
- case OrientModeEnum.Vertical:
- return surfaceRot;
-
- case OrientModeEnum.Full:
- return Quaternion.LookRotation(-surfaceNormal, Vector3.up);
-
- case OrientModeEnum.Blended:
- return Quaternion.Slerp(solverHandler.GoalRotation, surfaceRot, OrientBlend);
- default:
- return Quaternion.identity;
- }
- }
-
- /// <summary>
- /// Checks if a normal is nearly vertical
- /// </summary>
- /// <param name="normal"></param>
- /// <returns>bool</returns>
- bool IsNormalVertical(Vector3 normal)
- {
- return 1f - Mathf.Abs(normal.y) < 0.01f;
- }
-
- /// <summary>
- /// A constant scale override may be specified for volumetric raycasts, oherwise uses the current value of the solver link's alt scale
- /// </summary>
- /// <returns>float</returns>
- float GetScaleOverride()
- {
- if (UseLinkedAltScaleOverride)
- {
- return solverHandler.AltScale.Current.magnitude;
- }
- return VolumeCastSizeOverride;
- }
-
- public override void SolverUpdate()
- {
- // Pass-through by default
- this.GoalPosition = WorkingPos;
- this.GoalRotation = WorkingRot;
-
- // Determine raycast params
- Ray ray = new Ray(GetRaycastOrigin(), GetRaycastDirection());
-
- // Skip if there's no valid direction
- if (ray.direction == Vector3.zero)
- {
- return;
- }
-
- float ScaleOverride = GetScaleOverride();
- float len;
- bool bHit;
- RaycastResultHelper result;
- Vector3 hitDelta;
-
- switch (raycastMode)
- {
- case RaycastModeEnum.Simple:
- default:
-
- // Do the cast!
- bHit = DefaultRaycast(ray.origin, ray.direction, MaxDistance, MagneticSurface, out result);
-
- OnSurface = bHit;
-
- if (UseTexCoordNormals)
- {
- result.OverrideNormalFromTextureCoord();
- }
-
- // Enforce CloseDistance
- hitDelta = result.Point - ray.origin;
- len = hitDelta.magnitude;
- if (len < CloseDistance)
- {
- result.OverridePoint(ray.origin + ray.direction * CloseDistance);
- }
-
- // Apply results
- if (bHit)
- {
- GoalPosition = result.Point + SurfaceNormalOffset * result.Normal + SurfaceRayOffset * ray.direction;
- GoalRotation = CalculateMagnetismOrientation(ray.direction, result.Normal);
- }
- break;
-
- case RaycastModeEnum.Box:
-
- Vector3 scale = transform.lossyScale;
- if (ScaleOverride > 0)
- {
- scale = scale.normalized * ScaleOverride;
- }
-
- Quaternion orientation = orientationMode == OrientModeEnum.None ? Quaternion.LookRotation(ray.direction, Vector3.up) : CalculateMagnetismOrientation(ray.direction, Vector3.up);
- Matrix4x4 targetMatrix = Matrix4x4.TRS(Vector3.zero, orientation, scale);
-
- if (m_BoxCollider == null)
- {
- m_BoxCollider = this.GetComponent<BoxCollider>();
- }
-
- Vector3 extents = m_BoxCollider.size;
-
- Vector3[] positions;
- Vector3[] normals;
- bool[] hits;
-
- if (RaycastHelper.CastBoxExtents(extents, transform.position, targetMatrix, ray, MaxDistance, MagneticSurface, DefaultRaycast, BoxRaysPerEdge, OrthoBoxCast, out positions, out normals, out hits))
- {
- Plane plane;
- float distance;
-
- // place an unconstrained plane down the ray. Never use vertical constrain.
- FindPlacementPlane(ray.origin, ray.direction, positions, normals, hits, m_BoxCollider.size.x, MaximumNormalVariance, false, orientationMode == OrientModeEnum.None, out plane, out distance);
-
- // If placing on a horzizontal surface, need to adjust the calculated distance by half the app height
- float verticalCorrectionOffset = 0;
- if (IsNormalVertical(plane.normal) && !Mathf.Approximately(ray.direction.y, 0))
- {
- float boxSurfaceOffsetVert = targetMatrix.MultiplyVector(new Vector3(0, extents.y / 2f, 0)).magnitude;
- Vector3 correctionVec = boxSurfaceOffsetVert * (ray.direction / ray.direction.y);
- verticalCorrectionOffset = -correctionVec.magnitude;
- }
-
- float boxSurfaceOffset = targetMatrix.MultiplyVector(new Vector3(0, 0, extents.z / 2f)).magnitude;
-
- // Apply boxSurfaceOffset to rayDir and not surfaceNormalDir to reduce sliding
- GoalPosition = ray.origin + ray.direction * Mathf.Max(CloseDistance, distance + SurfaceRayOffset + boxSurfaceOffset + verticalCorrectionOffset) + plane.normal * (0 * boxSurfaceOffset + SurfaceNormalOffset);
- GoalRotation = CalculateMagnetismOrientation(ray.direction, plane.normal);
- OnSurface = true;
- }
- else
- {
- OnSurface = false;
- }
- break;
-
- case RaycastModeEnum.Sphere:
-
- // Do the cast!
- float size = ScaleOverride > 0 ? ScaleOverride : transform.lossyScale.x * SphereSize;
- bHit = DefaultSpherecast(ray.origin, ray.direction, size, MaxDistance, MagneticSurface, out result);
- OnSurface = bHit;
-
- // Enforce CloseDistance
- hitDelta = result.Point - ray.origin;
- len = hitDelta.magnitude;
- if (len < CloseDistance)
- {
- result.OverridePoint(ray.origin + ray.direction * CloseDistance);
- }
-
- // Apply results
- if (bHit)
- {
- GoalPosition = result.Point + SurfaceNormalOffset * result.Normal + SurfaceRayOffset * ray.direction;
- GoalRotation = CalculateMagnetismOrientation(ray.direction, result.Normal);
- }
- break;
- }
-
- // Do frame to frame updates of transform, smoothly toward the goal, if desired
- UpdateWorkingPosToGoal();
- UpdateWorkingRotToGoal();
- }
-
- /// <summary>
- /// Calculates a plane from all raycast hit locations upon which the object may align
- /// </summary>
- /// <param name="origin"></param>
- /// <param name="direction"></param>
- /// <param name="positions"></param>
- /// <param name="normals"></param>
- /// <param name="hits"></param>
- /// <param name="assetWidth"></param>
- /// <param name="maxNormalVariance"></param>
- /// <param name="constrainVertical"></param>
- /// <param name="bUseClosestDistance"></param>
- /// <param name="plane"></param>
- /// <param name="closestDistance"></param>
- 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)
- {
- bool debugEnabled = RaycastHelper.DebugEnabled;
-
- int numRays = positions.Length;
-
- Vector3 originalDirection = direction;
- if (constrainVertical)
- {
- direction.y = 0.0f;
- direction = direction.normalized;
- }
-
- // go through all the points and find the closest distance
- int closestPoint = -1;
- closestDistance = float.PositiveInfinity;
- float farthestDistance = 0f;
- int numHits = 0;
- Vector3 averageNormal = Vector3.zero;
-
- for (int i = 0; i < numRays; i++)
- {
- if (hits[i] != false)
- {
- float dist = Vector3.Dot(direction, positions[i] - origin);
-
- if (dist < closestDistance)
- {
- closestPoint = i;
- closestDistance = dist;
- }
- if (dist > farthestDistance)
- {
- farthestDistance = dist;
- }
-
- averageNormal += normals[i];
- ++numHits;
- }
- }
- averageNormal /= numHits;
-
- // Calculate variance of all normals
- float variance = 0;
- for (int i = 0; i < numRays; ++i)
- {
- if (hits[i] != false)
- {
- variance += (normals[i] - averageNormal).magnitude;
- }
- }
- variance /= numHits;
-
- // If variance is too high, I really don't want to deal with this surface
- // And if we don't even have enough rays, I'm not confident about this at all
- if (variance > maxNormalVariance || numHits < numRays / 4)
- {
- plane = new Plane(-direction, positions[closestPoint]);
- return;
- }
-
- // go through all the points and find the most orthagonal plane
- float lowAngle = float.PositiveInfinity;
- int lowIndex = -1;
- float highAngle = float.NegativeInfinity;
- int highIndex = -1;
-
- for (int i = 0; i < numRays; i++)
- {
- if (hits[i] == false || i == closestPoint)
- {
- continue;
- }
-
- Vector3 diff = (positions[i] - positions[closestPoint]);
- if (constrainVertical)
- {
- diff.y = 0.0f;
- diff.Normalize();
-
- if (diff == Vector3.zero)
- {
- continue;
- }
- }
- else
- {
- diff.Normalize();
- }
-
- float angle = Vector3.Dot(direction, diff);
-
- if (angle < lowAngle)
- {
- lowAngle = angle;
- lowIndex = i;
- }
- }
-
- if (!constrainVertical && lowIndex != -1)
- {
- for (int i = 0; i < numRays; i++)
- {
- if (hits[i] == false || i == closestPoint || i == lowIndex)
- {
- continue;
- }
-
- float dot = Mathf.Abs(Vector3.Dot((positions[i] - positions[closestPoint]).normalized, (positions[lowIndex] - positions[closestPoint]).normalized));
- if (dot > maxDot)
- {
- continue;
- }
-
- Vector3 normal = Vector3.Cross(positions[lowIndex] - positions[closestPoint], positions[i] - positions[closestPoint]).normalized;
-
- float nextAngle = Mathf.Abs(Vector3.Dot(direction, normal));
-
- if (nextAngle > highAngle)
- {
- highAngle = nextAngle;
- highIndex = i;
- }
- }
- }
-
- Vector3 placementNormal;
- if (lowIndex != -1)
- {
- if (debugEnabled)
- {
- Debug.DrawLine(positions[closestPoint], positions[lowIndex], Color.red);
- }
-
- if (highIndex != -1)
- {
- if (debugEnabled)
- {
- Debug.DrawLine(positions[closestPoint], positions[highIndex], Color.green);
- }
- placementNormal = Vector3.Cross(positions[lowIndex] - positions[closestPoint], positions[highIndex] - positions[closestPoint]).normalized;
- }
- else
- {
- Vector3 planeUp = Vector3.Cross(positions[lowIndex] - positions[closestPoint], direction);
- placementNormal = Vector3.Cross(positions[lowIndex] - positions[closestPoint], constrainVertical ? Vector3.up : planeUp).normalized;
- }
-
- if (debugEnabled)
- {
- Debug.DrawLine(positions[closestPoint], positions[closestPoint] + placementNormal, Color.blue);
- }
- }
- else
- {
- placementNormal = direction * -1.0f;
- }
-
- if (Vector3.Dot(placementNormal, direction) > 0.0f)
- {
- placementNormal *= -1.0f;
- }
-
- plane = new Plane(placementNormal, positions[closestPoint]);
-
- if (debugEnabled)
- {
- Debug.DrawRay(positions[closestPoint], placementNormal, Color.cyan);
- }
-
- // Figure out how far the plane should be.
- if (!bUseClosestDistance && closestPoint >= 0)
- {
- float centerPlaneDistance;
- Ray centerPlaneRay = new Ray(origin, originalDirection);
- if (plane.Raycast(centerPlaneRay, out centerPlaneDistance) || centerPlaneDistance != 0)
- {
- // When the plane is nearly parallel to the user, we need to clamp the distance to where the raycasts hit.
- closestDistance = Mathf.Clamp(centerPlaneDistance, closestDistance, farthestDistance + assetWidth * 0.5f);
- }
- else
- {
- Debug.LogError("FindPlacementPlane: Not expected to have the center point not intersect the plane.");
- }
- }
- }
- }
- }