// Copyright 2017 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using UnityEngine; using UnityEngine.EventSystems; /// This abstract class should be implemented for pointer based input, and used with /// the GvrPointerInputModule script. /// /// It provides methods called on pointer interaction with in-game objects and UI, /// trigger events, and 'BaseInputModule' class state changes. /// /// To have the methods called, an instance of this (implemented) class must be /// registered with the **GvrPointerManager** script in 'Start' by calling /// GvrPointerInputModule.OnPointerCreated. /// /// This abstract class should be implemented by pointers doing 1 of 2 things: /// 1. Responding to movement of the users head (Cardboard gaze-based-pointer). /// 2. Responding to the movement of the daydream controller (Daydream 3D pointer). public abstract class GvrBasePointer : MonoBehaviour, IGvrControllerInputDeviceReceiver { public enum RaycastMode { /// Casts a ray from the camera through the target of the pointer. /// This is ideal for reticles that are always rendered on top. /// The object that is selected will always be the object that appears /// underneath the reticle from the perspective of the camera. /// This also prevents the reticle from appearing to "jump" when it starts/stops hitting an object. /// /// Recommended for reticles that are always rendered on top such as the GvrReticlePointer /// prefab which is used for cardboard apps. /// /// Note: This will prevent the user from pointing around an object to hit something that is out of sight. /// This isn't a problem in a typical use case. /// /// When used with the standard daydream controller, /// the hit detection will not account for the laser correctly for objects that are closer to the /// camera than the end of the laser. /// In that case, it is recommended to do one of the following things: /// /// 1. Hide the laser. /// 2. Use a full-length laser pointer in Direct mode. /// 3. Use the Hybrid raycast mode. Camera, /// Cast a ray directly from the pointer origin. /// /// Recommended for full-length laser pointers. Direct, /// Default method for casting ray. /// /// Combines the Camera and Direct raycast modes. /// Uses a Direct ray up until the CameraRayIntersectionDistance, and then switches to use /// a Camera ray starting from the point where the two rays intersect. /// /// Recommended for use with the standard settings of the GvrControllerPointer prefab. /// This is the most versatile raycast mode. Like Camera mode, this prevents the reticle /// appearing jumpy. Additionally, it still allows the user to target objects that are close /// to them by using the laser as a visual reference. Hybrid, } /// Represents a ray segment for a series of intersecting rays. /// This is useful for Hybrid raycast mode, which uses two sequential rays. public struct PointerRay { /// The ray for this segment of the pointer. public Ray ray; /// The distance along the pointer from the origin of the first ray to this ray. public float distanceFromStart; /// Distance that this ray extends to. public float distance; } /// Determines which raycast mode to use for this raycaster. /// • Camera - Ray is cast from the camera through the pointer. /// • Direct - Ray is cast forward from the pointer. /// • Hybrid - Begins with a Direct ray and transitions to a Camera ray. [Tooltip("Determines which raycast mode to use for this raycaster.\n" + " • Camera - Ray is cast from camera.\n" + " • Direct - Ray is cast from pointer.\n" + " • Hybrid - Transitions from Direct ray to Camera ray.")] public RaycastMode raycastMode = RaycastMode.Hybrid; /// Determines the eventCamera for _GvrPointerPhysicsRaycaster_ and _GvrPointerGraphicRaycaster_. /// Additionaly, this is used to control what camera to use when calculating the Camera ray for /// the Hybrid and Camera raycast modes. [Tooltip("Optional: Use a camera other than Camera.main.")] public Camera overridePointerCamera; #if UNITY_EDITOR /// Determines if the rays used for raycasting will be drawn in the editor. [Tooltip("Determines if the rays used for raycasting will be drawn in the editor.")] public bool drawDebugRays = false; #endif // UNITY_EDITOR /// Convenience function to access what the pointer is currently hitting. public RaycastResult CurrentRaycastResult { get { return GvrPointerInputModule.CurrentRaycastResult; } } [System.Obsolete("Replaced by CurrentRaycastResult.worldPosition")] public Vector3 PointerIntersection { get { RaycastResult raycastResult = CurrentRaycastResult; return raycastResult.worldPosition; } } [System.Obsolete("Replaced by CurrentRaycastResult.gameObject != null")] public bool IsPointerIntersecting { get { RaycastResult raycastResult = CurrentRaycastResult; return raycastResult.gameObject != null; } } /// This is used to determine if the enterRadius or the exitRadius should be used for the raycast. /// It is set by GvrPointerInputModule and doesn't need to be controlled manually. public bool ShouldUseExitRadiusForRaycast { get; set; } /// If ShouldUseExitRadiusForRaycast is true, returns the exitRadius. /// Otherwise, returns the enterRadius. public float CurrentPointerRadius { get { float enterRadius, exitRadius; GetPointerRadius(out enterRadius, out exitRadius); if (ShouldUseExitRadiusForRaycast) { return exitRadius; } else { return enterRadius; } } } /// Returns the transform that represents this pointer. /// It is used by GvrBasePointerRaycaster as the origin of the ray. public virtual Transform PointerTransform { get { return transform; } } public GvrControllerInputDevice ControllerInputDevice { get; set; } /// If true, the trigger was just pressed. This is an event flag: /// it will be true for only one frame after the event happens. /// Defaults to mouse button 0 down on Cardboard or /// ControllerInputDevice.GetButtonDown(TouchPadButton) on Daydream. /// Can be overridden to change the trigger. public virtual bool TriggerDown { get { bool isTriggerDown = Input.GetMouseButtonDown(0); if (ControllerInputDevice != null) { isTriggerDown |= ControllerInputDevice.GetButtonDown(GvrControllerButton.TouchPadButton); } return isTriggerDown; } } /// If true, the trigger is currently being pressed. This is not /// an event: it represents the trigger's state (it remains true while the trigger is being /// pressed). /// Defaults to mouse button 0 state on Cardboard or /// ControllerInputDevice.GetButton(TouchPadButton) on Daydream. /// Can be overridden to change the trigger. public virtual bool Triggering { get { bool isTriggering = Input.GetMouseButton(0); if (ControllerInputDevice != null) { isTriggering |= ControllerInputDevice.GetButton(GvrControllerButton.TouchPadButton); } return isTriggering; } } /// If true, the trigger was just released. This is an event flag: /// it will be true for only one frame after the event happens. /// Defaults to mouse button 0 up on Cardboard or /// ControllerInputDevice.GetButtonUp(TouchPadButton) on Daydream. /// Can be overridden to change the trigger. public virtual bool TriggerUp { get { bool isTriggerUp = Input.GetMouseButtonUp(0); if (ControllerInputDevice == null) { isTriggerUp |= ControllerInputDevice.GetButtonUp(GvrControllerButton.TouchPadButton); } return isTriggerUp; } } /// If true, the user just started touching the touchpad. This is an event flag (it is true /// for only one frame after the event happens, then reverts to false). /// Used by _GvrPointerScrollInput_ to generate OnScroll events using Unity's Event System. /// Defaults to ControllerInputDevice.GetButtonDown(TouchPadTouch), can be overridden to change /// the input source. public virtual bool TouchDown { get { if (ControllerInputDevice == null) { return false; } else { return ControllerInputDevice.GetButtonDown(GvrControllerButton.TouchPadTouch); } } } /// If true, the user is currently touching the touchpad. /// Used by _GvrPointerScrollInput_ to generate OnScroll events using Unity's Event System. /// Defaults to ControllerInputDevice.GetButton(TouchPadTouch), can be overridden to change /// the input source. public virtual bool IsTouching { get { if (ControllerInputDevice == null) { return false; } else { return ControllerInputDevice.GetButton(GvrControllerButton.TouchPadTouch); } } } /// If true, the user just stopped touching the touchpad. This is an event flag (it is true /// for only one frame after the event happens, then reverts to false). /// Used by _GvrPointerScrollInput_ to generate OnScroll events using Unity's Event System. /// Defaults to ControllerInputDevice.GetButtonUp(TouchPadTouch), can be overridden to change /// the input source. public virtual bool TouchUp { get { if (ControllerInputDevice == null) { return false; } else { return ControllerInputDevice.GetButtonUp(GvrControllerButton.TouchPadTouch); } } } /// Position of the current touch, if touching the touchpad. /// If not touching, this is the position of the last touch (when the finger left the touchpad). /// The X and Y range is from 0 to 1. /// (0, 0) is the top left of the touchpad and (1, 1) is the bottom right of the touchpad. /// Used by `GvrPointerScrollInput` to generate OnScroll events using Unity's Event System. /// Defaults to `ControllerInputDevice.TouchPos` but translated to top-left-relative coordinates /// for backwards compatibility. Can be overridden to change the input source. public virtual Vector2 TouchPos { get { if (ControllerInputDevice == null) { return Vector2.zero; } else { Vector2 touchPos = ControllerInputDevice.TouchPos; touchPos.x = (touchPos.x / 2.0f) + 0.5f; touchPos.y = (-touchPos.y / 2.0f) + 0.5f; return touchPos; } } } /// Returns the end point of the pointer when it is MaxPointerDistance away from the origin. public virtual Vector3 MaxPointerEndPoint { get { Transform pointerTransform = PointerTransform; if (pointerTransform == null) { return Vector3.zero; } Vector3 maxEndPoint = GetPointAlongPointer(MaxPointerDistance); return maxEndPoint; } } /// If true, the pointer will be used for generating input events by _GvrPointerInputModule_. public virtual bool IsAvailable { get { Transform pointerTransform = PointerTransform; if (pointerTransform == null) { return false; } if (!enabled) { return false; } return pointerTransform.gameObject.activeInHierarchy; } } /// When using the Camera raycast mode, this is used to calculate /// where the ray from the pointer will intersect with the ray from the camera. public virtual float CameraRayIntersectionDistance { get { return MaxPointerDistance; } } public Camera PointerCamera { get { if (overridePointerCamera != null) { return overridePointerCamera; } return Camera.main; } } /// Returns the max distance from the pointer that raycast hits will be detected. public abstract float MaxPointerDistance { get; } /// Called when the pointer is facing a valid GameObject. This can be a 3D /// or UI element. /// /// **raycastResult** is the hit detection result for the object being pointed at. /// **isInteractive** is true if the object being pointed at is interactive. public abstract void OnPointerEnter(RaycastResult raycastResult, bool isInteractive); /// Called every frame the user is still pointing at a valid GameObject. This /// can be a 3D or UI element. /// /// **raycastResult** is the hit detection result for the object being pointed at. /// **isInteractive** is true if the object being pointed at is interactive. public abstract void OnPointerHover(RaycastResult raycastResultResult, bool isInteractive); /// Called when the pointer no longer faces an object previously /// intersected with a ray projected from the camera. /// This is also called just before **OnInputModuleDisabled** /// previousObject will be null in this case. /// /// **previousObject** is the object that was being pointed at the previous frame. public abstract void OnPointerExit(GameObject previousObject); /// Called when a click is initiated. public abstract void OnPointerClickDown(); /// Called when click is finished. public abstract void OnPointerClickUp(); /// Return the radius of the pointer. It is used by GvrPointerPhysicsRaycaster when /// searching for valid pointer targets. If a radius is 0, then a ray is used to find /// a valid pointer target. Otherwise it will use a SphereCast. /// The *enterRadius* is used for finding new targets while the *exitRadius* /// is used to see if you are still nearby the object currently pointed at /// to avoid a flickering effect when just at the border of the intersection. /// /// NOTE: This is only works with GvrPointerPhysicsRaycaster. To use it with uGUI, /// add 3D colliders to your canvas elements. public abstract void GetPointerRadius(out float enterRadius, out float exitRadius); /// Returns a point in worldspace a specified distance along the pointer. /// What this point will be is different depending on the raycastMode. /// /// Because raycast modes differ, use this function instead of manually calculating a point /// projected from the pointer. public Vector3 GetPointAlongPointer(float distance) { PointerRay pointerRay = GetRayForDistance(distance); return pointerRay.ray.GetPoint(distance - pointerRay.distanceFromStart); } /// Returns the ray used for projecting points out of the pointer for the given distance. /// In Hybrid raycast mode, the ray will be different depending upon the distance. /// In Camera or Direct raycast mode, the ray will always be the same. public PointerRay GetRayForDistance(float distance) { PointerRay result = new PointerRay(); if (raycastMode == RaycastMode.Hybrid) { float directDistance = CameraRayIntersectionDistance; if (distance < directDistance) { result = CalculateHybridRay(this, RaycastMode.Direct); } else { result = CalculateHybridRay(this, RaycastMode.Camera); } } else { result = CalculateRay(this, raycastMode); } return result; } /// Calculates the ray for a given Raycast mode. /// Will throw an exception if the raycast mode Hybrid is passed in. /// If you need to calculate the ray for the direct or camera segment of the Hybrid raycast, /// use CalculateHybridRay instead. public static PointerRay CalculateRay(GvrBasePointer pointer, RaycastMode mode) { PointerRay result = new PointerRay(); if (pointer == null || !pointer.IsAvailable) { Debug.LogError("Cannot calculate ray when the pointer isn't available."); return result; } Transform pointerTransform = pointer.PointerTransform; if (pointerTransform == null) { Debug.LogError("Cannot calculate ray when pointerTransform is null."); return result; } result.distance = pointer.MaxPointerDistance; switch (mode) { case RaycastMode.Camera: Camera camera = pointer.PointerCamera; if (camera == null) { Debug.LogError("Cannot calculate ray because pointer.PointerCamera is null." + "To fix this, either tag a Camera as \"MainCamera\" or set overridePointerCamera."); return result; } Vector3 rayPointerStart = pointerTransform.position; Vector3 rayPointerEnd = rayPointerStart + (pointerTransform.forward * pointer.CameraRayIntersectionDistance); Vector3 cameraLocation = camera.transform.position; Vector3 finalRayDirection = rayPointerEnd - cameraLocation; finalRayDirection.Normalize(); Vector3 finalRayStart = cameraLocation + (finalRayDirection * camera.nearClipPlane); result.ray = new Ray(finalRayStart, finalRayDirection); break; case RaycastMode.Direct: result.ray = new Ray(pointerTransform.position, pointerTransform.forward); break; default: throw new UnityException("Invalid RaycastMode " + mode + " passed into CalculateRay."); } return result; } /// Calculates the ray for the segment of the Hybrid raycast determined by the raycast mode /// passed in. Throws an exception if Hybrid is passed in. public static PointerRay CalculateHybridRay(GvrBasePointer pointer, RaycastMode hybridMode) { PointerRay result; switch (hybridMode) { case RaycastMode.Direct: result = CalculateRay(pointer, hybridMode); result.distance = pointer.CameraRayIntersectionDistance; break; case RaycastMode.Camera: result = CalculateRay(pointer, hybridMode); PointerRay directRay = CalculateHybridRay(pointer, RaycastMode.Direct); result.ray.origin = directRay.ray.GetPoint(directRay.distance); result.distanceFromStart = directRay.distance; result.distance = pointer.MaxPointerDistance - directRay.distance; break; default: throw new UnityException("Invalid RaycastMode " + hybridMode + " passed into CalculateHybridRay."); } return result; } protected virtual void Start() { GvrPointerInputModule.OnPointerCreated(this); } #if UNITY_EDITOR protected virtual void OnDrawGizmos() { if (drawDebugRays && Application.isPlaying && isActiveAndEnabled) { switch (raycastMode) { case RaycastMode.Camera: // Camera line. Gizmos.color = Color.green; PointerRay pointerRay = CalculateRay(this, RaycastMode.Camera); Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance)); Camera camera = PointerCamera; // Pointer to intersection dotted line. Vector3 intersection = PointerTransform.position + (PointerTransform.forward * CameraRayIntersectionDistance); UnityEditor.Handles.DrawDottedLine(PointerTransform.position, intersection, 1.0f); break; case RaycastMode.Direct: // Direct line. Gizmos.color = Color.blue; pointerRay = CalculateRay(this, RaycastMode.Direct); Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance)); break; case RaycastMode.Hybrid: // Direct line. Gizmos.color = Color.blue; pointerRay = CalculateHybridRay(this, RaycastMode.Direct); Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance)); // Camera line. Gizmos.color = Color.green; pointerRay = CalculateHybridRay(this, RaycastMode.Camera); Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance)); // Camera to intersection dotted line. camera = PointerCamera; if (camera != null) { UnityEditor.Handles.DrawDottedLine(camera.transform.position, pointerRay.ray.origin, 1.0f); } break; default: break; } } } #endif // UNITY_EDITOR }