Last active
November 29, 2023 22:36
-
-
Save rtlsilva/a1fa4f55f39e7d55a981b534c67aa803 to your computer and use it in GitHub Desktop.
Components and extensions that aid in working with AR Foundation using reactive programming
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using UniRx; | |
using UnityEngine; | |
using UnityEngine.UI; | |
using UnityEngine.XR.ARFoundation; | |
using MyNamespace.Utils; | |
using MyNamespace.Extensions; | |
namespace MyNamespace.AR | |
{ | |
/// <summary> | |
/// AR Foundation controller. | |
/// </summary> | |
[RequireComponent(typeof(ARSessionOrigin))] | |
public class ARFoundationController : MonoBehaviour | |
{ | |
/// <summary> | |
/// Holds the Foundation Controller state | |
/// </summary> | |
public struct ExperienceState | |
{ | |
/// <summary> | |
/// The number of planes detected | |
/// </summary> | |
public readonly int PlaneCount; | |
/// <summary> | |
/// Is the content placed? | |
/// </summary> | |
public readonly bool IsContentPlaced; | |
/// <summary> | |
/// Creates a new instance of the <see cref="ExperienceState"/> struct. | |
/// </summary> | |
/// <param name="planeCount">The number of planes detected</param> | |
/// <param name="isContentPlaced">Flag indicating if the content is placed</param> | |
public ExperienceState(int planeCount, bool isContentPlaced) | |
{ | |
this.PlaneCount = planeCount; | |
this.IsContentPlaced = isContentPlaced; | |
} | |
} | |
[Header("AR Components")] | |
/// <summary> | |
/// Reference to the AR session component | |
/// </summary> | |
[SerializeField] | |
private ARSession sessionAR; | |
/// <summary> | |
/// AR session origin component | |
/// </summary> | |
[SerializeField] | |
private ARSessionOrigin sessionOriginAR; | |
/// <summary> | |
/// AR Raycast manager component | |
/// </summary> | |
[SerializeField] | |
private ARRaycastManager raycastManagerAR; | |
/// <summary> | |
/// The AR camera that renders virtual content and the camera feed | |
/// </summary> | |
public Camera ArCamera => this.sessionOriginAR.camera; | |
/// <summary> | |
/// The AR camera's pose | |
/// </summary> | |
public Pose ArCameraPose => new Pose(this.sessionOriginAR.camera.transform.position, this.sessionOriginAR.camera.transform.rotation); | |
/// <summary> | |
/// AR point cloud component | |
/// </summary> | |
[SerializeField] | |
private ARPointCloudManager pointCloudManagerAR; | |
/// <summary> | |
/// AR plane manager component | |
/// </summary> | |
[SerializeField] | |
private ARPlaneManager planeManagerAR; | |
[Header("AR Content Configuration")] | |
/// <summary> | |
/// The minimum distance at which content must be placed. | |
/// </summary> | |
[SerializeField] | |
private float minimumContentDepth; | |
/// <summary> | |
/// Whether to enable scaling AR content based on the distance to the touched plane. | |
/// </summary> | |
[SerializeField] | |
private bool enableDistanceBasedScaling; | |
/// <summary> | |
/// The minimum scale factor that can be applied to the AR content | |
/// </summary> | |
[SerializeField] | |
private float minimumContentScaleFactor = 0.1f; | |
/// <summary> | |
/// The maximum scale factor that can be applied to the AR content | |
/// </summary> | |
[SerializeField] | |
private float maximumContentScaleFactor = 10f; | |
/// <summary> | |
/// The distance in meters at which the AR content should appear in its original scale | |
/// </summary> | |
[Tooltip("The distance in meters at which the AR content should appear in its original scale")] | |
[SerializeField] | |
private float contentReferenceDistance = 2; | |
/// <summary> | |
/// The root transform that will hold the AR content | |
/// </summary> | |
[SerializeField] | |
private Transform contentRoot; | |
/// <summary> | |
/// The AR content's apparent location. | |
/// </summary> | |
private Vector3 contentLocation = Vector3.zero; | |
/// <summary> | |
/// The AR content's apparent scale factor. | |
/// </summary> | |
private float contentScaleFactor = 1.0f; | |
[Header("AR Debug")] | |
/// <summary> | |
/// The panel that will display debug information from native AR subsystems | |
/// </summary> | |
[SerializeField] | |
private ARDebugPanel debugPanel; | |
/// <summary> | |
/// Refenence to the AR world scale slider | |
/// </summary> | |
[SerializeField] | |
private Slider scaleSlider; | |
/// <summary> | |
/// Reference to the player intruction text component | |
/// </summary> | |
[SerializeField] | |
private Text playerInstructionsText; | |
/// <summary> | |
/// Flag indicating if the model was already placed | |
/// </summary> | |
private ReactiveProperty<bool> isContentPlaced = new ReactiveProperty<bool>(); | |
/// <summary> | |
/// An observable that emits whether when the AR content is placed. | |
/// </summary> | |
public IObservable<bool> WhenContentPlaced => this.isContentPlaced.DistinctUntilChanged(); | |
/// <summary> | |
/// The object instantiated as a result of a successful raycast intersection with a plane. | |
/// </summary> | |
private GameObject spawnedObject; | |
/// <summary> | |
/// Status of the AR experience | |
/// </summary> | |
private bool initialized; | |
/// <summary> | |
/// Action triggered when a viable planned is detected | |
/// </summary> | |
private Action initExperience; | |
/// <summary> | |
/// Holds multiple disposable observable subscriptions | |
/// </summary> | |
private CompositeDisposable disposables; | |
#if UNITY_EDITOR | |
/// <summary> | |
/// An <see cref="IInputProvider"/> to handle touch/mouse input. | |
/// </summary> | |
private readonly IInputProvider inputProvider = new EditorInputProvider(); | |
#else | |
private readonly IInputProvider inputProvider = new MobileInputProvider(); | |
#endif | |
/// <summary> | |
/// Initialize the AR controller. | |
/// </summary> | |
/// <param name="onExperienceInitialized">An action to execute when the AR content is initialized.</param> | |
public void Initialize(Action onExperienceInitialized) | |
{ | |
this.initExperience = onExperienceInitialized; | |
this.disposables = new CompositeDisposable(); | |
this.sessionOriginAR = this.GetComponent<ARSessionOrigin>(); | |
if (this.enableDistanceBasedScaling && this.scaleSlider.isActiveAndEnabled) | |
{ | |
this.scaleSlider.minValue = this.minimumContentScaleFactor; | |
this.scaleSlider.maxValue = this.maximumContentScaleFactor; | |
} | |
#if DEBUG | |
// Toggle the debug panel by quadruple-tapping the screen | |
Utils.ObservableInput.WhenMouseButtonMultiClicked(minimumClicks: 4) | |
.Subscribe(_ => this.debugPanel.gameObject.SetActive(!this.debugPanel.gameObject.activeSelf)) | |
.AddTo(this.disposables, this); | |
#endif | |
// Disable all plane visuals when content placed, then keep disabling planes when changes happen to trackable planes | |
this.WhenContentPlaced.WhereTrue().First().Do(_ => this.planeManagerAR.SetPlaneVisualsActive(false)) | |
.ContinueWith(_ => | |
this.planeManagerAR.WhenPlaneChanged() | |
.SelectMany(planes => planes) | |
.Do(plane => this.planeManagerAR.SetPlaneVisualsActive(plane.trackableId, !this.isContentPlaced.Value))) | |
.Subscribe() | |
.AddTo(this.disposables, this); | |
this.initialized = true; | |
} | |
/// <summary> | |
/// Instantiates the AR content. | |
/// </summary> | |
/// <param name="data">The AR item data referencing the content prefab.</param> | |
public GameObject InstantiateExperience(ARItemData data) | |
{ | |
this.spawnedObject = Instantiate(data.ArFoundationPrefab, this.contentRoot); | |
this.isContentPlaced.Value = true; | |
return this.spawnedObject; | |
} | |
/// <summary> | |
/// Applies the given scale factor to the content. | |
/// <para>If the scale slider is enabled, this changes the slider's value. Otherwise, it scales the AR session.</para> | |
/// </summary> | |
/// <param name="scaleFactor">The target scale factor.</param> | |
/// <param name="relative">Whether the supplied factor is applied to the original scale factor or overwrites it.</param> | |
public void ApplyScaling(float scaleFactor, bool relative = false) | |
{ | |
var finalScaleFactor = (relative ? this.contentScaleFactor + scaleFactor : scaleFactor) | |
.Clamp(this.minimumContentScaleFactor, this.maximumContentScaleFactor); | |
if (this.scaleSlider.isActiveAndEnabled && this.scaleSlider.value != scaleFactor) | |
this.scaleSlider.value = finalScaleFactor; // this should trigger ScaleSession | |
else | |
this.ScaleSession(finalScaleFactor); | |
} | |
/// <summary> | |
/// Scales the AR session so that the content appears to have the given scale factor. | |
/// Note that the pivot point for scaling is set by <see cref="MoveSession(Vector3)"/>. | |
/// </summary> | |
/// <param name="scaleFactor">The factor by which the content should appear scaled.</param> | |
private void ScaleSession(float scaleFactor) | |
{ | |
this.contentScaleFactor = scaleFactor; | |
this.sessionOriginAR.transform.localScale = Vector3.one * this.contentScaleFactor; | |
this.debugPanel.DisplayLogMessage($"[ScaleSession] ARSessionOrigin scale: {this.sessionOriginAR.transform.localScale}\n" + | |
$"ARSessionOrigin offset {this.sessionOriginAR.transform.GetChild(0).position}\n" + | |
$"ARSessionOrigin rotation {this.sessionOriginAR.transform.rotation.eulerAngles}"); | |
} | |
/// <summary> | |
/// Moves the AR session so that the content appears to be at the given location. | |
/// </summary> | |
/// <param name="targetLocation">The location of the content in world space.</param> | |
public void MoveSession(Vector3 targetLocation) | |
{ | |
// MakeContentAppearAt applies the offset relatively, which accumulates error, so we reset it first | |
this.sessionOriginAR.MakeContentAppearAt(this.contentRoot, this.contentLocation.Negate()); | |
this.contentLocation = targetLocation; | |
this.sessionOriginAR.MakeContentAppearAt(this.contentRoot, this.contentLocation); | |
this.debugPanel.DisplayLogMessage($"[MoveSession] ARSessionOrigin scale: {this.sessionOriginAR.transform.localScale}\n" + | |
$"ARSessionOrigin offset {this.sessionOriginAR.transform.GetChild(0).position}\n" + | |
$"ARSessionOrigin rotation {this.sessionOriginAR.transform.rotation.eulerAngles}"); | |
} | |
/// <summary> | |
/// Rotates the AR session so that the content appears to be rotated the given angle around the Y axis. | |
/// </summary> | |
/// <param name="rotationAngle">The angle around the Y axis that the content should assume.</param> | |
public void RotateSession(float rotationAngle) | |
{ | |
this.RotateSession(rotationAngle, Vector3.up); | |
} | |
/// <summary> | |
/// Rotates the AR session so that the content appears to be rotated the given angle around the given axis. | |
/// </summary> | |
/// <param name="rotationAngle">The angle around the given axis that the content should assume.</param> | |
/// <param name="axis">The axis around which to appear to rotate the content.</param> | |
public void RotateSession(float rotationAngle, Vector3 axis) | |
{ | |
this.RotateSession(Quaternion.AngleAxis(rotationAngle, axis)); | |
} | |
/// <summary> | |
/// Rotates the AR session so that the content appears to be rotated the given rotation. | |
/// </summary> | |
/// <param name="rotation">The rotation that the content should assume.</param> | |
public void RotateSession(Quaternion rotation) | |
{ | |
this.sessionOriginAR.MakeContentAppearAt(this.contentRoot, rotation); | |
this.debugPanel.DisplayLogMessage($"[RotateSession] ARSessionOrigin scale: {this.sessionOriginAR.transform.localScale}\n" + | |
$"ARSessionOrigin offset {this.sessionOriginAR.transform.GetChild(0).position}\n" + | |
$"ARSessionOrigin rotation {this.sessionOriginAR.transform.rotation.eulerAngles}"); | |
} | |
private void Update() | |
{ | |
if (!this.initialized || this.isContentPlaced.Value || !Input.GetMouseButtonDown(0) || InputUtils.IsPointerOverUIObject()) | |
return; | |
if (this.raycastManagerAR.RaycastToPlane(this.inputProvider.GetTouch(0), this.planeManagerAR, out var hitPose, out var hitPlane, out var contentDistance)) | |
{ | |
this.debugPanel.DisplayLogMessage($"Touched a plane at {hitPose.position}"); | |
this.initExperience?.Invoke(); | |
this.MoveSession(hitPose.position); | |
var comparisonPos = this.ArCamera.transform.position.WithY(hitPose.position.y); | |
var lookDirection = comparisonPos - hitPose.position; | |
this.RotateSession(Quaternion.LookRotation(lookDirection, hitPlane.normal)); | |
if (this.enableDistanceBasedScaling) | |
{ | |
// Determine initial content scale factor based on distance to the plane | |
// The content appears to have the original real world scale if it spawns at the reference distance, | |
// scaling up to the maximum scale factor if it is farther away or to the minimum if it's closer. | |
// What actually gets scaled is the session origin transform, so we scale it by the inverse of the calculated factor. | |
var scaleFactor = (1 / contentDistance.Ratio(0, this.contentReferenceDistance)) | |
.Clamp(this.minimumContentScaleFactor, this.maximumContentScaleFactor); | |
this.debugPanel.DisplayLogMessage($"Distance based initial scaling:\nContent distance: {contentDistance}\nInitial scale factor: {scaleFactor}"); | |
this.ApplyScaling(scaleFactor); | |
} | |
foreach (var pointCloud in this.pointCloudManagerAR.trackables) | |
pointCloud.gameObject.SetActive(false); | |
} | |
} | |
/// <summary> | |
/// Resets the AR system | |
/// </summary> | |
public void ResetModelAR() | |
{ | |
Destroy(this.spawnedObject); | |
this.sessionAR.Reset(); | |
this.isContentPlaced.Value = false; | |
// re enable the managers after the reset | |
this.planeManagerAR.enabled = true; | |
this.pointCloudManagerAR.enabled = true; | |
} | |
/// <summary> | |
/// Closes the AR experience | |
/// </summary> | |
public void CloseExperience() | |
{ | |
this.disposables.Dispose(); | |
} | |
/// <summary> | |
/// Returns an observable that emits the the state of the experience whenever it changes to a new value | |
/// </summary> | |
public IObservable<ExperienceState> WhenExperienceStateChanged() | |
{ | |
// Count detected planes and content placement | |
return this.planeManagerAR.WhenPlaneAdded().Select(planes => planes.Count) | |
.Merge(this.planeManagerAR.WhenPlaneRemoved().Select(planes => -planes.Count)) | |
.StartWith(this.planeManagerAR?.trackables.count ?? 0) | |
.Scan((accumulator, current) => accumulator + current) | |
.CombineLatest(this.isContentPlaced, (planeCount, isContentPlaced) => new ExperienceState(planeCount, isContentPlaced)) | |
.DistinctUntilChanged(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UniRx; | |
using UnityEngine; | |
using UnityEngine.UI; | |
using UnityEngine.XR.ARFoundation; | |
using MyNamespace.Extensions; | |
namespace MyNamespace.AR | |
{ | |
/// <summary> | |
/// Controls the display of debug information from native AR subsystems | |
/// </summary> | |
public class ARDebugPanel : MonoBehaviour | |
{ | |
[Header("Managers")] | |
[SerializeField] | |
private ARCameraManager cameraManager; | |
[SerializeField] | |
private ARPlaneManager planeManager; | |
[Header("AR Debug")] | |
/// <summary> | |
/// Reference to the text log component | |
/// </summary> | |
[SerializeField] | |
private Text logText; | |
/// <summary> | |
/// Reference to the <see cref="ScrollRect"/> that contains the text log | |
/// </summary> | |
[SerializeField] | |
private ScrollRect logScrollRect; | |
/// <summary> | |
/// Reference to the light used to replicate the physical world's lighting conditions | |
/// </summary> | |
[SerializeField] | |
private Light environmentalLight; | |
/// <summary> | |
/// Reference to the average brightness text component | |
/// </summary> | |
[SerializeField] | |
private Text averageBrightnessText; | |
/// <summary> | |
/// Reference to the average temperature text component | |
/// </summary> | |
[SerializeField] | |
private Text averageColorTemperatureText; | |
/// <summary> | |
/// Reference to the average color correction text component | |
/// </summary> | |
[SerializeField] | |
private Text averageColorCorrectionText; | |
private void Start() | |
{ | |
ObservableARSubsystemManager.WhenSystemStateChanged() | |
.Subscribe(args => this.DisplayLogMessage($"System State Changed: {args.state}")) | |
.AddTo(this); | |
this.planeManager.WhenPlaneAdded() | |
.SelectMany(planes => planes) | |
.Subscribe(plane => this.DisplayLogMessage($"Plane ID {plane.trackableId} added at {plane.center}")) | |
.AddTo(this); | |
this.planeManager.WhenPlaneRemoved() | |
.SelectMany(planes => planes) | |
.Subscribe(plane => this.DisplayLogMessage($"Plane ID {plane.trackableId} removed at {plane.center}")) | |
.AddTo(this); | |
this.cameraManager.WhenBrightnessEstimationChanged() | |
.Subscribe(brightness => | |
{ | |
this.environmentalLight.intensity = brightness; | |
this.averageBrightnessText.text = $"Average brightness: {brightness}"; | |
}) | |
.AddTo(this); | |
this.cameraManager.WhenColorTemperatureEstimationChanged() | |
.Subscribe(colorTemperature => | |
{ | |
this.environmentalLight.colorTemperature = colorTemperature; | |
this.averageColorTemperatureText.text = $"Average color temperature: {colorTemperature}"; | |
}) | |
.AddTo(this); | |
this.cameraManager.WhenColorCorrectionEstimationChanged() | |
.Subscribe(colorCorrection => | |
{ | |
this.environmentalLight.color = colorCorrection; | |
this.averageColorCorrectionText.text = $"Color correction: {colorCorrection}"; | |
}) | |
.AddTo(this); | |
} | |
/// <summary> | |
/// Displays the given message on the text log UI component. | |
/// </summary> | |
/// <param name="message">The message to display.</param> | |
public void DisplayLogMessage(string message) | |
{ | |
this.logText.text += message.AddSuffixIfAbsent("\n"); | |
this.logScrollRect.verticalNormalizedPosition = 0; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.XR.ARFoundation; | |
using UnityEngine.XR.ARSubsystems; | |
namespace MyNamespace.AR | |
{ | |
/// <summary> | |
/// Extension methods for working with ARFoundation components | |
/// (e.g. <see cref="ARSessionOrigin"/>, <see cref="ARPlaneManager"/>, <see cref="ARPointCloudManager"/>, <see cref="ARReferencePointManager"/>) | |
/// </summary> | |
public static class ARFoundationComponentExtensions | |
{ | |
/// <summary> | |
/// Enables or disables the visual representation of all tracked planes. | |
/// </summary> | |
/// <param name="manager">This <see cref="ARPlaneManager"/> that holds detected planes.</param> | |
/// <param name="activeState">The state of the planes' visual representation.</param> | |
public static void SetPlaneVisualsActive(this ARPlaneManager manager, bool activeState) | |
{ | |
foreach (var plane in manager.trackables) | |
plane.gameObject.SetActive(activeState); | |
} | |
/// <summary> | |
/// Enables or disables the visual representation of the given tracked plane. | |
/// </summary> | |
/// <param name="manager">This <see cref="ARPlaneManager"/> that holds detected planes.</param> | |
/// <param name="planeId">The identifier of the plane to affect.</param> | |
/// <param name="activeState">The state of the planes' visual representation.</param> | |
public static void SetPlaneVisualsActive(this ARPlaneManager manager, TrackableId planeId, bool activeState) | |
{ | |
manager.GetPlane(planeId)?.gameObject.SetActive(activeState); | |
} | |
/// <summary> | |
/// Casts a ray from a point in screen space against trackable planes. | |
/// </summary> | |
/// <param name="raycastManager">This <see cref="ARRaycastManager"/> that will cast the ray.</param> | |
/// <param name="screenPoint">The point in screen space from which to cast the ray.</param> | |
/// <param name="planeManager">The <see cref="ARPlaneManager"/> that holds detected planes.</param> | |
/// <param name="pose">The pose at which the cast ray hit the plane.</param> | |
/// <param name="plane">The <see cref="ARPlane"/> hit by the cast ray.</param> | |
/// <param name="distance">The distance to the hit plane.</param> | |
public static bool RaycastToPlane(this ARRaycastManager raycastManager, Vector3 screenPoint, ARPlaneManager planeManager, out Pose pose, out ARPlane plane, out float distance) | |
{ | |
var hits = new List<ARRaycastHit>(); | |
pose = Pose.identity; | |
plane = null; | |
distance = 0; | |
if (raycastManager.Raycast(screenPoint, hits, TrackableType.PlaneWithinPolygon)) | |
{ | |
var hit = hits[0]; | |
pose = hit.pose; | |
plane = planeManager.GetPlane(hit.trackableId); | |
distance = hit.distance; | |
return true; | |
} | |
return false; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using UniRx; | |
using UnityEngine.XR.ARFoundation; | |
namespace MyNamespace.AR | |
{ | |
/// <summary> | |
/// Observable wrappers for <see cref="ARSubsystemManager"/> events. | |
/// </summary> | |
public static class ObservableARSubsystemManager | |
{ | |
/// <summary> | |
/// Returns an observable that emits <see cref="ARSystemStateChangedEventArgs"/> whenever the <see cref="ARSubsystemManager.systemState"/> changes. | |
/// </summary> | |
public static IObservable<ARSessionStateChangedEventArgs> WhenSystemStateChanged() | |
{ | |
return Observable.FromEvent<ARSessionStateChangedEventArgs>( | |
addHandler => ARSession.stateChanged += addHandler, | |
removeHandler => ARSession.stateChanged -= removeHandler); | |
} | |
/// <summary> | |
/// Returns an observable that emits <see cref="ARCameraFrameEventArgs"/> whenever a new camera frame is provided by the device. | |
/// </summary> | |
public static IObservable<ARCameraFrameEventArgs> WhenCameraFrameReceived(this ARCameraManager cameraManager) | |
{ | |
return Observable.FromEvent<ARCameraFrameEventArgs>( | |
addHandler => cameraManager.frameReceived += addHandler, | |
removeHandler => cameraManager.frameReceived -= removeHandler); | |
} | |
/// <summary> | |
/// Returns an observable that emits the average brightness in the scene, as estimated by the current AR device, whenever it changes to a new value. | |
/// </summary> | |
public static IObservable<float> WhenBrightnessEstimationChanged(this ARCameraManager cameraManager) | |
{ | |
return cameraManager.WhenCameraFrameReceived() | |
.Where(args => args.lightEstimation.averageBrightness.HasValue) | |
.Select(args => args.lightEstimation.averageBrightness.Value) | |
.DistinctUntilChanged(); | |
} | |
/// <summary> | |
/// Returns an observable that emits the average color temperature in the scene, as estimated by the current AR device, whenever it changes to a new value. | |
/// <para>Currently, this is only available on ARKit.</para> | |
/// </summary> | |
public static IObservable<float> WhenColorTemperatureEstimationChanged(this ARCameraManager cameraManager) | |
{ | |
return cameraManager.WhenCameraFrameReceived() | |
.Where(args => args.lightEstimation.averageColorTemperature.HasValue) | |
.Select(args => args.lightEstimation.averageColorTemperature.Value) | |
.DistinctUntilChanged(); | |
} | |
/// <summary> | |
/// Returns an observable that emits scaling factors used for color correction, as estimated by the current AR device, whenever it changes to a new value. | |
/// </summary> | |
public static IObservable<UnityEngine.Color> WhenColorCorrectionEstimationChanged(this ARCameraManager cameraManager) | |
{ | |
return cameraManager.WhenCameraFrameReceived() | |
.Where(args => args.lightEstimation.colorCorrection.HasValue) | |
.Select(args => args.lightEstimation.colorCorrection.Value) | |
.DistinctUntilChanged(); | |
} | |
/// <summary> | |
/// Returns an observable that emits added planes. | |
/// </summary> | |
public static IObservable<List<ARPlane>> WhenPlaneAdded(this ARPlaneManager planeManager) | |
{ | |
return planeManager.WhenAnyPlaneEvent().Select(args => args.added); | |
} | |
/// <summary> | |
/// Returns an observable that emits removed planes. | |
/// </summary> | |
public static IObservable<List<ARPlane>> WhenPlaneRemoved(this ARPlaneManager planeManager) | |
{ | |
return planeManager.WhenAnyPlaneEvent().Select(args => args.removed); | |
} | |
/// <summary> | |
/// Returns an observable that emits updated planes. | |
/// </summary> | |
public static IObservable<List<ARPlane>> WhenPlaneUpdated(this ARPlaneManager planeManager) | |
{ | |
return planeManager.WhenAnyPlaneEvent().Select(args => args.updated); | |
} | |
/// <summary> | |
/// Returns an observable that emits added, removed and updated planes. | |
/// </summary> | |
public static IObservable<List<ARPlane>> WhenPlaneChanged(this ARPlaneManager planeManager) | |
{ | |
return planeManager.WhenAnyPlaneEvent().Select(args => | |
args.added | |
.Concat(args.removed) | |
.Concat(args.updated) | |
.ToList()); | |
} | |
/// <summary> | |
/// Returns an observable that emits <see cref="ARPlanesChangedEventArgs"/> whenever any plane event occurs. | |
/// </summary> | |
private static IObservable<ARPlanesChangedEventArgs> WhenAnyPlaneEvent(this ARPlaneManager planeManager) | |
{ | |
return Observable.FromEvent<ARPlanesChangedEventArgs>( | |
addHandler => planeManager.planesChanged += addHandler, | |
removeHandler => planeManager.planesChanged -= removeHandler); | |
} | |
/// <summary> | |
/// Returns an observable that emits <see cref="ARPointCloudChangedEventArgs"/> whenever any point cloud event occurs. | |
/// </summary> | |
public static IObservable<ARPointCloudChangedEventArgs> WhenPointCloudUpdated(this ARPointCloudManager pointCloudManager) | |
{ | |
return Observable.FromEvent<ARPointCloudChangedEventArgs>( | |
addHandler => pointCloudManager.pointCloudsChanged += addHandler, | |
removeHandler => pointCloudManager.pointCloudsChanged-= removeHandler); | |
} | |
/// <summary> | |
/// Returns an observable that emits <see cref="ARReferencePointsChangedEventArgs"/> whenever any reference point event occurs. | |
/// </summary> | |
public static IObservable<ARReferencePointsChangedEventArgs> WhenReferencePointUpdated(this ARReferencePointManager referencePointManager) | |
{ | |
return Observable.FromEvent<ARReferencePointsChangedEventArgs>( | |
addHandler => referencePointManager.referencePointsChanged += addHandler, | |
removeHandler => referencePointManager.referencePointsChanged -= removeHandler); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
namespace MyNamespace.Utils | |
{ | |
/// <summary> | |
/// Abstract input provider. | |
/// </summary> | |
public interface IInputProvider | |
{ | |
/// <summary> | |
/// Returns the number of current touches. | |
/// </summary> | |
/// <returns>The number of current touches</returns> | |
int GetTouchCount(); | |
/// <summary> | |
/// Returns the position of the touch at the given index in pixel coordinates | |
/// </summary> | |
/// <param name="index">The index of the touch</param> | |
/// <returns>The position of the touch at the given index in pixel coordinates</returns> | |
Vector2 GetTouch(int index); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment