using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections; using System.Collections.Generic; using System; using Selectable = UnityEngine.UI.Selectable; namespace CurvedUI { #if CURVEDUI_GOOGLEVR public class CurvedUIRaycaster : GvrPointerGraphicRaycaster #else public class CurvedUIRaycaster : GraphicRaycaster #endif { [SerializeField] bool showDebug = false; //Settings--------------------------------------// // CurvedUIRaycaster must modify the position of the eventData to make it valid for the curved canvas. // It can either create a copy, or override the original. The copy will only be used for this canvas, in this frame. // The overridden original will be carried to other canvases and next frames. // // Set this to TRUE if this raycaster should override the original eventData. // Overriding eventData allows canvas to use 1:1 scrolling. Scroll rects and sliders behave as they should on a curved surface and follow the pointer. // This however breaks the interactions with flat canvases in the same scene as original eventData will not be correct for them any more. // // Setting this to FALSE will create a copy of the eventData for each canvas. // Flat canvases on the same scene will work fine, but scroll rects on curved canvases will move faster / slower than the pointer. // May break dragging and scrolling as there will be no past eventdata to calculate delta position from. // // default true. bool overrideEventData = true; //Variables --------------------------------------// Canvas myCanvas; CurvedUISettings mySettings; Vector3 cyllinderMidPoint; List objectsUnderPointer = new List(); Vector2 lastCanvasPos = Vector2.zero; GameObject colliderContainer; PointerEventData lastFrameEventData; PointerEventData curEventData; PointerEventData eventDataToUse; Ray cachedRay; Graphic gph; //gaze click List selectablesUnderGaze = new List(); List selectablesUnderGazeLastFrame = new List(); float objectsUnderGazeLastChangeTime; bool gazeClickExecuted = false; bool pointingAtCanvas = false; bool pointingAtCanvasLastFrame = false; #region LIFECYCLE protected override void Awake() { base.Awake(); mySettings = GetComponentInParent(); if (mySettings == null) return; myCanvas = mySettings.GetComponent(); cyllinderMidPoint = new Vector3(0, 0, -mySettings.GetCyllinderRadiusInCanvasSpace()); //this must be set to false to make sure proper interactions. //Otherwise, Unity may ignore Selectables on edges of heavily curved canvas. ignoreReversedGraphics = false; } protected override void Start() { if (mySettings == null) return; CheckEventCamera(); CreateCollider(); #if CURVEDUI_GOOGLEVR //Find if there is a GvrPointerPhysicsRaycaster on the scene that can override our Raycasts. if (Camera.main != null && Camera.main.GetComponent() != null) { LayerMask mask = Camera.main.GetComponent().eventMask; if (IsInLayerMask(this.gameObject.layer, mask)){ Debug.LogWarning("CURVEDUI: GvrPointerPhysicsRaycaster is raycasting over this canvas' layer (" +this.gameObject.name +" - " + LayerMask.LayerToName(this.gameObject.layer)+" layer). " + "This can make the UI unusable. It has been automatically fixed for this run, but your 3D objects may now be unusable. " + "Make sure your GvrPointerPhysicsRaycaster is not raycasting on this object's layer UI by editing its properties. Click here to highlight it.", Camera.main.gameObject); mask = mask & ~(1 << this.gameObject.layer); Camera.main.GetComponent().eventMask = mask; } } #endif } protected virtual void Update() { if (mySettings == null) return; //Gaze click process. if (CurvedUIInputModule.ControlMethod == CurvedUIInputModule.CUIControlMethod.GAZE && CurvedUIInputModule.Instance.GazeUseTimedClick) { if (pointingAtCanvas) { //first frame gaze enters canvas. Make sure we dont click immidiately upon entering canvas if (!pointingAtCanvasLastFrame) ResetGazeTimedClick(); ProcessGazeTimedClick(); //save current selectablesUnderGaze selectablesUnderGazeLastFrame.Clear(); selectablesUnderGazeLastFrame.AddRange(selectablesUnderGaze); //find selectables we're currently pointing at in objects under pointer selectablesUnderGaze.Clear(); selectablesUnderGaze.AddRange(objectsUnderPointer); selectablesUnderGaze.RemoveAll(obj => obj.GetComponent() == null || obj.GetComponent().interactable == false); //Animate progress bar if (GazeProgressImage) { if (GazeProgressImage.type != Image.Type.Filled) GazeProgressImage.type = Image.Type.Filled; GazeProgressImage.fillAmount = (Time.time - objectsUnderGazeLastChangeTime).RemapAndClamp(CurvedUIInputModule.Instance.GazeClickTimerDelay, CurvedUIInputModule.Instance.GazeClickTimer + CurvedUIInputModule.Instance.GazeClickTimerDelay, 0, 1); } } else if (!pointingAtCanvas && pointingAtCanvasLastFrame) //first frame after gaze pointer leaves this canvas. { //not poiting at canvas, reset the timer. ResetGazeTimedClick(); if (GazeProgressImage) GazeProgressImage.fillAmount = 0; } } pointingAtCanvasLastFrame = pointingAtCanvas; //reset this variable. It will be checked again during next Raycast method run. pointingAtCanvas = false; } #endregion #region GAZE INTERACTION void ProcessGazeTimedClick() { //debug //string str = " Object under pointer: "; //foreach (GameObject go in objectsUnderPointer) str += go.name + ", "; //Debug.Log(str); //two lists are not the same - selected objects changed if (selectablesUnderGazeLastFrame.Count == 0 || selectablesUnderGazeLastFrame.Count != selectablesUnderGaze.Count) { ResetGazeTimedClick(); return; } //Check if objects changed since last frame for (int i = 0; i < selectablesUnderGazeLastFrame.Count && i < selectablesUnderGaze.Count; i++) { if (selectablesUnderGazeLastFrame[i].GetInstanceID() != selectablesUnderGaze[i].GetInstanceID()) { ResetGazeTimedClick(); return; } } //Check if time is done and we havent executed the click yet if (!gazeClickExecuted && Time.time > objectsUnderGazeLastChangeTime + CurvedUIInputModule.Instance.GazeClickTimer + CurvedUIInputModule.Instance.GazeClickTimerDelay) { Click(); gazeClickExecuted = true; } } void ResetGazeTimedClick() { objectsUnderGazeLastChangeTime = Time.time; gazeClickExecuted = false; } #endregion #region PHYSICS RAYCASTING public override void Raycast(PointerEventData eventData, List resultAppendList) { if (mySettings == null) { base.Raycast(eventData, resultAppendList); return; } if (!mySettings.Interactable) return; //check if we have a world camera to process events by if (!CheckEventCamera()) { Debug.LogWarning("CurvedUI: No WORLD CAMERA assigned to Canvas " + this.gameObject.name + " to use for event processing!", myCanvas.gameObject); return; } //get a ray to raycast with depending on the control method cachedRay = CurvedUIInputModule.Instance.GetEventRay(myCanvas.worldCamera); //special case for GAZE and WORLD MOUSE if (CurvedUIInputModule.ControlMethod == CurvedUIInputModule.CUIControlMethod.GAZE) UpdateSelectedObjects(eventData); else if (CurvedUIInputModule.ControlMethod == CurvedUIInputModule.CUIControlMethod.WORLD_MOUSE) cachedRay = new Ray(myCanvas.worldCamera.transform.position, (mySettings.CanvasToCurvedCanvas(CurvedUIInputModule.Instance.WorldSpaceMouseInCanvasSpace) - myCanvas.worldCamera.transform.position)); //Create a copy of the eventData to be used by this canvas. if (curEventData == null) curEventData = new PointerEventData(EventSystem.current); if (!overrideEventData) { curEventData.pointerEnter = eventData.pointerEnter; curEventData.rawPointerPress = eventData.rawPointerPress; curEventData.pointerDrag = eventData.pointerDrag; curEventData.pointerCurrentRaycast = eventData.pointerCurrentRaycast; curEventData.pointerPressRaycast = eventData.pointerPressRaycast; curEventData.hovered.Clear(); curEventData.hovered.AddRange(eventData.hovered); curEventData.eligibleForClick = eventData.eligibleForClick; curEventData.pointerId = eventData.pointerId; curEventData.position = eventData.position; curEventData.delta = eventData.delta; curEventData.pressPosition = eventData.pressPosition; curEventData.clickTime = eventData.clickTime; curEventData.clickCount = eventData.clickCount; curEventData.scrollDelta = eventData.scrollDelta; curEventData.useDragThreshold = eventData.useDragThreshold; curEventData.dragging = eventData.dragging; curEventData.button = eventData.button; } if (mySettings.Angle != 0 && mySettings.enabled) { // use custom raycasting only if Curved effect is enabled //Getting remappedPosition on the curved canvas ------------------------------// //This will be later passed to GraphicRaycaster so it can discover interactions as usual. //If we did not hit the curved canvas, return - no interactions are possible //Physical raycast to find interaction point Vector2 remappedPosition = eventData.position; switch (mySettings.Shape) { case CurvedUISettings.CurvedUIShape.CYLINDER: { if (!RaycastToCyllinderCanvas(cachedRay, out remappedPosition, false)) return; break; } case CurvedUISettings.CurvedUIShape.CYLINDER_VERTICAL: { if (!RaycastToCyllinderVerticalCanvas(cachedRay, out remappedPosition, false)) return; break; } case CurvedUISettings.CurvedUIShape.RING: { if (!RaycastToRingCanvas(cachedRay, out remappedPosition, false)) return; break; } case CurvedUISettings.CurvedUIShape.SPHERE: { if (!RaycastToSphereCanvas(cachedRay, out remappedPosition, false)) return; break; } } //if we got here, it means user is pointing at this canvas. pointingAtCanvas = true; //Creating eventData for canvas Raycasting -------------------// //Which eventData were going to use? eventDataToUse = overrideEventData ? eventData : curEventData; // Swap event data pressPosition to our remapped pos if this is the frame of the press if (eventDataToUse.pressPosition == eventDataToUse.position) eventDataToUse.pressPosition = remappedPosition; // Swap event data position to our remapped pos eventDataToUse.position = remappedPosition; //Scroll Handling---------------------------------------------// //We must handle scroll a little differently on these platforms if (CurvedUIInputModule.ControlMethod == CurvedUIInputModule.CUIControlMethod.STEAMVR_LEGACY) { eventDataToUse.delta = remappedPosition - lastCanvasPos; lastCanvasPos = remappedPosition; } } //store objects under pointer so they can quickly retrieved if needed by other scripts objectsUnderPointer = eventData.hovered; lastFrameEventData = eventData; // Use base class raycast method to finish the raycast if we hit anything FlatRaycast(overrideEventData ? eventData : curEventData, resultAppendList); } public virtual bool RaycastToCyllinderCanvas(Ray ray3D, out Vector2 o_canvasPos, bool OutputInCanvasSpace = false) { if (showDebug) { Debug.DrawLine(ray3D.origin, ray3D.GetPoint(1000), Color.red); } RaycastHit hit = new RaycastHit(); if (Physics.Raycast(ray3D, out hit, float.PositiveInfinity, GetRaycastLayerMask())) { //find if we hit this canvas - this needs to be uncommented if (overrideEventData && hit.collider.gameObject != this.gameObject && (colliderContainer == null || hit.collider.transform.parent != colliderContainer.transform)) { o_canvasPos = Vector2.zero; return false; } //direction from the cyllinder center to the hit point Vector3 localHitPoint = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(hit.point); Vector3 directionFromCyllinderCenter = (localHitPoint - cyllinderMidPoint).normalized; //angle between middle of the projected canvas and hit point direction float angle = -AngleSigned(directionFromCyllinderCenter.ModifyY(0), mySettings.Angle < 0 ? Vector3.back : Vector3.forward, Vector3.up); //convert angle to canvas coordinates Vector2 canvasSize = myCanvas.GetComponent().rect.size; //map the intersection point to 2d point in canvas space Vector2 pointOnCanvas = new Vector3(0, 0, 0); pointOnCanvas.x = angle.Remap(-mySettings.Angle / 2.0f, mySettings.Angle / 2.0f, -canvasSize.x / 2.0f, canvasSize.x / 2.0f); pointOnCanvas.y = localHitPoint.y; if (OutputInCanvasSpace) o_canvasPos = pointOnCanvas; else //convert the result to screen point in camera. This will be later used by raycaster and world camera to determine what we're pointing at o_canvasPos = myCanvas.worldCamera.WorldToScreenPoint(myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(pointOnCanvas)); if (showDebug) { Debug.DrawLine(hit.point, hit.point.ModifyY(hit.point.y + 10), Color.green); Debug.DrawLine(hit.point, myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(cyllinderMidPoint), Color.yellow); } return true; } o_canvasPos = Vector2.zero; return false; } public virtual bool RaycastToCyllinderVerticalCanvas(Ray ray3D, out Vector2 o_canvasPos, bool OutputInCanvasSpace = false) { if (showDebug) { Debug.DrawLine(ray3D.origin, ray3D.GetPoint(1000), Color.red); } RaycastHit hit = new RaycastHit(); if (Physics.Raycast(ray3D, out hit, float.PositiveInfinity, GetRaycastLayerMask())) { //find if we hit this canvas if (overrideEventData && hit.collider.gameObject != this.gameObject && (colliderContainer == null || hit.collider.transform.parent != colliderContainer.transform)) { o_canvasPos = Vector2.zero; return false; } //direction from the cyllinder center to the hit point Vector3 localHitPoint = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(hit.point); Vector3 directionFromCyllinderCenter = (localHitPoint - cyllinderMidPoint).normalized; //angle between middle of the projected canvas and hit point direction float angle = -AngleSigned(directionFromCyllinderCenter.ModifyX(0), mySettings.Angle < 0 ? Vector3.back : Vector3.forward, Vector3.left); //convert angle to canvas coordinates Vector2 canvasSize = myCanvas.GetComponent().rect.size; //map the intersection point to 2d point in canvas space Vector2 pointOnCanvas = new Vector3(0, 0, 0); pointOnCanvas.y = angle.Remap(-mySettings.Angle / 2.0f, mySettings.Angle / 2.0f, -canvasSize.y / 2.0f, canvasSize.y / 2.0f); pointOnCanvas.x = localHitPoint.x; if (OutputInCanvasSpace) o_canvasPos = pointOnCanvas; else //convert the result to screen point in camera. This will be later used by raycaster and world camera to determine what we're pointing at o_canvasPos = myCanvas.worldCamera.WorldToScreenPoint(myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(pointOnCanvas)); if (showDebug) { Debug.DrawLine(hit.point, hit.point.ModifyY(hit.point.y + 10), Color.green); Debug.DrawLine(hit.point, myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(cyllinderMidPoint), Color.yellow); } return true; } o_canvasPos = Vector2.zero; return false; } public virtual bool RaycastToRingCanvas(Ray ray3D, out Vector2 o_canvasPos, bool OutputInCanvasSpace = false) { LayerMask myLayerMask = GetRaycastLayerMask(); RaycastHit hit = new RaycastHit(); if (Physics.Raycast(ray3D, out hit, float.PositiveInfinity, myLayerMask)) { //find if we hit this canvas if (overrideEventData && hit.collider.gameObject != this.gameObject && (colliderContainer == null || hit.collider.transform.parent != colliderContainer.transform)) { o_canvasPos = Vector2.zero; return false; } //local hit point on canvas and a direction from center Vector3 localHitPoint = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(hit.point); Vector3 directionFromRingCenter = localHitPoint.ModifyZ(0).normalized; Vector2 canvasSize = myCanvas.GetComponent().rect.size; //angle between middle of the projected canvas and hit point direction from center float angle = -AngleSigned(directionFromRingCenter.ModifyZ(0), Vector3.up, Vector3.back); //map the intersection point to 2d point in canvas space Vector2 pointOnCanvas = new Vector2(0, 0); if (showDebug) Debug.Log("angle: " + angle); //map x coordinate based on angle between vector up and direction to hitpoint if (angle < 0) { pointOnCanvas.x = angle.Remap(0, -mySettings.Angle, -canvasSize.x / 2.0f, canvasSize.x / 2.0f); } else { pointOnCanvas.x = angle.Remap(360, 360 - mySettings.Angle, -canvasSize.x / 2.0f, canvasSize.x / 2.0f); } //map y coordinate based on hitpoint distance from the center and external diameter pointOnCanvas.y = localHitPoint.magnitude.Remap(mySettings.RingExternalDiameter * 0.5f * (1 - mySettings.RingFill), mySettings.RingExternalDiameter * 0.5f, -canvasSize.y * 0.5f * (mySettings.RingFlipVertical ? -1 : 1), canvasSize.y * 0.5f * (mySettings.RingFlipVertical ? -1 : 1)); if (OutputInCanvasSpace) o_canvasPos = pointOnCanvas; else //convert the result to screen point in camera. This will be later used by raycaster and world camera to determine what we're pointing at o_canvasPos = myCanvas.worldCamera.WorldToScreenPoint(myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(pointOnCanvas)); return true; } o_canvasPos = Vector2.zero; return false; } public virtual bool RaycastToSphereCanvas(Ray ray3D, out Vector2 o_canvasPos, bool OutputInCanvasSpace = false) { RaycastHit hit = new RaycastHit(); if (Physics.Raycast(ray3D, out hit, float.PositiveInfinity, GetRaycastLayerMask())) { //find if we hit this canvas if (overrideEventData && hit.collider.gameObject != this.gameObject && (colliderContainer == null || hit.collider.transform.parent != colliderContainer.transform)) { o_canvasPos = Vector2.zero; return false; } Vector2 canvasSize = myCanvas.GetComponent().rect.size; float radius = (mySettings.PreserveAspect ? mySettings.GetCyllinderRadiusInCanvasSpace() : canvasSize.x / 2.0f); //local hit point on canvas, direction from its center and a vector perpendicular to direction, so we can use it to calculate its angle in both planes. Vector3 localHitPoint = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(hit.point); Vector3 SphereCenter = new Vector3(0, 0, mySettings.PreserveAspect ? -radius : 0); Vector3 directionFromSphereCenter = (localHitPoint - SphereCenter).normalized; Vector3 XZPlanePerpendicular = Vector3.Cross(directionFromSphereCenter, directionFromSphereCenter.ModifyY(0)).normalized * (directionFromSphereCenter.y < 0 ? 1 : -1); //horizontal and vertical angle between middle of the sphere and the hit point. //We do some fancy checks to determine vectors we compare them to, //to make sure they are negative on the left and bottom side of the canvas float hAngle = -AngleSigned(directionFromSphereCenter.ModifyY(0), (mySettings.Angle > 0 ? Vector3.forward : Vector3.back), (mySettings.Angle > 0 ? Vector3.up : Vector3.down)); float vAngle = -AngleSigned(directionFromSphereCenter, directionFromSphereCenter.ModifyY(0), XZPlanePerpendicular); //find the size of the canvas expressed as measure of the arc it occupies on the sphere float hAngularSize = Mathf.Abs(mySettings.Angle) * 0.5f; float vAngularSize = Mathf.Abs(mySettings.PreserveAspect ? hAngularSize * canvasSize.y / canvasSize.x : mySettings.VerticalAngle * 0.5f); //map the intersection point to 2d point in canvas space Vector2 pointOnCanvas = new Vector2(hAngle.Remap(-hAngularSize, hAngularSize, -canvasSize.x * 0.5f, canvasSize.x * 0.5f), vAngle.Remap(-vAngularSize, vAngularSize, -canvasSize.y * 0.5f, canvasSize.y * 0.5f)); if (showDebug) { Debug.Log("h: " + hAngle + " / v: " + vAngle + " poc: " + pointOnCanvas); Debug.DrawRay(myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(SphereCenter), myCanvas.transform.localToWorldMatrix.MultiplyVector(directionFromSphereCenter) * Mathf.Abs(radius), Color.red); Debug.DrawRay(myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(SphereCenter), myCanvas.transform.localToWorldMatrix.MultiplyVector(XZPlanePerpendicular) * 300, Color.magenta); } if (OutputInCanvasSpace) o_canvasPos = pointOnCanvas; else // convert the result to screen point in camera.This will be later used by raycaster and world camera to determine what we're pointing at o_canvasPos = myCanvas.worldCamera.WorldToScreenPoint(myCanvas.transform.localToWorldMatrix.MultiplyPoint3x4(pointOnCanvas)); return true; } o_canvasPos = Vector2.zero; return false; } #endregion #region GRAPHIC RAYCASTING [NonSerialized] private List m_RaycastResults = new List(); void FlatRaycast(PointerEventData eventData, List resultAppendList) { if (myCanvas == null) return; //no canvas? var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(myCanvas); if (canvasGraphics == null || canvasGraphics.Count == 0) return; // no graphics on canvas? //Multiple display handling-----------------------// int displayIndex; var currentEventCamera = eventCamera; // Property can call Camera.main, so cache the reference instead if (myCanvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null) displayIndex = myCanvas.targetDisplay; else displayIndex = currentEventCamera.targetDisplay; var eventPosition = Display.RelativeMouseAt(eventData.position); if (eventPosition != Vector3.zero) { // Support for multiple display and display identification based on event position. int eventDisplayIndex = (int)eventPosition.z; // Discard events that are not part of this display so the user does not interact with multiple displays at once. if (eventDisplayIndex != displayIndex) return; } else { // The multiple display system is not supported on all platforms - returned index is 0 so default to the event data. //We will process the event assuming it occured in our display. eventPosition = eventData.position; } //Graphic Raycast ------------------------------------// //Perform a Graphic Raycast of all objects on the canvas and sort them by their depth. m_RaycastResults.Clear(); FlatRaycastAndSort(myCanvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults); //create a ray going from camera, through pointer position Ray ray = new Ray(); if (currentEventCamera != null) ray = currentEventCamera.ScreenPointToRay(eventPosition); float hitDistance = float.MaxValue; int totalCount = m_RaycastResults.Count; for (var index = 0; index < totalCount; index++) { var go = m_RaycastResults[index].gameObject; //Check to see if the go is behind the camera. //http://geomalgorithms.com/a06-_intersect-2.html Transform trans = go.transform; Vector3 transForward = trans.forward; float distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction)); if (distance < 0 || distance >= hitDistance) continue; //Add to cast result list var castResult = new RaycastResult { gameObject = go, module = this, distance = distance, screenPosition = eventPosition, index = resultAppendList.Count, depth = m_RaycastResults[index].depth, sortingLayer = myCanvas.sortingLayerID, sortingOrder = myCanvas.sortingOrder }; resultAppendList.Add(castResult); } } /// /// Perform a raycast into the screen and collect all graphics underneath it. /// [NonSerialized] static readonly List s_SortedGraphics = new List(); private static void FlatRaycastAndSort(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList foundGraphics, List results) { int totalCount = foundGraphics.Count; for (int i = 0; i < totalCount; ++i) { Graphic graphic = foundGraphics[i]; // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn if (graphic.depth == -1 || !graphic.raycastTarget || graphic.canvasRenderer.cull) continue; if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera)) continue; if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane) continue; if (graphic.Raycast(pointerPosition, eventCamera)) s_SortedGraphics.Add(graphic); } s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth)); totalCount = s_SortedGraphics.Count; for (int i = 0; i < totalCount; ++i) results.Add(s_SortedGraphics[i]); s_SortedGraphics.Clear(); } #endregion #region COLLIDER MANAGEMENT /// /// Creates a mesh collider for curved canvas based on current angle and curve segments. /// /// The collider. protected void CreateCollider() { //remove all colliders on this object List Cols = new List(); Cols.AddRange(this.GetComponents()); for (int i = 0; i < Cols.Count; i++) { Destroy(Cols[i]); } if (!mySettings.BlocksRaycasts) return; //null; if (mySettings.Shape == CurvedUISettings.CurvedUIShape.SPHERE && !mySettings.PreserveAspect && mySettings.VerticalAngle == 0) return;// null; //create a collider based on mapping type switch (mySettings.Shape) { case CurvedUISettings.CurvedUIShape.CYLINDER: { //creating a convex (lower performance - many parts) collider for when we have a rigidbody attached if (mySettings.ForceUseBoxCollider || GetComponent() != null || GetComponentInParent() != null) { if (colliderContainer != null) GameObject.Destroy(colliderContainer); colliderContainer = CreateConvexCyllinderCollider(); } else // create a faster single mesh collier when possible { SetupMeshColliderUsingMesh(CreateCyllinderColliderMesh()); } return; } case CurvedUISettings.CurvedUIShape.CYLINDER_VERTICAL: { //creating a convex (lower performance - many parts) collider for when we have a rigidbody attached if (mySettings.ForceUseBoxCollider || GetComponent() != null || GetComponentInParent() != null) { if (colliderContainer != null) GameObject.Destroy(colliderContainer); colliderContainer = CreateConvexCyllinderCollider(true); } else // create a faster single mesh collier when possible { SetupMeshColliderUsingMesh(CreateCyllinderColliderMesh(true)); } return; } case CurvedUISettings.CurvedUIShape.RING: { BoxCollider col = this.gameObject.AddComponent(); col.size = new Vector3(mySettings.RingExternalDiameter, mySettings.RingExternalDiameter, 1.0f); return; } case CurvedUISettings.CurvedUIShape.SPHERE: { //rigidbody in parent? if (GetComponent() != null || GetComponentInParent() != null) Debug.LogWarning("CurvedUI: Sphere shape canvases as children of rigidbodies do not support user input. Switch to Cyllinder shape or remove the rigidbody from parent.", this.gameObject); SetupMeshColliderUsingMesh(CreateSphereColliderMesh()); return; } default: { return; } } } /// /// Adds neccessary components and fills them with given mesh data. /// /// void SetupMeshColliderUsingMesh(Mesh meshie) { MeshFilter mf = this.AddComponentIfMissing(); MeshCollider mc = this.gameObject.AddComponent(); mf.mesh = meshie; mc.sharedMesh = meshie; } GameObject CreateConvexCyllinderCollider(bool vertical = false) { GameObject go = new GameObject("_CurvedUIColliders"); go.layer = this.gameObject.layer; go.transform.SetParent(this.transform); go.transform.ResetTransform(); Mesh meshie = new Mesh(); Vector3[] Vertices = new Vector3[4]; (myCanvas.transform as RectTransform).GetWorldCorners(Vertices); meshie.vertices = Vertices; //rearrange them to be in an easy to interpolate order and convert to canvas local spce if (vertical) { Vertices[0] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[1]); Vertices[1] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[2]); Vertices[2] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[0]); Vertices[3] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[3]); } else { Vertices[0] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[1]); Vertices[1] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[0]); Vertices[2] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[2]); Vertices[3] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[3]); } meshie.vertices = Vertices; //create a new array of vertices, subdivided as needed List verts = new List(); int vertsCount = Mathf.Max(8, Mathf.RoundToInt(mySettings.BaseCircleSegments * Mathf.Abs(mySettings.Angle) / 360.0f)); for (int i = 0; i < vertsCount; i++) { verts.Add(Vector3.Lerp(meshie.vertices[0], meshie.vertices[2], (i * 1.0f) / (vertsCount - 1))); } //curve the verts in canvas local space if (mySettings.Angle != 0) { Rect canvasRect = myCanvas.GetComponent().rect; float radius = mySettings.GetCyllinderRadiusInCanvasSpace(); for (int i = 0; i < verts.Count; i++) { Vector3 newpos = verts[i]; if (vertical) { float theta = (verts[i].y / canvasRect.size.y) * mySettings.Angle * Mathf.Deg2Rad; newpos.y = Mathf.Sin(theta) * radius; newpos.z += Mathf.Cos(theta) * radius - radius; verts[i] = newpos; } else { float theta = (verts[i].x / canvasRect.size.x) * mySettings.Angle * Mathf.Deg2Rad; newpos.x = Mathf.Sin(theta) * radius; newpos.z += Mathf.Cos(theta) * radius - radius; verts[i] = newpos; } } } //create our box colliders and arrange them in a nice cyllinder float boxDepth = mySettings.GetTesslationSize(false).x / 10; for (int i = 0; i < verts.Count - 1; i++) { GameObject newBox = new GameObject("Box collider"); newBox.layer = this.gameObject.layer; newBox.transform.SetParent(go.transform); newBox.transform.ResetTransform(); newBox.AddComponent(); if (vertical) { newBox.transform.localPosition = new Vector3(0, (verts[i + 1].y + verts[i].y) * 0.5f, (verts[i + 1].z + verts[i].z) * 0.5f); newBox.transform.localScale = new Vector3(boxDepth, Vector3.Distance(Vertices[0], Vertices[1]), Vector3.Distance(verts[i + 1], verts[i])); newBox.transform.localRotation = Quaternion.LookRotation((verts[i + 1] - verts[i]), Vertices[0] - Vertices[1]); } else { newBox.transform.localPosition = new Vector3((verts[i + 1].x + verts[i].x) * 0.5f, 0, (verts[i + 1].z + verts[i].z) * 0.5f); newBox.transform.localScale = new Vector3(boxDepth, Vector3.Distance(Vertices[0], Vertices[1]), Vector3.Distance(verts[i + 1], verts[i])); newBox.transform.localRotation = Quaternion.LookRotation((verts[i + 1] - verts[i]), Vertices[0] - Vertices[1]); } } return go; } Mesh CreateCyllinderColliderMesh(bool vertical = false) { Mesh meshie = new Mesh(); Vector3[] Vertices = new Vector3[4]; (myCanvas.transform as RectTransform).GetWorldCorners(Vertices); meshie.vertices = Vertices; //rearrange them to be in an easy to interpolate order and convert to canvas local spce if (vertical) { Vertices[0] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[1]); Vertices[1] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[2]); Vertices[2] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[0]); Vertices[3] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[3]); } else { Vertices[0] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[1]); Vertices[1] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[0]); Vertices[2] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[2]); Vertices[3] = myCanvas.transform.worldToLocalMatrix.MultiplyPoint3x4(meshie.vertices[3]); } meshie.vertices = Vertices; //create a new array of vertices, subdivided as needed List verts = new List(); int vertsCount = Mathf.Max(8, Mathf.RoundToInt(mySettings.BaseCircleSegments * Mathf.Abs(mySettings.Angle) / 360.0f)); for (int i = 0; i < vertsCount; i++) { verts.Add(Vector3.Lerp(meshie.vertices[0], meshie.vertices[2], (i * 1.0f) / (vertsCount - 1))); verts.Add(Vector3.Lerp(meshie.vertices[1], meshie.vertices[3], (i * 1.0f) / (vertsCount - 1))); } //curve the verts in canvas local space if (mySettings.Angle != 0) { Rect canvasRect = myCanvas.GetComponent().rect; float radius = mySettings.GetCyllinderRadiusInCanvasSpace(); for (int i = 0; i < verts.Count; i++) { Vector3 newpos = verts[i]; if (vertical) { float theta = (verts[i].y / canvasRect.size.y) * mySettings.Angle * Mathf.Deg2Rad; newpos.y = Mathf.Sin(theta) * radius; newpos.z += Mathf.Cos(theta) * radius - radius; verts[i] = newpos; } else { float theta = (verts[i].x / canvasRect.size.x) * mySettings.Angle * Mathf.Deg2Rad; newpos.x = Mathf.Sin(theta) * radius; newpos.z += Mathf.Cos(theta) * radius - radius; verts[i] = newpos; } } } meshie.vertices = verts.ToArray(); //create triangles drom verts List tris = new List(); for (int i = 0; i < verts.Count / 2 - 1; i++) { if (vertical) { //forward tris tris.Add(i * 2 + 0); tris.Add(i * 2 + 1); tris.Add(i * 2 + 2); tris.Add(i * 2 + 1); tris.Add(i * 2 + 3); tris.Add(i * 2 + 2); } else { //forward tris tris.Add(i * 2 + 2); tris.Add(i * 2 + 1); tris.Add(i * 2 + 0); tris.Add(i * 2 + 2); tris.Add(i * 2 + 3); tris.Add(i * 2 + 1); } } meshie.triangles = tris.ToArray(); return meshie; } Mesh CreateSphereColliderMesh() { Mesh meshie = new Mesh(); Vector3[] Corners = new Vector3[4]; (myCanvas.transform as RectTransform).GetWorldCorners(Corners); List verts = new List(Corners); for (int i = 0; i < verts.Count; i++) { verts[i] = mySettings.transform.worldToLocalMatrix.MultiplyPoint3x4(verts[i]); } if (mySettings.Angle != 0) { // Tesselate quads and apply transformation int startingVertexCount = verts.Count; for (int i = 0; i < startingVertexCount; i += 4) ModifyQuad(verts, i, mySettings.GetTesslationSize(false)); // Remove old quads verts.RemoveRange(0, startingVertexCount); //curve verts float vangle = mySettings.VerticalAngle; float cylinder_angle = mySettings.Angle; Vector2 canvasSize = (myCanvas.transform as RectTransform).rect.size; float radius = mySettings.GetCyllinderRadiusInCanvasSpace(); //caluclate vertical angle for aspect - consistent mapping if (mySettings.PreserveAspect) { vangle = mySettings.Angle * (canvasSize.y / canvasSize.x); } else {//if we're not going for constant aspect, set the width of the sphere to equal width of the original canvas radius = canvasSize.x / 2.0f; } //curve the vertices for (int i = 0; i < verts.Count; i++) { float theta = (verts[i].x / canvasSize.x).Remap(-0.5f, 0.5f, (180 - cylinder_angle) / 2.0f - 90, 180 - (180 - cylinder_angle) / 2.0f - 90); theta *= Mathf.Deg2Rad; float gamma = (verts[i].y / canvasSize.y).Remap(-0.5f, 0.5f, (180 - vangle) / 2.0f, 180 - (180 - vangle) / 2.0f); gamma *= Mathf.Deg2Rad; verts[i] = new Vector3(Mathf.Sin(gamma) * Mathf.Sin(theta) * radius, -radius * Mathf.Cos(gamma), Mathf.Sin(gamma) * Mathf.Cos(theta) * radius + (mySettings.PreserveAspect ? -radius : 0)); } } meshie.vertices = verts.ToArray(); //create triangles from verts List tris = new List(); for (int i = 0; i < verts.Count; i += 4) { tris.Add(i + 0); tris.Add(i + 1); tris.Add(i + 2); tris.Add(i + 3); tris.Add(i + 0); tris.Add(i + 2); } meshie.triangles = tris.ToArray(); return meshie; } #endregion #region SUPPORT FUNCTIONS bool IsInLayerMask(int layer, LayerMask layermask) { return layermask == (layermask | (1 << layer)); } LayerMask GetRaycastLayerMask() { return CurvedUIInputModule.Instance.RaycastLayerMask; } Image GazeProgressImage { get { return CurvedUIInputModule.Instance.GazeTimedClickProgressImage; } } /// /// Determine the signed angle between two vectors, with normal 'n' /// as the rotation axis. /// float AngleSigned(Vector3 v1, Vector3 v2, Vector3 n) { return Mathf.Atan2( Vector3.Dot(n, Vector3.Cross(v1, v2)), Vector3.Dot(v1, v2)) * Mathf.Rad2Deg; } private bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold) { if (!useDragThreshold) return true; return (pressPos - currentPos).sqrMagnitude >= threshold * threshold; } protected virtual void ProcessMove(PointerEventData pointerEvent) { var targetGO = pointerEvent.pointerCurrentRaycast.gameObject; HandlePointerExitAndEnter(pointerEvent, targetGO); } protected void UpdateSelectedObjects(PointerEventData eventData) { //deselect last object if we moved beyond it bool selectedStillUnderGaze = false; foreach (GameObject go in eventData.hovered) { if (go == eventData.selectedObject) { selectedStillUnderGaze = true; break; } } if (!selectedStillUnderGaze) eventData.selectedObject = null; //find new object to select in hovered objects foreach (GameObject go in eventData.hovered) { if (go == null) continue; //go through only go that can be selected and are drawn by the canvas gph = go.GetComponent(); #if UNITY_5_1 if (go.GetComponent() != null && gph != null && gph.depth != -1) #else if (go.GetComponent() != null && gph != null && gph.depth != -1 && gph.raycastTarget) #endif { if (eventData.selectedObject != go) eventData.selectedObject = go; break; } } if (mySettings.ControlMethod == CurvedUIInputModule.CUIControlMethod.GAZE) { //Test for selected object being dragged and initialize dragging, if needed. //We do this here to trick unity's StandAloneInputModule into thinking we used a touch or mouse to do it. if (eventData.IsPointerMoving() && eventData.pointerDrag != null && !eventData.dragging && ShouldStartDrag(eventData.pressPosition, eventData.position, EventSystem.current.pixelDragThreshold, eventData.useDragThreshold)) { ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.beginDragHandler); eventData.dragging = true; } } } // walk up the tree till a common root between the last entered and the current entered is foung // send exit events up to (but not inluding) the common root. Then send enter events up to // (but not including the common root). protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget) { // if we have no target / pointerEnter has been deleted // just send exit events to anything we are tracking // then exit if (newEnterTarget == null || currentPointerData.pointerEnter == null) { for (var i = 0; i < currentPointerData.hovered.Count; ++i) ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler); currentPointerData.hovered.Clear(); if (newEnterTarget == null) { currentPointerData.pointerEnter = newEnterTarget; return; } } // if we have not changed hover target if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget) return; GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget); // and we already an entered object from last time if (currentPointerData.pointerEnter != null) { // send exit handler call to all elements in the chain // until we reach the new target, or null! Transform t = currentPointerData.pointerEnter.transform; while (t != null) { // if we reach the common root break out! if (commonRoot != null && commonRoot.transform == t) break; ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler); currentPointerData.hovered.Remove(t.gameObject); t = t.parent; } } // now issue the enter call up to but not including the common root currentPointerData.pointerEnter = newEnterTarget; if (newEnterTarget != null) { Transform t = newEnterTarget.transform; while (t != null && t.gameObject != commonRoot) { ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler); currentPointerData.hovered.Add(t.gameObject); t = t.parent; } } } protected static GameObject FindCommonRoot(GameObject g1, GameObject g2) { if (g1 == null || g2 == null) return null; var t1 = g1.transform; while (t1 != null) { var t2 = g2.transform; while (t2 != null) { if (t1 == t2) return t1.gameObject; t2 = t2.parent; } t1 = t1.parent; } return null; } /// /// REturns a screen point under which a ray intersects the curved canvas in its event camera view /// /// true, if screen space point by ray was gotten, false otherwise. /// Ray. /// O position on canvas. bool GetScreenSpacePointByRay(Ray ray, out Vector2 o_positionOnCanvas) { switch (mySettings.Shape) { case CurvedUISettings.CurvedUIShape.CYLINDER: { return RaycastToCyllinderCanvas(ray, out o_positionOnCanvas, false); } case CurvedUISettings.CurvedUIShape.CYLINDER_VERTICAL: { return RaycastToCyllinderVerticalCanvas(ray, out o_positionOnCanvas, false); } case CurvedUISettings.CurvedUIShape.RING: { return RaycastToRingCanvas(ray, out o_positionOnCanvas, false); } case CurvedUISettings.CurvedUIShape.SPHERE: { return RaycastToSphereCanvas(ray, out o_positionOnCanvas, false); } default: { o_positionOnCanvas = Vector2.zero; return false; } } } bool CheckEventCamera() { //check if we have a world camera to process events by if (myCanvas.worldCamera == null) { //try assigning from InputModule if (CurvedUIInputModule.Instance && CurvedUIInputModule.Instance.EventCamera) myCanvas.worldCamera = CurvedUIInputModule.Instance.EventCamera; else if (Camera.main) //asign Main Camera myCanvas.worldCamera = Camera.main; } return myCanvas.worldCamera != null; } #endregion #region PUBLIC /// /// Returns true if user's pointer is currently pointing inside this canvas. /// public bool PointingAtCanvas { get { return pointingAtCanvas; } } public void RebuildCollider() { cyllinderMidPoint = new Vector3(0, 0, -mySettings.GetCyllinderRadiusInCanvasSpace()); CreateCollider(); } /// /// Returns all objects currently under the pointer /// /// The objects under pointer. public List GetObjectsUnderPointer() { if (objectsUnderPointer == null) objectsUnderPointer = new List(); return objectsUnderPointer; } /// /// Returns all the canvas objects that are visible under given Screen Position of EventCamera /// public List GetObjectsUnderScreenPos(Vector2 screenPos, Camera eventCamera = null) { if (eventCamera == null) eventCamera = myCanvas.worldCamera; return GetObjectsHitByRay(eventCamera.ScreenPointToRay(screenPos)); } /// /// Returns all the canvas objects that are intersected by given ray /// /// The objects hit by ray. /// Ray. public List GetObjectsHitByRay(Ray ray) { List results = new List(); Vector2 pointerPosition; //ray outside the canvas, return null if (!GetScreenSpacePointByRay(ray, out pointerPosition)) return results; //lets find the graphics under ray! List s_SortedGraphics = new List(); var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(myCanvas); for (int i = 0; i < foundGraphics.Count; ++i) { Graphic graphic = foundGraphics[i]; // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn if (graphic.depth == -1 || !graphic.raycastTarget) continue; if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera)) continue; if (graphic.Raycast(pointerPosition, eventCamera)) s_SortedGraphics.Add(graphic); } s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth)); for (int i = 0; i < s_SortedGraphics.Count; ++i) results.Add(s_SortedGraphics[i].gameObject); s_SortedGraphics.Clear(); return results; } /// /// Sends OnClick event to every Button under pointer. /// public void Click() { for (int i = 0; i < GetObjectsUnderPointer().Count; i++) { if (GetObjectsUnderPointer()[i].GetComponent())//slider requires a diffrent way to click. { //Click calculated via RectTransformUtility - that's the way Slider class does it under the hood. Slider m_slider = GetObjectsUnderPointer()[i].GetComponent(); Vector2 clickPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle((m_slider.handleRect.parent as RectTransform), lastFrameEventData.position, myCanvas.worldCamera, out clickPoint); clickPoint -= m_slider.handleRect.parent.GetComponent().rect.position; if (m_slider.direction == Slider.Direction.LeftToRight || m_slider.direction == Slider.Direction.RightToLeft) m_slider.normalizedValue = clickPoint.x / (m_slider.handleRect.parent as RectTransform).rect.width; else m_slider.normalizedValue = clickPoint.y / (m_slider.handleRect.parent as RectTransform).rect.height; //prompt update from fill Graphic to avoid flicker GetObjectsUnderPointer()[i].GetComponent().fillRect.GetComponent().SetAllDirty(); //log //Debug.Log("x: " + clickPoint.x + ", width:" + (m_slider.transform as RectTransform).rect.width + ", value:" + clickPoint.x / (m_slider.transform as RectTransform).rect.width); } else { ExecuteEvents.Execute(GetObjectsUnderPointer()[i], lastFrameEventData, ExecuteEvents.pointerDownHandler); ExecuteEvents.Execute(GetObjectsUnderPointer()[i], lastFrameEventData, ExecuteEvents.pointerClickHandler); ExecuteEvents.Execute(GetObjectsUnderPointer()[i], lastFrameEventData, ExecuteEvents.pointerUpHandler); } } } #endregion #region TESSELATION void ModifyQuad(List verts, int vertexIndex, Vector2 requiredSize) { // Read the existing quad vertices List quad = new List(); for (int i = 0; i < 4; i++) quad.Add(verts[vertexIndex + i]); // horizotal and vertical directions of a quad. We're going to tesselate parallel to these. Vector3 horizontalDir = quad[2] - quad[1]; Vector3 verticalDir = quad[1] - quad[0]; // Find how many quads we need to create int horizontalQuads = Mathf.CeilToInt(horizontalDir.magnitude * (1.0f / Mathf.Max(1.0f, requiredSize.x))); int verticalQuads = Mathf.CeilToInt(verticalDir.magnitude * (1.0f / Mathf.Max(1.0f, requiredSize.y))); // Create the quads! float yStart = 0.0f; for (int y = 0; y < verticalQuads; ++y) { float yEnd = (y + 1.0f) / verticalQuads; float xStart = 0.0f; for (int x = 0; x < horizontalQuads; ++x) { float xEnd = (x + 1.0f) / horizontalQuads; //Add new quads to list verts.Add(TesselateQuad(quad, xStart, yStart)); verts.Add(TesselateQuad(quad, xStart, yEnd)); verts.Add(TesselateQuad(quad, xEnd, yEnd)); verts.Add(TesselateQuad(quad, xEnd, yStart)); //begin the next quad where we ened this one xStart = xEnd; } //begin the next row where we ended this one yStart = yEnd; } } Vector3 TesselateQuad(List quad, float x, float y) { Vector3 ret = Vector3.zero; //1. calculate weighting factors List weights = new List(){ (1-x) * (1-y), (1-x) * y, x * y, x * (1-y), }; //2. interpolate pos using weighting factors for (int i = 0; i < 4; i++) ret += quad[i] * weights[i]; return ret; } #endregion } }