/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* 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 Oculus.Interaction;
using Oculus.Interaction.UnityCanvas;
using System.Collections.Generic;
using UnityEngine;
///
/// Label use to tag the possible interaction in each object. It appears when the user faces the object and disappears if the object is
/// not being faced directly or an interaction was triggered. The default material makes sure the labels appear in front of everything,
/// and uses the CanvasRenderTexture to render anything that is added to the canvas.
///
public class InteractableObjectLabel : MonoBehaviour
{
[Tooltip("The positions of these transforms are used to check if the user is facing the object")]
public List viewTargets;
[Tooltip("The possible positions for the label, the component always selected the one that has the highest y position component")]
public List labelPositions;
[Tooltip("The position between the left and right cameras")]
public Transform playerHead;
[Tooltip("This group should contain all the interactions in the object, and when one is triggered the label is hidden")]
public InteractableGroupView interactableGroup;
private Vector3 _startScale;
[Space(10)]
[Tooltip("Canvas group at the root of the label canvas, used to make the canvas completely transparent")]
public CanvasGroup group;
public float alphaAnimationSpeed;
public float focusDelay;
public float hideDelay;
public float alignmentThreshold;
public float minScale;
public float positionAnimationSpeed;
[Space(10)]
public Mesh quadMesh;
public Material quadMaterial;
public RectTransform canvasTransform;
public CanvasRenderTexture canvasTexture;
LabelState _currentState;
enum LabelState
{
Hidden,
FocusCheck,
Focused,
HideCheck,
Used
}
private float _targetAlpha;
private float _currentAlpha;
private float _startTime;
private MaterialPropertyBlock _block;
private Transform _quadTransform;
private MeshRenderer _quadRenderer;
private Vector3 currentLabelPosition;
protected bool _started;
void Start()
{
_targetAlpha = 0.0f;
this.BeginStart(ref _started);
this.AssertIsTrue((alphaAnimationSpeed > 0.0f) && (positionAnimationSpeed > 0.0f), "Animation speed is 0.0 or less");
this.AssertField(playerHead, nameof(playerHead));
this.AssertField(interactableGroup, nameof(interactableGroup));
this.AssertField(group, nameof(group));
this.AssertField(quadMesh, nameof(quadMesh));
this.AssertField(quadMaterial, nameof(quadMaterial));
this.AssertField(canvasTransform, nameof(canvasTransform));
this.AssertField(canvasTexture, nameof(canvasTexture));
CreateTextureQuad();
_startScale = _quadTransform.localScale;
_block = new MaterialPropertyBlock();
this.EndStart(ref _started);
}
private void CreateTextureQuad()
{
var quadGO = new GameObject();
quadGO.name = "CanvasTexture";
_quadTransform = quadGO.transform;
_quadTransform.parent = transform;
_quadTransform.localScale = new Vector3()
{
x = canvasTransform.sizeDelta.x * canvasTransform.localScale.x,
y = canvasTransform.sizeDelta.y * canvasTransform.localScale.y,
z = 1.0f
};
var quadMeshFilter = quadGO.AddComponent();
quadMeshFilter.sharedMesh = quadMesh;
_quadRenderer = quadGO.AddComponent();
_quadRenderer.sharedMaterial = quadMaterial;
}
private void OnEnable()
{
interactableGroup.WhenStateChanged += InteractableStateChange;
}
private void OnDisable()
{
interactableGroup.WhenStateChanged -= InteractableStateChange;
}
void SetTargetAlpha()
{
switch (_currentState)
{
case LabelState.Hidden:
_targetAlpha = 0.0f;
break;
case LabelState.FocusCheck:
_targetAlpha = 0.0f;
break;
case LabelState.Focused:
_targetAlpha = 1.0f;
break;
case LabelState.HideCheck:
_targetAlpha = 1.0f;
break;
case LabelState.Used:
_targetAlpha = 0.0f;
break;
default:
break;
}
}
float MaximizedDotView()
{
if (viewTargets == null)
{
return 0.0f;
}
var maxDotView = -1.0f;
foreach (var target in viewTargets)
{
var viewVector = Vector3.Normalize(target.position - playerHead.position);
var dotView = Vector3.Dot(playerHead.forward, viewVector);
maxDotView = Mathf.Max(maxDotView, dotView);
}
return maxDotView;
}
void StateTransition()
{
var dotView = MaximizedDotView();
switch (_currentState)
{
case LabelState.Hidden:
if (dotView >= alignmentThreshold)
{
_currentState = LabelState.FocusCheck;
_startTime = Time.time;
}
break;
case LabelState.FocusCheck:
if (dotView < alignmentThreshold)
{
_currentState = LabelState.Hidden;
}
else if (Time.time - _startTime >= focusDelay)
{
_currentState = LabelState.Focused;
}
break;
case LabelState.Focused:
if (dotView <= alignmentThreshold)
{
_currentState = LabelState.HideCheck;
_startTime = Time.time;
}
break;
case LabelState.HideCheck:
if (dotView > alignmentThreshold)
{
_currentState = LabelState.Focused;
}
else if (Time.time - _startTime >= hideDelay)
{
_currentState = LabelState.Hidden;
}
break;
default:
break;
}
}
void InteractableStateChange(InteractableStateChangeArgs args)
{
switch (args.NewState)
{
case InteractableState.Select:
_currentState = LabelState.Used;
break;
case InteractableState.Normal:
if (_currentState == LabelState.Used)
{
_currentState = LabelState.Hidden;
}
break;
default:
break;
}
}
Vector3 FindHighestLabelPosition()
{
if (labelPositions == null)
{
return transform.position;
}
else
{
var selectedIndex = -1;
var selectedY = transform.position.y;
for (int i = 0; i < labelPositions.Count; i++)
{
var y = labelPositions[i].position.y;
if (y > selectedY)
{
selectedIndex = i;
selectedY = y;
}
}
if (selectedIndex == -1)
{
return transform.position;
}
else
{
return labelPositions[selectedIndex].position;
}
}
}
void UpdateLabelTransform()
{
_startScale = new Vector3()
{
x = canvasTransform.sizeDelta.x * canvasTransform.localScale.x,
y = canvasTransform.sizeDelta.y * canvasTransform.localScale.y,
z = 1.0f
};
var labelPosition = FindHighestLabelPosition();
currentLabelPosition = Vector3.Lerp(currentLabelPosition, labelPosition, positionAnimationSpeed);
var distance = Vector3.Distance(currentLabelPosition, playerHead.position);
var scaleMultiplier = Mathf.Max(minScale, distance);
_quadTransform.localScale = _startScale * scaleMultiplier;
var halfSize = _quadTransform.localScale.y * 0.5f;
_quadTransform.position = currentLabelPosition + _quadTransform.up * halfSize;
_quadTransform.LookAt(playerHead.position);
_quadTransform.localRotation *= Quaternion.Euler(0.0f, 180.0f, 0.0f);
}
void Update()
{
UpdateLabelTransform();
SetTargetAlpha();
StateTransition();
_currentAlpha = Mathf.Lerp(_currentAlpha, _targetAlpha, alphaAnimationSpeed);
group.alpha = Mathf.Clamp01(_currentAlpha);
_block.SetTexture("_MainTex", canvasTexture.Texture);
_quadRenderer.SetPropertyBlock(_block);
}
}