// 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. // The controller is not available for versions of Unity without the // GVR native integration. using UnityEngine; using UnityEngine.EventSystems; /// Implementation of GvrBasePointer for a laser pointer visual. /// This script should be attached to the controller object. /// The laser visual is important to help users locate their cursor /// when its not directly in their field of view. public class GvrLaserPointerImpl : GvrBasePointer { /// Small offset to prevent z-fighting of the reticle (meters). private const float Z_OFFSET_EPSILON = 0.1f; /// Final size of the reticle in meters when it is 1 meter from the camera. /// The reticle will be scaled based on the size of the mesh so that it's size /// matches this size. private const float RETICLE_SIZE_METERS = 0.1f; /// The percentage of the reticle mesh that shows the reticle. /// The rest of the reticle mesh is transparent. private const float RETICLE_VISUAL_RATIO = 0.1f; public Camera MainCamera { private get; set; } public Color LaserColor { private get; set; } public LineRenderer LaserLineRenderer { get; set; } public float MaxLaserDistance { private get; set; } public float MaxReticleDistance { private get; set; } private GameObject reticle; public GameObject Reticle { get { return reticle; } set { reticle = value; reticleMeshSizeMeters = 1.0f; reticleMeshSizeRatio = 1.0f; if (reticle != null) { MeshFilter meshFilter = reticle.GetComponent(); if (meshFilter != null && meshFilter.mesh != null) { reticleMeshSizeMeters = meshFilter.mesh.bounds.size.x; if (reticleMeshSizeMeters != 0.0f) { reticleMeshSizeRatio = 1.0f / reticleMeshSizeMeters; } } } } } // Properties exposed for testing purposes. public Vector3 PointerIntersection { get; private set; } public bool IsPointerIntersecting { get; private set; } public Ray PointerIntersectionRay { get; private set; } // The size of the reticle's mesh in meters. private float reticleMeshSizeMeters; // The ratio of the reticleMeshSizeMeters to 1 meter. // If reticleMeshSizeMeters is 10, then reticleMeshSizeRatio is 0.1. private float reticleMeshSizeRatio; private Vector3 lineEndPoint = Vector3.zero; public override Vector3 LineEndPoint { get { return lineEndPoint; } } public override float MaxPointerDistance { get { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) return MaxReticleDistance; #else return 0; #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } } public GvrLaserPointerImpl() { MaxLaserDistance = 0.75f; MaxReticleDistance = 2.5f; } #if !(UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)) public override void OnStart() { // Don't call base.Start() so that this pointer isn't activated when // the editor doesn't have UNITY_HAS_GOOGLE_VR. } #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) public override void OnInputModuleEnabled() { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) if (LaserLineRenderer != null) { LaserLineRenderer.enabled = true; } #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } public override void OnInputModuleDisabled() { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) if (LaserLineRenderer != null) { LaserLineRenderer.enabled = false; } #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } public override void OnPointerEnter(RaycastResult rayastResult, Ray ray, bool isInteractive) { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) PointerIntersection = rayastResult.worldPosition; PointerIntersectionRay = ray; IsPointerIntersecting = true; #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } public override void OnPointerHover(RaycastResult rayastResult, Ray ray, bool isInteractive) { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) PointerIntersection = rayastResult.worldPosition; PointerIntersectionRay = ray; #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } public override void OnPointerExit(GameObject previousObject) { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) PointerIntersection = Vector3.zero; PointerIntersectionRay = new Ray(); IsPointerIntersecting = false; #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } public override void OnPointerClickDown() { // User has performed a click on the target. In a derived class, you could // handle visual feedback such as laser or cursor color changes here. } public override void OnPointerClickUp() { // User has released a click from the target. In a derived class, you could // handle visual feedback such as laser or cursor color changes here. } public override void GetPointerRadius(out float enterRadius, out float exitRadius) { #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) if (Reticle != null) { float reticleScale = Reticle.transform.localScale.x; // Fixed size for enter radius to avoid flickering. // This will cause some slight variability based on the distance of the object // from the camera, and is optimized for the average case. enterRadius = RETICLE_SIZE_METERS * 0.5f * RETICLE_VISUAL_RATIO; // Dynamic size for exit radius. // Always correct because we know the intersection point of the object and can // therefore use the correct radius based on the object's distance from the camera. exitRadius = reticleScale * reticleMeshSizeMeters * RETICLE_VISUAL_RATIO; } else { enterRadius = 0.0f; exitRadius = 0.0f; } #else enterRadius = 0.0f; exitRadius = 0.0f; #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) } #if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) public void OnUpdate() { // Set the reticle's position and scale if (Reticle != null) { if (IsPointerIntersecting) { Vector3 controllerDiff = PointerTransform.position - PointerIntersectionRay.origin; Vector3 proj = Vector3.Project(controllerDiff, PointerIntersectionRay.direction); Vector3 controllerAlongRay = PointerIntersectionRay.origin + proj; Vector3 difference = PointerIntersection - controllerAlongRay; Vector3 clampedDifference = Vector3.ClampMagnitude(difference, MaxReticleDistance); Vector3 clampedPosition = controllerAlongRay + clampedDifference; Reticle.transform.position = clampedPosition; } else { Reticle.transform.localPosition = new Vector3(0, 0, MaxReticleDistance); } float reticleDistanceFromCamera = (Reticle.transform.position - MainCamera.transform.position).magnitude; float scale = RETICLE_SIZE_METERS * reticleMeshSizeRatio * reticleDistanceFromCamera; Reticle.transform.localScale = new Vector3(scale, scale, scale); } if (LaserLineRenderer == null) { Debug.LogWarning("Line renderer is null, returning"); return; } // Set the line renderer positions. if (IsPointerIntersecting) { Vector3 laserDiff = PointerIntersection - base.PointerTransform.position; float intersectionDistance = laserDiff.magnitude; Vector3 direction = laserDiff.normalized; float laserDistance = intersectionDistance > MaxLaserDistance ? MaxLaserDistance : intersectionDistance; lineEndPoint = base.PointerTransform.position + (direction * laserDistance); } else { lineEndPoint = base.PointerTransform.position + (base.PointerTransform.forward * MaxLaserDistance); } LaserLineRenderer.SetPosition(0,base.PointerTransform.position); LaserLineRenderer.SetPosition(1,lineEndPoint); // Adjust transparency float alpha = GvrArmModel.Instance.preferredAlpha; #if UNITY_5_6_OR_NEWER LaserLineRenderer.startColor = Color.Lerp(Color.clear, LaserColor, alpha); LaserLineRenderer.endColor = Color.clear; #else LaserLineRenderer.SetColors(Color.Lerp(Color.clear, LaserColor, alpha), Color.clear); #endif // UNITY_5_6_OR_NEWER } #endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR) }