This commit is contained in:
2025-12-11 13:14:43 +01:00
parent 09c838043f
commit 008efafae0
2079 changed files with 659264 additions and 0 deletions
@@ -0,0 +1,310 @@
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
namespace UnityEngine.XR.Templates.MR
{
// Copied from AR Foundation Samples project. Commit: 8970b5a
[RequireComponent(typeof(ARBoundingBox))]
[RequireComponent(typeof(DebugInfoDisplayController))]
[RequireComponent(typeof(BoundingBoxEdgeVisualizer))]
public class ARBoundingBoxDebugVisualizer : MonoBehaviour
{
static readonly Vector3 k_CanvasVerticalOffset = new(0, 0.15f, 0);
[Header("References")]
[SerializeField, Tooltip("The prefab to visualize the bounding box's orientation.")]
GameObject m_OrientationVisualizerPrefab;
[SerializeField, Tooltip("The mesh renderer of the bounding box visualizer.")]
MeshRenderer m_MeshRenderer;
[SerializeField, Tooltip("The Text Mesh Pro material for drawing overlay text to always render debug text info on top of Geometry.")]
Material m_TmpOverlayMaterial;
[Header("Debug Options")]
[SerializeField, Tooltip("Show the trackableId visualizer.")]
bool m_ShowTrackableId;
/// <summary>
/// Show the trackableId visualizer.
/// </summary>
public bool showTrackableId
{
get => m_ShowTrackableId;
set => m_ShowTrackableId = value;
}
[SerializeField, Tooltip("Show the bounding box position and orientation visualizer.")]
bool m_ShowOrientation = true;
/// <summary>
/// Show the bounding box position and orientation visualizer.
/// </summary>
public bool showOrientation
{
get => m_ShowOrientation;
set => m_ShowOrientation = value;
}
[SerializeField, Tooltip("Show the classifications visualizer.")]
bool m_ShowClassifications = true;
/// <summary>
/// Show the classifications visualizer.
/// </summary>
public bool showClassifications
{
get => m_ShowClassifications;
set => m_ShowClassifications = value;
}
[SerializeField, Tooltip("Show the tracking state visualizer.")]
bool m_ShowTrackingState = true;
/// <summary>
/// Show the tracking state visualizer.
/// </summary>
public bool showTrackingState
{
get => m_ShowTrackingState;
set => m_ShowTrackingState = value;
}
[Header("Tracking state visualization settings")]
[SerializeField, Tooltip("The mesh color when the tracking state is set to tracking")]
Color m_TrackingMeshColor;
[SerializeField, Tooltip("The outline color gradient when the tracking state is set to tracking.")]
Gradient m_TrackingOutlineGradient;
[Space]
[SerializeField, Tooltip("The outline color gradient when the tracking state is set to limited.")]
Gradient m_LimitedTrackingOutlineGradient;
[Space]
[SerializeField, Tooltip("The texture the bounding box will have when the tracking state is set to none.")]
Texture m_NoneTrackingTexture;
[SerializeField, Tooltip("The mesh color when the tracking state is set to none.")]
Color m_NoneTrackingMeshColor;
[SerializeField, Tooltip("The mesh texture color when the tracking state is set to none.")]
Color m_NoneTrackingMeshTextureColor;
[SerializeField, Tooltip("The outline color gradient when the tracking state is set to none.")]
Gradient m_NoneTrackingOutlineGradient;
[SerializeField, HideInInspector]
BoundingBoxEdgeVisualizer m_BoundingBoxEdgeVisualizer;
[SerializeField, HideInInspector]
DebugInfoDisplayController m_DebugInfoDisplayController;
ARBoundingBox m_ARBoundingBox;
GameObject m_OrientationVisualizer;
TrackableId m_TrackableId;
BoundingBoxClassifications m_Classifications;
TrackingState m_TrackingState;
bool m_ShowDebugInfoCanvas;
void Reset()
{
m_BoundingBoxEdgeVisualizer = GetComponent<BoundingBoxEdgeVisualizer>();
m_DebugInfoDisplayController = GetComponent<DebugInfoDisplayController>();
m_ARBoundingBox = GetComponent<ARBoundingBox>();
}
void Awake()
{
if (m_BoundingBoxEdgeVisualizer == null)
m_BoundingBoxEdgeVisualizer = GetComponent<BoundingBoxEdgeVisualizer>();
if (m_DebugInfoDisplayController == null)
m_DebugInfoDisplayController = GetComponent<DebugInfoDisplayController>();
if (m_DebugInfoDisplayController != null)
m_DebugInfoDisplayController.Show(m_ShowDebugInfoCanvas && (m_ShowTrackingState || m_ShowClassifications || m_ShowTrackableId));
if (m_ARBoundingBox == null)
m_ARBoundingBox = GetComponent<ARBoundingBox>();
if (m_MeshRenderer == null)
Debug.LogError($"{nameof(m_MeshRenderer)} is null.");
if (m_ShowOrientation && m_OrientationVisualizerPrefab == null)
{
Debug.LogWarning($"{nameof(m_ShowOrientation)} is enabled but {nameof(m_OrientationVisualizerPrefab)} is not assigned. To show the bounding box orientation visualizer assign a prefab to the {nameof(m_OrientationVisualizerPrefab)} in the inspector.", this);
}
if (m_OrientationVisualizerPrefab != null)
{
m_OrientationVisualizer = Instantiate(m_OrientationVisualizerPrefab, transform);
m_OrientationVisualizer.SetActive(false);
}
var controller = FindFirstObjectByType<ARFeatureController>();
if (controller != null)
{
if (!controller.BoundingBoxVisualsEnabled)
enabled = false;
}
}
void Start()
{
if (m_ShowOrientation)
m_DebugInfoDisplayController.SetBottomPivot();
else
m_DebugInfoDisplayController.SetCenterPivot();
}
void OnEnable()
{
if (m_OrientationVisualizer != null && m_ShowOrientation)
m_OrientationVisualizer.SetActive(true);
if (m_DebugInfoDisplayController != null)
m_DebugInfoDisplayController.Show(true);
if (m_BoundingBoxEdgeVisualizer != null)
m_BoundingBoxEdgeVisualizer.enabled = true;
if (m_MeshRenderer != null)
m_MeshRenderer.enabled = true;
}
void OnDisable()
{
if (m_OrientationVisualizer != null)
m_OrientationVisualizer.SetActive(false);
if (m_DebugInfoDisplayController != null)
m_DebugInfoDisplayController.Show(false);
if (m_BoundingBoxEdgeVisualizer != null)
m_BoundingBoxEdgeVisualizer.enabled = false;
if (m_MeshRenderer != null)
m_MeshRenderer.enabled = false;
}
void OnDestroy()
{
if (m_OrientationVisualizer != null)
Destroy(m_OrientationVisualizer);
}
void Update()
{
UpdateDebugInfo();
UpdateVisualizers();
}
/// <summary>
/// Toggles the visibility of the AR Bounding Box debug information.
/// </summary>
/// <param name="isOn">If <see langword="true"/>, the debug information for AR Bounding Boxes will be visible. Otherwise, the debug information for the AR Bounding Boxes will not be visible.</param>
public void ShowDebugInfoCanvas(bool isOn)
{
if (m_DebugInfoDisplayController == null)
return;
m_ShowDebugInfoCanvas = isOn;
UpdateDebugInfo();
}
void UpdateDebugInfo()
{
if (m_DebugInfoDisplayController == null)
return;
m_DebugInfoDisplayController.Show(m_ShowDebugInfoCanvas && (m_ShowTrackingState || m_ShowClassifications || m_ShowTrackableId));
var canvasPosition = m_ARBoundingBox.transform.position;
canvasPosition += m_ShowOrientation ? k_CanvasVerticalOffset : Vector3.zero;
m_DebugInfoDisplayController.SetPosition(canvasPosition);
if (m_ARBoundingBox.trackableId == m_TrackableId &&
m_ARBoundingBox.classifications == m_Classifications &&
m_ARBoundingBox.trackingState == m_TrackingState)
return;
m_TrackableId = m_ARBoundingBox.trackableId;
m_Classifications = m_ARBoundingBox.classifications;
m_TrackingState = m_ARBoundingBox.trackingState;
UpdateTrackingStateVisualization(m_TrackingState);
if (m_ShowTrackableId)
m_DebugInfoDisplayController.AppendDebugEntry("TrackableId:", m_TrackableId.ToString());
if (m_ShowClassifications)
m_DebugInfoDisplayController.AppendDebugEntry("Classifications:", m_Classifications.ToString());
if (m_ShowTrackingState)
m_DebugInfoDisplayController.AppendDebugEntry("Tracking State:", m_TrackingState.ToString());
m_DebugInfoDisplayController.RefreshDisplayInfo();
}
void UpdateVisualizers()
{
UpdateVisualizersEnabledState();
UpdateVisualizerTransforms();
}
void UpdateVisualizersEnabledState()
{
if (m_OrientationVisualizer != null && m_ShowOrientation != m_OrientationVisualizer.activeSelf)
{
m_OrientationVisualizer.SetActive(m_ShowOrientation);
}
}
void UpdateVisualizerTransforms()
{
if (m_MeshRenderer != null)
{
m_MeshRenderer.transform.localScale = m_ARBoundingBox.size;
}
if (m_ShowOrientation && m_OrientationVisualizer != null)
{
var boundingBoxPose = m_ARBoundingBox.pose;
m_OrientationVisualizer.transform.position = boundingBoxPose.position;
m_OrientationVisualizer.transform.rotation = boundingBoxPose.rotation;
}
}
void UpdateTrackingStateVisualization(TrackingState trackingState)
{
switch (trackingState)
{
case TrackingState.Tracking:
m_MeshRenderer.material.SetTexture("_MainTex", default);
m_MeshRenderer.material.mainTextureScale = new(1, 1);
m_MeshRenderer.material.SetColor("_Color", m_TrackingMeshColor);
m_MeshRenderer.material.SetColor("_TexColorTint", m_TrackingMeshColor);
m_BoundingBoxEdgeVisualizer.SetGradient(m_TrackingOutlineGradient);
break;
case TrackingState.Limited:
m_MeshRenderer.material.SetTexture("_MainTex", default);
m_MeshRenderer.material.mainTextureScale = new(1, 1);
m_MeshRenderer.material.SetColor("_Color", m_TrackingMeshColor);
m_MeshRenderer.material.SetColor("_TexColorTint", m_TrackingMeshColor);
m_BoundingBoxEdgeVisualizer.SetGradient(m_TrackingOutlineGradient);
break;
case TrackingState.None:
m_MeshRenderer.material.SetTexture("_MainTex", m_NoneTrackingTexture);
m_MeshRenderer.material.mainTextureScale = new(2, 2);
m_MeshRenderer.material.SetColor("_Color", m_NoneTrackingMeshColor);
m_MeshRenderer.material.SetColor("_TexColorTint", m_NoneTrackingMeshTextureColor);
m_BoundingBoxEdgeVisualizer.SetGradient(m_NoneTrackingOutlineGradient);
break;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e676a4fe85b15f142b5df169fd2423b1
@@ -0,0 +1,230 @@
using UnityEngine.Events;
using UnityEngine.XR.ARFoundation;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Utility class used to control various AR features like occlusion, AR bounding boxes, and AR planes.
/// </summary>
public class ARFeatureController : MonoBehaviour
{
[SerializeField, Tooltip("AR Occlusion Manager that is in charge of changing MR Occlusion Features.")]
OcclusionManager m_OcclusionManager;
/// <summary>
/// AR Occlusion Manager that is in charge of changing MR Occlusion Features.
/// </summary>
public OcclusionManager occlusionManager
{
get => m_OcclusionManager;
set => m_OcclusionManager = value;
}
[SerializeField, Tooltip("AR Plane Manager that is in charge of spawning new AR Plane prefabs into the scene.")]
ARPlaneManager m_PlaneManager;
/// <summary>
/// AR Plane Manager that is in charge of spawning new AR Plane prefabs into the scene.
/// </summary>
public ARPlaneManager planeManager
{
get => m_PlaneManager;
set => m_PlaneManager = value;
}
[SerializeField, Tooltip("Toggle that dictates whether AR Planes should be visualized at runtime.")]
bool m_PlaneVisualsEnabled = true;
/// <summary>
/// Toggle that dictates whether AR Planes should be visualized at runtime.
/// </summary>
public bool PlaneVisualsEnabled => m_PlaneVisualsEnabled;
[SerializeField, Tooltip("AR Bounding Box Manager that is in charge of spawning new AR Bounding Box prefabs into the scene")]
ARBoundingBoxManager m_BoundingBoxManager;
/// <summary>
/// AR Bounding Box Manager that is in charge of spawning new AR Bounding Box prefabs into the scene.
/// </summary>
public ARBoundingBoxManager BoundingBoxManager
{
get => m_BoundingBoxManager;
set => m_BoundingBoxManager = value;
}
[SerializeField, Tooltip("Toggle that dictates whether AR Bounding Boxes should be visualized at runtime.")]
bool m_BoundingBoxVisualsEnabled = true;
/// <summary>
/// Toggle that dictates whether AR Bounding Boxes should be visualized at runtime.
/// </summary>
public bool BoundingBoxVisualsEnabled => m_BoundingBoxVisualsEnabled;
[SerializeField, Tooltip("Toggle that dictates whether AR Bounding Box visualizations should show additional debug information.")]
bool m_BoundingBoxDebugInfoEnabled = true;
/// <summary>
/// Toggle that dictates whether AR Bounding Box visualizations should show additional debug information.
/// </summary>
public bool boundingBoxDebugInfoEnabled => m_BoundingBoxDebugInfoEnabled;
[Header("Feature Changed Events")]
[SerializeField]
UnityEvent<bool> m_OnARPlaneFeatureChanged = new UnityEvent<bool>();
public UnityEvent<bool> onARPlaneFeatureChanged => m_OnARPlaneFeatureChanged;
[SerializeField]
UnityEvent<bool> m_OnARPlaneFeatureVisualizationChanged = new UnityEvent<bool>();
public UnityEvent<bool> onARPlaneFeatureVisualizationChanged => m_OnARPlaneFeatureVisualizationChanged;
[SerializeField]
UnityEvent<bool> m_OnARBoundingBoxFeatureChanged = new UnityEvent<bool>();
public UnityEvent<bool> onARBoundingBoxFeatureChanged => m_OnARBoundingBoxFeatureChanged;
[SerializeField]
UnityEvent<bool> m_OnARBoundingBoxFeatureVisualizationChanged = new UnityEvent<bool>();
public UnityEvent<bool> onARBoundingBoxFeatureVisualizationChanged => m_OnARBoundingBoxFeatureVisualizationChanged;
[SerializeField]
UnityEvent<bool> m_OnARBoundingBoxFeatureDebugVisualizationChanged = new UnityEvent<bool>();
public UnityEvent<bool> onARBoundingBoxFeatureDebugVisualizationChanged => m_OnARBoundingBoxFeatureDebugVisualizationChanged;
/// <summary>
/// Allows access to easily see if the AR Features are enabled and there is at least one bounding box
/// </summary>
/// <returns>Will return True if there is 1 or more AR Bounding Boxes found in the AR Scene.</returns>
public bool HasBoundingBoxes() => m_BoundingBoxManager != null && m_BoundingBoxManager.trackables.count > 0;
bool m_BoundingBoxManagerEnabled;
bool m_PlaneManagerEnabled;
/// <summary>
/// Functionally turns AR Planes on and off in a scene.
/// </summary>
/// <param name="enabled">Whether to enable or disable the currently detected planes.</param>
public void TogglePlanes(bool enabled)
{
if (m_PlaneManager == null)
return;
m_PlaneManagerEnabled = enabled;
m_OnARPlaneFeatureChanged?.Invoke(m_PlaneManagerEnabled);
if (m_PlaneManagerEnabled)
{
m_PlaneManager.enabled = m_PlaneManagerEnabled;
m_PlaneManager.SetTrackablesActive(m_PlaneManagerEnabled);
}
else
{
m_PlaneManager.SetTrackablesActive(m_PlaneManagerEnabled);
m_PlaneManager.enabled = m_PlaneManagerEnabled;
}
}
/// <summary>
/// Toggles the AR plane visualizations in a scene.
/// </summary>
/// <param name="enabled">If <see langword="true"/>, AR plane visualizations will be enabled. Otherwise AR plane visualizations be disabled.</param>
public void TogglePlaneVisualization(bool enabled)
{
if (m_PlaneManager == null)
return;
m_PlaneVisualsEnabled = enabled;
m_OnARPlaneFeatureVisualizationChanged?.Invoke(m_PlaneVisualsEnabled);
var trackables = m_PlaneManager.trackables;
foreach (var trackable in trackables)
{
if (trackable.TryGetComponent(out FadePlaneMaterial fader))
{
fader.FadePlane(m_PlaneVisualsEnabled);
}
if (trackable.TryGetComponent(out ARPlaneMeshVisualizer visualizer))
{
visualizer.enabled = m_PlaneVisualsEnabled;
}
}
}
/// <summary>
/// Functionally turns AR Bounding Boxes on and off in a scene.
/// </summary>
/// <param name="enabled">Whether to enable or disable the currently detected bounding boxes.</param>
public void ToggleBoundingBoxes(bool enabled)
{
if (m_BoundingBoxManager == null)
return;
m_BoundingBoxManagerEnabled = enabled;
m_OnARBoundingBoxFeatureChanged?.Invoke(m_BoundingBoxManagerEnabled);
if (m_BoundingBoxManagerEnabled)
{
m_BoundingBoxManager.enabled = m_BoundingBoxManagerEnabled;
m_BoundingBoxManager.SetTrackablesActive(m_BoundingBoxManagerEnabled);
}
else
{
m_BoundingBoxManager.SetTrackablesActive(m_BoundingBoxManagerEnabled);
m_BoundingBoxManager.enabled = m_BoundingBoxManagerEnabled;
}
}
/// <summary>
/// Toggles the AR Bounding Boxes visualizations in a scene.
/// </summary>
/// <param name="enabled">If <see langword="true"/>, AR Bounding Boxes visualizations will be enabled. Otherwise AR Bounding Boxes visualizations be disabled.</param>
public void ToggleBoundingBoxVisualization(bool enabled)
{
if (m_BoundingBoxManager == null)
return;
m_BoundingBoxVisualsEnabled = enabled;
m_OnARBoundingBoxFeatureVisualizationChanged?.Invoke(m_BoundingBoxVisualsEnabled);
var trackables = m_BoundingBoxManager.trackables;
foreach (var trackable in trackables)
{
if (trackable.TryGetComponent(out ARBoundingBoxDebugVisualizer visualizer))
{
visualizer.enabled = m_BoundingBoxVisualsEnabled;
visualizer.ShowDebugInfoCanvas(m_BoundingBoxVisualsEnabled && m_BoundingBoxDebugInfoEnabled);
}
}
}
/// <summary>
/// Toggles the visualization of the debug information for AR Bounding Boxes.
/// </summary>
/// <param name="enabled">If <see langword="true"/>, debug information will be shown for AR Bounding Boxes. Otherwise, debug information will not be shown.</param>
public void ToggleDebugInfo(bool enabled)
{
if (m_BoundingBoxManager == null)
return;
m_BoundingBoxDebugInfoEnabled = enabled;
m_OnARBoundingBoxFeatureDebugVisualizationChanged?.Invoke(m_BoundingBoxDebugInfoEnabled);
// If general bounding box visuals are not enabled, do not enable the debug info.
if (!m_BoundingBoxVisualsEnabled)
return;
var trackables = m_BoundingBoxManager.trackables;
foreach (var trackable in trackables)
{
if (trackable.TryGetComponent(out ARBoundingBoxDebugVisualizer visualizer))
{
visualizer.ShowDebugInfoCanvas(m_BoundingBoxDebugInfoEnabled);
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 382a7d029b7cc7a4c985d053963fc9d8
@@ -0,0 +1,90 @@
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Controls the visual states of a boolean toggle switch UI
/// </summary>
[RequireComponent(typeof(Toggle))]
public class BooleanToggleVisualsController : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
const float k_TargetPositionX = 17f;
#pragma warning disable 649
[SerializeField, Tooltip("The boolean toggle knob.")]
RectTransform m_Knob;
[SerializeField, Tooltip("How much to translate the button imagery on the z on hover.")]
float m_ZTranslation = 5f;
#pragma warning restore 649
Toggle m_Toggle;
float m_InitialBackground;
Coroutine m_ColorFade;
Coroutine m_LocalMove;
void Awake()
{
m_Toggle = gameObject.GetComponent<Toggle>();
//Add listener for when the state of the Toggle changes, to take action
m_Toggle.onValueChanged.AddListener(ToggleValueChanged);
if (m_Knob != null)
{
m_InitialBackground = m_Knob.localPosition.z;
}
}
void OnEnable()
{
ToggleValueChanged(m_Toggle.isOn);
}
/// <inheritdoc />
void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData)
{
PerformEntranceActions();
}
/// <inheritdoc />
void IPointerExitHandler.OnPointerExit(PointerEventData eventData)
{
PerformExitActions();
}
void ToggleValueChanged(bool value)
{
if (value)
{
m_Knob.localPosition = new Vector3(k_TargetPositionX, m_Knob.localPosition.y, m_Knob.localPosition.z);
}
else
{
m_Knob.localPosition = new Vector3(-k_TargetPositionX, m_Knob.localPosition.y, m_Knob.localPosition.z);
}
}
void PerformEntranceActions()
{
if (m_Knob != null)
{
var backgroundLocalPosition = m_Knob.localPosition;
backgroundLocalPosition.z = m_InitialBackground - m_ZTranslation;
m_Knob.localPosition = backgroundLocalPosition;
}
}
void PerformExitActions()
{
if (m_Knob != null)
{
var backgroundLocalPosition = m_Knob.localPosition;
backgroundLocalPosition.z = m_InitialBackground;
m_Knob.localPosition = backgroundLocalPosition;
m_Knob.localScale = Vector3.one;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1f47872a1879dff4c8a322f9226c58f2
@@ -0,0 +1,182 @@
using UnityEngine.XR.ARFoundation;
namespace UnityEngine.XR.Templates.MR
{
// Copied from AR Foundation Samples project. Commit: 8970b5a
[RequireComponent(typeof(ARBoundingBox))]
public class BoundingBoxEdgeVisualizer : MonoBehaviour
{
const float k_EdgeThickness = 0.01f;
[SerializeField, HideInInspector]
ARBoundingBox m_ARBoundingBox;
[Header("Top Edges")]
[SerializeField]
Renderer m_TopEdge1;
[SerializeField]
Renderer m_TopEdge2;
[SerializeField]
Renderer m_TopEdge3;
[SerializeField]
Renderer m_TopEdge4;
[Header("Vertical Edges")]
[SerializeField]
Renderer m_VerticalEdge1;
[SerializeField]
Renderer m_VerticalEdge2;
[SerializeField]
Renderer m_VerticalEdge3;
[SerializeField]
Renderer m_VerticalEdge4;
[Header("BottomEdges")]
[SerializeField]
Renderer m_BottomEdge1;
[SerializeField]
Renderer m_BottomEdge2;
[SerializeField]
Renderer m_BottomEdge3;
[SerializeField]
Renderer m_BottomEdge4;
Vector3 m_PreviouseSize;
public void SetGradient(Gradient gradient)
{
// top edges
m_TopEdge1.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_TopEdge1.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_TopEdge2.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_TopEdge2.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_TopEdge3.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_TopEdge3.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_TopEdge4.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_TopEdge4.material.SetColor("_ColorB", gradient.colorKeys[1].color);
// vertical edges
m_VerticalEdge1.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_VerticalEdge1.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_VerticalEdge2.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_VerticalEdge2.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_VerticalEdge3.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_VerticalEdge3.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_VerticalEdge4.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_VerticalEdge4.material.SetColor("_ColorB", gradient.colorKeys[1].color);
// bottom edges
m_BottomEdge1.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_BottomEdge1.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_BottomEdge2.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_BottomEdge2.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_BottomEdge3.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_BottomEdge3.material.SetColor("_ColorB", gradient.colorKeys[1].color);
m_BottomEdge4.material.SetColor("_ColorA", gradient.colorKeys[0].color);
m_BottomEdge4.material.SetColor("_ColorB", gradient.colorKeys[1].color);
}
void Reset()
{
m_ARBoundingBox = GetComponent<ARBoundingBox>();
}
void Awake()
{
if (m_ARBoundingBox == null)
m_ARBoundingBox = GetComponent<ARBoundingBox>();
}
void OnEnable()
{
m_TopEdge1.enabled = m_TopEdge2.enabled = m_TopEdge3.enabled = m_TopEdge4.enabled = true;
m_VerticalEdge1.enabled = m_VerticalEdge2.enabled = m_VerticalEdge3.enabled = m_VerticalEdge4.enabled = true;
m_BottomEdge1.enabled = m_BottomEdge2.enabled = m_BottomEdge3.enabled = m_BottomEdge4.enabled = true;
}
void OnDisable()
{
m_TopEdge1.enabled = m_TopEdge2.enabled = m_TopEdge3.enabled = m_TopEdge4.enabled = false;
m_VerticalEdge1.enabled = m_VerticalEdge2.enabled = m_VerticalEdge3.enabled = m_VerticalEdge4.enabled = false;
m_BottomEdge1.enabled = m_BottomEdge2.enabled = m_BottomEdge3.enabled = m_BottomEdge4.enabled = false;
}
void Update()
{
if (m_ARBoundingBox.size == m_PreviouseSize)
return;
m_PreviouseSize = m_ARBoundingBox.size;
var size = m_ARBoundingBox.size;
var halfSize = size * 0.5f;
UpdateTopEdges(size, halfSize);
UpdateVerticalEdges(size, halfSize);
UpdateBottomEdges(size, halfSize);
}
void UpdateTopEdges(Vector3 size, Vector3 halfSize)
{
// update sizes
m_TopEdge1.transform.localScale = new(k_EdgeThickness, k_EdgeThickness, size.z);
m_TopEdge2.transform.localScale = new(size.x, k_EdgeThickness, k_EdgeThickness);
m_TopEdge3.transform.localScale = new(k_EdgeThickness, k_EdgeThickness, size.z);
m_TopEdge4.transform.localScale = new(size.x, k_EdgeThickness, k_EdgeThickness);
// update positions
m_TopEdge1.transform.localPosition = new(halfSize.x, halfSize.y, 0);
m_TopEdge2.transform.localPosition = new(0, halfSize.y, halfSize.z);
m_TopEdge3.transform.localPosition = new(-halfSize.x, halfSize.y, 0);
m_TopEdge4.transform.localPosition = new(0, halfSize.y, -halfSize.z);
}
void UpdateVerticalEdges(Vector3 size, Vector3 halfSize)
{
// update sizes
m_VerticalEdge1.transform.localScale = new(k_EdgeThickness, size.y, k_EdgeThickness);
m_VerticalEdge2.transform.localScale = new(k_EdgeThickness, size.y, k_EdgeThickness);
m_VerticalEdge3.transform.localScale = new(k_EdgeThickness, size.y, k_EdgeThickness);
m_VerticalEdge4.transform.localScale = new(k_EdgeThickness, size.y, k_EdgeThickness);
// update positions
m_VerticalEdge1.transform.localPosition = new(halfSize.x, 0, -halfSize.z);
m_VerticalEdge2.transform.localPosition = new(halfSize.x, 0, halfSize.z);
m_VerticalEdge3.transform.localPosition = new(-halfSize.x, 0, halfSize.z);
m_VerticalEdge4.transform.localPosition = new(-halfSize.x, 0, -halfSize.z);
}
void UpdateBottomEdges(Vector3 size, Vector3 halfSize)
{
// update sizes
m_BottomEdge1.transform.localScale = new(k_EdgeThickness, k_EdgeThickness, size.z);
m_BottomEdge2.transform.localScale = new(size.x, k_EdgeThickness, k_EdgeThickness);
m_BottomEdge3.transform.localScale = new(k_EdgeThickness, k_EdgeThickness, size.z);
m_BottomEdge4.transform.localScale = new(size.x, k_EdgeThickness, k_EdgeThickness);
// update positions
m_BottomEdge1.transform.localPosition = new(halfSize.x, -halfSize.y, 0);
m_BottomEdge2.transform.localPosition = new(0, -halfSize.y, halfSize.z);
m_BottomEdge3.transform.localPosition = new(-halfSize.x, -halfSize.y, 0);
m_BottomEdge4.transform.localPosition = new(0, -halfSize.y, -halfSize.z);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d30583e31550c4344acca889b5bdc7b1
@@ -0,0 +1,157 @@
using System;
using System.Text;
using TMPro;
namespace UnityEngine.XR.Templates.MR
{
// Copied from AR Foundation Samples project. Commit: 8970b5a
public class DebugInfoDisplayController : MonoBehaviour
{
const float k_DebugInfoBackgroundPadding = 0.04f;
[SerializeField]
Canvas m_Canvas;
[SerializeField]
RectTransform m_BackgroundRT;
[SerializeField]
RectTransform m_DebugLabelOffsetRT;
[SerializeField]
TextMeshProUGUI m_DebugLabelTypes;
[SerializeField]
TextMeshProUGUI m_DebugLabelValues;
[SerializeField]
RectTransform m_Icon;
Camera m_MainCamera;
Transform m_MainCameraTransform;
RectTransform m_MainCameraRT;
StringBuilder m_TypesBuilder = new();
StringBuilder m_ValuesBuilder = new();
Transform m_CanvasTransform;
float m_ColumnWidth;
Vector2 m_CanvasSizeInWorld;
float m_HalfCanvasHeight;
/// <summary>
/// Sets the canvas pivot to the center when centering the canvas at
/// its position is required.
/// </summary>
public void SetCenterPivot()
{
m_MainCameraRT.pivot = new(0.5f, m_HalfCanvasHeight);
}
/// <summary>
/// Sets the canvas pivot at the bottom when positioning the canvas
/// offset above a position is required.
/// </summary>
public void SetBottomPivot()
{
if (m_MainCameraRT != null)
m_MainCameraRT.pivot = new(0.5f, 0f);
}
/// <summary>
/// Shows or hides the debug info. Debug info is shown by default.
/// </summary>
/// <param name="isOn">Whether or not the canvas GameObject will be set active.</param>
public void Show(bool isOn)
{
m_Canvas.gameObject.SetActive(isOn);
}
public void SetPosition(Vector3 position)
{
if (m_MainCameraRT != null)
m_CanvasTransform.position = position;
}
public void AppendDebugEntry(string dataType, string dataValue)
{
FormatDebugText(dataType, dataValue);
}
public void RefreshDisplayInfo()
{
m_DebugLabelTypes.text = m_TypesBuilder.ToString();
m_TypesBuilder.Clear();
m_DebugLabelValues.text = m_ValuesBuilder.ToString();
m_ValuesBuilder.Clear();
// update background size and position
var debugLabelTypesSize = m_DebugLabelTypes.GetPreferredValues();
var debugLabelValuesSize = m_DebugLabelValues.GetPreferredValues();
m_DebugLabelTypes.GetComponent<RectTransform>().sizeDelta = debugLabelTypesSize;
m_DebugLabelValues.GetComponent<RectTransform>().sizeDelta = debugLabelValuesSize;
var width = debugLabelTypesSize.x + debugLabelValuesSize.x;
var widthPlusSpacingAndPadding = width + m_ColumnWidth + k_DebugInfoBackgroundPadding;
var height = Math.Max(debugLabelTypesSize.y, debugLabelValuesSize.y);
m_BackgroundRT.sizeDelta = new(widthPlusSpacingAndPadding, height + m_Icon.sizeDelta.y + k_DebugInfoBackgroundPadding);
// shift background to the left to center
var halfWidth = width * 0.5f;
var positionOffset = Vector2.zero;
positionOffset.x -= debugLabelTypesSize.x - halfWidth;
positionOffset.y = -k_DebugInfoBackgroundPadding * 0.5f;
m_BackgroundRT.anchoredPosition = positionOffset;
// place icon in center
m_Icon.anchoredPosition = new(positionOffset.x, height);
// shift the label offset to the right to center
var anchoredPosition = new Vector2(-positionOffset.x, 0);
m_DebugLabelOffsetRT.anchoredPosition = anchoredPosition;
}
void Awake()
{
m_MainCamera = Camera.main!;
m_MainCameraTransform = m_MainCamera.transform;
m_Canvas.worldCamera = m_MainCamera;
m_BackgroundRT.sizeDelta = Vector2.zero;
m_CanvasTransform = m_Canvas.transform;
m_MainCameraRT = m_Canvas.GetComponent<RectTransform>();
var debugLabelValuesRT = m_DebugLabelValues.GetComponent<RectTransform>();
m_ColumnWidth = debugLabelValuesRT.anchoredPosition.x * 2f;
m_HalfCanvasHeight = m_MainCameraRT.sizeDelta.y * 0.5f;
m_DebugLabelTypes.text = string.Empty;
m_DebugLabelValues.text = string.Empty;
}
void Update()
{
var cameraToCanvasVector = transform.position - m_MainCameraTransform.position;
m_CanvasTransform.LookAt(transform.position + cameraToCanvasVector);
}
void OnDestroy()
{
Destroy(m_DebugLabelValues.gameObject);
}
void FormatDebugText(string dataType, string value)
{
if (m_TypesBuilder.Length != 0)
m_TypesBuilder.AppendLine();
m_TypesBuilder.Append($"<b>{dataType}</b>");
if (m_ValuesBuilder.Length != 0)
m_ValuesBuilder.AppendLine();
m_ValuesBuilder.Append(value);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2cf09e924d5427b438ec9d08cdf4f7f5
@@ -0,0 +1,53 @@
using System.Collections;
namespace UnityEngine.XR.Templates.MR
{
public class FadeMaterial : MonoBehaviour
{
// attached game object for fading
public GameObject Environment;
// fade speed length
public float fadeSpeed;
Coroutine m_FadeCoroutine;
public void FadeSkybox(bool visible)
{
if (m_FadeCoroutine != null)
StopCoroutine(m_FadeCoroutine);
m_FadeCoroutine = StartCoroutine(Fade(visible));
}
//Fade Coroutine
public IEnumerator Fade(bool visible)
{
Renderer rend = Environment.transform.GetComponent<Renderer>();
float alphaValue = rend.material.GetFloat("_Alpha");
if (visible)
{
//while loop to deincrement Alpha value until object is invisible
while (rend.material.GetFloat("_Alpha") > 0f)
{
alphaValue -= Time.deltaTime / fadeSpeed;
rend.material.SetFloat("_Alpha", alphaValue);
yield return null;
}
rend.material.SetFloat("_Alpha", 0f);
}
else if (!visible)
{
//while loop to increment object Alpha value until object is opaque
while (rend.material.GetFloat("_Alpha") < 1f)
{
alphaValue += Time.deltaTime / fadeSpeed;
rend.material.SetFloat("_Alpha", alphaValue);
yield return null;
}
rend.material.SetFloat("_Alpha", 1f);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: afa06ae48506e0c43b85c8f4f70ce3e4
@@ -0,0 +1,110 @@
using System.Collections;
namespace UnityEngine.XR.Templates.MR
{
// Original source: AR Mobile Template project
public class FadePlaneMaterial : MonoBehaviour
{
// attached game object for fading
public GameObject Plane;
// fade speed length
public float fadeSpeed = 1f;
// fade alpha speed length
public float fadeAlphaSpeed = 0.5f;
// view radius
public float viewRadius = 2.5f;
// alpha
public float alpha = 0.5f;
Coroutine m_FadeDotsCoroutine;
Coroutine m_FadeAlphaCoroutine;
static readonly int k_DotViewRadius = Shader.PropertyToID("_DotViewRadius");
static readonly int k_Alpha = Shader.PropertyToID("_Alpha");
void Awake()
{
Renderer rend = Plane.transform.GetComponent<Renderer>();
rend.material.SetFloat(k_DotViewRadius, 0f);
rend.material.SetFloat(k_Alpha, 0f);
FadePlane(true);
}
public void FadePlane(bool visible)
{
if (m_FadeDotsCoroutine != null)
StopCoroutine(m_FadeDotsCoroutine);
if (m_FadeAlphaCoroutine != null)
StopCoroutine(m_FadeAlphaCoroutine);
m_FadeAlphaCoroutine = StartCoroutine(FadeAlpha(visible));
m_FadeDotsCoroutine = StartCoroutine(FadeDots(visible));
}
//Fade Coroutine
public IEnumerator FadeDots(bool visible)
{
yield return new WaitForSeconds(fadeAlphaSpeed);
Renderer rend = Plane.transform.GetComponent<Renderer>();
float viewRadiusValue = rend.material.GetFloat(k_DotViewRadius);
if (!visible)
{
//while loop to deincrement Alpha value until object is invisible
while (rend.material.GetFloat(k_DotViewRadius) > 0f)
{
viewRadiusValue -= Time.deltaTime / fadeSpeed;
rend.material.SetFloat(k_DotViewRadius, viewRadiusValue);
yield return null;
}
rend.material.SetFloat(k_DotViewRadius, 0f);
}
else if (visible)
{
//while loop to increment object Alpha value until object is opaque
while (rend.material.GetFloat(k_DotViewRadius) < viewRadius)
{
viewRadiusValue += Time.deltaTime / fadeSpeed;
rend.material.SetFloat(k_DotViewRadius, viewRadiusValue);
yield return null;
}
rend.material.SetFloat(k_DotViewRadius, viewRadius);
}
}
public IEnumerator FadeAlpha(bool visible)
{
Renderer rend = Plane.transform.GetComponent<Renderer>();
float alphaValue = rend.material.GetFloat(k_Alpha);
if (!visible)
{
//while loop to deincrement Alpha value until object is invisible
while (rend.material.GetFloat(k_Alpha) > 0f)
{
alphaValue -= Time.deltaTime / fadeAlphaSpeed;
rend.material.SetFloat(k_DotViewRadius, alphaValue);
yield return null;
}
rend.material.SetFloat(k_Alpha, 0f);
}
else if (visible)
{
//while loop to increment object Alpha value until object is opaque
while (rend.material.GetFloat(k_Alpha) < alpha)
{
alphaValue += Time.deltaTime / fadeAlphaSpeed;
rend.material.SetFloat(k_Alpha, alphaValue);
yield return null;
}
rend.material.SetFloat(k_Alpha, alpha);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e2c97b728a740f4da44def78496027c
@@ -0,0 +1,111 @@
using System.Collections.Generic;
using Unity.XR.CoreUtils;
using UnityEngine.XR.Interaction.Toolkit.Samples.ARStarterAssets;
namespace UnityEngine.XR.Templates.MR
{
public class GazeTooltips : MonoBehaviour
{
[SerializeField]
XROrigin m_XROrigin;
[SerializeField]
Transform m_Tooltip;
[SerializeField]
bool m_TapTooltip = true;
Transform m_XRCameraTransform;
LayerMask m_PlaneMask;
ZoneScale m_ZoneScale;
GameObject m_LastPlane = null;
readonly List<ARContactSpawnTrigger> m_SpawnerContacts = new();
const float k_SphereCastRadius = 0.1f;
const string k_PlaneLayer = "Placeable Surface";
void Start()
{
if (m_XROrigin == null)
{
#if UNITY_2022_3_OR_NEWER
m_XROrigin = FindFirstObjectByType<XROrigin>();
#else
m_XROrigin = FindObjectOfType<XROrigin>();
#endif
}
m_XRCameraTransform = m_XROrigin.Camera.transform;
m_XROrigin.GetComponentsInChildren(true, m_SpawnerContacts);
m_ZoneScale = GetComponentInChildren<ZoneScale>(true);
m_PlaneMask = LayerMask.GetMask(k_PlaneLayer);
}
void OnEnable()
{
m_Tooltip.gameObject.SetActive(false);
}
void LateUpdate()
{
PlaceTooltip();
}
void PlaceTooltip()
{
RaycastHit hitInfo;
if (Physics.SphereCast(new Ray(m_XRCameraTransform.position, m_XRCameraTransform.forward), k_SphereCastRadius, out hitInfo, float.MaxValue, m_PlaneMask))
{
var spawnSurfaceFound = false;
Vector3 surfacePosition = default;
// plane tap tooltip
if (m_TapTooltip)
{
foreach (var spawnerContact in m_SpawnerContacts)
{
if (spawnerContact.isActiveAndEnabled && spawnerContact.TryGetSpawnSurfaceData(hitInfo.collider, out surfacePosition, out _))
{
spawnSurfaceFound = true;
break;
}
}
}
if (spawnSurfaceFound)
{
m_Tooltip.position = surfacePosition;
var facePosition = m_XRCameraTransform.position;
var forward = facePosition - m_Tooltip.position;
var targetRotation = forward.sqrMagnitude > float.Epsilon ? Quaternion.LookRotation(forward, Vector3.up) : Quaternion.identity;
targetRotation *= Quaternion.Euler(new Vector3(0f, 180f, 0f));
var targetEuler = targetRotation.eulerAngles;
var currentEuler = m_Tooltip.rotation.eulerAngles;
targetRotation = Quaternion.Euler
(
currentEuler.x,
targetEuler.y,
currentEuler.z
);
m_Tooltip.rotation = targetRotation;
if (!m_Tooltip.gameObject.activeSelf)
m_Tooltip.gameObject.SetActive(true);
if (m_LastPlane != hitInfo.transform.gameObject)
m_ZoneScale.Snap();
m_LastPlane = hitInfo.transform.gameObject;
}
else
{
if (m_Tooltip.gameObject.activeSelf)
{
m_Tooltip.gameObject.SetActive(false);
}
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 174562462553ba843bd95fe681f15de4
@@ -0,0 +1,490 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets;
using TMPro;
using LazyFollow = UnityEngine.XR.Interaction.Toolkit.UI.LazyFollow;
namespace UnityEngine.XR.Templates.MR
{
public struct Goal
{
public GoalManager.OnboardingGoals CurrentGoal;
public bool Completed;
public Goal(GoalManager.OnboardingGoals goal)
{
CurrentGoal = goal;
Completed = false;
}
}
public class GoalManager : MonoBehaviour
{
public enum OnboardingGoals
{
Empty,
FindSurfaces,
TapSurface,
}
Queue<Goal> m_OnboardingGoals;
Goal m_CurrentGoal;
bool m_AllGoalsFinished;
int m_SurfacesTapped;
int m_CurrentGoalIndex = 0;
[Serializable]
class Step
{
[SerializeField]
public GameObject stepObject;
[SerializeField]
public string buttonText;
public bool includeSkipButton;
}
[SerializeField]
List<Step> m_StepList = new List<Step>();
[SerializeField]
public TextMeshProUGUI m_StepButtonTextField;
[SerializeField]
public GameObject m_SkipButton;
[SerializeField]
GameObject m_LearnButton;
[SerializeField]
GameObject m_LearnModal;
[SerializeField]
Button m_LearnModalButton;
[SerializeField]
GameObject m_CoachingUIParent;
[SerializeField]
FadeMaterial m_FadeMaterial;
[SerializeField]
Toggle m_PassthroughToggle;
[SerializeField]
LazyFollow m_GoalPanelLazyFollow;
[SerializeField]
GameObject m_TapTooltip;
[SerializeField]
GameObject m_VideoPlayer;
[SerializeField]
Toggle m_VideoPlayerToggle;
[SerializeField]
ARFeatureController m_FeatureController;
[SerializeField]
ObjectSpawner m_ObjectSpawner;
const int k_NumberOfSurfacesTappedToCompleteGoal = 1;
Vector3 m_TargetOffset = new Vector3(-.5f, -.25f, 1.5f);
void Start()
{
m_OnboardingGoals = new Queue<Goal>();
var welcomeGoal = new Goal(OnboardingGoals.Empty);
var findSurfaceGoal = new Goal(OnboardingGoals.FindSurfaces);
var tapSurfaceGoal = new Goal(OnboardingGoals.TapSurface);
var endGoal = new Goal(OnboardingGoals.Empty);
m_OnboardingGoals.Enqueue(welcomeGoal);
m_OnboardingGoals.Enqueue(findSurfaceGoal);
m_OnboardingGoals.Enqueue(tapSurfaceGoal);
m_OnboardingGoals.Enqueue(endGoal);
m_CurrentGoal = m_OnboardingGoals.Dequeue();
if (m_TapTooltip != null)
m_TapTooltip.SetActive(false);
if (m_VideoPlayer != null)
{
m_VideoPlayer.SetActive(false);
if (m_VideoPlayerToggle != null)
m_VideoPlayerToggle.isOn = false;
}
if (m_FadeMaterial != null)
{
m_FadeMaterial.FadeSkybox(false);
if (m_PassthroughToggle != null)
m_PassthroughToggle.isOn = false;
}
if (m_LearnButton != null)
{
m_LearnButton.GetComponent<Button>().onClick.AddListener(OpenModal); ;
m_LearnButton.SetActive(false);
}
if (m_LearnModal != null)
{
m_LearnModal.transform.localScale = Vector3.zero;
}
if (m_LearnModalButton != null)
{
m_LearnModalButton.onClick.AddListener(CloseModal);
}
if (m_ObjectSpawner == null)
{
#if UNITY_2023_1_OR_NEWER
m_ObjectSpawner = FindAnyObjectByType<ObjectSpawner>();
#else
m_ObjectSpawner = FindObjectOfType<ObjectSpawner>();
#endif
}
if (m_FeatureController == null)
{
#if UNITY_2023_1_OR_NEWER
m_FeatureController = FindAnyObjectByType<ARFeatureController>();
#else
m_FeatureController = FindObjectOfType<ARFeatureController>();
#endif
}
}
void OpenModal()
{
if (m_LearnModal != null)
{
m_LearnModal.transform.localScale = Vector3.one;
}
}
void CloseModal()
{
if (m_LearnModal != null)
{
m_LearnModal.transform.localScale = Vector3.zero;
}
}
void Update()
{
if (!m_AllGoalsFinished)
{
ProcessGoals();
}
// Debug Input
#if UNITY_EDITOR
if (Keyboard.current.spaceKey.wasPressedThisFrame)
{
CompleteGoal();
}
#endif
}
void ProcessGoals()
{
if (!m_CurrentGoal.Completed)
{
switch (m_CurrentGoal.CurrentGoal)
{
case OnboardingGoals.Empty:
m_GoalPanelLazyFollow.positionFollowMode = LazyFollow.PositionFollowMode.Follow;
break;
case OnboardingGoals.FindSurfaces:
m_GoalPanelLazyFollow.positionFollowMode = LazyFollow.PositionFollowMode.Follow;
break;
case OnboardingGoals.TapSurface:
if (m_TapTooltip != null)
{
m_TapTooltip.SetActive(true);
}
m_GoalPanelLazyFollow.positionFollowMode = LazyFollow.PositionFollowMode.None;
break;
}
}
}
void CompleteGoal()
{
if (m_CurrentGoal.CurrentGoal == OnboardingGoals.TapSurface)
m_ObjectSpawner.objectSpawned -= OnObjectSpawned;
// disable tooltips before setting next goal
DisableTooltips();
m_CurrentGoal.Completed = true;
m_CurrentGoalIndex++;
if (m_OnboardingGoals.Count > 0)
{
m_CurrentGoal = m_OnboardingGoals.Dequeue();
m_StepList[m_CurrentGoalIndex - 1].stepObject.SetActive(false);
m_StepList[m_CurrentGoalIndex].stepObject.SetActive(true);
m_StepButtonTextField.text = m_StepList[m_CurrentGoalIndex].buttonText;
m_SkipButton.SetActive(m_StepList[m_CurrentGoalIndex].includeSkipButton);
}
else
{
m_AllGoalsFinished = true;
ForceEndAllGoals();
}
if (m_CurrentGoal.CurrentGoal == OnboardingGoals.FindSurfaces)
{
if (m_FadeMaterial != null)
m_FadeMaterial.FadeSkybox(true);
if (m_PassthroughToggle != null)
m_PassthroughToggle.isOn = true;
if (m_LearnButton != null)
{
m_LearnButton.SetActive(true);
}
StartCoroutine(TurnOnPlanes(true));
}
else if (m_CurrentGoal.CurrentGoal == OnboardingGoals.TapSurface)
{
if (m_LearnButton != null)
{
m_LearnButton.SetActive(false);
}
m_SurfacesTapped = 0;
m_ObjectSpawner.objectSpawned += OnObjectSpawned;
}
}
public IEnumerator TurnOnPlanes(bool visualize)
{
yield return new WaitForSeconds(1f);
if (m_FeatureController != null)
{
m_FeatureController.TogglePlaneVisualization(visualize);
m_FeatureController.TogglePlanes(true);
}
}
IEnumerator TurnOnARFeatures()
{
if (m_FeatureController == null)
yield return null;
yield return new WaitForSeconds(0.5f);
// We are checking the bounding box count here so that we disable plane visuals so there is no
// visual Z fighting. If the user has not defined any furniture in space setup or the platform
// does not support bounding boxes, we want to enable plane visuals, but disable bounding box visuals.
m_FeatureController.ToggleBoundingBoxes(true);
m_FeatureController.TogglePlanes(true);
// Quick hack for for async await race condition.
// TODO: -- Probably better to listen to trackable change events in the ARFeatureController and update accordingly there
yield return new WaitForSeconds(0.5f);
m_FeatureController.ToggleDebugInfo(false);
// If there are bounding boxes, we want to hide the planes so they don't cause z-fighting.
if (m_FeatureController.HasBoundingBoxes())
{
m_FeatureController.TogglePlaneVisualization(false);
m_FeatureController.ToggleBoundingBoxVisualization(true);
}
else
{
m_FeatureController.ToggleBoundingBoxVisualization(true);
}
m_FeatureController.occlusionManager.SetupManager();
}
void TurnOffARFeatureVisualization()
{
if (m_FeatureController == null)
return;
m_FeatureController.TogglePlaneVisualization(false);
m_FeatureController.ToggleBoundingBoxVisualization(false);
}
void DisableTooltips()
{
if (m_CurrentGoal.CurrentGoal == OnboardingGoals.TapSurface)
{
if (m_TapTooltip != null)
{
m_TapTooltip.SetActive(false);
}
}
}
public void ForceCompleteGoal()
{
CompleteGoal();
}
public void ForceEndAllGoals()
{
m_CoachingUIParent.transform.localScale = Vector3.zero;
TurnOnVideoPlayer();
if (m_VideoPlayerToggle != null)
m_VideoPlayerToggle.isOn = true;
if (m_FadeMaterial != null)
{
m_FadeMaterial.FadeSkybox(true);
if (m_PassthroughToggle != null)
m_PassthroughToggle.isOn = true;
}
if (m_LearnButton != null)
{
m_LearnButton.SetActive(false);
}
if (m_LearnModal != null)
{
m_LearnModal.transform.localScale = Vector3.zero;
}
StartCoroutine(TurnOnARFeatures());
}
public void ResetCoaching()
{
TurnOffARFeatureVisualization();
m_CoachingUIParent.transform.localScale = Vector3.one;
m_OnboardingGoals.Clear();
m_OnboardingGoals = new Queue<Goal>();
var welcomeGoal = new Goal(OnboardingGoals.Empty);
var findSurfaceGoal = new Goal(OnboardingGoals.FindSurfaces);
var tapSurfaceGoal = new Goal(OnboardingGoals.TapSurface);
var endGoal = new Goal(OnboardingGoals.Empty);
m_OnboardingGoals.Enqueue(welcomeGoal);
m_OnboardingGoals.Enqueue(findSurfaceGoal);
m_OnboardingGoals.Enqueue(tapSurfaceGoal);
m_OnboardingGoals.Enqueue(endGoal);
for (int i = 0; i < m_StepList.Count; i++)
{
if (i == 0)
{
m_StepList[i].stepObject.SetActive(true);
m_SkipButton.SetActive(m_StepList[i].includeSkipButton);
m_StepButtonTextField.text = m_StepList[i].buttonText;
}
else
{
m_StepList[i].stepObject.SetActive(false);
}
}
m_CurrentGoal = m_OnboardingGoals.Dequeue();
m_AllGoalsFinished = false;
if (m_TapTooltip != null)
m_TapTooltip.SetActive(false);
if (m_LearnButton != null)
{
m_LearnButton.SetActive(false);
}
if (m_LearnModal != null)
{
m_LearnModal.transform.localScale = Vector3.zero;
}
m_CurrentGoalIndex = 0;
}
void OnObjectSpawned(GameObject spawnedObject)
{
m_SurfacesTapped++;
if (m_CurrentGoal.CurrentGoal == OnboardingGoals.TapSurface && m_SurfacesTapped >= k_NumberOfSurfacesTappedToCompleteGoal)
{
CompleteGoal();
m_GoalPanelLazyFollow.positionFollowMode = LazyFollow.PositionFollowMode.Follow;
}
}
public void TooglePlayer(bool visibility)
{
if (visibility)
{
TurnOnVideoPlayer();
}
else
{
if (m_VideoPlayer.activeSelf)
{
m_VideoPlayer.SetActive(false);
if (m_VideoPlayerToggle.isOn)
m_VideoPlayerToggle.isOn = false;
}
}
}
void TurnOnVideoPlayer()
{
if (m_VideoPlayer.activeSelf)
return;
var follow = m_VideoPlayer.GetComponent<LazyFollow>();
if (follow != null)
follow.rotationFollowMode = LazyFollow.RotationFollowMode.None;
m_VideoPlayer.SetActive(false);
var target = Camera.main.transform;
var targetRotation = target.rotation;
var newTransform = target;
var targetEuler = targetRotation.eulerAngles;
targetRotation = Quaternion.Euler
(
0f,
targetEuler.y,
targetEuler.z
);
newTransform.rotation = targetRotation;
var targetPosition = target.position + newTransform.TransformVector(m_TargetOffset);
m_VideoPlayer.transform.position = targetPosition;
var forward = target.position - m_VideoPlayer.transform.position;
var targetPlayerRotation = forward.sqrMagnitude > float.Epsilon ? Quaternion.LookRotation(forward, Vector3.up) : Quaternion.identity;
targetPlayerRotation *= Quaternion.Euler(new Vector3(0f, 180f, 0f));
var targetPlayerEuler = targetPlayerRotation.eulerAngles;
var currentEuler = m_VideoPlayer.transform.rotation.eulerAngles;
targetPlayerRotation = Quaternion.Euler
(
currentEuler.x,
targetPlayerEuler.y,
currentEuler.z
);
m_VideoPlayer.transform.rotation = targetPlayerRotation;
m_VideoPlayer.SetActive(true);
if (follow != null)
follow.rotationFollowMode = LazyFollow.RotationFollowMode.LookAtWithWorldUp;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b628c55da6fc2bb4f88b18dff59bfb91
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using UnityEngine.XR.Hands;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// This class is a convenience wrapper to handle external start/stop
/// of a currently running XR Hand Subsystem.
/// </summary>
/// <seealso cref="XRHandSubsystem"/>
public class HandSubsystemManager : MonoBehaviour
{
static List<XRHandSubsystem> s_HandSubsystems;
XRHandSubsystem m_HandSubsystem;
void OnEnable()
{
if (m_HandSubsystem == null)
{
TryGetHandSubsystem(out m_HandSubsystem);
}
}
/// <summary>
/// This function will attempt to find a currently running hand tracking subsystem and stop it.
/// </summary>
/// <seealso cref="XRHandSubsystem"/>
public void DisableHandTracking()
{
if (m_HandSubsystem != null || TryGetHandSubsystem(out m_HandSubsystem))
{
m_HandSubsystem.Stop();
}
}
/// <summary>
/// This function will attempt to find a current hand tracking subsystem and start it up.
/// </summary>
/// <seealso cref="XRHandSubsystem"/>
public void EnableHandTracking()
{
if (m_HandSubsystem != null || TryGetHandSubsystem(out m_HandSubsystem))
{
m_HandSubsystem.Start();
}
}
// This is taken from XRInputTrackingAggregator and should be removed once the internal version
// has been made publicly available.
static bool TryGetHandSubsystem(out XRHandSubsystem handSubsystem)
{
s_HandSubsystems ??= new List<XRHandSubsystem>();
SubsystemManager.GetSubsystems(s_HandSubsystems);
if (s_HandSubsystems.Count == 0)
{
handSubsystem = default;
return false;
}
if (s_HandSubsystems.Count > 1)
{
for (var i = 0; i < s_HandSubsystems.Count; ++i)
{
handSubsystem = s_HandSubsystems[i];
if (handSubsystem.running)
return true;
}
}
handSubsystem = s_HandSubsystems[0];
return true;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7d52987a218afbc4c81f20e47e2cb295
@@ -0,0 +1,33 @@
namespace UnityEngine.XR.Templates.MR
{
// Copied from XRI Examples. Commit: c40b958
/// <summary>
/// Apply forward force to instantiated prefab
/// </summary>
public class LaunchProjectile : MonoBehaviour
{
[SerializeField]
[Tooltip("The projectile that's created")]
GameObject m_ProjectilePrefab = null;
[SerializeField]
[Tooltip("The point that the project is created")]
Transform m_StartPoint = null;
[SerializeField]
[Tooltip("The speed at which the projectile is launched")]
float m_LaunchSpeed = 1.0f;
public void Fire()
{
GameObject newObject = Instantiate(m_ProjectilePrefab, m_StartPoint.position, m_StartPoint.rotation, null);
if (newObject.TryGetComponent(out Rigidbody rigidBody))
{
Vector3 force = m_StartPoint.forward * m_LaunchSpeed;
rigidBody.AddForce(force);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b3f39acb2169f3042ab977cbdbcffeb4
@@ -0,0 +1,219 @@
#if USING_META_OCCLUSION && (UNITY_ANDROID || UNITY_EDITOR)
#define META_OCCLUSION_AVAILABLE
using UnityEngine.XR.OpenXR.Features.Meta;
using UnityEngine.XR.OpenXR.NativeTypes;
#endif
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Utility class that provides configuration for occlusion support.
/// </summary>
public class OcclusionManager : MonoBehaviour
{
[SerializeField, Tooltip("Enable the occlusion manager on start.")]
bool m_EnableManagerOnStart = false;
/// <summary>
/// Enable the occlusion manager on start.
/// </summary>
public bool enableManagerOnStart
{
get => m_EnableManagerOnStart;
set => m_EnableManagerOnStart = value;
}
[SerializeField, Tooltip("The UI Toggle GameObject that will be disabled if occlusion is not supported.")]
GameObject m_UIToggleObject;
/// <summary>
/// The UI Toggle GameObject that will be disabled if occlusion is not supported.
/// </summary>
public GameObject uiToggleObject
{
get => m_UIToggleObject;
set => m_UIToggleObject = value;
}
[SerializeField, Tooltip("The GameObject containing the Quest-specific settings.")]
GameObject m_QuestSettings;
/// <summary>
/// The GameObject containing the Quest-specific settings.
/// </summary>
public GameObject questSettings
{
get => m_QuestSettings;
set => m_QuestSettings = value;
}
[SerializeField, Tooltip("The GameObject containing the Android XR-specific settings.")]
GameObject m_AndroidXRSettings;
/// <summary>
/// The GameObject containing the Android XR-specific settings.
/// </summary>
public GameObject AndroidXRSettings
{
get => m_AndroidXRSettings;
set => m_AndroidXRSettings = value;
}
AROcclusionManager m_AROcclusionManager;
ARShaderOcclusion m_ARShaderOcclusion;
void Start()
{
if (TrySetupOcclusion() && m_EnableManagerOnStart)
{
SetupManager();
}
}
bool TrySetupOcclusion()
{
// Check if the platform supports occlusion
if (LoaderUtility
.GetActiveLoader()?
.GetLoadedSubsystem<XROcclusionSubsystem>() != null)
{
// XROcclusionSubsystem was loaded. The platform supports occlusion.
m_AROcclusionManager = FindAnyObjectByType<AROcclusionManager>();
if (m_AROcclusionManager == null)
{
Debug.LogWarning("No AROcclusionManager found, yet Use Occlusion is enabled. Disabling Object.", this);
m_UIToggleObject.SetActive(false);
return false;
}
else
{
if (!m_AROcclusionManager.gameObject.TryGetComponent(out m_ARShaderOcclusion))
{
Debug.LogWarning($"No {nameof(ARShaderOcclusion)} component found. Adding one manually now.", this);
m_ARShaderOcclusion = m_AROcclusionManager.gameObject.AddComponent<ARShaderOcclusion>();
}
}
SetupDeviceSpecificSettings();
return true;
}
// If the platform does not support occlusion, disable the object
else
{
Debug.LogWarning("Occlusion is not supported on this platform. Disabling Object.", this);
m_UIToggleObject.SetActive(false);
return false;
}
}
/// <summary>
/// Sets up device-specific settings for the occlusion manager.
/// This is called in the Start method to ensure that the correct settings are applied based on the current XR platform.
/// This function is important for ensuring that the occlusion manager works correctly on different devices based on the platform specifics.
/// </summary>
void SetupDeviceSpecificSettings()
{
Debug.Log($"Current XR Platform: {XRPlatformUnderstanding.CurrentPlatform}");
switch (XRPlatformUnderstanding.CurrentPlatform)
{
case XRPlatformType.Quest:
m_QuestSettings.SetActive(true);
m_AndroidXRSettings.SetActive(false);
break;
case XRPlatformType.AndroidXR:
m_QuestSettings.SetActive(false);
m_AndroidXRSettings.SetActive(true);
m_AROcclusionManager.environmentDepthTemporalSmoothingRequested = true;
SetShaderMode(AROcclusionShaderMode.HardOcclusion);
break;
case XRPlatformType.Other:
m_QuestSettings.SetActive(false);
m_AndroidXRSettings.SetActive(false);
break;
}
}
/// <summary>
/// Sets up the occlusion manager and enables it if necessary.
/// This is called from the goal manager to ensure that the occlusion manager is set up correctly and enabled when needed.
/// </summary>
public void SetupManager()
{
if (m_AROcclusionManager != null && !m_AROcclusionManager.enabled)
m_AROcclusionManager.enabled = true;
if (m_ARShaderOcclusion != null && !m_ARShaderOcclusion.enabled)
m_ARShaderOcclusion.enabled = true;
}
/// <summary>
/// Sets the occlusion manager to be enabled or disabled.
/// This is called from the UI toggle to enable or disable the occlusion manager based on user input.
/// </summary>
/// <param name="isOn">Enables or disables <see cref="AROcclusionManager"/> based on value.</param>
public void SetOcclusionIsOn(bool isOn)
{
if (m_AROcclusionManager != null)
{
m_AROcclusionManager.enabled = isOn;
}
}
/// <summary>
/// Sets the shader mode for the occlusion manager.
/// This is called from the UI dropdown to set the shader mode based on user input.
/// </summary>
/// <param name="shaderMode">The shader mode to set for the occlusion manager.</param>
/// <remarks>
/// The shader mode is an integer value that corresponds to the <see cref="AROcclusionShaderMode"/> enum.
/// The value is offset by 1 because the first value in the enum is None, which is not used in this context.
/// </remarks>
public void SetShaderMode(int shaderMode)
{
// Offset by 1 because we don't use None
SetShaderMode((AROcclusionShaderMode)(++shaderMode));
}
void SetShaderMode(AROcclusionShaderMode mode)
{
if (m_ARShaderOcclusion != null)
{
m_ARShaderOcclusion.occlusionShaderMode = mode;
Debug.Log("Setting shader mode to " + mode);
}
}
/// <summary>
/// Sets the state of temporal smoothing for the <see cref="OcclusionManager"/>.
/// </summary>
/// <param name="isEnabled">If true, temporal smoothing will be applied to the environment depth image.</param>
public void SetTemporalSmoothingEnabled(bool isEnabled)
{
if (m_AROcclusionManager != null)
{
m_AROcclusionManager.environmentDepthTemporalSmoothingRequested = isEnabled;
}
}
/// <summary>
/// Sets the hand removal feature for the Meta OpenXR occlusion subsystem.
/// </summary>
/// <param name="isEnabled">Sets the Hand Removal Feature value.</param>
public void SetHandHandRemovalEnabled(bool isEnabled)
{
#if META_OCCLUSION_AVAILABLE
var subsystem = m_AROcclusionManager.subsystem as MetaOpenXROcclusionSubsystem;
var result = subsystem.TrySetHandRemovalEnabled(isEnabled);
if (result.IsError())
{
// Handle error
Debug.LogWarning("Error setting hand removal enabled: " + result.ToString());
}
#endif
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de26ff91ad797714fb7efe631f55592b
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
#if UNITY_ANDROID
using UnityEngine.Android;
#endif // UNITY_ANDROID
using UnityEngine.Events;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Utility class to help define and manage Android device permissions and specify corresponding permission callbacks via <see cref="UnityEvent"/>.
/// </summary>
[DefaultExecutionOrder(-9999)]
public class PermissionsManager : MonoBehaviour
{
const string k_DefaultPermissionId = "com.oculus.permission.USE_SCENE";
[SerializeField, Tooltip("Enables or disables the processing of permissions on Start. If disabled, permissions will not be processed until the ProcessPermissions method is called.")]
private bool m_ProcessPermissionsOnStart = true;
[SerializeField, Tooltip("The system permissions to request when this component Starts.")]
List<PermissionRequestGroup> m_PermissionGroups = new List<PermissionRequestGroup>();
/// <summary>
/// Current platform permission group to process. This is determined during the <see cref="Awake"/> method using based on <see cref="XRPlatformUnderstanding"/>
/// </summary>
PermissionRequestGroup m_CurrentPlatformPermissionGroup = new PermissionRequestGroup();
/// <summary>
/// A group of permissions to request based on a specific platform.
/// </summary>
[Serializable]
class PermissionRequestGroup
{
[Tooltip("The platform type for which these permissions is intended for.")]
public XRPlatformType platformType;
public List<PermissionRequest> permissions;
}
/// <summary>
/// A permission request to be made to the Android operating system.
/// </summary>
[Serializable]
class PermissionRequest
{
[Tooltip("The Android system permission to request when this component starts.")]
public string permissionId = k_DefaultPermissionId;
[Tooltip("Whether to request permission from the operating system.")]
public bool enabled = true;
[HideInInspector]
public bool requested = false;
[HideInInspector]
public bool responseReceived = false;
[HideInInspector]
public bool granted = false;
public UnityEvent<string> onPermissionGranted;
public UnityEvent<string> onPermissionDenied;
}
void Awake()
{
if (m_ProcessPermissionsOnStart)
ProcessPermissions();
}
/// <summary>
/// Process the permissions defined in the <see cref="m_PermissionGroups"/> list.
/// </summary>
public void ProcessPermissions()
{
var permissions = new List<string>();
#if UNITY_ANDROID
// Grab the current platform permission group based on the current platform in use.
m_CurrentPlatformPermissionGroup = m_PermissionGroups.Find(g => g.platformType == XRPlatformUnderstanding.CurrentPlatform);
if (m_CurrentPlatformPermissionGroup == null)
{
Debug.LogWarning($"No permission group found for platform {XRPlatformUnderstanding.CurrentPlatform}. Disabling {nameof(PermissionsManager)} component.", this);
enabled = false;
return;
}
// Loop through the current platform's permissions and add them to the
// list of permissions to request if they are enabled and not already requested.
for(int i = 0; i < m_CurrentPlatformPermissionGroup.permissions.Count; i++)
{
if (!m_CurrentPlatformPermissionGroup.permissions[i].enabled)
continue;
// If permission is not granted and not requested, add it to the list of permissions to request
if (!Permission.HasUserAuthorizedPermission(m_CurrentPlatformPermissionGroup.permissions[i].permissionId) && !m_CurrentPlatformPermissionGroup.permissions[i].requested)
{
permissions.Add(m_CurrentPlatformPermissionGroup.permissions[i].permissionId);
m_CurrentPlatformPermissionGroup.permissions[i].requested = true;
}
else
{
Debug.Log($"User has permission for: {m_CurrentPlatformPermissionGroup.permissions[i].permissionId}", this);
}
}
// Process permissions that were not already granted
if (permissions.Count > 0)
{
var callbacks = new PermissionCallbacks();
callbacks.PermissionDenied += OnPermissionDenied;
callbacks.PermissionGranted += OnPermissionGranted;
Permission.RequestUserPermissions(permissions.ToArray(), callbacks);
}
#endif // UNITY_ANDROID
}
void OnPermissionGranted(string permissionStr)
{
var permission = m_CurrentPlatformPermissionGroup.permissions.Find(p => p.permissionId == permissionStr);
if (permission == null)
{
Debug.LogWarning($"Permission granted callback received for an unexpected permission request, permission ID {permissionStr}", this);
return;
}
// Enable permission
permission.granted = true;
permission.responseReceived = true;
Debug.Log($"User granted permission for: {permissionStr}", this);
permission.onPermissionGranted.Invoke(permissionStr);
}
void OnPermissionDenied(string permissionStr)
{
// Find the permission with LINQ
var permission = m_CurrentPlatformPermissionGroup.permissions.Find(p => p.permissionId == permissionStr);
if (permission == null)
{
Debug.LogWarning($"Permission denied callback received for an unexpected permission request, permission ID {permissionStr}", this);
return;
}
// Disable permission
permission.granted = false;
permission.responseReceived = true;
Debug.LogWarning($"User denied permission for: {permissionStr}", this);
permission.onPermissionDenied.Invoke(permissionStr);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a210aa19b985bf849ba74812660e6f87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using UnityEngine.XR.Interaction.Toolkit.Interactors;
namespace UnityEngine.XR.Templates.MR
{
public class PinchVizController : MonoBehaviour
{
[SerializeField]
SkinnedMeshRenderer m_Pointer;
XRRayInteractor m_Interactor;
// Start is called before the first frame update
void Start()
{
m_Interactor = this.GetComponent<XRRayInteractor>();
}
// Update is called once per frame
void Update()
{
var inputValue = Mathf.Max(m_Interactor.selectInput.ReadValue(), m_Interactor.uiPressInput.ReadValue());
m_Pointer.SetBlendShapeWeight(0, inputValue * 100f);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7989cfc5520236c418f8e58b8a0e19f0
@@ -0,0 +1,53 @@
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
namespace UnityEngine.XR.Templates.MR
{
// Copied from AR Foundation Samples project. Commit: 8970b5a
/// <summary>
/// Manages the plane material color for each recognized plane based on
/// the <see cref="UnityEngine.XR.ARSubsystems.TrackingState"/> enumeration defined in ARSubsystems.
/// </summary>
[RequireComponent(typeof(ARPlane))]
[RequireComponent(typeof(MeshRenderer))]
public class PlaneTrackingModeVisualizer : MonoBehaviour
{
ARPlane m_ARPlane;
MeshRenderer m_PlaneMeshRenderer;
Color m_OriginalColor;
void Awake()
{
m_ARPlane = GetComponent<ARPlane>();
m_PlaneMeshRenderer = GetComponent<MeshRenderer>();
m_OriginalColor = m_PlaneMeshRenderer.material.color;
}
void Update()
{
UpdatePlaneColor();
}
void UpdatePlaneColor()
{
Color planeMatColor = Color.cyan;
switch (m_ARPlane.trackingState)
{
case TrackingState.None:
planeMatColor = Color.grey;
break;
case TrackingState.Limited:
planeMatColor = Color.red;
break;
case TrackingState.Tracking:
planeMatColor = m_OriginalColor;
break;
}
planeMatColor.a = m_OriginalColor.a;
m_PlaneMeshRenderer.material.color = planeMatColor;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7b0c26ae29dd55c42bfc4e2f3d1e17fa
@@ -0,0 +1,83 @@
using UnityEngine.XR.ARSubsystems;
using UnityEngine.XR.ARFoundation;
namespace UnityEngine.XR.Templates.MR
{
public enum XRPlatformType
{
Quest,
AndroidXR,
Other,
None
}
public class XRPlatformUnderstanding
{
// Copied from ARF Samples repo. Commit: 4a7e7b6 - Class: MenuLoader.cs
/// <summary>
/// The current platform based on the active XRSessionSubsystem.
/// </summary>
public static XRPlatformType CurrentPlatform
{
get
{
if (!k_Initialized)
{
k_CurrentPlatform = GetCurrentXRPlatform();
k_Initialized = true;
}
return k_CurrentPlatform;
}
}
static XRPlatformType k_CurrentPlatform = XRPlatformType.None;
static bool k_Initialized = false;
/// <summary>
/// Returns the current platform based on the active XRSessionSubsystem.
/// </summary>
/// <returns>The current platform based on the active XRSessionSubsystem.</returns>
static XRPlatformType GetCurrentXRPlatform()
{
// If we have already initialized, just return the current platform
if (k_Initialized)
return k_CurrentPlatform;
var loader = LoaderUtility.GetActiveLoader();
var sessionSubsystem = loader != null ? loader.GetLoadedSubsystem<XRSessionSubsystem>() : null;
if (sessionSubsystem == null)
{
Debug.Log("No active XRSessionSubsystem found. Defaulting to XRPlatformType.None.");
k_Initialized = true;
k_CurrentPlatform = XRPlatformType.None;
return k_CurrentPlatform;
}
// We switch on Session Descriptor id because we can't guarantee with current preprocessor directives whether
// a provider package (and its types) will be present. For example, UNITY_ANDROID could signal that either
// ARCore or OpenXR loader is present. Because we don't know for sure, we are unable to switch on the loader
// type without introducing a build-time error in case that package was stripped.
switch (sessionSubsystem.subsystemDescriptor.id)
{
case "Meta-Session":
Debug.Log("Meta-Session detected.");
k_CurrentPlatform = XRPlatformType.Quest;
break;
case "Android-Session":
Debug.Log("Android-Session detected.");
k_CurrentPlatform = XRPlatformType.AndroidXR;
break;
default:
// Default case includes other third-party providers
Debug.Log($"Unknown platform detected, setting platform to Other. Subsystem id: {sessionSubsystem.subsystemDescriptor.id}");
k_CurrentPlatform = XRPlatformType.Other;
break;
}
return k_CurrentPlatform;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 037903c13b5769e419bf6d16f11e2a62
@@ -0,0 +1,39 @@
using System.Collections;
namespace UnityEngine.XR.Templates.MR
{
public class PositionAtStart : MonoBehaviour
{
[SerializeField]
[Tooltip("The starting target.")]
Transform m_Target;
[SerializeField]
[Tooltip("Adjusts the follow point from the target by this amount.")]
Vector3 m_TargetOffset = Vector3.forward;
bool m_IgnoreX = true;
Vector3 m_TargetPosition;
private const float k_StartDelay = 2.0f;
// Start is called before the first frame update
IEnumerator Start()
{
yield return new WaitForSeconds(k_StartDelay);
var targetRotation = m_Target.rotation;
var newTransform = m_Target;
var targetEuler = targetRotation.eulerAngles;
targetRotation = Quaternion.Euler
(
m_IgnoreX ? 0f : targetEuler.x,
targetEuler.y,
targetEuler.z
);
newTransform.rotation = targetRotation;
m_TargetPosition = m_Target.position + newTransform.TransformVector(m_TargetOffset);
transform.position = m_TargetPosition;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f50756c02c4d2e143aa345b9f9f44834
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine.XR.ARSubsystems;
namespace UnityEngine.XR.Templates.MR
{
// Copied from ARF Samples repo. Commit: 4a7e7b6
public class SaveAndLoadAnchorDataToFile
{
readonly string m_FilePath = Path.Combine(Application.persistentDataPath, "SavedAnchorIds.json");
public bool initialized => m_Initialized;
bool m_Initialized;
public Awaitable initializeAwaitable => m_InitializeAwaitable;
Awaitable m_InitializeAwaitable;
public Dictionary<SerializableGuid, int> SavedAnchorsData => m_SavedAnchorsData;
Dictionary<SerializableGuid, int> m_SavedAnchorsData = new();
public SaveAndLoadAnchorDataToFile()
{
m_InitializeAwaitable = PopulateSavedAnchorIdsFromFile();
}
/// <summary>
/// Saves a `SerializableGuid` to a file asynchronously, appending to the list of ids already saved.
/// If no file exists or the file is unreadable, a new file is created.
/// </summary>
/// <param name="savedAnchorId">The `SerializableGuid` to save.</param>
public async Awaitable SaveAnchorIdAsync(SerializableGuid savedAnchorId, int prefabId)
{
try
{
if (!m_Initialized)
await m_InitializeAwaitable;
if (!m_SavedAnchorsData.TryAdd(savedAnchorId, prefabId))
m_SavedAnchorsData[savedAnchorId] = prefabId;
await WriteSavedAnchorIdsToFile();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
public async Awaitable EraseAnchorIdAsync(SerializableGuid savedAnchorId)
{
try
{
if (!m_Initialized)
await m_InitializeAwaitable;
m_SavedAnchorsData.Remove(savedAnchorId);
await WriteSavedAnchorIdsToFile();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
/// <summary>
/// Returns the set of `SerializableGuid`s from the save file.
/// </summary>
/// <returns>The set of `SerializableGuid`s that were saved to the file.
/// If no file exists or the file is unreadable, an an empty set is returned.</returns>
public async Awaitable<Dictionary<SerializableGuid, int>> LoadSavedAnchorsDataAsync()
{
if (!m_Initialized)
await m_InitializeAwaitable;
return m_SavedAnchorsData;
}
async Awaitable PopulateSavedAnchorIdsFromFile()
{
try
{
m_SavedAnchorsData.Clear();
if (!File.Exists(m_FilePath))
return;
using var streamReader = File.OpenText(m_FilePath);
using var jsonTextReader = new JsonTextReader(streamReader);
var kvp = (JObject)await JToken.ReadFromAsync(jsonTextReader);
foreach (var (idAsString, prefabId) in kvp)
{
var tokens = idAsString.Split("-");
var low = Convert.ToUInt64(tokens[0], 16);
var high = Convert.ToUInt64(tokens[1], 16);
var serializableGuid = new SerializableGuid(low, high);
m_SavedAnchorsData.Add(serializableGuid, (int)prefabId);
}
}
catch (Exception e)
{
Debug.LogException(e);
}
finally
{
m_Initialized = true;
}
}
async Awaitable WriteSavedAnchorIdsToFile()
{
var jsonString = JsonConvert.SerializeObject(m_SavedAnchorsData, Formatting.Indented);
await File.WriteAllTextAsync(m_FilePath, jsonString);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3c77086553d2fae4da4e63ccc83ef475
@@ -0,0 +1,433 @@
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets;
namespace UnityEngine.XR.Templates.MR
{
[RequireComponent(typeof(ObjectSpawner))]
public class SpawnedObjectsManager : MonoBehaviour
{
[Tooltip("Spawn objects with a persistent anchor.")]
[SerializeField]
bool m_SpawnAsPersistentAnchor = true;
/// <summary>
/// Spawn objects with a persistent anchor.
/// </summary>
public bool spawnAsPersistentAnchor
{
get => m_SpawnAsPersistentAnchor;
set => m_SpawnAsPersistentAnchor = value;
}
[SerializeField]
[Tooltip("Load saved anchors on start.")]
bool m_LoadSavedAnchorsOnStart = false;
/// <summary>
/// Load saved anchors on start.
/// </summary>
public bool loadSavedAnchorsOnStart
{
get => m_LoadSavedAnchorsOnStart;
set => m_LoadSavedAnchorsOnStart = value;
}
/// <summary>
/// UI drop down representing object spawn selection for the scene.
/// </summary>
[SerializeField]
TMP_Dropdown m_ObjectSelectorDropdown;
/// <summary>
/// UGUI Button clicked to destroy all spawned objects by this spawner.
/// </summary>
[SerializeField]
Button m_DestroyObjectsButton;
[SerializeField]
TMP_Text m_AnchorText;
[SerializeField]
ARAnchorManager m_AnchorManager;
ObjectSpawner m_Spawner;
readonly List<SpawnedObjectHelper> m_SpawnedObjects = new();
SaveAndLoadAnchorDataToFile m_SaveAndLoadAnchorIdsToFile;
async void Start()
{
m_SaveAndLoadAnchorIdsToFile = new SaveAndLoadAnchorDataToFile();
// Wait for the SaveAndLoadAnchorIdsToFile to initialize before continuing.
if (!m_SaveAndLoadAnchorIdsToFile.initialized)
await m_SaveAndLoadAnchorIdsToFile.initializeAwaitable;
if (m_LoadSavedAnchorsOnStart)
{
LoadAnchors();
}
else
{
m_AnchorText.text = "<b><u><align=center>- Currently Saved Objects -</b></u></align>\n\n";
if (m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.Count == 0)
{
m_AnchorText.text += "<align=center>- No Anchors Saved -</align>";
}
else
{
foreach (var kvp in m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData)
{
m_AnchorText.text += $"GUID: [{kvp.Key}] Object: {SpawnedObjectName(kvp.Value)}\n\n";
}
}
}
}
void OnEnable()
{
if (m_Spawner == null)
m_Spawner = GetComponent<ObjectSpawner>();
if (m_SpawnAsPersistentAnchor)
{
m_Spawner.onlySpawnInView = false;
m_Spawner.spawnAsChildren = false;
}
if (m_AnchorManager == null)
m_AnchorManager = FindAnyObjectByType<ARAnchorManager>();
if (m_ObjectSelectorDropdown != null)
{
OnObjectSelectorDropdownValueChanged(m_ObjectSelectorDropdown.value);
m_ObjectSelectorDropdown.onValueChanged.AddListener(OnObjectSelectorDropdownValueChanged);
}
if (m_DestroyObjectsButton != null)
m_DestroyObjectsButton.onClick.AddListener(OnDestroyObjectsButtonClicked);
m_Spawner.objectSpawned += ObjectSpawned;
}
void OnDisable()
{
if (m_ObjectSelectorDropdown != null)
m_ObjectSelectorDropdown.onValueChanged.RemoveListener(OnObjectSelectorDropdownValueChanged);
if (m_DestroyObjectsButton != null)
m_DestroyObjectsButton.onClick.RemoveListener(OnDestroyObjectsButtonClicked);
m_Spawner.objectSpawned -= ObjectSpawned;
}
/// <summary>
/// Called from the UI to destroy all spawned objects.
/// It will remove non persistent anchors and delete the objects.
/// </summary>
public void OnDestroyObjectsButtonClicked()
{
while (m_SpawnedObjects.Count > 0)
{
SpawnedObjectHelper spawnedObject = m_SpawnedObjects[0];
m_SpawnedObjects.RemoveAt(0);
if (spawnedObject.attachedAnchor != null)
{
if (!m_AnchorManager.TryRemoveAnchor(spawnedObject.attachedAnchor))
{
Debug.LogWarning("Failed to remove anchor, manually destroying anchor object.", this);
Destroy(spawnedObject.attachedAnchor);
return;
}
}
Destroy(spawnedObject.gameObject);
}
}
/// <summary>
/// Deletes all anchors and clears the saved anchor data list.
/// This method is called when the Delete Anchors button is clicked from the UI.
/// </summary>
public async void DeleteAnchors()
{
m_AnchorText.text = "<b><u><align=center>- Deleted Persistent Anchors -</b></u></align>\n";
await EraseAnchorsAsync();
if (m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.Count != 0)
{
m_AnchorText.text += "\n\n<align=center>Failed to delete all anchors.</align>\n\n";
foreach (var kvp in m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData)
{
m_AnchorText.text += $"GUID failed to remove: [{kvp.Key}]\n\n";
}
}
else
{
m_AnchorText.text += "\n\n<align=center>All anchors deleted.</align>";
}
}
/// <summary>
/// Loads all anchors from the saved anchor data list.
/// This method is called when the Load Anchors button is clicked from the UI.
/// </summary>
public async void LoadAnchors()
{
m_AnchorText.text = "<b><u><align=center>- Loaded Persistent Anchors -</b></u></align>\n";
await LoadAnchorsAsync();
}
/// <summary>
/// Saves all anchors from the spawned objects list.
/// This method is called when the Save Anchors button is clicked from the UI.
/// </summary>
public async void SaveAnchors()
{
if (!m_AnchorManager.descriptor.supportsSaveAnchor)
{
Debug.LogWarning("Save anchor is not supported on this device.", this);
m_AnchorText.text = "Save anchor is not supported on this device.";
return;
}
// Clear existing anchors
await EraseAnchorsAsync();
// Save the current spawned anchors
m_AnchorText.text = "<b><u><align=center>- Saved Persistent Anchors -</b></u></align>\n";
await SaveAchorsAsync();
}
// This method is called when the Object Selector Dropdown value changes.
void OnObjectSelectorDropdownValueChanged(int value)
{
if (m_Spawner == null)
return;
if (value == 0)
{
m_Spawner.RandomizeSpawnOption();
return;
}
m_Spawner.spawnOptionIndex = value - 1;
}
// This method is called when an object is spawned by the ObjectSpawner.
void ObjectSpawned(GameObject spawnedObject)
{
SpawnedObjectHelper spawnedObjectHelper = new SpawnedObjectHelper
{
gameObject = spawnedObject,
spawnObjectIdx = m_Spawner.spawnOptionIndex,
isPersistent = m_SpawnAsPersistentAnchor,
spawnWithAnchor = true
};
m_SpawnedObjects.Add(spawnedObjectHelper);
CreateAndParentAnchorForObject();
}
// This method Adds the spawned object to an anchor.
async void CreateAndParentAnchorForObject()
{
SpawnedObjectHelper spawnedObjectHelper = m_SpawnedObjects[^1];
var result = await m_AnchorManager.TryAddAnchorAsync(new Pose(spawnedObjectHelper.gameObject.transform.position, spawnedObjectHelper.gameObject.transform.rotation));
if (result.status.IsSuccess())
{
var anchor = result.value;
spawnedObjectHelper.gameObject.transform.SetParent(anchor.transform);
spawnedObjectHelper.attachedAnchor = anchor;
m_SpawnedObjects[^1] = spawnedObjectHelper;
}
}
// This method deletes all anchors from the saved anchor data list.
async Awaitable EraseAnchorsAsync()
{
try
{
var deletionList = m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.Keys.ToArray();
foreach (var guid in deletionList)
{
await EraseAnchorAsync(guid);
}
}
catch (System.Exception e)
{
Debug.LogError($"Error deleting anchors: {e.Message}", this);
}
}
// This method erases the anchor based on the GUID.
async Awaitable EraseAnchorAsync(SerializableGuid guid)
{
var result = await m_AnchorManager.TryEraseAnchorAsync(guid);
if (!result.IsError() && result.statusCode != XRResultStatus.StatusCode.UnqualifiedSuccess)
{
// handle error
Debug.LogError($"Error erasing GUID: [{guid}].\n\nStatus Code:{result.statusCode}\n\nError: {result}", this);
return;
}
// The anchor was successfully erased.
await m_SaveAndLoadAnchorIdsToFile.EraseAnchorIdAsync(guid);
}
// Loads All anchors from the saved anchor data list.
async Awaitable LoadAnchorsAsync(bool updateText = true)
{
foreach (var kvp in m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData)
{
await TryLoadAnchorAsync(kvp.Key);
}
if (m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.Count == 0 && updateText)
{
m_AnchorText.text += "\n\n<align=center>- No Anchors To Load -</align>";
}
}
// Lodas the individual anchor based on the GUID.
async Awaitable TryLoadAnchorAsync(SerializableGuid guid)
{
// Don't respawn objects that are already loaded
foreach (var spawnedObjectHelper in m_SpawnedObjects)
{
if (spawnedObjectHelper.persistentGuid == guid && spawnedObjectHelper.gameObject != null)
{
m_AnchorText.text += $"Object {SpawnedObjectName(spawnedObjectHelper.spawnObjectIdx)} is already loaded.\nGUID {guid}.\n\n";
return;
}
}
var result = await m_AnchorManager.TryLoadAnchorAsync(guid);
if (result.status.IsError())
{
// handle error
Debug.Log($"Error Loading Anchor - Status Code:{result.status.statusCode}\n\nError: {result.status} GUID: [{guid}].\n\n", this);
m_AnchorText.text += $"Error loading GUID: [{guid}].\n{result.status.statusCode}\n";
return;
}
var newAnchor = result.value;
if (m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.ContainsKey(guid))
{
spawnAsPersistentAnchor = true;
int spawnId = m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData[guid];
CreateObjectForLoadedAnchor(newAnchor, guid, spawnId);
m_AnchorText.text += $"Loaded Object{SpawnedObjectName(spawnId)}\nGUID {guid}.\n\n";
}
else
{
m_AnchorText.text += $"Persistent Dictionary did not contain GUID: [{guid}].\n\n";
m_AnchorManager.TryRemoveAnchor(newAnchor);
}
}
// This method creates a new object for the loaded anchor reference.
void CreateObjectForLoadedAnchor(ARAnchor newAnchor, SerializableGuid guid, int spawnId)
{
int nonZeroIndex = spawnId < 0 ? Random.Range(0, m_Spawner.objectPrefabs.Count) : spawnId;
GameObject respawnedObject = Instantiate(m_Spawner.objectPrefabs[nonZeroIndex], newAnchor.transform);
respawnedObject.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
// Create new SpawnedObjectHelper object to store the new anchor reference.
SpawnedObjectHelper spawnedObjectHelper = new SpawnedObjectHelper
{
gameObject = respawnedObject,
attachedAnchor = newAnchor,
spawnObjectIdx = spawnId,
isPersistent = true,
spawnWithAnchor = true,
persistentGuid = guid
};
bool anchorAlreadyExists = false;
// Find and replace the SpawnedObjectHelper in the list with the new reference.
for (int i = 0; i < m_SpawnedObjects.Count; i++)
{
if (m_SpawnedObjects[i].persistentGuid == guid)
{
m_SpawnedObjects[i] = spawnedObjectHelper;
anchorAlreadyExists = true;
break;
}
}
if (!anchorAlreadyExists)
m_SpawnedObjects.Add(spawnedObjectHelper);
}
// Saves all anchors from the spawned objects list.
async Awaitable SaveAchorsAsync(bool updateText = true)
{
for (int i = 0; i < m_SpawnedObjects.Count; i++)
{
if (m_SpawnedObjects[i].isPersistent && m_SpawnedObjects[i].spawnWithAnchor)
await TrySaveAnchorAsync(i);
}
if (m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.Count == 0 && updateText)
{
m_AnchorText.text += "\n\n<align=center>- No Anchors To Save -</align>";
}
}
// Loop through the spawned objects and save the anchors.
async Awaitable TrySaveAnchorAsync(int idx)
{
if (m_SaveAndLoadAnchorIdsToFile.SavedAnchorsData.ContainsKey(m_SpawnedObjects[idx].persistentGuid))
{
m_AnchorText.text += $"Object {SpawnedObjectName(m_SpawnedObjects[idx].spawnObjectIdx)} already saved.\nGUID: [{m_SpawnedObjects[idx].persistentGuid}].\n\n";
return;
}
var result = await m_AnchorManager.TrySaveAnchorAsync(m_SpawnedObjects[idx].attachedAnchor);
if (result.status.IsError())
{
// handle error
Debug.Log($"Error Saving Object: [{SpawnedObjectName(m_SpawnedObjects[idx].spawnObjectIdx)}].\n\nStatus Code:{result.status.statusCode}\n\nError: {result.status}", this);
m_AnchorText.text += $"Failed to save {SpawnedObjectName(m_SpawnedObjects[idx].spawnObjectIdx)}\n\n";
return;
}
// Save this value, then use it as an input parameter
SerializableGuid guid = result.value;
m_SaveAndLoadAnchorIdsToFile ??= new SaveAndLoadAnchorDataToFile();
await m_SaveAndLoadAnchorIdsToFile.SaveAnchorIdAsync(guid, m_SpawnedObjects[idx].spawnObjectIdx);
var spawnedObjectHelper = m_SpawnedObjects[idx];
spawnedObjectHelper.persistentGuid = guid;
m_SpawnedObjects[idx] = spawnedObjectHelper;
m_AnchorText.text += $"Saved Object {SpawnedObjectName(m_SpawnedObjects[idx].spawnObjectIdx)}\nGUID: [{guid}].\n\n";
}
string SpawnedObjectName(int id)
{
return id < 0 ? "Random" : m_Spawner.objectPrefabs[id].name;
}
}
/// <summary>
/// Helper struct to store the spawned object, anchor, and spawn object index, persistence, and persistent GUID.
/// </summary>
struct SpawnedObjectHelper
{
public GameObject gameObject;
public ARAnchor attachedAnchor;
public int spawnObjectIdx;
public bool isPersistent;
public bool spawnWithAnchor;
public SerializableGuid persistentGuid;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aa5e7045ec6a31048afad1c854a0d6a9
@@ -0,0 +1,124 @@
using System.Collections.Generic;
namespace UnityEngine.XR.Templates.MR
{
// Copied from XRI Examples. Commit: c40b958
/// <summary>
/// Makes this object face a target smoothly and along specific axes
/// </summary>
public class TurnToFace : MonoBehaviour
{
#pragma warning disable 649
[SerializeField, Tooltip("Target to face towards. If not set, this will default to the main camera")]
Transform m_FaceTarget;
[SerializeField, Tooltip("Speed to turn")]
float m_TurnToFaceSpeed = 5f;
[SerializeField, Tooltip("Local rotation offset")]
Vector3 m_RotationOffset = Vector3.zero;
[SerializeField, Tooltip("If enabled, ignore the x axis when rotating")]
bool m_IgnoreX;
[SerializeField, Tooltip("If enabled, ignore the y axis when rotating")]
bool m_IgnoreY;
[SerializeField, Tooltip("If enabled, ignore the z axis when rotating")]
bool m_IgnoreZ;
#pragma warning restore 649
static readonly HashSet<TurnToFace> k_EnabledInstances = new HashSet<TurnToFace>();
void OnEnable()
{
transform.rotation = GetTargetRotation(transform.position);
k_EnabledInstances.Add(this);
}
void OnDisable()
{
k_EnabledInstances.Remove(this);
}
void Update()
{
LookAtTarget();
}
/// <summary>
/// Causes all turn to face layout components to snap to the correct rotation immediately.
/// </summary>
public static void SnapAll()
{
foreach (var turnToFace in k_EnabledInstances)
{
var instanceTransform = turnToFace.transform;
instanceTransform.rotation = turnToFace.GetTargetRotation(instanceTransform.position);
}
}
/// <summary>
/// Get the current easing lerp amount that will be used for rotation.
/// </summary>
/// <returns>The easing value based on current delta time.</returns>
public float GetCurrentRotationEase()
{
return 1f - Mathf.Exp(-m_TurnToFaceSpeed * Time.unscaledDeltaTime);
}
/// <summary>
/// Ease the rotation towards the target rotation
/// </summary>
public void LookAtTarget()
{
var thisTransform = transform;
var targetRotation = GetTargetRotation(thisTransform.position);
var ease = GetCurrentRotationEase();
thisTransform.rotation = Quaternion.Slerp(thisTransform.rotation, targetRotation, ease);
}
void SetDefaultTargetIfNeeded()
{
// Default to main camera
if (m_FaceTarget == null)
{
var mainCamera = Camera.main;
m_FaceTarget = mainCamera == null ? null : mainCamera.transform;
}
}
/// <summary>
/// Get the target rotation to use for a given position
/// </summary>
/// <param name="position">The position to compare to the face target when calculating the look direction.</param>
/// <returns>The rotation that faces towards the target from the given position.</returns>
public Quaternion GetTargetRotation(Vector3 position)
{
SetDefaultTargetIfNeeded();
if (m_FaceTarget == null)
return Quaternion.identity;
var facePosition = m_FaceTarget.position;
var forward = facePosition - position;
var targetRotation = forward.sqrMagnitude > float.Epsilon ? Quaternion.LookRotation(forward, Vector3.up) : Quaternion.identity;
targetRotation *= Quaternion.Euler(m_RotationOffset);
if (m_IgnoreX || m_IgnoreY || m_IgnoreZ)
{
var targetEuler = targetRotation.eulerAngles;
var currentEuler = transform.rotation.eulerAngles;
targetRotation = Quaternion.Euler
(
m_IgnoreX ? currentEuler.x : targetEuler.x,
m_IgnoreY ? currentEuler.y : targetEuler.y,
m_IgnoreZ ? currentEuler.z : targetEuler.z
);
}
return targetRotation;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 06d2696e115df2c4591657f92bc483e3
@@ -0,0 +1,57 @@
using UnityEngine.Rendering;
using UnityEngine.Video;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Create a RenderTexture for rendering video to a target renderer.
/// </summary>
[RequireComponent(typeof(VideoPlayer))]
public class VideoPlayerRenderTexture : MonoBehaviour
{
private string k_ShaderName;
string GetShaderName()
{
if (GraphicsSettings.currentRenderPipeline)
{
string pipelineName = GraphicsSettings.currentRenderPipeline.name;
if (pipelineName.Contains("UniversalRenderPipeline"))
{
return "Universal Render Pipeline/Unlit";
}
else if (pipelineName.Contains("HDRenderPipeline"))
{
return "HDRP/Unlit";
}
}
// Fallback for the built-in render pipeline
return "Unlit/Texture";
}
[SerializeField, Tooltip("The target Renderer which will display the video.")]
Renderer m_Renderer;
[SerializeField, Tooltip("The width of the RenderTexture which will be created.")]
int m_RenderTextureWidth = 1920;
[SerializeField, Tooltip("The height of the RenderTexture which will be created.")]
int m_RenderTextureHeight = 1080;
[SerializeField, Tooltip("The bit depth of the depth channel for the RenderTexture which will be created.")]
int m_RenderTextureDepth;
void Start()
{
k_ShaderName = GetShaderName();
var renderTexture = new RenderTexture(m_RenderTextureWidth, m_RenderTextureHeight, m_RenderTextureDepth);
renderTexture.Create();
var material = new Material(Shader.Find(k_ShaderName));
material.mainTexture = renderTexture;
GetComponent<VideoPlayer>().targetTexture = renderTexture;
m_Renderer.material = material;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1b45269ca3a28fe47924e08799d6178f
@@ -0,0 +1,191 @@
using System;
using System.Collections;
using TMPro;
using UnityEngine.UI;
using UnityEngine.Video;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Connects a UI slider control to a video player, allowing users to scrub to a particular time in th video.
/// </summary>
[RequireComponent(typeof(VideoPlayer))]
public class VideoTimeScrubControl : MonoBehaviour
{
[SerializeField]
[Tooltip("Video play/pause button GameObject")]
GameObject m_ButtonPlayOrPause;
[SerializeField]
[Tooltip("Slider that controls the video")]
Slider m_Slider;
[SerializeField]
[Tooltip("Play icon sprite")]
Sprite m_IconPlay;
[SerializeField]
[Tooltip("Pause icon sprite")]
Sprite m_IconPause;
[SerializeField]
[Tooltip("Play or pause button image.")]
Image m_ButtonPlayOrPauseIcon;
[SerializeField]
[Tooltip("Text that displays the current time of the video.")]
TextMeshProUGUI m_VideoTimeText;
[SerializeField]
[Tooltip("If checked, the slider will fade off after a few seconds. If unchecked, the slider will remain on.")]
bool m_HideSliderAfterFewSeconds;
bool m_IsDragging;
bool m_VideoIsPlaying;
bool m_VideoJumpPending;
long m_LastFrameBeforeScrub;
VideoPlayer m_VideoPlayer;
void Start()
{
m_VideoPlayer = GetComponent<VideoPlayer>();
if (!m_VideoPlayer.playOnAwake)
{
m_VideoPlayer.playOnAwake = true; // Set play on awake for next enable.
m_VideoPlayer.Play(); // Play video to load first frame.
VideoStop(); // Stop the video to set correct state and pause frame.
}
else
{
VideoPlay(); // Play to ensure correct state.
}
if (m_ButtonPlayOrPause != null)
m_ButtonPlayOrPause.SetActive(false);
}
void OnEnable()
{
if (m_VideoPlayer != null)
{
m_VideoPlayer.frame = 0;
VideoPlay(); // Ensures correct UI state update if paused.
}
m_Slider.value = 0.0f;
m_Slider.onValueChanged.AddListener(OnSliderValueChange);
m_Slider.gameObject.SetActive(true);
if (m_HideSliderAfterFewSeconds)
StartCoroutine(HideSliderAfterSeconds());
}
void Update()
{
if (m_VideoJumpPending)
{
// We're trying to jump to a new position, but we're checking to make sure the video player is updated to our new jump frame.
if (m_LastFrameBeforeScrub == m_VideoPlayer.frame)
return;
// If the video player has been updated with desired jump frame, reset these values.
m_LastFrameBeforeScrub = long.MinValue;
m_VideoJumpPending = false;
}
if (!m_IsDragging && !m_VideoJumpPending)
{
if (m_VideoPlayer.frameCount > 0)
{
var progress = (float)m_VideoPlayer.frame / m_VideoPlayer.frameCount;
m_Slider.value = progress;
}
}
}
public void OnPointerDown()
{
m_VideoJumpPending = true;
VideoStop();
VideoJump();
}
public void OnRelease()
{
m_IsDragging = false;
VideoPlay();
VideoJump();
}
void OnSliderValueChange(float sliderValue)
{
UpdateVideoTimeText();
}
IEnumerator HideSliderAfterSeconds(float duration = 1f)
{
yield return new WaitForSeconds(duration);
m_Slider.gameObject.SetActive(false);
}
public void OnDrag()
{
m_IsDragging = true;
m_VideoJumpPending = true;
}
void VideoJump()
{
m_VideoJumpPending = true;
var frame = m_VideoPlayer.frameCount * m_Slider.value;
m_LastFrameBeforeScrub = m_VideoPlayer.frame;
m_VideoPlayer.frame = (long)frame;
}
public void PlayOrPauseVideo()
{
if (m_VideoIsPlaying)
{
VideoStop();
}
else
{
VideoPlay();
}
}
void UpdateVideoTimeText()
{
if (m_VideoPlayer != null && m_VideoTimeText != null)
{
var currentTimeTimeSpan = TimeSpan.FromSeconds(m_VideoPlayer.time);
var totalTimeTimeSpan = TimeSpan.FromSeconds(m_VideoPlayer.length);
var currentTimeString = string.Format("{0:D1}:{1:D2}",
currentTimeTimeSpan.Minutes,
currentTimeTimeSpan.Seconds
);
var totalTimeString = string.Format("{0:D1}:{1:D2}",
totalTimeTimeSpan.Minutes,
totalTimeTimeSpan.Seconds
);
m_VideoTimeText.SetText(currentTimeString + " / " + totalTimeString);
}
}
void VideoStop()
{
m_VideoIsPlaying = false;
m_VideoPlayer.Pause();
m_ButtonPlayOrPauseIcon.sprite = m_IconPlay;
m_ButtonPlayOrPause.SetActive(true);
}
void VideoPlay()
{
m_VideoIsPlaying = true;
m_VideoPlayer.Play();
m_ButtonPlayOrPauseIcon.sprite = m_IconPause;
m_ButtonPlayOrPause.SetActive(false);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f061770d897d9940b53cbfb589c72f2
@@ -0,0 +1,83 @@
using System;
using UnityEngine.Events;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// An interactable that aligns/faces the position of the interactor
/// </summary>
public class XRBlaster : XRGrabInteractable
{
[Serializable]
public class ValueChangeEvent : UnityEvent<float> { }
IXRSelectInteractor m_Interactor;
bool m_Front;
protected override void OnEnable()
{
base.OnEnable();
selectEntered.AddListener(StartGrab);
selectExited.AddListener(EndGrab);
}
protected override void OnDisable()
{
selectEntered.RemoveListener(StartGrab);
selectExited.RemoveListener(EndGrab);
base.OnDisable();
}
void StartGrab(SelectEnterEventArgs args)
{
m_Interactor = args.interactorObject;
var interactorTransform = m_Interactor.GetAttachTransform(this);
Vector3 posRelative = transform.InverseTransformPoint(interactorTransform.position);
if (posRelative.z > 0)
m_Front = true;
else
m_Front = false;
UpdateRotation(true);
}
void EndGrab(SelectExitEventArgs args)
{
m_Interactor = null;
}
public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
{
base.ProcessInteractable(updatePhase);
if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
{
if (isSelected)
{
UpdateRotation();
}
}
}
void UpdateRotation(bool freshCheck = false)
{
var interactorTransform = m_Interactor.GetAttachTransform(this);
if (m_Front)
{
transform.LookAt(interactorTransform.position);
}
else
{
transform.LookAt(interactorTransform.position);
transform.rotation *= Quaternion.Euler(new Vector3(0, 180f, 0f));
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 566a0ffd7d209ab4dbfd2fbb8cdb4b45
@@ -0,0 +1,234 @@
using Unity.Mathematics;
using Unity.XR.CoreUtils.Bindings;
using UnityEngine.XR.Interaction.Toolkit.Filtering;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
namespace UnityEngine.XR.Templates.MR
{
/// <summary>
/// Follow animation affordance for <see cref="IPokeStateDataProvider"/>, such as <see cref="XRPokeFilter"/>.
/// Used to animate a pressed transform, such as a button to follow the poke position.
/// </summary>
[AddComponentMenu("XR/XR Poke Follow Affordance Fill", 22)]
public class XRPokeFollowAffordanceFill : MonoBehaviour
{
[SerializeField]
[Tooltip("Transform that will move in the poke direction when this or a parent GameObject is poked." +
"\nNote: Should be a direct child GameObject.")]
Transform m_PokeFollowTransform;
[SerializeField]
[Tooltip("Transform that will scale the mask when this interactable is poked.")]
RectTransform m_PokeFill;
/// <summary>
/// Transform that will animate along the axis of interaction when this interactable is poked.
/// Note: Must be a direct child GameObject as it moves in local space relative to the poke target's transform.
/// </summary>
public Transform pokeFollowTransform
{
get => m_PokeFollowTransform;
set => m_PokeFollowTransform = value;
}
[SerializeField]
[Range(0f, 20f)]
[Tooltip("Multiplies transform position interpolation as a factor of Time.deltaTime. If 0, no smoothing will be applied.")]
float m_SmoothingSpeed = 8f;
/// <summary>
/// Multiplies transform position interpolation as a factor of <see cref="Time.deltaTime"/>. If <c>0</c>, no smoothing will be applied.
/// </summary>
public float smoothingSpeed
{
get => m_SmoothingSpeed;
set => m_SmoothingSpeed = value;
}
[SerializeField]
[Tooltip("When this component is no longer the target of the poke, the Poke Follow Transform returns to the original position.")]
bool m_ReturnToInitialPosition = true;
/// <summary>
/// When this component is no longer the target of the poke, the <see cref="pokeFollowTransform"/> returns to the original position.
/// </summary>
public bool returnToInitialPosition
{
get => m_ReturnToInitialPosition;
set => m_ReturnToInitialPosition = value;
}
[SerializeField]
[Tooltip("Whether to apply the follow animation if the target of the poke is a child of this transform. " +
"This is useful for UI objects that may have child graphics.")]
bool m_ApplyIfChildIsTarget = true;
/// <summary>
/// Whether to apply the follow animation if the target of the poke is a child of this transform.
/// This is useful for UI objects that may have child graphics.
/// </summary>
public bool applyIfChildIsTarget
{
get => m_ApplyIfChildIsTarget;
set => m_ApplyIfChildIsTarget = value;
}
[Header("Distance Clamping")]
[SerializeField]
[Tooltip("Whether to keep the Poke Follow Transform from moving past a minimum distance from the poke target.")]
bool m_ClampToMinDistance;
/// <summary>
/// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="minDistance"/> from the poke target.
/// </summary>
public bool clampToMinDistance
{
get => m_ClampToMinDistance;
set => m_ClampToMinDistance = value;
}
[SerializeField]
[Tooltip("The minimum distance from this transform that the Poke Follow Transform can move.")]
float m_MinDistance;
/// <summary>
/// The minimum distance from this transform that the <see cref="pokeFollowTransform"/> can move when
/// <see cref="clampToMinDistance"/> is <see langword="true"/>.
/// </summary>
public float minDistance
{
get => m_MinDistance;
set => m_MinDistance = value;
}
[Space]
[SerializeField]
[Tooltip("Whether to keep the Poke Follow Transform from moving past a maximum distance from the poke target.")]
bool m_ClampToMaxDistance;
/// <summary>
/// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="maxDistance"/> from the poke target.
/// </summary>
public bool clampToMaxDistance
{
get => m_ClampToMaxDistance;
set => m_ClampToMaxDistance = value;
}
[SerializeField]
[Tooltip("The maximum distance from this transform that the Poke Follow Transform can move. Will shrink to the distance of initial position if that is smaller, or if this is 0.")]
float m_MaxDistance;
/// <summary>
/// The maximum distance from this transform that the <see cref="pokeFollowTransform"/> can move when
/// <see cref="clampToMaxDistance"/> is <see langword="true"/>.
/// </summary>
public float maxDistance
{
get => m_MaxDistance;
set => m_MaxDistance = value;
}
IPokeStateDataProvider m_PokeDataProvider;
#pragma warning disable CS0618 // Type or member is obsolete
readonly Vector3TweenableVariable m_TransformTweenableVariable = new Vector3TweenableVariable();
readonly FloatTweenableVariable m_PokeStrengthTweenableVariable = new FloatTweenableVariable();
#pragma warning restore CS0618 // Type or member is obsolete
readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
Vector3 m_InitialPosition;
bool m_IsFirstFrame;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void Awake()
{
m_PokeDataProvider = GetComponentInParent<IPokeStateDataProvider>();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void Start()
{
if (m_PokeFollowTransform != null)
{
m_InitialPosition = m_PokeFollowTransform.localPosition;
m_MaxDistance = m_MaxDistance > 0f ? Mathf.Min(m_InitialPosition.magnitude, m_MaxDistance) : m_InitialPosition.magnitude;
m_BindingsGroup.AddBinding(m_TransformTweenableVariable.Subscribe(OnTransformTweenableVariableUpdated));
m_BindingsGroup.AddBinding(m_PokeStrengthTweenableVariable.Subscribe(OnPokeStrengthChanged));
m_BindingsGroup.AddBinding(m_PokeDataProvider.pokeStateData.SubscribeAndUpdate(OnPokeStateDataUpdated));
}
else
{
enabled = false;
Debug.LogWarning($"Missing Poke Follow Transform assignment on {this}. Disabling component.", this);
}
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void OnDestroy()
{
m_BindingsGroup.Clear();
m_TransformTweenableVariable?.Dispose();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void LateUpdate()
{
if (m_IsFirstFrame)
{
m_TransformTweenableVariable.HandleTween(1f);
m_PokeStrengthTweenableVariable.target = 0f;
m_PokeStrengthTweenableVariable.HandleTween(1f);
m_IsFirstFrame = false;
return;
}
float tweenAmt = m_SmoothingSpeed > 0f ? Time.deltaTime * m_SmoothingSpeed : 1f;
m_TransformTweenableVariable.HandleTween(tweenAmt);
m_PokeStrengthTweenableVariable.HandleTween(tweenAmt);
}
void OnTransformTweenableVariableUpdated(float3 position)
{
m_PokeFollowTransform.localPosition = position;
}
void OnPokeStrengthChanged(float newStrength)
{
m_PokeFill.transform.localScale = Vector3.one * newStrength;
}
void OnPokeStateDataUpdated(PokeStateData data)
{
var pokeTarget = data.target;
var applyFollow = m_ApplyIfChildIsTarget
? pokeTarget != null && pokeTarget.IsChildOf(transform)
: pokeTarget == transform;
if (applyFollow)
{
var targetPosition = pokeTarget.InverseTransformPoint(data.axisAlignedPokeInteractionPoint);
if (m_ClampToMinDistance && targetPosition.sqrMagnitude < m_MinDistance * m_MinDistance)
targetPosition = Vector3.ClampMagnitude(targetPosition, m_MinDistance);
if (m_ClampToMaxDistance && targetPosition.sqrMagnitude > m_MaxDistance * m_MaxDistance)
targetPosition = Vector3.ClampMagnitude(targetPosition, m_MaxDistance);
m_TransformTweenableVariable.target = targetPosition;
m_PokeStrengthTweenableVariable.target = Mathf.Clamp01(data.interactionStrength);
}
else if (m_ReturnToInitialPosition)
{
m_TransformTweenableVariable.target = m_InitialPosition;
m_PokeStrengthTweenableVariable.target = 0f;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ef5a10acb20b89b408b1854b6ef1cc35
@@ -0,0 +1,92 @@
namespace UnityEngine.XR.Templates.MR
{
// Copied from XRI Examples. Commit: c40b958
/// <summary>
/// Component that controls the scale of the GameObject based on the distance from the main camera.
/// The distance is divided into zones, and the transform scale is smoothly transitioned when the object moves into a different zone.
/// The size of the zones are relative to the viewer's scale.
/// </summary>
public class ZoneScale : MonoBehaviour
{
const float k_SmoothTime = 0.3f;
#pragma warning disable 649
[SerializeField]
bool m_Clamp;
[SerializeField]
float m_ClampMax = 10f;
[SerializeField]
float m_ClampMin = 1f;
[SerializeField]
float m_ZoneSize = 0.2f;
[SerializeField]
float m_DefaultScale = 1f;
#pragma warning restore 649
float m_YVelocity;
float m_LastScale = 1.0f;
bool m_Snap;
Transform m_MainCameraTransform;
int m_CurrentZone;
//IProvidesViewerScale IFunctionalitySubscriber<IProvidesViewerScale>.provider { get; set; }
/// <summary>
/// Skip the transition interpolation to the target scale immediately
/// </summary>
public void Snap()
{
m_Snap = true;
SetScaleForCurrentDistance();
}
void OnEnable()
{
var mainCamera = Camera.main;
if (mainCamera != null)
m_MainCameraTransform = mainCamera.transform;
Snap();
}
void LateUpdate()
{
SetScaleForCurrentDistance();
}
void SetScaleForCurrentDistance()
{
if (m_MainCameraTransform == null)
return;
var cameraPosition = m_MainCameraTransform.position;
var deltaToCamera = cameraPosition - transform.position;
var adjustedDistance = deltaToCamera.magnitude;
var scaledZoneSize = m_ZoneSize * Camera.main.transform.lossyScale.x;
var zone = Mathf.CeilToInt(adjustedDistance / scaledZoneSize);
var bufferSize = scaledZoneSize * 0.5f;
if (adjustedDistance > m_CurrentZone * scaledZoneSize + bufferSize ||
adjustedDistance < m_CurrentZone * scaledZoneSize - bufferSize)
{
m_CurrentZone = zone;
}
var targetScale = m_CurrentZone * scaledZoneSize;
var newScale = m_Snap ? targetScale : Mathf.SmoothDamp(m_LastScale, targetScale, ref m_YVelocity, k_SmoothTime);
if (m_Snap)
m_Snap = false;
if (m_Clamp)
newScale = Mathf.Clamp(newScale, m_ClampMin, m_ClampMax);
transform.localScale = Vector3.one * (newScale * m_DefaultScale);
m_LastScale = newScale;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d2c297c58ecf31f4084e39a103b22a1d