// "Wave SDK
// © 2020 HTC Corporation. All Rights Reserved.
//
// Unless otherwise required by copyright law and practice,
// upon the execution of HTC SDK license agreement,
// HTC grants you access to and use of the Wave SDK(s).
// You shall fully comply with all of HTC’s SDK license agreement terms and
// conditions signed by you and all SDK and API requirements,
// specifications, and documentation provided by HTC to You."
using UnityEngine;
using UnityEngine.EventSystems;
using Wave.Native;
namespace Wave.Essence.InputModule
{
///
/// Draws a pointer of controller to indicate to which object is pointed.
///
[DisallowMultipleComponent]
[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
sealed class ControllerPointer : MonoBehaviour
{
const string LOG_TAG = "Wave.Essence.InputModule.ControllerPointer";
private void DEBUG(string msg)
{
if (Log.EnableDebugLog)
Log.d(LOG_TAG, m_PointerType + ", " + msg, true);
}
private void INFO(string msg)
{
Log.i(LOG_TAG, m_PointerType + ", " + msg, true);
}
#region Customized Settings
[Tooltip("Dominant or NonDominant pointer.")]
[SerializeField]
private XR_Hand m_PointerType = XR_Hand.Dominant;
public XR_Hand PointerType { get { return m_PointerType; } set { m_PointerType = value; } }
[Tooltip("Show or hide the pointer.")]
[SerializeField]
private bool m_ShowPointer = true;
public bool ShowPointer { get { return m_ShowPointer; } set { m_ShowPointer = value; } }
private bool m_AutoControlPointer = true;
public bool AutoControlPointer { get { return m_AutoControlPointer; } set { m_AutoControlPointer = value; } }
[Tooltip("The minimal diameter of pointer.")]
[SerializeField]
private float m_MinimalPointerDiameter = 0.03f;
public float MinimalPointerDiameter { get { return m_MinimalPointerDiameter; } set { m_MinimalPointerDiameter = value; } }
/// The pointer shader uses pointerOuterDiameter as _OuterDiameter to set the pointer diameter.
/// pointerOuterDiameter is calcuated by m_MinimalPointerDiameter and pointerGrowthMultiple.
private float pointerGrowthMultiple = 0.02f;
private float pointerOuterDiameter = 0;
[Tooltip("Set to use texture.")]
[SerializeField]
private bool m_UseTexture = true;
public bool UseTexture { get { return m_UseTexture; } set { m_UseTexture = value; } }
// If not use texture, the pointer color will be set to colorFactor.
private Color colorFactor = Color.white;
[Tooltip("True for using the default texture, false for using the custom texture.")]
[SerializeField]
private bool m_UseDefaultTexture = true;
public bool UseDefaultTexture { get { return m_UseDefaultTexture; } set { m_UseDefaultTexture = value; } }
const string kPointerTexture = "Textures/ControllerPointer01";
private Texture2D m_DefaultTexture = null;
[Tooltip("Set the custom texture used when UseDefaultTexture is not set.")]
[SerializeField]
private Texture2D m_CustomTexture = null;
public Texture2D CustomTexture { get { return m_CustomTexture; } set { m_CustomTexture = value; } }
const float kMinimalPointerDistance = 1;
const float kMaximalPointerDistance = 100.0f; // Max end offset of Beam + 0.5m
[Tooltip("Current pointer distance in meters.")]
private float m_PointerDistanceInMeters = 50f;
public float PointerDistanceInMeters { get { return m_PointerDistanceInMeters; } set { m_PointerDistanceInMeters = value; } }
const int kPointerRenderQueueMin = 1000;
const int kPointerRenderQueueMax = 5000;
[Tooltip("Set the Material renderQueue.")]
[SerializeField]
private int m_PointerRenderQueue = kPointerRenderQueueMax;
public int PointerRenderQueue { get { return m_PointerRenderQueue; } set { m_PointerRenderQueue = value; } }
#endregion
/// Material resource of pointer.
/// It contains shader **ControllerPointer01** and there are 5 attributes can be changed in runtime:
///
/// - _OuterDiameter
/// - _DistanceInMeters
/// - _MainTex
/// - _Color
/// - _useTexture
///
/// If _useTexture is set (default), the texture assign in _MainTex will be used.
const string kPointerMaterial = "Materials/ControllerPointer01";
private Material pointerMaterial = null;
private Material pointerMaterialInstance = null;
const string kPointerMeshName = "WaveEssencePointer01";
const string kUnityMeshName = "CtrlQuadPointer";
private Mesh m_Mesh = null;
private MeshFilter m_MeshFilter = null;
private MeshRenderer m_MeshRenderer = null;
/**
* OEM Config
* \"pointer\": {
* \"diameter\": 0.01,
* \"distance\": 1.3,
* \"use_texture\": true,
* \"color\": \"#FFFFFFFF\",
* \"border_color\": \"#777777FF\",
* \"focus_color\": \"#FFFFFFFF\",
* \"focus_border_color\": \"#777777FF\",
* \"texture_name\": null,
* \"Blink\": false
* },
**/
#region MonoBehaviour overrides
private bool mEnabled = false;
void OnEnable()
{
if (!mEnabled)
{
// Load default pointer material resource and create instance.
pointerMaterial = Resources.Load(kPointerMaterial) as Material;
if (pointerMaterial != null)
pointerMaterialInstance = Instantiate(pointerMaterial);
if (pointerMaterialInstance == null)
INFO("OnEnable() Can NOT load default material");
else
INFO("OnEnable() Controller pointer material: " + pointerMaterialInstance.name);
// Load default pointer texture resource.
m_DefaultTexture = (Texture2D)Resources.Load(kPointerTexture);
if (m_DefaultTexture == null)
Log.e(LOG_TAG, "OnEnable() Can NOT load default texture " + kPointerTexture, true);
// Get MeshFilter instance.
m_MeshFilter = GetComponent();
// Get Quad mesh as default pointer mesh.
// m_Mesh will be re-generated in CreatePointerMesh() when not using texture.
GameObject prim_go = GameObject.CreatePrimitive(PrimitiveType.Quad);
m_Mesh = Instantiate(prim_go.GetComponent().sharedMesh);
m_Mesh.name = kUnityMeshName;
prim_go.SetActive(false);
Destroy(prim_go);
InitializePointer();
ControllerPointerProvider.Instance.SetControllerPointer(m_PointerType, gameObject);
mEnabled = true;
}
}
void OnDisable()
{
INFO("OnDisable()");
pointerInitialized = false;
mEnabled = false;
}
///
/// The attributes
///
/// - _Color
/// - _OuterDiameter
/// - _DistanceInMeters
/// can be updated directly by changing
/// - colorFactor
/// - pointerOuterDiameter
/// - PointerDistanceInMeters
///
void Update()
{
UpdateInputModule();
ActivatePointer(m_ShowPointer && (m_ControllerInputModule && m_ControllerInputModule.enabled));
m_PointerDistanceInMeters = Mathf.Clamp(m_PointerDistanceInMeters, kMinimalPointerDistance, kMaximalPointerDistance);
UpdatePointerDiameter();
if (pointerMaterialInstance != null)
{
pointerMaterialInstance.renderQueue = m_PointerRenderQueue;
pointerMaterialInstance.SetColor("_Color", colorFactor);
pointerMaterialInstance.SetFloat("_useTexture", m_UseTexture ? 1.0f : 0.0f);
pointerMaterialInstance.SetFloat("_OuterDiameter", pointerOuterDiameter);
pointerMaterialInstance.SetFloat("_DistanceInMeters", m_PointerDistanceInMeters);
}
else
{
if (Log.gpl.Print)
DEBUG("Update() Pointer material is null!!");
}
if (Log.gpl.Print)
{
DEBUG("Update() " + gameObject.name + " is " + (m_MeshRenderer.enabled ? "shown" : "hidden")
+ ", show pointer? " + m_ShowPointer
+ ", pointer color: " + colorFactor
+ ", use texture: " + m_UseTexture
+ ", pointer outer diameter: " + pointerOuterDiameter
+ ", pointer distance: " + m_PointerDistanceInMeters
+ ", render queue: " + m_PointerRenderQueue);
}
}
#endregion
private ControllerInputModule m_ControllerInputModule = null;
private void UpdateInputModule()
{
if (m_ControllerInputModule != null)
return;
if (EventSystem.current != null)
m_ControllerInputModule = EventSystem.current.gameObject.GetComponent();
}
#region Pointer Object
const int kReticleSegments = 20;
private void CreatePointerMesh()
{
int vertexCount = (kReticleSegments + 1) * 2;
Vector3[] vertices = new Vector3[vertexCount];
for (int vi = 0, si = 0; si <= kReticleSegments; si++)
{
float angle = (float)si / (float)kReticleSegments * Mathf.PI * 2.0f;
float x = Mathf.Sin(angle);
float y = Mathf.Cos(angle);
vertices[vi++] = new Vector3(x, y, 0.0f);
vertices[vi++] = new Vector3(x, y, 1.0f);
}
int indicesCount = (kReticleSegments + 1) * 6;
int[] indices = new int[indicesCount];
int vert = 0;
for (int ti = 0, si = 0; si < kReticleSegments; si++)
{
indices[ti++] = vert + 1;
indices[ti++] = vert;
indices[ti++] = vert + 2;
indices[ti++] = vert + 1;
indices[ti++] = vert + 2;
indices[ti++] = vert + 3;
vert += 2;
}
DEBUG("CreatePointerMesh() Create Mesh and add MeshFilter component.");
m_Mesh = new Mesh();
m_Mesh.vertices = vertices;
m_Mesh.triangles = indices;
m_Mesh.name = kPointerMeshName;
m_Mesh.RecalculateBounds();
}
private bool pointerInitialized = false; // true: the m_Mesh of reticle is created, false: the m_Mesh of reticle is not ready
private void InitializePointer()
{
if (pointerInitialized)
{
INFO("InitializePointer() Pointer is already initialized.");
return;
}
if (m_UseTexture == false)
{
CreatePointerMesh();
DEBUG("InitializePointer() Create a mesh " + kPointerMeshName);
}
else
{
DEBUG("InitializePointer() Use default mesh " + kUnityMeshName);
}
m_MeshFilter.mesh = m_Mesh;
if (pointerMaterialInstance != null)
{
if (m_UseDefaultTexture || (null == m_CustomTexture))
{
DEBUG("InitializePointer() Use default texture.");
pointerMaterialInstance.mainTexture = m_DefaultTexture;
pointerMaterialInstance.SetTexture("_MainTex", m_DefaultTexture);
}
else
{
DEBUG("InitializePointer() Use custom texture.");
pointerMaterialInstance.mainTexture = m_CustomTexture;
pointerMaterialInstance.SetTexture("_MainTex", m_CustomTexture);
}
}
else
{
Log.e(LOG_TAG, "InitializePointer() Pointer material is null!!", true);
}
m_MeshRenderer = GetComponent();
m_MeshRenderer.material = pointerMaterialInstance;
m_MeshRenderer.sortingOrder = 32767;
pointerInitialized = true;
}
private void ActivatePointer(bool show)
{
if (!pointerInitialized)
InitializePointer();
if (m_MeshRenderer.enabled != show)
{
m_MeshRenderer.enabled = show;
DEBUG("ActivatePointer() " + m_MeshRenderer.enabled);
}
}
#endregion
#region Pointer Distance and Size
public Vector3 GetPointerPosition()
{
return transform.position + transform.forward.normalized * m_PointerDistanceInMeters;
}
public void OnPointerEnter(GameObject target, Vector3 intersectionPosition, bool isInteractive)
{
if (m_AutoControlPointer)
m_ShowPointer = true;
if (isInteractive)
SetPointerTarget(intersectionPosition, isInteractive);
}
private void SetPointerTarget(Vector3 target, bool interactive)
{
Vector3 targetLocalPosition = transform.InverseTransformPoint(target);
m_PointerDistanceInMeters = Mathf.Clamp(targetLocalPosition.z, kMinimalPointerDistance, kMaximalPointerDistance);
}
public void OnPointerExit(GameObject target)
{
if (m_AutoControlPointer)
m_ShowPointer = false;
DEBUG("OnPointerExit() " + (target != null ? target.name : "null"));
}
private void UpdatePointerDiameter()
{
pointerOuterDiameter = m_MinimalPointerDiameter + ((m_PointerDistanceInMeters - 1) * pointerGrowthMultiple);
}
#endregion
}
}