// Base Pointer Renderer|PointerRenderers|10010 namespace VRTK { using UnityEngine; using System; using System.Collections.Generic; #if UNITY_5_5_OR_NEWER using UnityEngine.AI; #endif /// /// Provides a base that all pointer renderers can inherit from. /// /// /// **Script Usage:** /// > This is an abstract class that is to be inherited to a concrete class that provides pointer renderer functionality, therefore this script should not be directly used. /// public abstract class VRTK_BasePointerRenderer : MonoBehaviour { /// /// States of Pointer Visibility. /// public enum VisibilityStates { /// /// Only shows the object when the pointer is active. /// OnWhenActive, /// /// Ensures the object is always. /// AlwaysOn, /// /// Ensures the object beam is never visible. /// AlwaysOff } [Serializable] public sealed class PointerOriginSmoothingSettings { [Tooltip("Whether or not to smooth the position of the pointer origin when positioning the pointer tip.")] public bool smoothsPosition; [Tooltip("The maximum allowed distance between the unsmoothed pointer origin and the smoothed pointer origin per frame to use for smoothing.")] public float maxAllowedPerFrameDistanceDifference = 0.003f; [Tooltip("Whether or not to smooth the rotation of the pointer origin when positioning the pointer tip.")] public bool smoothsRotation; [Tooltip("The maximum allowed angle between the unsmoothed pointer origin and the smoothed pointer origin per frame to use for smoothing.")] public float maxAllowedPerFrameAngleDifference = 1.5f; } [Header("Renderer Supplement Settings")] [Tooltip("An optional Play Area Cursor generator to add to the destination position of the pointer tip.")] public VRTK_PlayAreaCursor playareaCursor; [Tooltip("A custom VRTK_PointerDirectionIndicator to use to determine the rotation given to the destination set event.")] public VRTK_PointerDirectionIndicator directionIndicator; [Header("General Renderer Settings")] [Tooltip("A custom raycaster to use for the pointer's raycasts to ignore.")] public VRTK_CustomRaycast customRaycast; [Tooltip("Specifies the smoothing to be applied to the pointer origin when positioning the pointer tip.")] public PointerOriginSmoothingSettings pointerOriginSmoothingSettings = new PointerOriginSmoothingSettings(); [Header("General Appearance Settings")] [Tooltip("The colour to change the pointer materials when the pointer collides with a valid object. Set to `Color.clear` to bypass changing material colour on valid collision.")] public Color validCollisionColor = Color.green; [Tooltip("The colour to change the pointer materials when the pointer is not colliding with anything or with an invalid object. Set to `Color.clear` to bypass changing material colour on invalid collision.")] public Color invalidCollisionColor = Color.red; [Tooltip("Determines when the main tracer of the pointer renderer will be visible.")] public VisibilityStates tracerVisibility = VisibilityStates.OnWhenActive; [Tooltip("Determines when the cursor/tip of the pointer renderer will be visible.")] public VisibilityStates cursorVisibility = VisibilityStates.OnWhenActive; protected const float BEAM_ADJUST_OFFSET = 0.0001f; protected VRTK_Pointer controllingPointer; protected RaycastHit destinationHit = new RaycastHit(); protected Material defaultMaterial; protected Color previousColor; protected Color currentColor; protected VRTK_PolicyList invalidListPolicy; protected VRTK_NavMeshData navMeshData; protected bool headsetPositionCompensation; protected GameObject objectInteractor; protected GameObject objectInteractorAttachPoint; protected GameObject pointerOriginTransformFollowGameObject; protected VRTK_TransformFollow pointerOriginTransformFollow; protected VRTK_InteractGrab controllerGrabScript; protected Rigidbody savedAttachPoint; protected bool attachedToInteractorAttachPoint = false; protected float savedBeamLength = 0f; protected HashSet makeRendererVisible = new HashSet(); protected bool tracerVisible; protected bool cursorVisible; protected LayerMask defaultIgnoreLayer = Physics.IgnoreRaycastLayer; protected SDK_BaseController.ControllerHand cachedAttachedHand = SDK_BaseController.ControllerHand.None; protected Transform cachedPointerAttachPoint = null; /// /// The GetPointerObjects returns an array of the auto generated GameObjects associated with the pointer. /// /// An array of pointer auto generated GameObjects. public abstract GameObject[] GetPointerObjects(); /// /// The InitalizePointer method is used to set up the state of the pointer renderer. /// /// The VRTK_Pointer that is controlling the pointer renderer. /// The VRTK_PolicyList for managing valid and invalid pointer locations. /// The given distance from a nav mesh that the pointer can be to be valid. /// Determines whether the play area cursor will take the headset position within the play area into account when being displayed. [Obsolete("`VRTK_BasePointerRenderer.InitalizePointer(givenPointer, givenInvalidListPolicy, givenNavMeshCheckDistance, givenHeadsetPositionCompensation)` has been replaced with the method `VRTK_BasePointerRenderer.InitalizePointer(givenPointer, givenInvalidListPolicy, givenNavMeshData, givenHeadsetPositionCompensation)`. This method will be removed in a future version of VRTK.")] public virtual void InitalizePointer(VRTK_Pointer givenPointer, VRTK_PolicyList givenInvalidListPolicy, float givenNavMeshCheckDistance, bool givenHeadsetPositionCompensation) { VRTK_NavMeshData givenData = gameObject.AddComponent(); givenData.distanceLimit = givenNavMeshCheckDistance; InitalizePointer(givenPointer, givenInvalidListPolicy, givenData, givenHeadsetPositionCompensation); } /// /// The InitalizePointer method is used to set up the state of the pointer renderer. /// /// The VRTK_Pointer that is controlling the pointer renderer. /// The VRTK_PolicyList for managing valid and invalid pointer locations. /// The NavMeshData object that contains the Nav Mesh restriction options. /// Determines whether the play area cursor will take the headset position within the play area into account when being displayed. public virtual void InitalizePointer(VRTK_Pointer givenPointer, VRTK_PolicyList givenInvalidListPolicy, VRTK_NavMeshData givenNavMeshData, bool givenHeadsetPositionCompensation) { controllingPointer = givenPointer; invalidListPolicy = givenInvalidListPolicy; navMeshData = givenNavMeshData; headsetPositionCompensation = givenHeadsetPositionCompensation; if (controllingPointer != null && controllingPointer.interactWithObjects && controllingPointer.controllerEvents != null && objectInteractor == null) { controllerGrabScript = controllingPointer.controllerEvents.GetComponentInChildren(); CreateObjectInteractor(); } SetupDirectionIndicator(); } /// /// The ResetPointerObjects method is used to destroy any existing pointer objects and recreate them at runtime. /// public virtual void ResetPointerObjects() { DestroyPointerOriginTransformFollow(); DestroyPointerObjects(); CreatePointerOriginTransformFollow(); CreatePointerObjects(); } /// /// The Toggle Method is used to enable or disable the pointer renderer. /// /// The activation state of the pointer. /// The actual state of the activation button press. public virtual void Toggle(bool pointerState, bool actualState) { if (pointerState) { destinationHit = new RaycastHit(); } else if (controllingPointer != null) { controllingPointer.ResetActivationTimer(); PointerExit(destinationHit); } ToggleInteraction(pointerState); ToggleRenderer(pointerState, actualState); } /// /// The ToggleInteraction method is used to enable or disable the controller extension interactions. /// /// If true then the object interactor will be enabled. public virtual void ToggleInteraction(bool state) { ToggleObjectInteraction(state); } /// /// The UpdateRenderer method is used to run an Update routine on the pointer. /// public virtual void UpdateRenderer() { if (playareaCursor != null) { playareaCursor.SetHeadsetPositionCompensation(headsetPositionCompensation); playareaCursor.ToggleState(IsCursorVisible()); } if (directionIndicator != null) { UpdateDirectionIndicator(); } } /// /// The GetDestinationHit method is used to get the RaycastHit of the pointer destination. /// /// The RaycastHit containing the information where the pointer is hitting. public virtual RaycastHit GetDestinationHit() { return destinationHit; } /// /// The ValidPlayArea method is used to determine if there is a valid play area and if it has had any collisions. /// /// Returns true if there is a valid play area and no collisions. Returns false if there is no valid play area or there is but with a collision detected. public virtual bool ValidPlayArea() { return (playareaCursor == null || !playareaCursor.IsActive() || !playareaCursor.HasCollided()); } /// /// The IsVisible method determines if the pointer renderer is at all visible by checking the state of the tracer and the cursor. /// /// Returns true if either the tracer or cursor renderers are visible. Returns false if none are visible. public virtual bool IsVisible() { return (IsTracerVisible() || IsCursorVisible()); } /// /// The IsTracerVisible method determines if the pointer tracer renderer is visible. /// /// Returns true if the tracer renderers are visible. public virtual bool IsTracerVisible() { return (tracerVisibility == VisibilityStates.AlwaysOn || tracerVisible); } /// /// The IsCursorVisible method determines if the pointer cursor renderer is visible. /// /// Returns true if the cursor renderers are visible. public virtual bool IsCursorVisible() { return (cursorVisibility == VisibilityStates.AlwaysOn || cursorVisible); } /// /// The IsValidCollision method determines if the pointer is currently in it's valid collision state. /// /// Returns true if the pointer is in a valid collision, returns false if the pointer is in an invalid collision state. public virtual bool IsValidCollision() { return (currentColor != invalidCollisionColor); } /// /// The GetObjectInteractor method returns the auto generated GameObject that acts as the controller extension for interacting with objects. /// /// The auto generated object interactor GameObject. /// public virtual GameObject GetObjectInteractor() { return objectInteractor; } protected abstract void CreatePointerObjects(); protected abstract void DestroyPointerObjects(); protected abstract void ToggleRenderer(bool pointerState, bool actualState); protected virtual void Awake() { VRTK_SDKManager.AttemptAddBehaviourToToggleOnLoadedSetupChange(this); } protected virtual void OnEnable() { cachedPointerAttachPoint = null; cachedAttachedHand = SDK_BaseController.ControllerHand.None; defaultMaterial = Resources.Load("WorldPointer") as Material; makeRendererVisible.Clear(); CreatePointerOriginTransformFollow(); CreatePointerObjects(); } protected virtual void OnDisable() { DestroyPointerObjects(); if (objectInteractor != null) { Destroy(objectInteractor); } controllerGrabScript = null; DestroyPointerOriginTransformFollow(); } protected virtual void OnDestroy() { VRTK_SDKManager.AttemptRemoveBehaviourToToggleOnLoadedSetupChange(this); } protected virtual void OnValidate() { pointerOriginSmoothingSettings.maxAllowedPerFrameDistanceDifference = Mathf.Max(0.0001f, pointerOriginSmoothingSettings.maxAllowedPerFrameDistanceDifference); pointerOriginSmoothingSettings.maxAllowedPerFrameAngleDifference = Mathf.Max(0.0001f, pointerOriginSmoothingSettings.maxAllowedPerFrameAngleDifference); } protected virtual void FixedUpdate() { if (controllingPointer != null && controllingPointer.interactWithObjects && objectInteractor != null && objectInteractor.activeInHierarchy) { UpdateObjectInteractor(); } if (pointerOriginTransformFollow != null) { UpdatePointerOriginTransformFollow(); } } protected virtual void ToggleObjectInteraction(bool state) { if (controllingPointer != null && controllingPointer.interactWithObjects) { if (state && controllingPointer.grabToPointerTip && controllerGrabScript != null && objectInteractorAttachPoint != null) { savedAttachPoint = controllerGrabScript.controllerAttachPoint; controllerGrabScript.controllerAttachPoint = objectInteractorAttachPoint.GetComponent(); attachedToInteractorAttachPoint = true; } if (!state && controllingPointer.grabToPointerTip && controllerGrabScript != null) { if (attachedToInteractorAttachPoint) { controllerGrabScript.ForceRelease(true); } if (savedAttachPoint != null) { controllerGrabScript.controllerAttachPoint = savedAttachPoint; savedAttachPoint = null; } attachedToInteractorAttachPoint = false; savedBeamLength = 0f; } if (objectInteractor != null) { objectInteractor.SetActive(state); } } } protected virtual void UpdateObjectInteractor() { objectInteractor.transform.position = destinationHit.point; } protected virtual VRTK_ControllerReference GetControllerReference(GameObject reference = null) { reference = (reference == null && controllingPointer != null && controllingPointer.controllerEvents != null ? controllingPointer.controllerEvents.gameObject : reference); return VRTK_ControllerReference.GetControllerReference(reference); } protected virtual Transform GetPointerOriginTransform() { VRTK_ControllerReference controllerReference = GetControllerReference((controllingPointer != null ? controllingPointer.attachedTo : null)); if (VRTK_ControllerReference.IsValid(controllerReference) && (cachedAttachedHand != controllerReference.hand || cachedPointerAttachPoint == null)) { cachedPointerAttachPoint = controllerReference.model.transform.Find(VRTK_SDK_Bridge.GetControllerElementPath(SDK_BaseController.ControllerElements.AttachPoint, controllerReference.hand)); cachedAttachedHand = controllerReference.hand; pointerOriginTransformFollow.gameObject.SetActive(false); } return (cachedPointerAttachPoint != null ? cachedPointerAttachPoint : transform); } protected virtual void UpdatePointerOriginTransformFollow() { pointerOriginTransformFollow.gameObject.SetActive((controllingPointer != null)); if (controllingPointer != null) { pointerOriginTransformFollow.gameObjectToFollow = (controllingPointer.customOrigin == null ? GetPointerOriginTransform() : controllingPointer.customOrigin).gameObject; pointerOriginTransformFollow.enabled = controllingPointer != null; pointerOriginTransformFollowGameObject.SetActive(controllingPointer != null); pointerOriginTransformFollow.smoothsPosition = pointerOriginSmoothingSettings.smoothsPosition; pointerOriginTransformFollow.maxAllowedPerFrameDistanceDifference = pointerOriginSmoothingSettings.maxAllowedPerFrameDistanceDifference; pointerOriginTransformFollow.smoothsRotation = pointerOriginSmoothingSettings.smoothsRotation; pointerOriginTransformFollow.maxAllowedPerFrameAngleDifference = pointerOriginSmoothingSettings.maxAllowedPerFrameAngleDifference; } } protected Transform GetOrigin(bool smoothed = true) { return (smoothed ? pointerOriginTransformFollow.gameObjectToChange.transform : (controllingPointer.customOrigin == null ? GetPointerOriginTransform() : controllingPointer.customOrigin)); } protected virtual void PointerEnter(RaycastHit givenHit) { controllingPointer.PointerEnter(givenHit); } protected virtual void PointerExit(RaycastHit givenHit) { controllingPointer.PointerExit(givenHit); } protected virtual bool ValidDestination() { bool validNavMeshLocation = false; if (navMeshData != null) { if (destinationHit.transform != null) { NavMeshHit hit; validNavMeshLocation = NavMesh.SamplePosition(destinationHit.point, out hit, navMeshData.distanceLimit, navMeshData.validAreas); } } else { validNavMeshLocation = true; } return (validNavMeshLocation && destinationHit.collider != null && !(VRTK_PolicyList.Check(destinationHit.collider.gameObject, invalidListPolicy))); } protected virtual void ToggleElement(GameObject givenObject, bool pointerState, bool actualState, VisibilityStates givenVisibility, ref bool currentVisible) { if (givenObject != null) { currentVisible = (givenVisibility == VisibilityStates.AlwaysOn ? true : pointerState); givenObject.SetActive(currentVisible); if (givenVisibility == VisibilityStates.AlwaysOff) { currentVisible = false; ToggleRendererVisibility(givenObject, false); } else { if (actualState && givenVisibility != VisibilityStates.AlwaysOn) { ToggleRendererVisibility(givenObject, false); AddVisibleRenderer(givenObject); } else { ToggleRendererVisibility(givenObject, true); } } } } protected virtual void AddVisibleRenderer(GameObject givenObject) { makeRendererVisible.Add(givenObject); } protected virtual void MakeRenderersVisible() { foreach (GameObject currentRenderer in new HashSet(makeRendererVisible)) { ToggleRendererVisibility(currentRenderer, true); } makeRendererVisible.Clear(); } protected virtual void ToggleRendererVisibility(GameObject givenObject, bool state) { if (givenObject != null) { Renderer[] renderers = givenObject.GetComponentsInChildren(); for (int i = 0; i < renderers.Length; i++) { renderers[i].enabled = state; } } } protected virtual void SetupMaterialRenderer(GameObject givenObject) { if (givenObject != null) { MeshRenderer pointerRenderer = givenObject.GetComponent(); pointerRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; pointerRenderer.receiveShadows = false; pointerRenderer.material = defaultMaterial; } } protected virtual void ChangeColor(Color givenColor) { previousColor = currentColor; if ((playareaCursor != null && playareaCursor.IsActive() && playareaCursor.HasCollided()) || !ValidDestination() || (controllingPointer != null && !controllingPointer.CanSelect())) { givenColor = invalidCollisionColor; } if (givenColor != Color.clear) { currentColor = givenColor; ChangeMaterial(givenColor); } if (previousColor != currentColor) { EmitStateEvent(); } } protected virtual void EmitStateEvent() { if (controllingPointer != null) { if (IsValidCollision()) { controllingPointer.OnPointerStateValid(); } else { controllingPointer.OnPointerStateInvalid(); } } } protected virtual void ChangeMaterial(Color givenColor) { if (playareaCursor != null) { playareaCursor.SetMaterialColor(givenColor, IsValidCollision()); } if (directionIndicator != null) { directionIndicator.SetMaterialColor(givenColor, IsValidCollision()); } } protected virtual void ChangeMaterialColor(GameObject givenObject, Color givenColor) { if (givenObject != null) { Renderer[] foundRenderers = givenObject.GetComponentsInChildren(); for (int i = 0; i < foundRenderers.Length; i++) { Renderer foundRenderer = foundRenderers[i]; if (foundRenderer.material != null) { foundRenderer.material.EnableKeyword("_EMISSION"); if (foundRenderer.material.HasProperty("_Color")) { foundRenderer.material.color = givenColor; } if (foundRenderer.material.HasProperty("_EmissionColor")) { foundRenderer.material.SetColor("_EmissionColor", VRTK_SharedMethods.ColorDarken(givenColor, 50)); } } } } } protected virtual void CreateObjectInteractor() { objectInteractor = new GameObject(VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BasePointerRenderer_ObjectInteractor_Container")); objectInteractor.transform.SetParent(controllingPointer.controllerEvents.transform); objectInteractor.transform.localPosition = Vector3.zero; objectInteractor.layer = LayerMask.NameToLayer("Ignore Raycast"); VRTK_PlayerObject.SetPlayerObject(objectInteractor, VRTK_PlayerObject.ObjectTypes.Pointer); GameObject objectInteractorCollider = new GameObject(VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BasePointerRenderer_ObjectInteractor_Collider")); objectInteractorCollider.transform.SetParent(objectInteractor.transform); objectInteractorCollider.transform.localPosition = Vector3.zero; objectInteractorCollider.layer = LayerMask.NameToLayer("Ignore Raycast"); SphereCollider tmpCollider = objectInteractorCollider.AddComponent(); tmpCollider.isTrigger = true; VRTK_PlayerObject.SetPlayerObject(objectInteractorCollider, VRTK_PlayerObject.ObjectTypes.Pointer); if (controllingPointer.grabToPointerTip) { objectInteractorAttachPoint = new GameObject(VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BasePointerRenderer_ObjectInteractor_AttachPoint")); objectInteractorAttachPoint.transform.SetParent(objectInteractor.transform); objectInteractorAttachPoint.transform.localPosition = Vector3.zero; objectInteractorAttachPoint.layer = LayerMask.NameToLayer("Ignore Raycast"); Rigidbody objectInteratorRigidBody = objectInteractorAttachPoint.AddComponent(); objectInteratorRigidBody.isKinematic = true; objectInteratorRigidBody.freezeRotation = true; #if UNITY_2018_3_OR_NEWER objectInteratorRigidBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; #else objectInteratorRigidBody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; #endif VRTK_PlayerObject.SetPlayerObject(objectInteractorAttachPoint, VRTK_PlayerObject.ObjectTypes.Pointer); } ScaleObjectInteractor(Vector3.one); objectInteractor.SetActive(false); } protected virtual void ScaleObjectInteractor(Vector3 scaleAmount) { if (objectInteractor != null) { VRTK_SharedMethods.SetGlobalScale(objectInteractor.transform, scaleAmount); } } protected virtual void CreatePointerOriginTransformFollow() { pointerOriginTransformFollowGameObject = new GameObject(VRTK_SharedMethods.GenerateVRTKObjectName(true, gameObject.name, "BasePointerRenderer_Origin_Smoothed")); pointerOriginTransformFollow = pointerOriginTransformFollowGameObject.AddComponent(); pointerOriginTransformFollow.enabled = false; pointerOriginTransformFollow.moment = VRTK_TransformFollow.FollowMoment.OnFixedUpdate; pointerOriginTransformFollow.followsScale = false; } protected virtual void DestroyPointerOriginTransformFollow() { if (pointerOriginTransformFollowGameObject != null) { Destroy(pointerOriginTransformFollowGameObject); pointerOriginTransformFollowGameObject = null; pointerOriginTransformFollow = null; } } protected virtual float OverrideBeamLength(float currentLength) { if (controllerGrabScript == null || !controllerGrabScript.GetGrabbedObject()) { savedBeamLength = 0f; } if (controllingPointer != null && controllingPointer.interactWithObjects && controllingPointer.grabToPointerTip && attachedToInteractorAttachPoint && controllerGrabScript != null && controllerGrabScript.GetGrabbedObject()) { savedBeamLength = (savedBeamLength == 0f ? currentLength : savedBeamLength); return savedBeamLength; } return currentLength; } protected virtual void UpdateDependencies(Vector3 location) { if (playareaCursor != null) { playareaCursor.SetPlayAreaCursorTransform(location); } } protected virtual void SetupDirectionIndicator() { if (directionIndicator != null && controllingPointer != null && controllingPointer.controllerEvents != null) { directionIndicator.Initialize(controllingPointer.controllerEvents); } } protected virtual void UpdateDirectionIndicator() { RaycastHit destinationHit = GetDestinationHit(); directionIndicator.SetPosition((ShowDirectionIndicator() && destinationHit.collider != null), destinationHit.point); } protected virtual bool ShowDirectionIndicator() { switch (directionIndicator.indicatorVisibility) { case VRTK_PointerDirectionIndicator.VisibilityState.OnWhenPointerActive: return controllingPointer.IsPointerActive(); case VRTK_PointerDirectionIndicator.VisibilityState.AlwaysOnWithPointerCursor: return (IsCursorVisible() || controllingPointer.IsPointerActive()); } return false; } } }