version 2.5.0

This commit is contained in:
Sean Lu
2025-01-10 17:17:03 +08:00
parent ddc3c4c6d8
commit 2372c9429a
1086 changed files with 290974 additions and 77367 deletions

View File

@@ -3,8 +3,11 @@ using System;
using UnityEngine;
using UnityEngine.XR.OpenXR;
using VIVE.OpenXR.Feature;
using VIVE.OpenXR.Anchor;
using static VIVE.OpenXR.Anchor.ViveAnchor;
using static VIVE.OpenXR.Feature.ViveAnchor;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace VIVE.OpenXR.Toolkits.Anchor
{
@@ -12,8 +15,9 @@ namespace VIVE.OpenXR.Toolkits.Anchor
{
static ViveAnchor feature = null;
static bool isSupported = false;
static bool isPersistedAnchorSupported = false;
static void CheckFeature()
static void EnsureFeature()
{
if (feature != null) return;
@@ -22,28 +26,32 @@ namespace VIVE.OpenXR.Toolkits.Anchor
throw new NotSupportedException("ViveAnchor feature is not enabled");
}
static void EnsureCollection()
{
if (taskAcquirePAC != null)
{
Debug.Log("AnchorManager: Wait for AcquirePersistedAnchorCollection task.");
taskAcquirePAC.Wait();
}
if (persistedAnchorCollection == IntPtr.Zero)
throw new Exception("Should create Persisted Anchor Collection first.");
}
/// <summary>
/// Helper to get the extention feature instance.
/// Helper to get the extension feature instance.
/// </summary>
/// <returns></returns>
/// <returns>Instance of ViveAnchor feature.</returns>
public static ViveAnchor GetFeature()
{
try
{
CheckFeature();
}
catch (NotSupportedException)
{
Debug.LogWarning("ViveAnchor feature is not enabled");
return null;
}
if (feature != null) return feature;
feature = OpenXRSettings.Instance.GetFeature<ViveAnchor>();
return feature;
}
/// <summary>
/// Check if the extension is supported.
/// Check if the extensions are supported. Should always check this before using the other functions.
/// </summary>
/// <returns></returns>
/// <returns>True if the extension is supported, false otherwise.</returns>
public static bool IsSupported()
{
if (GetFeature() == null) return false;
@@ -52,18 +60,32 @@ namespace VIVE.OpenXR.Toolkits.Anchor
var ret = false;
if (feature.GetProperties(out XrSystemAnchorPropertiesHTC properties) == XrResult.XR_SUCCESS)
{
Debug.Log("Anchor: IsSupported() properties.supportedFeatures: " + properties.supportsAnchor);
ret = properties.supportsAnchor;
Debug.Log("ViveAnchor: IsSupported() properties.supportedFeatures: " + properties.supportsAnchor);
ret = properties.supportsAnchor > 0;
isSupported = ret;
}
else
{
Debug.Log("Anchor: IsSupported() GetSystemProperties failed.");
Debug.Log("ViveAnchor: IsSupported() GetSystemProperties failed.");
}
return ret;
}
/// <summary>
/// Check if the persisted anchor extension is supported and enabled.
/// Should always check this before using the other persistance function.
/// </summary>
/// <returns>True if persisted anchor extension is supported, false otherwise.</returns>
public static bool IsPersistedAnchorSupported()
{
if (GetFeature() == null) return false;
if (isPersistedAnchorSupported) return true;
else
isPersistedAnchorSupported = feature.IsPersistedAnchorSupported();
return isPersistedAnchorSupported;
}
/// <summary>
/// Create a spatial anchor at tracking space (Camera Rig).
/// </summary>
@@ -71,38 +93,51 @@ namespace VIVE.OpenXR.Toolkits.Anchor
/// <returns>Anchor container</returns>
public static Anchor CreateAnchor(Pose pose, string name)
{
try
{
CheckFeature();
XrSpace baseSpace = feature.GetTrackingSpace();
XrSpatialAnchorCreateInfoHTC createInfo = new XrSpatialAnchorCreateInfoHTC();
createInfo.type = XrStructureType.XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_HTC;
createInfo.poseInSpace = new XrPosef();
createInfo.poseInSpace.position = pose.position.ToOpenXRVector();
createInfo.poseInSpace.orientation = pose.rotation.ToOpenXRQuaternion();
createInfo.name.name = name;
createInfo.space = baseSpace;
EnsureFeature();
if (feature.CreateSpatialAnchor(createInfo, out XrSpace anchor) == XrResult.XR_SUCCESS)
{
return new Anchor(anchor, name);
}
} catch (Exception) { }
if (string.IsNullOrEmpty(name))
throw new ArgumentException("The name should not be empty.");
XrSpace baseSpace = feature.GetTrackingSpace();
XrSpatialAnchorCreateInfoHTC createInfo = new XrSpatialAnchorCreateInfoHTC();
createInfo.type = XrStructureType.XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_HTC;
createInfo.poseInSpace = new XrPosef();
createInfo.poseInSpace.position = pose.position.ToOpenXRVector();
createInfo.poseInSpace.orientation = pose.rotation.ToOpenXRQuaternion();
createInfo.name = new XrSpatialAnchorNameHTC(name);
createInfo.space = baseSpace;
if (feature.CreateSpatialAnchor(createInfo, out XrSpace anchor) == XrResult.XR_SUCCESS)
{
return new Anchor(anchor, name);
}
return null;
}
/// <summary>
/// Get the name of the spatial anchor.
/// </summary>
/// <param name="anchor">The anchor instance.</param>
/// <param name="name">Output parameter to hold the name of the anchor.</param>
/// <returns>True if the name is successfully retrieved, false otherwise.</returns>
public static bool GetSpatialAnchorName(Anchor anchor, out string name)
{
return GetSpatialAnchorName(anchor.GetXrSpace(), out name);
}
/// <summary>
/// Get the name of the spatial anchor.
/// </summary>
/// <param name="anchor">The XrSpace representing the anchor.</param>
/// <param name="name">Output parameter to hold the name of the anchor.</param>
/// <returns>True if the name is successfully retrieved, false otherwise.</returns>
public static bool GetSpatialAnchorName(XrSpace anchor, out string name)
{
name = "";
CheckFeature();
EnsureFeature();
XrResult ret = feature.GetSpatialAnchorName(anchor, out XrSpatialAnchorNameHTC xrName);
if (ret == XrResult.XR_SUCCESS)
name = xrName.name;
name = xrName.ToString();
return ret == XrResult.XR_SUCCESS;
}
@@ -112,7 +147,7 @@ namespace VIVE.OpenXR.Toolkits.Anchor
/// <returns></returns>
public static XrSpace GetTrackingSpace()
{
CheckFeature();
EnsureFeature();
return feature.GetTrackingSpace();
}
@@ -128,9 +163,435 @@ namespace VIVE.OpenXR.Toolkits.Anchor
return anchor.GetRelatedPose(feature.GetTrackingSpace(), ViveInterceptors.Instance.GetPredictTime(), out pose);
}
// Use SemaphoreSlim to make sure only one anchor's task is running at the same time.
static readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
// Use lock to make sure taskAcquirePAC and persistedAnchorCollection assignment is atomic.
static readonly object asyncLock = new object();
static FutureTask<(XrResult, IntPtr)> taskAcquirePAC = null;
static IntPtr persistedAnchorCollection = System.IntPtr.Zero;
private static (XrResult, IntPtr) CompletePAC(IntPtr future)
{
Debug.Log("AnchorManager: AcquirePersistedAnchorCollectionComplete");
var ret = feature.AcquirePersistedAnchorCollectionComplete(future, out var completion);
lock (asyncLock)
{
taskAcquirePAC = null;
if (ret == XrResult.XR_SUCCESS)
{
ret = completion.futureResult;
Debug.Log("AnchorManager: AcquirePersistedAnchorCollection: Complete");
persistedAnchorCollection = completion.persistedAnchorCollection;
return (ret, persistedAnchorCollection);
}
else
{
//Debug.LogError("AcquirePersistedAnchorCollection: Complete: PersistedAnchorCollection=" + completion.persistedAnchorCollection);
persistedAnchorCollection = System.IntPtr.Zero;
return (ret, persistedAnchorCollection);
}
}
}
/// <summary>
/// Anchor is a named Space. It can be used to create a spatial anchor, or get the anchor's name.
/// After use it, you should call Dispose() to release the anchor.
/// Enable the persistance anchor feature. It will acquire a persisted anchor collection.
/// The first time PAC's acquiration may take time. You can to cancel the process by calling <see cref="ReleasePersistedAnchorCollection"/>.
/// You can wait for the returned task to complete, or by calling <see cref="IsPersistedAnchorCollectionAcquired"/> to check if the collection is ready.
/// Use <see cref="ReleasePersistedAnchorCollection"/> to free resource when no any persisted anchor operations are needed.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public static FutureTask<(XrResult, IntPtr)> AcquirePersistedAnchorCollection()
{
EnsureFeature();
if (!feature.IsPersistedAnchorSupported())
return FutureTask<(XrResult, IntPtr)>.FromResult((XrResult.XR_ERROR_EXTENSION_NOT_PRESENT, IntPtr.Zero));
lock (asyncLock)
{
if (persistedAnchorCollection != System.IntPtr.Zero)
return FutureTask<(XrResult, IntPtr)>.FromResult((XrResult.XR_SUCCESS, persistedAnchorCollection));
// If the persistedAnchorCollection is not ready, and the task is started, wait for it.
if (taskAcquirePAC != null)
return taskAcquirePAC;
}
Debug.Log("ViveAnchor: AcquirePersistedAnchorCollectionAsync");
var ret = feature.AcquirePersistedAnchorCollectionAsync(out IntPtr future);
if (ret != XrResult.XR_SUCCESS)
{
Debug.LogError("AcquirePersistedAnchorCollection failed: " + ret);
return FutureTask<(XrResult, IntPtr)>.FromResult((ret, IntPtr.Zero));
}
else
{
var task = new FutureTask<(XrResult, IntPtr)>(future, CompletePAC, 10, autoComplete: true);
lock (asyncLock)
{
taskAcquirePAC = task;
}
return task;
}
}
/// <summary>
/// Check if the persisted anchor collection is acquired.
/// </summary>
/// <returns>True if the persisted anchor collection is acquired, false otherwise.</returns>
public static bool IsPersistedAnchorCollectionAcquired()
{
return persistedAnchorCollection != System.IntPtr.Zero;
}
/// <summary>
/// Call this function when no any persisted anchor operations are needed.
/// Destroy the persisted anchor collection. If task is running, the task will be canceled.
/// </summary>
public static void ReleasePersistedAnchorCollection()
{
IntPtr tmp;
if (taskAcquirePAC != null)
{
taskAcquirePAC.Cancel();
taskAcquirePAC.Dispose();
taskAcquirePAC = null;
}
lock (asyncLock)
{
if (persistedAnchorCollection == System.IntPtr.Zero) return;
tmp = persistedAnchorCollection;
persistedAnchorCollection = System.IntPtr.Zero;
}
EnsureFeature();
Task.Run(async () =>
{
Debug.Log("ViveAnchor: ReleasePersistedAnchorCollection task is started.");
await semaphoreSlim.WaitAsync();
try
{
feature?.ReleasePersistedAnchorCollection(tmp);
}
finally
{
semaphoreSlim.Release();
}
Debug.Log("ViveAnchor: ReleasePersistedAnchorCollection task is done.");
});
}
private static XrResult CompletePA(IntPtr future) {
Debug.Log("AnchorManager: CompletePA");
var ret = feature.PersistSpatialAnchorComplete(future, out var completion);
if (ret == XrResult.XR_SUCCESS)
{
return completion.futureResult;
}
else
{
Debug.LogError("AcquirePersistedAnchorCollection failed: " + ret);
}
return ret;
}
/// <summary>
/// Persist an anchor with the given name. The persistanceAnchorName should be unique.
/// The persistance might fail if the anchor is not trackable. Check the result from the task.
/// </summary>
/// <param name="anchor">The anchor instance.</param>
/// <param name="persistanceAnchorName">The name of the persisted anchor.</param>
/// <param name="cts">PersistAnchor may take time. If you want to cancel it, use cts.</param>
/// <returns>The task to get persisted anchor's result.</returns>
public static FutureTask<XrResult> PersistAnchor(Anchor anchor, string persistanceAnchorName)
{
EnsureFeature();
EnsureCollection();
if (string.IsNullOrEmpty(persistanceAnchorName))
throw new ArgumentException("The persistanceAnchorName should not be empty.");
var name = new XrSpatialAnchorNameHTC(persistanceAnchorName);
var ret = feature.PersistSpatialAnchorAsync(persistedAnchorCollection, anchor.GetXrSpace(), name, out IntPtr future);
if (ret == XrResult.XR_SUCCESS)
{
// If no auto complete, you can cancel the task and no need to free resouce.
// Once it completed, you need handle the result.
return new FutureTask<XrResult>(future, CompletePA, 10, autoComplete: false);
}
return FutureTask<XrResult>.FromResult(ret);
}
/// <summary>
/// Unpersist the anchor by the name. The anchor created from persisted anchor will still be trackable.
/// </summary>
/// <param name="persistanceAnchorName">The name of the persisted anchor to be removed.</param>
/// <returns>The result of the operation.</returns>
public static XrResult UnpersistAnchor(string persistanceAnchorName)
{
EnsureFeature();
EnsureCollection();
if (string.IsNullOrEmpty(persistanceAnchorName))
throw new ArgumentException("The persistanceAnchorName should not be empty.");
var name = new XrSpatialAnchorNameHTC(persistanceAnchorName);
var ret = feature.UnpersistSpatialAnchor(persistedAnchorCollection, name);
return ret;
}
/// <summary>
/// Get the number of persisted anchors.
/// </summary>
/// <param name="count">Output parameter to hold the number of persisted anchors.</param>
/// <returns>The result of the operation.</returns>
public static XrResult GetNumberOfPersistedAnchors(out int count)
{
EnsureFeature();
EnsureCollection();
XrSpatialAnchorNameHTC[] xrNames = null;
uint xrCount = 0;
XrResult ret = feature.EnumeratePersistedAnchorNames(persistedAnchorCollection, 0, ref xrCount, ref xrNames);
if (ret != XrResult.XR_SUCCESS)
count = 0;
else
count = (int)xrCount;
return ret;
}
/// <summary>
/// List all persisted anchors.
/// </summary>
/// <param name="names">Output parameter to hold the names of the persisted anchors.</param>
/// <returns>The result of the operation.</returns>
public static XrResult EnumeratePersistedAnchorNames(out string[] names)
{
EnsureFeature();
EnsureCollection();
XrSpatialAnchorNameHTC[] xrNames = null;
uint countOut = 0;
uint countIn = 0;
XrResult ret = feature.EnumeratePersistedAnchorNames(persistedAnchorCollection, countIn, ref countOut, ref xrNames);
if (ret != XrResult.XR_SUCCESS)
{
names = null;
return ret;
}
// If Insufficient size, try again.
do
{
countIn = countOut;
xrNames = new XrSpatialAnchorNameHTC[countIn];
ret = feature.EnumeratePersistedAnchorNames(persistedAnchorCollection, countIn, ref countOut, ref xrNames);
}
while (ret == XrResult.XR_ERROR_SIZE_INSUFFICIENT);
if (ret != XrResult.XR_SUCCESS)
{
names = null;
return ret;
}
names = new string[countIn];
for (int i = 0; i < countIn; i++)
{
string v = xrNames[i].ToString();
names[i] = v;
}
return ret;
}
private static (XrResult, Anchor) CompleteCreateSAfromPA(IntPtr future)
{
Debug.Log("AnchorManager: CompleteCreateSAfromPA");
var ret = feature.CreateSpatialAnchorFromPersistedAnchorComplete(future, out var completion);
if (ret == XrResult.XR_SUCCESS)
{
var anchor = new Anchor(completion.anchor);
anchor.isTrackable = true;
return (completion.futureResult, anchor);
}
else
{
Debug.LogError("CreateSpatialAnchorFromPersistedAnchor failed: " + ret);
return (ret, new Anchor(0));
}
}
/// <summary>
/// Create a spatial anchor from a persisted anchor. This will also mark the anchor as trackable.
/// </summary>
/// <param name="persistanceAnchorName">The name of the persisted anchor.</param>
/// <param name="spatialAnchorName">The name of the new spatial anchor.</param>
/// <param name="anchor">Output parameter to hold the new anchor instance.</param>
/// <returns>The result of the operation.</returns>
public static FutureTask<(XrResult, Anchor)> CreateSpatialAnchorFromPersistedAnchor(string persistanceAnchorName, string spatialAnchorName)
{
EnsureFeature();
EnsureCollection();
Debug.Log("AnchorManager: CreateSpatialAnchorFromPersistedAnchor: " + persistanceAnchorName + " -> " + spatialAnchorName);
if (string.IsNullOrEmpty(persistanceAnchorName) || string.IsNullOrEmpty(spatialAnchorName))
throw new ArgumentException("The persistanceAnchorName and spatialAnchorName should not be empty.");
var createInfo = new XrSpatialAnchorFromPersistedAnchorCreateInfoHTC() {
type = XrStructureType.XR_TYPE_SPATIAL_ANCHOR_FROM_PERSISTED_ANCHOR_CREATE_INFO_HTC,
persistedAnchorCollection = persistedAnchorCollection,
persistedAnchorName = new XrSpatialAnchorNameHTC(persistanceAnchorName),
spatialAnchorName = new XrSpatialAnchorNameHTC(spatialAnchorName)
};
var ret = feature.CreateSpatialAnchorFromPersistedAnchorAsync(createInfo, out var future);
if (ret == XrResult.XR_SUCCESS)
{
// If no auto complete, you can cancel the task and no need to free resouce.
// Once it completed, you need handle the result.
return new FutureTask<(XrResult, Anchor)>(future, CompleteCreateSAfromPA, 10, autoComplete: false);
}
else
{
return FutureTask<(XrResult, Anchor)>.FromResult((ret, new Anchor(0)));
}
}
/// <summary>
/// Clear all persisted anchors. Those anchors created from or to the persisted anchor will still be trackable.
/// </summary>
/// <returns>The result of the operation.</returns>
public static XrResult ClearPersistedAnchors()
{
EnsureFeature();
EnsureCollection();
return feature.ClearPersistedAnchors(persistedAnchorCollection);
}
/// <summary>
/// Get the properties of the persisted anchor.
/// maxPersistedAnchorCount in XrPersistedAnchorPropertiesGetInfoHTC will be set to the max count of the persisted anchor.
/// </summary>
/// <param name="properties">Output parameter to hold the properties of the persisted anchor.</param>
/// <returns>The result of the operation.</returns>
public static XrResult GetPersistedAnchorProperties(out XrPersistedAnchorPropertiesGetInfoHTC properties)
{
EnsureFeature();
EnsureCollection();
return feature.GetPersistedAnchorProperties(persistedAnchorCollection, out properties);
}
/// <summary>
/// Export the persisted anchor to a buffer. The buffer can be used to import the anchor later or save it to a file.
/// Export takes time, so it is an async function. The buffer will be null if the export failed.
/// </summary>
/// <param name="persistanceAnchorName">The name of the persisted anchor to be exported.</param>
/// <param name="buffer">Output parameter to hold the buffer containing the exported anchor.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task<(XrResult, string, byte[])> ExportPersistedAnchor(string persistanceAnchorName)
{
EnsureFeature();
EnsureCollection();
if (string.IsNullOrEmpty(persistanceAnchorName))
return Task.FromResult<(XrResult, string, byte[])>((XrResult.XR_ERROR_HANDLE_INVALID, "", null));
var name = new XrSpatialAnchorNameHTC(persistanceAnchorName);
return Task.Run(async () =>
{
Debug.Log($"ExportPersistedAnchor({persistanceAnchorName}) task is started.");
XrResult ret = XrResult.XR_ERROR_VALIDATION_FAILURE;
await semaphoreSlim.WaitAsync();
try
{
lock (asyncLock)
{
if (persistedAnchorCollection == System.IntPtr.Zero)
{
return (XrResult.XR_ERROR_HANDLE_INVALID, "", null);
}
}
ret = feature.ExportPersistedAnchor(persistedAnchorCollection, name, out var buffer);
Debug.Log($"ExportPersistedAnchor({persistanceAnchorName}) task is done. ret=" + ret);
lock (asyncLock)
{
if (ret != XrResult.XR_SUCCESS)
{
buffer = null;
return (ret, "", null);
}
return (ret, persistanceAnchorName, buffer);
}
}
finally
{
semaphoreSlim.Release();
}
});
}
/// <summary>
/// Import the persisted anchor from a buffer. The buffer should be created by ExportPersistedAnchor.
/// Import takes time, so it is an async function. Check imported anchor by EnumeratePersistedAnchorNames.
/// </summary>
/// <param name="buffer">The buffer containing the persisted anchor data.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task<XrResult> ImportPersistedAnchor(byte[] buffer) {
EnsureFeature();
EnsureCollection();
return Task.Run(async () =>
{
Debug.Log($"ImportPersistedAnchor task is started.");
XrResult ret = XrResult.XR_ERROR_VALIDATION_FAILURE;
await semaphoreSlim.WaitAsync();
try
{
lock (asyncLock)
{
if (persistedAnchorCollection == System.IntPtr.Zero)
return XrResult.XR_ERROR_HANDLE_INVALID;
ret = feature.ImportPersistedAnchor(persistedAnchorCollection, buffer);
return ret;
}
}
finally
{
semaphoreSlim.Release();
Debug.Log($"ImportPersistedAnchor task is done. ret=" + ret);
}
});
}
/// <summary>
/// Get the persisted anchor name from the buffer. The buffer should be created by ExportPersistedAnchor.
/// </summary>
/// <returns>True if the name is successfully retrieved, false otherwise.</returns>
public static bool GetPersistedAnchorNameFromBuffer(byte[] buffer, out string name)
{
EnsureFeature();
EnsureCollection();
var ret = feature.GetPersistedAnchorNameFromBuffer(persistedAnchorCollection, buffer, out var xrName);
if (ret == XrResult.XR_SUCCESS)
name = xrName.ToString();
else
name = "";
return ret == XrResult.XR_SUCCESS;
}
/// <summary>
/// Anchor is a named Space. It can be used to create a spatial anchor, or get the anchor's name.
/// After use, you should call Dispose() to release the anchor.
/// IsTrackable is true if the anchor is created persisted anchor or created from persisted anchor.
/// IsPersisted is true if the anchor is ever persisted.
/// </summary>
public class Anchor : VIVE.OpenXR.Feature.Space
{
@@ -152,29 +613,60 @@ namespace VIVE.OpenXR.Toolkits.Anchor
}
}
internal bool isTrackable = false;
/// <summary>
/// If the anchor is created persisted anchor or created from persisted anchor, it will be trackable.
/// </summary>
public bool IsTrackable => isTrackable;
internal bool isPersisted = false;
/// <summary>
/// If the anchor is ever persisted, it will be true.
/// </summary>
public bool IsPersisted => isPersisted;
internal Anchor(XrSpace anchor, string name) : base(anchor)
{
Debug.Log($"Anchor: new Anchor({anchor}, {name})"); // Remove this line later.
// Get the current tracking space.
this.name = name;
}
internal Anchor(XrSpace anchor) : base(anchor)
{
Debug.Log($"Anchor: new Anchor({anchor})"); // Remove this line later.
// Get the current tracking space.
name = GetSpatialAnchorName();
}
internal Anchor(Anchor other) : base(other.space)
{
// Get the current tracking space.
name = other.name;
isTrackable = other.isTrackable;
isPersisted = other.isPersisted;
}
/// <summary>
/// Get the anchor's name by using this anchor's handle, instead of the anchor's Name. This will update the anchor's Name.
/// </summary>
/// <returns></returns>
/// <returns>Anchor's name. Always return non null string.</returns>
public string GetSpatialAnchorName()
{
AnchorManager.CheckFeature();
if (space == 0)
{
Debug.LogError("Anchor: GetSpatialAnchorName: The anchor is invalid.");
return "";
}
AnchorManager.EnsureFeature();
if (AnchorManager.GetSpatialAnchorName(this, out string name))
return name;
return null;
Debug.LogError("Anchor: GetSpatialAnchorName: Failed to get Anchor name.");
return "";
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5544ca439cd389c4ba5255504ce221a4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,815 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.OpenXR;
using VIVE.OpenXR.Hand;
#if UNITY_XR_HANDS
using UnityEngine.XR.Hands;
using UnityEngine.XR.Hands.OpenXR;
#endif
namespace VIVE.OpenXR.Toolkits.Common
{
public enum DeviceCategory
{
None = 0,
HMD = 1,
CenterEye = 2,
LeftController = 3,
RightController = 4,
LeftHand = 5,
RightHand = 6,
Tracker0 = 7,
Tracker1 = 8,
Tracker2 = 9,
Tracker3 = 10,
Tracker4 = 11,
}
public enum PoseState
{
None = 0,
IsTracked = 1,
Position = 2,
Rotation = 3,
Velocity = 4,
AngularVelocity = 5,
Acceleration = 6,
AngularAcceleration = 7,
}
public enum Handedness
{
None = -1,
Right = 0,
Left = 1,
}
public enum HandEvent
{
None = 0,
PinchValue = 0x00000001,
PinchPose = 0x00000002,
GraspValue = 0x00000010,
GraspPose = 0x00000020,
}
public enum ButtonEvent
{
None = 0,
GripValue = 0x00000001,
GripPress = 0x00000002,
TriggerValue = 0x00000010,
TriggerTouch = 0x00000020,
TriggerPress = 0x00000040,
Primary2DAxisValue = 0x00000100,
Primary2DAxisTouch = 0x00000200,
Primary2DAxisPress = 0x00000400,
Secondary2DAxisValue = 0x00001000,
Secondary2DAxisTouch = 0x00002000,
Secondary2DAxisPress = 0x00004000,
PrimaryButton = 0x00010000,
SecondaryButton = 0x00020000,
ParkingTouch = 0x00100000,
Menu = 0x01000000,
}
public enum HandJointType : Int32
{
Palm = XrHandJointEXT.XR_HAND_JOINT_PALM_EXT,
Wrist = XrHandJointEXT.XR_HAND_JOINT_WRIST_EXT,
Thumb_Joint0 = XrHandJointEXT.XR_HAND_JOINT_THUMB_METACARPAL_EXT,
Thumb_Joint1 = XrHandJointEXT.XR_HAND_JOINT_THUMB_PROXIMAL_EXT,
Thumb_Joint2 = XrHandJointEXT.XR_HAND_JOINT_THUMB_DISTAL_EXT,
Thumb_Tip = XrHandJointEXT.XR_HAND_JOINT_THUMB_TIP_EXT,
Index_Joint0 = XrHandJointEXT.XR_HAND_JOINT_INDEX_METACARPAL_EXT,
Index_Joint1 = XrHandJointEXT.XR_HAND_JOINT_INDEX_PROXIMAL_EXT,
Index_Joint2 = XrHandJointEXT.XR_HAND_JOINT_INDEX_INTERMEDIATE_EXT,
Index_Joint3 = XrHandJointEXT.XR_HAND_JOINT_INDEX_DISTAL_EXT,
Index_Tip = XrHandJointEXT.XR_HAND_JOINT_INDEX_TIP_EXT,
Middle_Joint0 = XrHandJointEXT.XR_HAND_JOINT_MIDDLE_METACARPAL_EXT,
Middle_Joint1 = XrHandJointEXT.XR_HAND_JOINT_MIDDLE_PROXIMAL_EXT,
Middle_Joint2 = XrHandJointEXT.XR_HAND_JOINT_MIDDLE_INTERMEDIATE_EXT,
Middle_Joint3 = XrHandJointEXT.XR_HAND_JOINT_MIDDLE_DISTAL_EXT,
Middle_Tip = XrHandJointEXT.XR_HAND_JOINT_MIDDLE_TIP_EXT,
Ring_Joint0 = XrHandJointEXT.XR_HAND_JOINT_RING_METACARPAL_EXT,
Ring_Joint1 = XrHandJointEXT.XR_HAND_JOINT_RING_PROXIMAL_EXT,
Ring_Joint2 = XrHandJointEXT.XR_HAND_JOINT_RING_INTERMEDIATE_EXT,
Ring_Joint3 = XrHandJointEXT.XR_HAND_JOINT_RING_DISTAL_EXT,
Ring_Tip = XrHandJointEXT.XR_HAND_JOINT_RING_TIP_EXT,
Pinky_Joint0 = XrHandJointEXT.XR_HAND_JOINT_LITTLE_METACARPAL_EXT,
Pinky_Joint1 = XrHandJointEXT.XR_HAND_JOINT_LITTLE_PROXIMAL_EXT,
Pinky_Joint2 = XrHandJointEXT.XR_HAND_JOINT_LITTLE_INTERMEDIATE_EXT,
Pinky_Joint3 = XrHandJointEXT.XR_HAND_JOINT_LITTLE_DISTAL_EXT,
Pinky_Tip = XrHandJointEXT.XR_HAND_JOINT_LITTLE_TIP_EXT,
Count = XrHandJointEXT.XR_HAND_JOINT_MAX_ENUM_EXT,
}
public static class VIVEInput
{
private struct InputActionMapping
{
public DeviceCategory device;
public PoseState poseState;
public ButtonEvent buttonEvent;
public HandEvent handEvent;
public InputAction inputAction { get; private set; }
public InputActionMapping(string bindingPath, DeviceCategory device,
PoseState poseState = PoseState.None, ButtonEvent buttonEvent = ButtonEvent.None, HandEvent handEvent = HandEvent.None)
{
inputAction = new InputAction(binding: bindingPath);
inputAction.Enable();
this.device = device;
this.poseState = poseState;
this.buttonEvent = buttonEvent;
this.handEvent = handEvent;
}
public static InputActionMapping Identify => new InputActionMapping("", DeviceCategory.None);
public override bool Equals(object obj)
{
return obj is InputActionMapping inputActionMapping &&
device == inputActionMapping.device &&
poseState == inputActionMapping.poseState &&
buttonEvent == inputActionMapping.buttonEvent &&
handEvent == inputActionMapping.handEvent &&
inputAction == inputActionMapping.inputAction;
}
public override int GetHashCode()
{
return device.GetHashCode() ^ poseState.GetHashCode() ^ buttonEvent.GetHashCode() ^ handEvent.GetHashCode() ^ inputAction.GetHashCode();
}
public static bool operator ==(InputActionMapping source, InputActionMapping target) => source.Equals(target);
public static bool operator !=(InputActionMapping source, InputActionMapping target) => !(source == (target));
}
private struct JointData
{
public bool isValid { get; private set; }
public Vector3 position { get; private set; }
public Quaternion rotation { get; private set; }
public JointData(bool isValid, Vector3 position, Quaternion rotation)
{
this.isValid = isValid;
this.position = position;
this.rotation = rotation;
}
public static JointData Identify => new JointData(false, Vector3.zero, Quaternion.identity);
}
private struct HandData
{
public bool isTracked { get; private set; }
public int updateTime { get; private set; }
public JointData[] joints { get; private set; }
public HandData(JointData[] joints)
{
this.joints = joints;
isTracked = !this.joints.Any(x => x.isValid == false);
updateTime = Time.frameCount;
}
public void Update(JointData[] joints)
{
this.joints = joints;
isTracked = !this.joints.Any(x => x.isValid == false);
updateTime = Time.frameCount;
}
public static HandData Identify
{
get
{
JointData[] newJoints = new JointData[(int)HandJointType.Count];
for (int i = 0; i < newJoints.Length; i++)
{
newJoints[i] = JointData.Identify;
}
return new HandData(newJoints);
}
}
}
private static bool isInitInputActions = false;
private static List<InputActionMapping> inputActions = new List<InputActionMapping>();
private static HandData leftHand = HandData.Identify;
private static HandData rightHand = HandData.Identify;
#if UNITY_XR_HANDS
private static XRHandSubsystem handSubsystem = null;
#endif
#region Public Interface
/// <summary>
/// Get the pose state of the specified device.
/// </summary>
/// <param name="device">The device category.</param>
/// <param name="poseState">The pose state to be retrieved.</param>
/// <param name="eventResult">The result of the event.</param>
/// <returns>True if the pose state was successfully retrieved; otherwise, false.</returns>
public static bool GetPoseState(DeviceCategory device, PoseState poseState, out bool eventResult)
{
CheckInitialize();
eventResult = false;
if ((device == DeviceCategory.LeftHand || device == DeviceCategory.RightHand) && poseState == PoseState.IsTracked)
{
eventResult = IsHandTracked(GetHandedness(device));
return true;
}
else
{
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == device && x.poseState == poseState);
if (inputActionMapping == null) { return false; }
try
{
eventResult = inputActionMapping.inputAction.ReadValue<float>() > 0;
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}
/// <summary>
/// Get the pose state of the specified device.
/// </summary>
/// <param name="device">The device category.</param>
/// <param name="poseState">The pose state to be retrieved.</param>
/// <param name="eventResult">The result of the event.</param>
/// <returns>True if the pose state was successfully retrieved; otherwise, false.</returns>
public static bool GetPoseState(DeviceCategory device, PoseState poseState, out Vector3 eventResult)
{
CheckInitialize();
eventResult = Vector3.zero;
if ((device == DeviceCategory.LeftHand || device == DeviceCategory.RightHand) && poseState == PoseState.Position)
{
GetJointPose(GetHandedness(device), HandJointType.Wrist, out Pose jointPose);
eventResult = jointPose.position;
return true;
}
else
{
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == device && x.poseState == poseState);
if (inputActionMapping == null) { return false; }
try
{
eventResult = inputActionMapping.inputAction.ReadValue<Vector3>();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}
/// <summary>
/// Get the pose state of the specified device.
/// </summary>
/// <param name="device">The device category.</param>
/// <param name="poseState">The pose state to be retrieved.</param>
/// <param name="eventResult">The result of the event.</param>
/// <returns>True if the pose state was successfully retrieved; otherwise, false.</returns>
public static bool GetPoseState(DeviceCategory device, PoseState poseState, out Quaternion eventResult)
{
CheckInitialize();
eventResult = Quaternion.identity;
if ((device == DeviceCategory.LeftHand || device == DeviceCategory.RightHand) && poseState == PoseState.Rotation)
{
GetJointPose(GetHandedness(device), HandJointType.Wrist, out Pose jointPose);
eventResult = jointPose.rotation;
return true;
}
else
{
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == device && x.poseState == poseState);
if (inputActionMapping == null) { return false; }
try
{
eventResult = inputActionMapping.inputAction.ReadValue<Quaternion>();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}
/// <summary>
/// Check if a specified button event has toggled at this frame and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the button event for.</param>
/// <param name="buttonEvent">The specified button event to check.</param>
/// <param name="eventResult">Output whether the button event has toggled.</param>
/// <returns>Returns true if the button event was successfully retrieved, otherwise false.</returns>
public static bool GetButtonDown(Handedness handedness, ButtonEvent buttonEvent, out bool eventResult)
{
CheckInitialize();
eventResult = false;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetController(handedness) && x.buttonEvent == buttonEvent);
if (inputActionMapping != null)
{
eventResult = inputActionMapping.inputAction.WasPressedThisFrame();
return true;
}
return false;
}
/// <summary>
/// Check if a specified button event has toggled at this frame and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the button event for.</param>
/// <param name="buttonEvent">The specified button event to check.</param>
/// <param name="eventResult">Output whether the button event has toggled.</param>
/// <returns>Returns true if the button event was successfully retrieved, otherwise false.</returns>
public static bool GetButtonUp(Handedness handedness, ButtonEvent buttonEvent, out bool eventResult)
{
CheckInitialize();
eventResult = false;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetController(handedness) && x.buttonEvent == buttonEvent);
if (inputActionMapping != null)
{
eventResult = inputActionMapping.inputAction.WasReleasedThisFrame();
return true;
}
return false;
}
/// <summary>
/// Check if a specified button event has toggled and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the button event for.</param>
/// <param name="buttonEvent">The specified button event to check.</param>
/// <param name="eventResult">Output for the button event.</param>
/// <returns>Returns true if the button event was successfully retrieved, otherwise false.</returns>
public static bool GetButtonValue(Handedness handedness, ButtonEvent buttonEvent, out bool eventResult)
{
CheckInitialize();
eventResult = false;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetController(handedness) && x.buttonEvent == buttonEvent);
if (inputActionMapping != null)
{
try
{
eventResult = inputActionMapping.inputAction.ReadValue<float>() == 1;
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
return false;
}
/// <summary>
/// Check if a specified button event has toggled and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the button event for.</param>
/// <param name="buttonEvent">The specified button event to check.</param>
/// <param name="eventResult">Output for the button event.</param>
/// <returns>Returns true if the button event was successfully retrieved, otherwise false.</returns>
public static bool GetButtonValue(Handedness handedness, ButtonEvent buttonEvent, out float eventResult)
{
CheckInitialize();
eventResult = 0f;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetController(handedness) && x.buttonEvent == buttonEvent);
if (inputActionMapping != null)
{
try
{
eventResult = inputActionMapping.inputAction.ReadValue<float>();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
return false;
}
/// <summary>
/// Check if a specified button event has toggled and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the button event for.</param>
/// <param name="buttonEvent">The specified button event to check.</param>
/// <param name="eventResult">Output for the button event.</param>
/// <returns>Returns true if the button event was successfully retrieved, otherwise false.</returns>
public static bool GetButtonValue(Handedness handedness, ButtonEvent buttonEvent, out Vector2 eventResult)
{
CheckInitialize();
eventResult = Vector2.zero;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetController(handedness) && x.buttonEvent == buttonEvent);
if (inputActionMapping != null)
{
try
{
eventResult = inputActionMapping.inputAction.ReadValue<Vector2>();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
return false;
}
/// <summary>
/// Check if a specified hand event has toggled and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the hand event for.</param>
/// <param name="handEvent">The specified hand event to check.</param>
/// <param name="eventResult">Output for the hand event.</param>
/// <returns>Returns true if the hand event was successfully retrieved, otherwise false.</returns>
public static bool GetHandValue(Handedness handedness, HandEvent handEvent, out float eventResult)
{
CheckInitialize();
eventResult = 0;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetHand(handedness) && x.handEvent == handEvent);
if (inputActionMapping != null)
{
try
{
eventResult = inputActionMapping.inputAction.ReadValue<float>();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
return false;
}
/// <summary>
/// Check if a specified hand event has toggled and return the result.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check the hand event for.</param>
/// <param name="handEvent">The specified hand event to check.</param>
/// <param name="eventResult">Output for the hand event.</param>
/// <returns>Returns true if the hand event was successfully retrieved, otherwise false.</returns>
public static bool GetHandValue(Handedness handedness, HandEvent handEvent, out Pose eventResult)
{
CheckInitialize();
eventResult = Pose.identity;
InputActionMapping inputActionMapping = inputActions.FirstOrDefault(x => x.device == GetHand(handedness) && x.handEvent == handEvent);
if (inputActionMapping != null)
{
try
{
UnityEngine.XR.OpenXR.Input.Pose pose = inputActionMapping.inputAction.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>();
eventResult = new Pose(pose.position, pose.rotation);
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
return false;
}
/// <summary>
/// Retrieves the pose of a specified hand joint for the given handedness.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to get the joint pose for.</param>
/// <param name="joint">The specific hand joint to retrieve the pose of.</param>
/// <param name="jointPose">Outputs the pose of the specified hand joint.</param>
/// <returns>Returns true if the joint pose was successfully retrieved, otherwise false.</returns>
public static bool GetJointPose(Handedness handedness, HandJointType joint, out Pose jointPose)
{
CheckHandUpdated();
jointPose = Pose.identity;
if (handedness == Handedness.Left)
{
jointPose = new Pose(leftHand.joints[(int)joint].position, leftHand.joints[(int)joint].rotation);
return leftHand.joints[(int)joint].isValid;
}
else
{
jointPose = new Pose(rightHand.joints[(int)joint].position, rightHand.joints[(int)joint].rotation);
return rightHand.joints[(int)joint].isValid;
}
}
/// <summary>
/// Determines if the specified hand is currently being tracked.
/// </summary>
/// <param name="handedness">The handedness (left or right hand) to check for tracking.</param>
/// <returns>Returns true if the specified hand is being tracked, otherwise false.</returns>
public static bool IsHandTracked(Handedness handedness)
{
CheckHandUpdated();
return handedness == Handedness.Left ? leftHand.isTracked : rightHand.isTracked;
}
public static bool IsHandValidate()
{
ViveHandTracking viveHand = OpenXRSettings.Instance.GetFeature<ViveHandTracking>();
if (viveHand)
{
return true;
}
#if UNITY_XR_HANDS
HandTracking xrHand = OpenXRSettings.Instance.GetFeature<HandTracking>();
if (xrHand)
{
return true;
}
#endif
return false;
}
#endregion
[RuntimeInitializeOnLoadMethod]
private static bool CheckInitialize()
{
if (!isInitInputActions)
{
Initialized();
isInitInputActions = true;
}
return isInitInputActions;
}
private static void Initialized()
{
#region Head
inputActions.Add(new InputActionMapping("<XRHMD>/isTracked", DeviceCategory.HMD, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<XRHMD>/centerEyePosition", DeviceCategory.HMD, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<XRHMD>/centerEyeRotation", DeviceCategory.HMD, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<XRHMD>/centerEyeVelocity", DeviceCategory.HMD, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<XRHMD>/centerEyeAngularVelocity", DeviceCategory.HMD, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<XRHMD>/centerEyeAcceleration", DeviceCategory.HMD, poseState: PoseState.Acceleration));
inputActions.Add(new InputActionMapping("<XRHMD>/centerEyeAngularAcceleration", DeviceCategory.HMD, poseState: PoseState.AngularAcceleration));
#endregion
#region Eye
inputActions.Add(new InputActionMapping("<EyeGaze>/pose/isTracked", DeviceCategory.CenterEye, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<EyeGaze>/pose/position", DeviceCategory.CenterEye, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<EyeGaze>/pose/rotation", DeviceCategory.CenterEye, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<EyeGaze>/pose/velocity", DeviceCategory.CenterEye, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<EyeGaze>/pose/angularVelocity", DeviceCategory.CenterEye, poseState: PoseState.AngularVelocity));
#endregion
#region Controller
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/isTracked", DeviceCategory.LeftController, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/pointerPosition", DeviceCategory.LeftController, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/pointerRotation", DeviceCategory.LeftController, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/deviceVelocity", DeviceCategory.LeftController, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/deviceAngularVelocity", DeviceCategory.LeftController, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/deviceAcceleration", DeviceCategory.LeftController, poseState: PoseState.Acceleration));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/deviceAngularAcceleration", DeviceCategory.LeftController, poseState: PoseState.AngularAcceleration));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{grip}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.GripValue));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{gripButton}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.GripPress));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{trigger}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.TriggerValue));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/triggerTouched", DeviceCategory.LeftController, buttonEvent: ButtonEvent.TriggerTouch));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{triggerButton}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.TriggerPress));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{primary2DAxis}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Primary2DAxisValue));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{primary2DAxisTouch}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Primary2DAxisTouch));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{primary2DAxisClick}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Primary2DAxisPress));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{secondary2DAxis}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Secondary2DAxisValue));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{secondary2DAxisTouch}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Secondary2DAxisTouch));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{secondary2DAxisClick}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Secondary2DAxisPress));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{primaryButton}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.PrimaryButton));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/{secondaryButton}", DeviceCategory.LeftController, buttonEvent: ButtonEvent.SecondaryButton));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/parkingTouched", DeviceCategory.LeftController, buttonEvent: ButtonEvent.ParkingTouch));
inputActions.Add(new InputActionMapping("<XRController>{LeftHand}/menu", DeviceCategory.LeftController, buttonEvent: ButtonEvent.Menu));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/isTracked", DeviceCategory.RightController, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/pointerPosition", DeviceCategory.RightController, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/pointerRotation", DeviceCategory.RightController, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/deviceVelocity", DeviceCategory.RightController, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/deviceAngularVelocity", DeviceCategory.RightController, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/deviceAcceleration", DeviceCategory.RightController, poseState: PoseState.Acceleration));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/deviceAngularAcceleration", DeviceCategory.RightController, poseState: PoseState.AngularAcceleration));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{grip}", DeviceCategory.RightController, buttonEvent: ButtonEvent.GripValue));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{gripButton}", DeviceCategory.RightController, buttonEvent: ButtonEvent.GripPress));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{trigger}", DeviceCategory.RightController, buttonEvent: ButtonEvent.TriggerValue));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/triggerTouched", DeviceCategory.RightController, buttonEvent: ButtonEvent.TriggerTouch));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{triggerButton}", DeviceCategory.RightController, buttonEvent: ButtonEvent.TriggerPress));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{primary2DAxis}", DeviceCategory.RightController, buttonEvent: ButtonEvent.Primary2DAxisValue));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{primary2DAxisTouch}", DeviceCategory.RightController, buttonEvent: ButtonEvent.Primary2DAxisTouch));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{primary2DAxisClick}", DeviceCategory.RightController, buttonEvent: ButtonEvent.Primary2DAxisPress));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{secondary2DAxis}", DeviceCategory.RightController, buttonEvent: ButtonEvent.Secondary2DAxisValue));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{secondary2DAxisTouch}", DeviceCategory.RightController, buttonEvent: ButtonEvent.Secondary2DAxisTouch));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{secondary2DAxisClick}", DeviceCategory.RightController, buttonEvent: ButtonEvent.Secondary2DAxisPress));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{primaryButton}", DeviceCategory.RightController, buttonEvent: ButtonEvent.PrimaryButton));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/{secondaryButton}", DeviceCategory.RightController, buttonEvent: ButtonEvent.SecondaryButton));
inputActions.Add(new InputActionMapping("<XRController>{RightHand}/parkingTouched", DeviceCategory.RightController, buttonEvent: ButtonEvent.ParkingTouch));
#endregion
#region Hand
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{LeftHand}/selectValue", DeviceCategory.LeftHand, handEvent: HandEvent.PinchValue));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{LeftHand}/pointerPose", DeviceCategory.LeftHand, handEvent: HandEvent.PinchPose));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{LeftHand}/gripValue", DeviceCategory.LeftHand, handEvent: HandEvent.GraspValue));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{LeftHand}/devicePose", DeviceCategory.LeftHand, handEvent: HandEvent.GraspPose));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{RightHand}/selectValue", DeviceCategory.RightHand, handEvent: HandEvent.PinchValue));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{RightHand}/pointerPose", DeviceCategory.RightHand, handEvent: HandEvent.PinchPose));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{RightHand}/gripValue", DeviceCategory.RightHand, handEvent: HandEvent.GraspValue));
inputActions.Add(new InputActionMapping("<ViveHandInteraction>{RightHand}/devicePose", DeviceCategory.RightHand, handEvent: HandEvent.GraspPose));
#endregion
#region Tracker
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 0}/devicePose/isTracked", DeviceCategory.Tracker0, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 0}/devicePosition", DeviceCategory.Tracker0, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 0}/deviceRotation", DeviceCategory.Tracker0, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 0}/devicePose/velocity", DeviceCategory.Tracker0, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 0}/devicePose/angularVelocity", DeviceCategory.Tracker0, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 1}/devicePose/isTracked", DeviceCategory.Tracker1, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 1}/devicePosition", DeviceCategory.Tracker1, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 1}/deviceRotation", DeviceCategory.Tracker1, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 1}/devicePose/velocity", DeviceCategory.Tracker1, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 1}/devicePose/angularVelocity", DeviceCategory.Tracker1, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 2}/devicePose/isTracked", DeviceCategory.Tracker2, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 2}/devicePosition", DeviceCategory.Tracker2, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 2}/deviceRotation", DeviceCategory.Tracker2, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 2}/devicePose/velocity", DeviceCategory.Tracker2, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 2}/devicePose/angularVelocity", DeviceCategory.Tracker2, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 3}/devicePose/isTracked", DeviceCategory.Tracker3, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 3}/devicePosition", DeviceCategory.Tracker3, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 3}/deviceRotation", DeviceCategory.Tracker3, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 3}/devicePose/velocity", DeviceCategory.Tracker3, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 3}/devicePose/angularVelocity", DeviceCategory.Tracker3, poseState: PoseState.AngularVelocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 4}/devicePose/isTracked", DeviceCategory.Tracker4, poseState: PoseState.IsTracked));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 4}/devicePosition", DeviceCategory.Tracker4, poseState: PoseState.Position));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 4}/deviceRotation", DeviceCategory.Tracker4, poseState: PoseState.Rotation));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 4}/devicePose/velocity", DeviceCategory.Tracker4, poseState: PoseState.Velocity));
inputActions.Add(new InputActionMapping("<ViveXRTracker>{Ultimate Tracker 4}/devicePose/angularVelocity", DeviceCategory.Tracker4, poseState: PoseState.AngularVelocity));
#endregion
}
private static void CheckHandUpdated()
{
if (Time.frameCount > leftHand.updateTime ||
Time.frameCount > rightHand.updateTime)
{
ViveHandTracking viveHand = OpenXRSettings.Instance.GetFeature<ViveHandTracking>();
if (viveHand)
{
UpdateViveHand(true, viveHand);
UpdateViveHand(false, viveHand);
}
#if UNITY_XR_HANDS
HandTracking xrHand = OpenXRSettings.Instance.GetFeature<HandTracking>();
if (xrHand)
{
if (handSubsystem == null || !handSubsystem.running)
{
if (handSubsystem != null && !handSubsystem.running)
{
handSubsystem.updatedHands -= OnUpdatedHands;
handSubsystem = null;
}
var handSubsystems = new List<XRHandSubsystem>();
SubsystemManager.GetSubsystems(handSubsystems);
for (var i = 0; i < handSubsystems.Count; ++i)
{
var xrHnad = handSubsystems[i];
if (xrHnad.running)
{
handSubsystem = xrHnad;
break;
}
}
if (handSubsystem != null && handSubsystem.running)
{
handSubsystem.updatedHands += OnUpdatedHands;
}
}
}
#endif
}
}
private static void UpdateViveHand(bool isLeft, ViveHandTracking viveHand)
{
bool isUpdated = viveHand.GetJointLocations(isLeft, out XrHandJointLocationEXT[] viveJoints);
JointData[] joints = new JointData[viveJoints.Length];
for (int i = 0; i < joints.Length; i++)
{
bool isValid = isUpdated &&
viveJoints[i].locationFlags.HasFlag(XrSpaceLocationFlags.XR_SPACE_LOCATION_POSITION_TRACKED_BIT) &&
viveJoints[i].locationFlags.HasFlag(XrSpaceLocationFlags.XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT);
Vector3 position = viveJoints[i].pose.position.ToUnityVector();
Quaternion rotation = viveJoints[i].pose.orientation.ToUnityQuaternion();
joints[i] = new JointData(isValid, position, rotation);
}
if (isLeft)
{
leftHand.Update(joints);
}
else
{
rightHand.Update(joints);
}
}
#if UNITY_XR_HANDS
private static void OnUpdatedHands(XRHandSubsystem xrHnad, XRHandSubsystem.UpdateSuccessFlags flags, XRHandSubsystem.UpdateType type)
{
if (xrHnad != null && xrHnad.running)
{
UpdateXRHand(true, xrHnad, flags.HasFlag(XRHandSubsystem.UpdateSuccessFlags.LeftHandJoints));
UpdateXRHand(false, xrHnad, flags.HasFlag(XRHandSubsystem.UpdateSuccessFlags.RightHandJoints));
}
}
private static void UpdateXRHand(bool isLeft, XRHandSubsystem xrHnad, bool isUpdated)
{
JointData[] joints = new JointData[(int)HandJointType.Count];
for (int i = 0; i < joints.Length; i++)
{
XRHandJointID jointId = JointTypeToXRId(i);
XRHandJoint joint = (isLeft ? xrHnad.leftHand : xrHnad.rightHand).GetJoint(jointId);
bool isValid = isUpdated && joint.trackingState.HasFlag(XRHandJointTrackingState.Pose);
joint.TryGetPose(out Pose pose);
joints[i] = new JointData(isValid, pose.position, pose.rotation);
}
if (isLeft)
{
leftHand.Update(joints);
}
else
{
rightHand.Update(joints);
}
}
private static XRHandJointID JointTypeToXRId(int id)
{
switch (id)
{
case 0:
return XRHandJointID.Palm;
case 1:
return XRHandJointID.Wrist;
default:
return (XRHandJointID)(id + 1);
}
}
#endif
private static DeviceCategory GetController(Handedness handedness)
{
DeviceCategory device = DeviceCategory.None;
switch (handedness)
{
case Handedness.Left:
device = DeviceCategory.LeftController;
break;
case Handedness.Right:
device = DeviceCategory.RightController;
break;
}
return device;
}
private static DeviceCategory GetHand(Handedness handedness)
{
DeviceCategory device = DeviceCategory.None;
switch (handedness)
{
case Handedness.Left:
device = DeviceCategory.LeftHand;
break;
case Handedness.Right:
device = DeviceCategory.RightHand;
break;
}
return device;
}
private static Handedness GetHandedness(DeviceCategory device)
{
Handedness handedness = Handedness.None;
switch (device)
{
case DeviceCategory.LeftHand:
handedness = Handedness.Left;
break;
case DeviceCategory.RightHand:
handedness = Handedness.Right;
break;
}
return handedness;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9a887cb158a37cf45b17458a4f27d7ee
guid: 744a8b8476e9f394b87927173e7994b0
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a3a84b60570d8b243984fbe7169ca44a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,390 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VIVE.OpenXR.Feature;
using static VIVE.OpenXR.Feature.FutureWrapper;
namespace VIVE.OpenXR.Toolkits
{
/// <summary>
/// FutureTask is not a C# Task. It is a wrapper for OpenXR Future.
/// Each OpenXR Future may have its own FutureTask type because the result and the
/// complete function are different.
/// However the poll and complete are common. This class use c# Task to poll the future.
/// You can <see cref="Cancel the future"/> if you do not want to wait for the result.
/// However Cancel should be called before the Complete.
/// When <see cref="IsPollCompleted"/> is true, call <see cref="Complete"/> to complete the future and get the result.
/// </summary>
/// <typeparam name="TResult">You can customize the type depending on the complete's result.</typeparam>
public class FutureTask<TResult> : IDisposable
{
private Task<(XrResult, XrFutureStateEXT)> pollTask;
private Task autoCompleteTask;
Func<IntPtr, TResult> completeFunc;
private CancellationTokenSource cts;
private IntPtr future;
bool autoComplete = false;
private int pollIntervalMS = 10;
/// <summary>
/// Set poll inverval in milliseconds. The value will be clamped between 1 and 2000.
/// </summary>
public int PollIntervalMS {
get => pollIntervalMS;
set => pollIntervalMS = Math.Clamp(value, 1, 2000);
}
public bool IsAutoComplete => autoComplete;
bool isCompleted = false;
public bool IsCompleted => isCompleted;
public bool Debug { get; set; } = false;
/// <summary>
/// The FutureTask is used to poll and complete the future.
/// Once the FutureTask is create, poll will start running in the period of pollIntervalMS.
/// if auto complete is set, the future will be do completed once user check IsPollCompleted and IsCompleted.
/// I prefered to use non-autoComplete.
/// If no auto complete, you can cancel the task and no need to free resouce.
/// Once it completed, you need handle the result to avoid leakage.
/// </summary>
/// <param name="future"></param>
/// <param name="completeFunc"></param>
/// <param name="pollIntervalMS">Set poll inverval in milliseconds. The value will be clamped between 1 and 2000.</param>
/// <param name="autoComplete">If true, do Complete when check IsPollCompleted and IsCompleted</param>
public FutureTask(IntPtr future, Func<IntPtr, TResult> completeFunc, int pollIntervalMS = 10, bool autoComplete = false)
{
cts = new CancellationTokenSource();
this.completeFunc = completeFunc;
this.future = future;
this.pollIntervalMS = Math.Clamp(pollIntervalMS, 1, 2000);
// User may get PollTask and run. So, we need to make sure the pollTask is created.
pollTask = MakePollTask(this, cts.Token);
// will set autoComplete true in AutoComplete.
this.autoComplete = false;
if (autoComplete)
AutoComplete();
}
/// <summary>
/// AutoComplete will complete the future once the poll task is ready and success.
/// If you want to handle error, you should not use AutoComplete.
/// </summary>
public void AutoComplete()
{
if (autoComplete)
return;
autoComplete = true;
autoCompleteTask = pollTask.ContinueWith(task =>
{
// If the task is cancelled or faulted, we do not need to complete the future.
if (task.IsCanceled || task.IsFaulted)
{
isCompleted = true;
return;
}
var result = task.Result;
// Make sure call Complete only if poll task is ready and success.
if (result.Item1 == XrResult.XR_SUCCESS)
{
if (result.Item2 == XrFutureStateEXT.Ready)
{
Complete();
}
}
isCompleted = true;
});
}
/// <summary>
/// Used for create FromResult if you need return the result immediately.
/// </summary>
/// <param name="pollTask"></param>
/// <param name="completeFunc"></param>
FutureTask(Task<(XrResult, XrFutureStateEXT)> pollTask, Func<IntPtr, TResult> completeFunc)
{
this.pollTask = pollTask;
this.completeFunc = completeFunc;
this.future = IntPtr.Zero;
}
public Task<(XrResult, XrFutureStateEXT)> PollTask => pollTask;
/// <summary>
/// If AutoComplete is set, the task will be created. Otherwise, it will be null.
/// </summary>
public Task AutoCompleteTask => autoCompleteTask;
public bool IsPollCompleted => pollTask.IsCompleted;
public XrResult PollResult => pollTask.Result.Item1;
public IntPtr Future => future;
/// <summary>
/// Cancel the future. If the future is not completed yet, it will be cancelled. Otherwise, nothing will happen.
/// </summary>
public void Cancel()
{
if (!isCompleted)
{
cts?.Cancel();
FutureWrapper.Instance?.CancelFuture(future);
}
future = IntPtr.Zero;
}
/// <summary>
/// Make sure do Complete after IsPollCompleted. If the future is not poll completed yet, throw exception.
/// </summary>
/// <returns>The result of the completeFunc.</returns>
/// <exception cref="Exception">Thrown when the pollTask is not completed yet.</exception>
public TResult Complete()
{
if (isCompleted)
return result;
if (pollTask.IsCompleted)
{
if (this.Debug)
UnityEngine.Debug.Log("FutureTask is completed.");
isCompleted = true;
if (pollTask.Result.Item1 == XrResult.XR_SUCCESS)
{
result = completeFunc(future);
isCompleted = true;
return result;
}
if (this.Debug)
UnityEngine.Debug.Log("FutureTask is completed with error. Check if pollTask result error.");
return default;
}
else
{
throw new Exception("FutureTask is not completed yet.");
}
}
/// <summary>
/// Wait until poll task is completed. If the task is not completed, it will block the thread.
/// If AutoComplete is set, wait until the complete task is completed.
/// </summary>
public void Wait()
{
pollTask.Wait();
if (autoComplete)
autoCompleteTask.Wait();
}
TResult result;
private bool disposedValue;
/// <summary>
/// This Result did not block the thread. If not completed, it will return undefined value. Make sure you call it when Complete is done.
/// </summary>
public TResult Result => result;
public static FutureTask<TResult> FromResult(TResult result)
{
return new FutureTask<TResult>(Task.FromResult((XrResult.XR_SUCCESS, XrFutureStateEXT.Ready)), (future) => result);
}
/// <summary>
/// Poll until the future is ready. Caceled if the cts is cancelled. But the future will not be cancelled.
/// </summary>
/// <param name="futureTask"></param>
/// <param name="pollIntervalMS"></param>
/// <param name="cts"></param>
/// <returns></returns>
static async Task<(XrResult, XrFutureStateEXT)> MakePollTask(FutureTask<TResult> futureTask, CancellationToken ct)
{
XrFuturePollInfoEXT pollInfo = new XrFuturePollInfoEXT()
{
type = XrStructureType.XR_TYPE_FUTURE_POLL_INFO_EXT,
next = IntPtr.Zero,
future = futureTask.Future
};
do
{
ct.ThrowIfCancellationRequested();
XrResult ret = FutureWrapper.Instance.PollFuture(ref pollInfo, out FutureWrapper.XrFuturePollResultEXT pollResult);
if (ret == XrResult.XR_SUCCESS)
{
if (pollResult.state == XrFutureStateEXT.Ready)
{
if (futureTask.Debug)
UnityEngine.Debug.Log("Future is ready.");
return (XrResult.XR_SUCCESS, pollResult.state);
}
else if (pollResult.state == XrFutureStateEXT.Pending)
{
if (futureTask.Debug)
UnityEngine.Debug.Log("Wait for future.");
await Task.Delay(futureTask.pollIntervalMS);
continue;
}
}
else
{
return (ret, XrFutureStateEXT.None);
}
} while (true);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
pollTask?.Dispose();
pollTask = null;
autoCompleteTask?.Dispose();
autoCompleteTask = null;
cts?.Dispose();
cts = null;
}
if (future != IntPtr.Zero && !isCompleted)
FutureWrapper.Instance?.CancelFuture(future);
future = IntPtr.Zero;
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Help to manage the future task. Tasks are name less. In order to manager tasks,
/// additonal information are required. And this class is used to store those information.
/// Helps to retrive the task by the identity, or retrive the identity by the task.
/// </summary>
/// <typeparam name="Identity">What the task work for. How to identify this task.</typeparam>
/// <typeparam name="Output">The task's output type, for example, XrResult or Tuple.</typeparam>
public class FutureTaskManager<Identity, TResult> : IDisposable
{
readonly List<(Identity, FutureTask<TResult>)> tasks = new List<(Identity, FutureTask<TResult>)>();
private bool disposedValue;
public FutureTaskManager() { }
public FutureTask<TResult> GetTask(Identity identity)
{
return tasks.FirstOrDefault(x => x.Item1.Equals(identity)).Item2;
}
/// <summary>
/// Add a task to the manager.
/// </summary>
/// <param name="identity"></param>
/// <param name="task"></param>
public void AddTask(Identity identity, FutureTask<TResult> task)
{
tasks.Add((identity, task));
}
/// <summary>
/// Remove keeped task and cancel it If task is not completed.
/// </summary>
/// <param name="task"></param>
public void RemoveTask(Identity identity)
{
var task = tasks.FirstOrDefault(x => x.Item1.Equals(identity));
if (task.Item2 != null)
{
task.Item2.Cancel();
task.Item2.Dispose();
}
tasks.Remove(task);
}
/// <summary>
/// Remove keeped task and cancel it If task is not completed.
/// </summary>
/// <param name="task"></param>
public void RemoveTask(FutureTask<TResult> task)
{
var t = tasks.FirstOrDefault(x => x.Item2 == task);
if (t.Item2 != null)
{
t.Item2.Cancel();
t.Item2.Dispose();
}
tasks.Remove(t);
}
/// <summary>
/// Get all tasks's list.
/// </summary>
/// <returns></returns>
public List<(Identity, FutureTask<TResult>)> GetTasks()
{
return tasks;
}
/// <summary>
/// Check if has any task.
/// </summary>
/// <returns></returns>
public bool IsEmpty()
{
return tasks.Count == 0;
}
/// <summary>
/// Clear all tasks and cancel them. If tasks are auto completed, make sure their results handled.
/// Otherwise, the resource will be leaked.
/// </summary>
/// <param name="cancelTask"></param>
public void Clear(bool cancelTask = true)
{
if (cancelTask)
{
foreach (var task in tasks)
{
if (task.Item2 != null)
{
task.Item2.Cancel();
task.Item2.Dispose();
}
}
}
tasks.Clear();
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
Clear();
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f234725d9eefc7540843fd691e94553a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 10e073adb659279408a85f7b31ed9d91
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,783 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.XR.OpenXR;
using System.Threading.Tasks;
using VIVE.OpenXR;
namespace VIVE.OpenXR.Passthrough
{
public static class PassthroughAPI
{
#region LOG
const string LOG_TAG = "VIVE.OpenXR.Passthrough.PassthroughAPI";
static void DEBUG(string msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
static void WARNING(string msg) { Debug.LogWarningFormat("{0} {1}", LOG_TAG, msg); }
static void ERROR(string msg) { Debug.LogErrorFormat("{0} {1}", LOG_TAG, msg); }
#endregion
private static VivePassthrough passthroughFeature = null;
private static bool AssertFeature()
{
if (passthroughFeature == null) { passthroughFeature = OpenXRSettings.Instance.GetFeature<VivePassthrough>(); }
if (passthroughFeature) { return true; }
return false;
}
#if UNITY_STANDALONE
private static Dictionary<XrPassthroughHTC, XrCompositionLayerPassthroughHTC> passthrough2Layer = new Dictionary<XrPassthroughHTC, XrCompositionLayerPassthroughHTC>();
private static Dictionary<XrPassthroughHTC, IntPtr> passthrough2LayerPtr = new Dictionary<XrPassthroughHTC, IntPtr>();
private static Dictionary<XrPassthroughHTC, bool> passthrough2IsUnderLay= new Dictionary<XrPassthroughHTC, bool>();
private static Dictionary<XrPassthroughHTC, XrPassthroughMeshTransformInfoHTC> passthrough2meshTransform = new Dictionary<XrPassthroughHTC, XrPassthroughMeshTransformInfoHTC>();
private static Dictionary<XrPassthroughHTC, IntPtr> passthrough2meshTransformInfoPtr = new Dictionary<XrPassthroughHTC, IntPtr>();
#endif
#region Public APIs
/// <summary>
/// Creates a fullscreen passthrough.
/// Passthroughs will be destroyed automatically when the current <see cref="XrSession"/> is destroyed.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="layerType">The <see cref="LayerType"/> specifies whether the passthrough is an overlay or underlay.</param>
/// <param name="onDestroyPassthroughSessionHandler">A <see cref="VivePassthrough.OnPassthroughSessionDestroyDelegate">delegate</see> will be invoked when the current OpenXR Session is going to be destroyed.</param>
/// <param name="alpha">The alpha value of the passthrough layer within the range [0, 1] where 1 (Opaque) is default.</param>
/// <param name="compositionDepth">The composition depth relative to other composition layers if present where 0 is default.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static XrResult CreatePlanarPassthrough(out XrPassthroughHTC passthrough, CompositionLayer.LayerType layerType, VivePassthrough.OnPassthroughSessionDestroyDelegate onDestroyPassthroughSessionHandler = null, float alpha = 1f, uint compositionDepth = 0)
{
passthrough = 0;
XrResult res = XrResult.XR_ERROR_RUNTIME_FAILURE;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return res;
}
XrPassthroughCreateInfoHTC createInfo = new XrPassthroughCreateInfoHTC(
XrStructureType.XR_TYPE_PASSTHROUGH_CREATE_INFO_HTC,
new IntPtr(6), //Enter IntPtr(0) for backward compatibility (using createPassthrough to enable the passthrough feature), or enter IntPtr(6) to enable the passthrough feature based on the layer submitted to endframe.
XrPassthroughFormHTC.XR_PASSTHROUGH_FORM_PLANAR_HTC
);
#if UNITY_ANDROID
res = passthroughFeature.CreatePassthroughHTC(createInfo, out passthrough, layerType, compositionDepth, onDestroyPassthroughSessionHandler);
DEBUG("CreatePlanarPassthrough() CreatePassthroughHTC result: " + res + ", passthrough: " + passthrough);
#endif
#if UNITY_STANDALONE
res = XR_HTC_passthrough.xrCreatePassthroughHTC(createInfo, out passthrough);
if(res == XrResult.XR_SUCCESS)
{
XrPassthroughColorHTC passthroughColor = new XrPassthroughColorHTC(
in_type: XrStructureType.XR_TYPE_PASSTHROUGH_COLOR_HTC,
in_next: IntPtr.Zero,
in_alpha: alpha);
XrCompositionLayerPassthroughHTC compositionLayerPassthrough = new XrCompositionLayerPassthroughHTC(
in_type: XrStructureType.XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_HTC,
in_next: IntPtr.Zero,
in_layerFlags: (UInt64)XrCompositionLayerFlagBits.XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT,
in_space: 0,
in_passthrough: passthrough,
in_color: passthroughColor);
passthrough2Layer.Add(passthrough, compositionLayerPassthrough);
IntPtr layerPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(XrCompositionLayerPassthroughHTC)));
passthrough2LayerPtr.Add(passthrough, layerPtr);
if (layerType == CompositionLayer.LayerType.Underlay)
passthrough2IsUnderLay.Add(passthrough, true);
if (layerType == CompositionLayer.LayerType.Overlay)
passthrough2IsUnderLay.Add(passthrough, false);
}
#endif
if (res == XrResult.XR_SUCCESS)
{
SetPassthroughAlpha(passthrough, alpha);
}
return res;
}
/// <summary>
/// Creates a projected passthrough (i.e. Passthrough is only partially visible). Visible region of the projected passthrough is determined by the mesh and its transform.
/// Passthroughs will be destroyed automatically when the current <see cref="XrSession"/> is destroyed.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="layerType">The <see cref="LayerType"/> specifies whether the passthrough is an overlay or underlay.</param>
/// <param name="vertexBuffer">Positions of the vertices in the mesh.</param>
/// <param name="indexBuffer">List of triangles represented by indices into the <paramref name="vertexBuffer"/>.</param>
/// <param name="spaceType">The projected passthrough's <see cref="ProjectedPassthroughSpaceType"/></param>
/// <param name="meshPosition">Position of the mesh.</param>
/// <param name="meshOrientation">Orientation of the mesh.</param>
/// <param name="meshScale">Scale of the mesh.</param>
/// <param name="onDestroyPassthroughSessionHandler">A <see cref="VivePassthrough.OnPassthroughSessionDestroyDelegate">delegate</see> will be invoked when the current OpenXR Session is going to be destroyed.</param>
/// <param name="alpha">The alpha value of the passthrough layer within the range [0, 1] where 1 (Opaque) is default.</param>
/// <param name="compositionDepth">The composition depth relative to other composition layers if present where 0 is default.</param>
/// <param name="trackingToWorldSpace">Specify whether or not the position and rotation of the mesh transform have to be converted from tracking space to world space.</param>
/// <param name="convertFromUnityToOpenXR">Specify whether or not the parameters <paramref name="vertexBuffer"/>, <paramref name="indexBuffer"/>, <paramref name="meshPosition"/> and <paramref name="meshOrientation"/> have to be converted to OpenXR coordinate.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static XrResult CreateProjectedPassthrough(out XrPassthroughHTC passthrough, CompositionLayer.LayerType layerType,
[In, Out] Vector3[] vertexBuffer, [In, Out] int[] indexBuffer, //For Mesh
ProjectedPassthroughSpaceType spaceType, Vector3 meshPosition, Quaternion meshOrientation, Vector3 meshScale, //For Mesh Transform
VivePassthrough.OnPassthroughSessionDestroyDelegate onDestroyPassthroughSessionHandler = null,
float alpha = 1f, uint compositionDepth = 0, bool trackingToWorldSpace = true, bool convertFromUnityToOpenXR = true)
{
passthrough = 0;
XrResult res = XrResult.XR_ERROR_RUNTIME_FAILURE;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return res;
}
if (vertexBuffer.Length < 3 || indexBuffer.Length % 3 != 0) //Must have at least 3 vertices and complete triangles
{
ERROR("Mesh data invalid.");
return res;
}
XrPassthroughCreateInfoHTC createInfo = new XrPassthroughCreateInfoHTC(
XrStructureType.XR_TYPE_PASSTHROUGH_CREATE_INFO_HTC,
new IntPtr(6), //Enter IntPtr(0) for backward compatibility (using createPassthrough to enable the passthrough feature), or enter IntPtr(6) to enable the passthrough feature based on the layer submitted to endframe.
XrPassthroughFormHTC.XR_PASSTHROUGH_FORM_PROJECTED_HTC
);
#if UNITY_STANDALONE
res = XR_HTC_passthrough.xrCreatePassthroughHTC(createInfo, out passthrough);
if (res == XrResult.XR_SUCCESS)
{
XrPassthroughMeshTransformInfoHTC PassthroughMeshTransformInfo = new XrPassthroughMeshTransformInfoHTC(
in_type: XrStructureType.XR_TYPE_PASSTHROUGH_MESH_TRANSFORM_INFO_HTC,
in_next: IntPtr.Zero,
in_vertexCount: 0,
in_vertices: new XrVector3f[0],
in_indexCount: 0,
in_indices: new UInt32[0],
in_baseSpace: XR_HTC_passthrough.Interop.GetTrackingSpace(),
in_time: XR_HTC_passthrough.Interop.GetFrameState().predictedDisplayTime,
in_pose: new XrPosef(),
in_scale: new XrVector3f()
);
IntPtr meshTransformInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(XrPassthroughMeshTransformInfoHTC)));
Marshal.StructureToPtr(PassthroughMeshTransformInfo, meshTransformInfoPtr, false);
XrPassthroughColorHTC passthroughColor = new XrPassthroughColorHTC(
in_type: XrStructureType.XR_TYPE_PASSTHROUGH_COLOR_HTC,
in_next: IntPtr.Zero,
in_alpha: alpha);
XrCompositionLayerPassthroughHTC compositionLayerPassthrough = new XrCompositionLayerPassthroughHTC(
in_type: XrStructureType.XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_HTC,
in_next: meshTransformInfoPtr,
in_layerFlags: (UInt64)XrCompositionLayerFlagBits.XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT,
in_space: 0,
in_passthrough: passthrough,
in_color: passthroughColor);
passthrough2meshTransform.Add(passthrough, PassthroughMeshTransformInfo);
passthrough2meshTransformInfoPtr.Add(passthrough, meshTransformInfoPtr);
passthrough2Layer.Add(passthrough, compositionLayerPassthrough);
IntPtr layerPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(XrCompositionLayerPassthroughHTC)));
passthrough2LayerPtr.Add(passthrough, layerPtr);
if (layerType == CompositionLayer.LayerType.Underlay)
passthrough2IsUnderLay.Add(passthrough, true);
if (layerType == CompositionLayer.LayerType.Overlay)
passthrough2IsUnderLay.Add(passthrough, false);
}
#endif
#if UNITY_ANDROID
res = passthroughFeature.CreatePassthroughHTC(createInfo, out passthrough, layerType, compositionDepth, onDestroyPassthroughSessionHandler);
DEBUG("CreateProjectedPassthrough() CreatePassthroughHTC result: " + res + ", passthrough: " + passthrough);
#endif
if (res == XrResult.XR_SUCCESS)
{
SetPassthroughAlpha(passthrough, alpha);
SetProjectedPassthroughMesh(passthrough, vertexBuffer, indexBuffer, convertFromUnityToOpenXR);
SetProjectedPassthroughMeshTransform(passthrough, spaceType, meshPosition, meshOrientation, meshScale, trackingToWorldSpace, convertFromUnityToOpenXR);
}
return res;
}
/// <summary>
/// Creates a projected passthrough (i.e. Passthrough is only partially visible). Visible region of the projected passthrough is determined by the mesh and its transform.
/// Passthroughs will be destroyed automatically when the current <see cref="XrSession"/> is destroyed.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="layerType">The <see cref="LayerType"/> specifies whether the passthrough is an overlay or underlay.</param>
/// <param name="onDestroyPassthroughSessionHandler">A <see cref="VivePassthrough.OnPassthroughSessionDestroyDelegate">delegate</see> will be invoked when the current OpenXR Session is going to be destroyed.</param>
/// <param name="alpha">The alpha value of the passthrough layer within the range [0, 1] where 1 (Opaque) is default.</param>
/// <param name="compositionDepth">The composition depth relative to other composition layers if present where 0 is default.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static XrResult CreateProjectedPassthrough(out XrPassthroughHTC passthrough, CompositionLayer.LayerType layerType, VivePassthrough.OnPassthroughSessionDestroyDelegate onDestroyPassthroughSessionHandler = null, float alpha = 1f, uint compositionDepth = 0)
{
passthrough = 0;
XrResult res = XrResult.XR_ERROR_RUNTIME_FAILURE;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return res;
}
XrPassthroughCreateInfoHTC createInfo = new XrPassthroughCreateInfoHTC(
XrStructureType.XR_TYPE_PASSTHROUGH_CREATE_INFO_HTC,
new IntPtr(6), //Enter IntPtr(0) for backward compatibility (using createPassthrough to enable the passthrough feature), or enter IntPtr(6) to enable the passthrough feature based on the layer submitted to endframe.
XrPassthroughFormHTC.XR_PASSTHROUGH_FORM_PROJECTED_HTC
);
#if UNITY_STANDALONE
res = XR_HTC_passthrough.xrCreatePassthroughHTC(createInfo, out passthrough);
if (res == XrResult.XR_SUCCESS)
{
XrPassthroughMeshTransformInfoHTC PassthroughMeshTransformInfo = new XrPassthroughMeshTransformInfoHTC(
in_type: XrStructureType.XR_TYPE_PASSTHROUGH_MESH_TRANSFORM_INFO_HTC,
in_next: IntPtr.Zero,
in_vertexCount: 0,
in_vertices: new XrVector3f[0],
in_indexCount: 0,
in_indices: new UInt32[0],
in_baseSpace: XR_HTC_passthrough.Interop.GetTrackingSpace(),
in_time: XR_HTC_passthrough.Interop.GetFrameState().predictedDisplayTime,
in_pose: new XrPosef(),
in_scale: new XrVector3f()
);
IntPtr meshTransformInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(XrPassthroughMeshTransformInfoHTC)));
Marshal.StructureToPtr(PassthroughMeshTransformInfo, meshTransformInfoPtr, false);
XrPassthroughColorHTC passthroughColor = new XrPassthroughColorHTC(
in_type: XrStructureType.XR_TYPE_PASSTHROUGH_COLOR_HTC,
in_next: IntPtr.Zero,
in_alpha: alpha);
XrCompositionLayerPassthroughHTC compositionLayerPassthrough = new XrCompositionLayerPassthroughHTC(
in_type: XrStructureType.XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_HTC,
in_next: meshTransformInfoPtr,
in_layerFlags: (UInt64)XrCompositionLayerFlagBits.XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT,
in_space: 0,
in_passthrough: passthrough,
in_color: passthroughColor);
passthrough2meshTransform.Add(passthrough, PassthroughMeshTransformInfo);
passthrough2meshTransformInfoPtr.Add(passthrough, meshTransformInfoPtr);
passthrough2Layer.Add(passthrough, compositionLayerPassthrough);
IntPtr layerPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(XrCompositionLayerPassthroughHTC)));
passthrough2LayerPtr.Add(passthrough, layerPtr);
if (layerType == CompositionLayer.LayerType.Underlay)
passthrough2IsUnderLay.Add(passthrough, true);
if (layerType == CompositionLayer.LayerType.Overlay)
passthrough2IsUnderLay.Add(passthrough, false);
}
#endif
#if UNITY_ANDROID
res = passthroughFeature.CreatePassthroughHTC(createInfo, out passthrough, layerType, onDestroyPassthroughSessionHandler);
DEBUG("CreateProjectedPassthrough() CreatePassthroughHTC result: " + res + ", passthrough: " + passthrough);
#endif
if (res == XrResult.XR_SUCCESS)
{
SetPassthroughAlpha(passthrough, alpha);
}
return res;
}
#if UNITY_STANDALONE
private static async void SubmitLayer()
{
await Task.Run(() => {
int layerListCount = 0;
while(layerListCount == 0)
{
System.Threading.Thread.Sleep(1);
XR_HTC_passthrough.Interop.GetOriginEndFrameLayerList(out List<IntPtr> layerList);//GetOriginEndFrameLayers
layerListCount = layerList.Count;
foreach (var passthrough in passthrough2IsUnderLay.Keys)
{
//Get and submit layer list
if (layerListCount != 0)
{
Marshal.StructureToPtr(passthrough2Layer[passthrough], passthrough2LayerPtr[passthrough], false);
if (passthrough2IsUnderLay[passthrough])
layerList.Insert(0, passthrough2LayerPtr[passthrough]);
else
layerList.Insert(1, passthrough2LayerPtr[passthrough]);
}
}
if(layerListCount != 0)
XR_HTC_passthrough.Interop.SubmitLayers(layerList);
}
});
}
#endif
/// <summary>
/// To Destroying a passthrough.
/// You should call this function when the <see cref="VivePassthrough.OnPassthroughSessionDestroyDelegate">delegate</see> is invoked.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <returns>XR_SUCCESS for success.</returns>
public static XrResult DestroyPassthrough(XrPassthroughHTC passthrough)
{
XrResult res = XrResult.XR_ERROR_RUNTIME_FAILURE;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return res;
}
if (!passthroughFeature.PassthroughList.Contains(passthrough))
{
ERROR("Passthrough to be destroyed not found");
return res;
}
#if UNITY_STANDALONE
XrPassthroughHTC pt = passthrough2Layer[passthrough].passthrough;
XR_HTC_passthrough.xrDestroyPassthroughHTC(pt);
passthrough2IsUnderLay.Remove(passthrough);
SubmitLayer();
passthrough2Layer.Remove(pt);
if(passthrough2LayerPtr.ContainsKey(passthrough)) Marshal.FreeHGlobal(passthrough2LayerPtr[passthrough]);
passthrough2LayerPtr.Remove(passthrough);
if(passthrough2meshTransformInfoPtr.ContainsKey(passthrough)) Marshal.FreeHGlobal(passthrough2meshTransformInfoPtr[passthrough]);
passthrough2meshTransformInfoPtr.Remove(passthrough);
passthrough2meshTransform.Remove(passthrough);
res = XrResult.XR_SUCCESS;
#elif UNITY_ANDROID
res = passthroughFeature.DestroyPassthroughHTC(passthrough);
DEBUG("DestroyPassthrough() DestroyPassthroughHTC result: " + res + ", passthrough: " + passthrough);
#endif
return res;
}
/// <summary>
/// Modifies the opacity of a specific passthrough layer.
/// Can be used for both Planar and Projected passthroughs.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="alpha">The alpha value of the passthrough layer within the range [0, 1] where 1 (Opaque) is default.</param>
/// <param name="autoClamp">
/// Specify whether out of range alpha values should be clamped automatically.
/// When set to true, the function will clamp and apply the alpha value automatically.
/// When set to false, the function will return false if the alpha is out of range.
/// Default is true.
/// </param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetPassthroughAlpha(XrPassthroughHTC passthrough, float alpha, bool autoClamp = true)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
#if UNITY_ANDROID
if (autoClamp)
{
ret = passthroughFeature.SetAlpha(passthrough, Mathf.Clamp01(alpha));
}
else
{
if (alpha < 0f || alpha > 1f)
{
ERROR("SetPassthroughAlpha: Alpha out of range");
return ret;
}
ret = passthroughFeature.SetAlpha(passthrough, alpha);
}
DEBUG("SetPassthroughAlpha() SetAlpha result: " + ret + ", passthrough: " + passthrough);
#endif
#if UNITY_STANDALONE
if (passthrough2Layer.ContainsKey(passthrough))
{
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.color.alpha = alpha;
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
return ret;
}
/// <summary>
/// Modifies the mesh data of a projected passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="vertexBuffer">Positions of the vertices in the mesh.</param>
/// <param name="indexBuffer">List of triangles represented by indices into the <paramref name="vertexBuffer"/>.</param>
/// <param name="convertFromUnityToOpenXR">Specify whether or not the parameters <paramref name="vertexBuffer"/>, <paramref name="indexBuffer"/>, <paramref name="meshPosition"/> and <paramref name="meshOrientation"/> have to be converted to OpenXR coordinate.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetProjectedPassthroughMesh(XrPassthroughHTC passthrough, [In, Out] Vector3[] vertexBuffer, [In, Out] int[] indexBuffer, bool convertFromUnityToOpenXR = true)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
if (vertexBuffer.Length < 3 || indexBuffer.Length % 3 != 0) //Must have at least 3 vertices and complete triangles
{
ERROR("Mesh data invalid.");
return ret;
}
XrVector3f[] vertexBufferXrVector = new XrVector3f[vertexBuffer.Length];
for (int i = 0; i < vertexBuffer.Length; i++)
{
vertexBufferXrVector[i] = OpenXRHelper.ToOpenXRVector(vertexBuffer[i], convertFromUnityToOpenXR);
}
uint[] indexBufferUint = new uint[indexBuffer.Length];
for (int i = 0; i < indexBuffer.Length; i++)
{
indexBufferUint[i] = (uint)indexBuffer[i];
}
#if UNITY_STANDALONE
if (passthrough2meshTransformInfoPtr.ContainsKey(passthrough))
{
XrPassthroughMeshTransformInfoHTC MeshTransformInfo = passthrough2meshTransform[passthrough];
MeshTransformInfo.vertexCount = (uint)vertexBuffer.Length;
MeshTransformInfo.vertices = vertexBufferXrVector;
MeshTransformInfo.indexCount = (uint)indexBuffer.Length;
MeshTransformInfo.indices = indexBufferUint;
passthrough2meshTransform[passthrough] = MeshTransformInfo;
Marshal.StructureToPtr(MeshTransformInfo, passthrough2meshTransformInfoPtr[passthrough], false);
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.next = passthrough2meshTransformInfoPtr[passthrough];
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
//Note: Ignore Clock-Wise definition of index buffer for now as passthrough extension does not have back-face culling
#if UNITY_ANDROID
ret = passthroughFeature.SetMesh(passthrough, (uint)vertexBuffer.Length, vertexBufferXrVector, (uint)indexBuffer.Length, indexBufferUint); ;
DEBUG("SetProjectedPassthroughMesh() SetMesh result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// Modifies the mesh transform of a projected passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="spaceType">The projected passthrough's <see cref="ProjectedPassthroughSpaceType"/></param>
/// <param name="meshPosition">Position of the mesh.</param>
/// <param name="meshOrientation">Orientation of the mesh.</param>
/// <param name="meshScale">Scale of the mesh.</param>
/// <param name="trackingToWorldSpace">Specify whether or not the position and rotation of the mesh transform have to be converted from tracking space to world space.</param>
/// <param name="convertFromUnityToOpenXR">Specify whether or not the parameters <paramref name="vertexBuffer"/>, <paramref name="indexBuffer"/>, <paramref name="meshPosition"/> and <paramref name="meshOrientation"/> have to be converted to OpenXR coordinate.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetProjectedPassthroughMeshTransform(XrPassthroughHTC passthrough, ProjectedPassthroughSpaceType spaceType, Vector3 meshPosition, Quaternion meshOrientation, Vector3 meshScale, bool trackingToWorldSpace = true, bool convertFromUnityToOpenXR = true)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
Vector3 trackingSpaceMeshPosition = meshPosition;
Quaternion trackingSpaceMeshRotation = meshOrientation;
TrackingSpaceOrigin currentTrackingSpaceOrigin = TrackingSpaceOrigin.Instance;
if (currentTrackingSpaceOrigin != null && trackingToWorldSpace) //Apply origin correction to the mesh pose
{
Matrix4x4 trackingSpaceOriginTRS = Matrix4x4.TRS(currentTrackingSpaceOrigin.transform.position, currentTrackingSpaceOrigin.transform.rotation, Vector3.one);
Matrix4x4 worldSpaceLayerPoseTRS = Matrix4x4.TRS(meshPosition, meshOrientation, Vector3.one);
Matrix4x4 trackingSpaceLayerPoseTRS = trackingSpaceOriginTRS.inverse * worldSpaceLayerPoseTRS;
trackingSpaceMeshPosition = trackingSpaceLayerPoseTRS.GetColumn(3); //4th Column of TRS Matrix is the position
trackingSpaceMeshRotation = Quaternion.LookRotation(trackingSpaceLayerPoseTRS.GetColumn(2), trackingSpaceLayerPoseTRS.GetColumn(1));
}
XrPosef meshXrPose;
meshXrPose.position = OpenXRHelper.ToOpenXRVector(trackingSpaceMeshPosition, convertFromUnityToOpenXR);
meshXrPose.orientation = OpenXRHelper.ToOpenXRQuaternion(trackingSpaceMeshRotation, convertFromUnityToOpenXR);
XrVector3f meshXrScale = OpenXRHelper.ToOpenXRVector(meshScale, false);
#if UNITY_STANDALONE
if (passthrough2meshTransformInfoPtr.ContainsKey(passthrough))
{
XrPassthroughMeshTransformInfoHTC MeshTransformInfo = passthrough2meshTransform[passthrough];
MeshTransformInfo.pose = meshXrPose;
MeshTransformInfo.scale = meshXrScale;
passthrough2meshTransform[passthrough] = MeshTransformInfo;
Marshal.StructureToPtr(MeshTransformInfo, passthrough2meshTransformInfoPtr[passthrough], false);
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.next = passthrough2meshTransformInfoPtr[passthrough];
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
#if UNITY_ANDROID
ret = passthroughFeature.SetMeshTransform(passthrough, passthroughFeature.GetXrSpaceFromSpaceType(spaceType), meshXrPose, meshXrScale);
DEBUG("SetProjectedPassthroughMeshTransform() SetMeshTransform result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// Modifies layer type and composition depth of a passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="layerType">The <see cref="LayerType"/> specifies whether the passthrough is an overlay or underlay.</param>
/// <param name="compositionDepth">The composition depth relative to other composition layers if present where 0 is default.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetPassthroughLayerType(XrPassthroughHTC passthrough, CompositionLayer.LayerType layerType, uint compositionDepth = 0)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
#if UNITY_STANDALONE
if (passthrough2IsUnderLay.ContainsKey(passthrough))
{
passthrough2IsUnderLay[passthrough] = layerType == CompositionLayer.LayerType.Underlay ? true : false;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
#if UNITY_ANDROID
ret = passthroughFeature.SetLayerType(passthrough, layerType, compositionDepth);
DEBUG("SetPassthroughLayerType() SetLayerType result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// Modifies the space of a projected passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="spaceType">The projected passthrough's <see cref="ProjectedPassthroughSpaceType"/></param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetProjectedPassthroughSpaceType(XrPassthroughHTC passthrough, ProjectedPassthroughSpaceType spaceType)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
#if UNITY_STANDALONE
if (passthrough2meshTransformInfoPtr.ContainsKey(passthrough))
{
XrPassthroughMeshTransformInfoHTC MeshTransformInfo = passthrough2meshTransform[passthrough];
MeshTransformInfo.baseSpace = passthroughFeature.GetXrSpaceFromSpaceType(spaceType);
passthrough2meshTransform[passthrough] = MeshTransformInfo;
Marshal.StructureToPtr(MeshTransformInfo, passthrough2meshTransformInfoPtr[passthrough], false);
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.next = passthrough2meshTransformInfoPtr[passthrough];
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
#if UNITY_ANDROID
ret = passthroughFeature.SetMeshTransformSpace(passthrough, passthroughFeature.GetXrSpaceFromSpaceType(spaceType));
DEBUG("SetProjectedPassthroughSpaceType() SetMeshTransformSpace result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// Modifies the mesh position of a projected passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="meshPosition">Position of the mesh.</param>
/// <param name="trackingToWorldSpace">Specify whether or not the position and rotation of the mesh transform have to be converted from tracking space to world space.</param>
/// <param name="convertFromUnityToOpenXR">Specify whether or not the parameters <paramref name="vertexBuffer"/>, <paramref name="indexBuffer"/>, <paramref name="meshPosition"/> and <paramref name="meshOrientation"/> have to be converted to OpenXR coordinate.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetProjectedPassthroughMeshPosition(XrPassthroughHTC passthrough, Vector3 meshPosition, bool trackingToWorldSpace = true, bool convertFromUnityToOpenXR = true)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
Vector3 trackingSpaceMeshPosition = meshPosition;
TrackingSpaceOrigin currentTrackingSpaceOrigin = TrackingSpaceOrigin.Instance;
if (currentTrackingSpaceOrigin != null && trackingToWorldSpace) //Apply origin correction to the mesh pose
{
Matrix4x4 trackingSpaceOriginTRS = Matrix4x4.TRS(currentTrackingSpaceOrigin.transform.position, Quaternion.identity, Vector3.one);
Matrix4x4 worldSpaceLayerPoseTRS = Matrix4x4.TRS(meshPosition, Quaternion.identity, Vector3.one);
Matrix4x4 trackingSpaceLayerPoseTRS = trackingSpaceOriginTRS.inverse * worldSpaceLayerPoseTRS;
trackingSpaceMeshPosition = trackingSpaceLayerPoseTRS.GetColumn(3); //4th Column of TRS Matrix is the position
}
#if UNITY_STANDALONE
if (passthrough2meshTransformInfoPtr.ContainsKey(passthrough))
{
XrPassthroughMeshTransformInfoHTC MeshTransformInfo = passthrough2meshTransform[passthrough];
XrPosef meshXrPose = MeshTransformInfo.pose;
meshXrPose.position = OpenXRHelper.ToOpenXRVector(trackingSpaceMeshPosition, convertFromUnityToOpenXR); ;
MeshTransformInfo.pose = meshXrPose;
passthrough2meshTransform[passthrough] = MeshTransformInfo;
Marshal.StructureToPtr(MeshTransformInfo, passthrough2meshTransformInfoPtr[passthrough], false);
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.next = passthrough2meshTransformInfoPtr[passthrough];
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
#if UNITY_ANDROID
ret = passthroughFeature.SetMeshTransformPosition(passthrough, OpenXRHelper.ToOpenXRVector(trackingSpaceMeshPosition, convertFromUnityToOpenXR));
DEBUG("SetProjectedPassthroughMeshPosition() SetMeshTransformPosition result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// Modifies the mesh orientation of a projected passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="meshOrientation">Orientation of the mesh.</param>
/// <param name="trackingToWorldSpace">Specify whether or not the position and rotation of the mesh transform have to be converted from tracking space to world space.</param>
/// <param name="convertFromUnityToOpenXR">Specify whether or not the parameters <paramref name="vertexBuffer"/>, <paramref name="indexBuffer"/>, <paramref name="meshPosition"/> and <paramref name="meshOrientation"/> have to be converted to OpenXR coordinate.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetProjectedPassthroughMeshOrientation(XrPassthroughHTC passthrough, Quaternion meshOrientation, bool trackingToWorldSpace = true, bool convertFromUnityToOpenXR = true)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
Quaternion trackingSpaceMeshRotation = meshOrientation;
TrackingSpaceOrigin currentTrackingSpaceOrigin = TrackingSpaceOrigin.Instance;
if (currentTrackingSpaceOrigin != null && trackingToWorldSpace) //Apply origin correction to the mesh pose
{
Matrix4x4 trackingSpaceOriginTRS = Matrix4x4.TRS(Vector3.zero, currentTrackingSpaceOrigin.transform.rotation, Vector3.one);
Matrix4x4 worldSpaceLayerPoseTRS = Matrix4x4.TRS(Vector3.zero, meshOrientation, Vector3.one);
Matrix4x4 trackingSpaceLayerPoseTRS = trackingSpaceOriginTRS.inverse * worldSpaceLayerPoseTRS;
trackingSpaceMeshRotation = Quaternion.LookRotation(trackingSpaceLayerPoseTRS.GetColumn(2), trackingSpaceLayerPoseTRS.GetColumn(1));
}
#if UNITY_STANDALONE
if (passthrough2meshTransformInfoPtr.ContainsKey(passthrough))
{
XrPassthroughMeshTransformInfoHTC MeshTransformInfo = passthrough2meshTransform[passthrough];
XrPosef meshXrPose = MeshTransformInfo.pose;
meshXrPose.orientation = OpenXRHelper.ToOpenXRQuaternion(trackingSpaceMeshRotation, convertFromUnityToOpenXR);
MeshTransformInfo.pose = meshXrPose;
passthrough2meshTransform[passthrough] = MeshTransformInfo;
Marshal.StructureToPtr(MeshTransformInfo, passthrough2meshTransformInfoPtr[passthrough], false);
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.next = passthrough2meshTransformInfoPtr[passthrough];
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
#if UNITY_ANDROID
ret = passthroughFeature.SetMeshTransformOrientation(passthrough, OpenXRHelper.ToOpenXRQuaternion(trackingSpaceMeshRotation, convertFromUnityToOpenXR));
DEBUG("SetProjectedPassthroughMeshOrientation() SetMeshTransformOrientation result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// Modifies the mesh scale of a passthrough layer.
/// </summary>
/// <param name="passthrough">The created <see cref="XrPassthroughHTC"/></param>
/// <param name="meshScale">Scale of the mesh.</param>
/// <returns>XR_SUCCESS for success.</returns>
public static bool SetProjectedPassthroughScale(XrPassthroughHTC passthrough, Vector3 meshScale)
{
bool ret = false;
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return ret;
}
#if UNITY_STANDALONE
if (passthrough2meshTransformInfoPtr.ContainsKey(passthrough))
{
XrPassthroughMeshTransformInfoHTC MeshTransformInfo = passthrough2meshTransform[passthrough];
MeshTransformInfo.scale = OpenXRHelper.ToOpenXRVector(meshScale, false);
passthrough2meshTransform[passthrough] = MeshTransformInfo;
Marshal.StructureToPtr(MeshTransformInfo, passthrough2meshTransformInfoPtr[passthrough], false);
XrCompositionLayerPassthroughHTC layer = passthrough2Layer[passthrough];
layer.next = passthrough2meshTransformInfoPtr[passthrough];
passthrough2Layer[passthrough] = layer;
SubmitLayer();
ret = true;
}
else
ret = false;
#endif
#if UNITY_ANDROID
ret = passthroughFeature.SetMeshTransformScale(passthrough, OpenXRHelper.ToOpenXRVector(meshScale, false));
DEBUG("SetProjectedPassthroughScale() SetMeshTransformScale result: " + ret + ", passthrough: " + passthrough);
#endif
return ret;
}
/// <summary>
/// To get the list of IDs of active passthrough layers.
/// </summary>
/// <returns>
/// The a copy of the list of IDs of active passthrough layers.
/// </returns>
public static List<XrPassthroughHTC> GetCurrentPassthroughLayerIDs()
{
if (!AssertFeature())
{
ERROR("HTC_Passthrough feature instance not found.");
return null;
}
return passthroughFeature.PassthroughList;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9bfb5ce0ea49a0a4cb998989754606c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,51 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace VIVE.OpenXR
{
public class MemoryTools
{
public static IntPtr ToIntPtr<T>(T[] array) where T : Enum
{
int size = Marshal.SizeOf(typeof(T)) * array.Length;
IntPtr ptr = Marshal.AllocHGlobal(size);
int[] intArray = new int[array.Length];
for (int i = 0; i < array.Length; i++)
intArray[i] = (int)(object)array[i];
Marshal.Copy(intArray, 0, ptr, array.Length);
return ptr;
}
// Make the same size raw buffer from input array.
public static IntPtr MakeRawMemory<T>(T[] refArray)
{
int size = Marshal.SizeOf(typeof(T)) * refArray.Length;
return Marshal.AllocHGlobal(size);
}
// Make the same size raw buffer from input array.
public static void CopyFromRawMemory<T>(T[] array, IntPtr raw)
{
int step = Marshal.SizeOf(typeof(T));
for (int i = 0; i < array.Length; i++)
{
array[i] = Marshal.PtrToStructure<T>(IntPtr.Add(raw, i * step));
}
}
// Make the same size raw buffer from input array. Make sure the raw has enough size.
public static void CopyToRawMemory<T>(IntPtr raw, T[] array)
{
int step = Marshal.SizeOf(typeof(T));
for (int i = 0; i < array.Length; i++)
{
Marshal.StructureToPtr<T>(array[i], IntPtr.Add(raw, i * step), false);
}
}
public static void ReleaseRawMemory(IntPtr ptr)
{
Marshal.FreeHGlobal(ptr);
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee430aebc144da341979174bcccf7e5a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 58897b8e0a7d7b24999d04f6ab923561
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f6f2f61f885577442b9a4cafff9a62e6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: GazePointerDef
m_Shader: {fileID: 4800000, guid: 105680c0299ff144b972446cb07a81c5, type: 3}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _ColorMask: 15
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _Stencil: 0
- _StencilComp: 8
- _StencilOp: 0
- _StencilReadMask: 255
- _StencilWriteMask: 255
- _UVSec: 0
- _UseUIAlphaClip: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 58d737ad5355ee04eb3f7f57de71df1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 571cc732a01df91488bf9c7a5909fead
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,88 @@
Shader "VIVE/OpenXR/Raycast/GazePointerDef"
{
Properties{
_MainTex("Font Texture", 2D) = "white" {}
_Color("Text Color", Color) = (1,1,1,1)
_StencilComp("Stencil Comparison", Float) = 8
_Stencil("Stencil ID", Float) = 0
_StencilOp("Stencil Operation", Float) = 0
_StencilWriteMask("Stencil Write Mask", Float) = 255
_StencilReadMask("Stencil Read Mask", Float) = 255
_ColorMask("Color Mask", Float) = 15
}
SubShader {
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Lighting Off
Cull Off
ZTest Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata_t {
float4 vertex : POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform fixed4 _Color;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color * _Color;
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
#ifdef UNITY_HALF_TEXEL_OFFSET
o.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
#endif
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = i.color;
col.a *= tex2D(_MainTex, i.texcoord).a;
clip (col.a - 0.01);
return col;
}
ENDCG
}
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 105680c0299ff144b972446cb07a81c5
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
preprocessorOverride: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 827931b60f4bb6c438a0a3473ead12a5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
// Copyright HTC Corporation All Rights Reserved.
using System.Collections.Generic;
using UnityEngine;
namespace VIVE.OpenXR.Raycast
{
public static class CanvasProvider
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.CanvasProvider";
private static void DEBUG(string msg) { Debug.Log(LOG_TAG + " " + msg); }
private static List<Canvas> s_TargetCanvases = new List<Canvas>();
public static bool RegisterTargetCanvas(Canvas canvas)
{
if (canvas != null && !s_TargetCanvases.Contains(canvas))
{
DEBUG("RegisterTargetCanvas() " + canvas.gameObject.name);
s_TargetCanvases.Add(canvas);
return true;
}
return false;
}
public static bool RemoveTargetCanvas(Canvas canvas)
{
if (canvas != null && s_TargetCanvases.Contains(canvas))
{
DEBUG("RemoveTargetCanvas() " + canvas.gameObject.name);
s_TargetCanvases.Remove(canvas);
return true;
}
return false;
}
public static Canvas[] GetTargetCanvas() { return s_TargetCanvases.ToArray(); }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c68c9c4b6f2ba4843ac07957f9727968
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
// Copyright HTC Corporation All Rights Reserved.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using System.Text;
namespace VIVE.OpenXR.Raycast
{
public class ControllerRaycastPointer : RaycastPointer
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.ControllerRaycastPointer";
void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
#region Inspector
[SerializeField]
private InputActionReference m_IsTracked = null;
public InputActionReference IsTracked { get => m_IsTracked; set => m_IsTracked = value; }
[Tooltip("Keys for control.")]
[SerializeField]
private List<InputActionReference> m_ActionsKeys = new List<InputActionReference>();
public List<InputActionReference> ActionKeys { get { return m_ActionsKeys; } set { m_ActionsKeys = value; } }
bool getBool(InputActionReference actionReference)
{
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
if (actionReference.action.activeControl.valueType == typeof(bool))
return actionReference.action.ReadValue<bool>();
if (actionReference.action.activeControl.valueType == typeof(float))
return actionReference.action.ReadValue<float>() > 0;
}
return false;
}
[Tooltip("To show the ray anymore.")]
[SerializeField]
private bool m_AlwaysEnable = false;
public bool AlwaysEnable { get { return m_AlwaysEnable; } set { m_AlwaysEnable = value; } }
#endregion
#region MonoBehaviour overrides
protected override void Awake()
{
base.Awake();
}
protected override void Update()
{
base.Update();
if (!IsInteractable()) { return; }
UpdateButtonStates();
}
protected override void Start()
{
base.Start();
sb.Clear().Append("Start()"); DEBUG(sb);
}
private void OnApplicationPause(bool pause)
{
sb.Clear().Append("OnApplicationPause() ").Append(pause); DEBUG(sb);
}
#endregion
private bool IsInteractable()
{
bool enabled = RaycastSwitch.Controller.Enabled;
bool validPose = getBool(m_IsTracked);
#if UNITY_XR_OPENXR_1_6_0
m_Interactable = (m_AlwaysEnable || enabled); // The isTracked value of Pose will always be flase in OpenXR 1.6.0
#else
m_Interactable = (m_AlwaysEnable || enabled) && validPose;
#endif
if (printIntervalLog)
{
sb.Clear().Append("IsInteractable() enabled: ").Append(enabled)
.Append(", validPose: ").Append(validPose)
.Append(", m_AlwaysEnable: ").Append(m_AlwaysEnable)
.Append(", m_Interactable: ").Append(m_Interactable);
DEBUG(sb);
}
return m_Interactable;
}
private void UpdateButtonStates()
{
if (m_ActionsKeys == null) { return; }
down = false;
for (int i = 0; i < m_ActionsKeys.Count; i++)
{
if (!hold)
{
down |= getBool(m_ActionsKeys[i]);
}
}
hold = false;
for (int i = 0; i < m_ActionsKeys.Count; i++)
{
hold |= getBool(m_ActionsKeys[i]);
}
}
#region RaycastImpl Actions overrides
internal bool down = false, hold = false;
protected override bool OnDown()
{
return down;
}
protected override bool OnHold()
{
return hold;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0ed4455b73284942bc407f2d8a98eee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,323 @@
// Copyright HTC Corporation All Rights Reserved.
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR;
namespace VIVE.OpenXR.Raycast
{
public class GazeRaycastRing : RaycastRing
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.GazeRaycastRing";
void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
#region Inspector
[SerializeField]
[Tooltip("Use Eye Tracking data for Gaze.")]
private bool m_EyeTracking = false;
public bool EyeTracking { get { return m_EyeTracking; } set { m_EyeTracking = value; } }
[SerializeField]
private InputActionReference m_EyePose = null;
public InputActionReference EyePose { get => m_EyePose; set => m_EyePose = value; }
bool getTracked(InputActionReference actionReference)
{
bool tracked = false;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
tracked = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().isTracked;
#else
tracked = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().isTracked;
#endif
if (printIntervalLog)
{
sb.Clear().Append("getTracked(").Append(tracked).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append("getTracked() invalid input: ").Append(value);
DEBUG(sb);
}
}
return tracked;
}
InputTrackingState getTrackingState(InputActionReference actionReference)
{
InputTrackingState state = InputTrackingState.None;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
state = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().trackingState;
#else
state = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().trackingState;
#endif
if (printIntervalLog)
{
sb.Clear().Append("getTrackingState(").Append(state).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append("getTrackingState() invalid input: ").Append(value);
DEBUG(sb);
}
}
return state;
}
Vector3 getDirection(InputActionReference actionReference)
{
Quaternion rotation = Quaternion.identity;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
rotation = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().rotation;
#else
rotation = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().rotation;
#endif
if (printIntervalLog)
{
sb.Clear().Append("getDirection(").Append(rotation.x).Append(", ").Append(rotation.y).Append(", ").Append(rotation.z).Append(", ").Append(rotation.w).Append(")");
DEBUG(sb);
}
return (rotation * Vector3.forward);
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append("getDirection() invalid input: ").Append(value);
DEBUG(sb);
}
}
return Vector3.forward;
}
Vector3 getOrigin(InputActionReference actionReference)
{
var origin = Vector3.zero;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
origin = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().position;
#else
origin = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().position;
#endif
if (printIntervalLog)
{
sb.Clear().Append("getOrigin(").Append(origin.x).Append(", ").Append(origin.y).Append(", ").Append(origin.z).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append("getOrigin() invalid input: ").Append(value);
DEBUG(sb);
}
}
return origin;
}
[Tooltip("Event triggered by gaze.")]
[SerializeField]
private GazeEvent m_InputEvent = GazeEvent.Down;
public GazeEvent InputEvent { get { return m_InputEvent; } set { m_InputEvent = value; } }
[Tooltip("Keys for control.")]
[SerializeField]
private List<InputActionReference> m_ActionsKeys = new List<InputActionReference>();
public List<InputActionReference> ActionKeys { get { return m_ActionsKeys; } set { m_ActionsKeys = value; } }
bool getButton(InputActionReference actionReference)
{
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
if (actionReference.action.activeControl.valueType == typeof(bool))
return actionReference.action.ReadValue<bool>();
if (actionReference.action.activeControl.valueType == typeof(float))
return actionReference.action.ReadValue<float>() > 0;
}
else
{
if (printIntervalLog)
{
sb.Clear().Append("getButton() invalid input: ").Append(value);
DEBUG(sb);
}
}
return false;
}
[SerializeField]
private bool m_AlwaysEnable = false;
public bool AlwaysEnable { get { return m_AlwaysEnable; } set { m_AlwaysEnable = value; } }
#endregion
#region MonoBehaviour overrides
protected override void Awake()
{
base.Awake();
}
private bool m_KeyDown = false;
protected override void Update()
{
base.Update();
if (!IsInteractable()) { return; }
m_KeyDown = ButtonPressed();
}
#endregion
private bool IsInteractable()
{
bool enabled = RaycastSwitch.Gaze.Enabled;
m_Interactable = (m_AlwaysEnable || enabled);
if (printIntervalLog)
{
sb.Clear().Append("IsInteractable() enabled: ").Append(enabled).Append(", m_AlwaysEnable: ").Append(m_AlwaysEnable);
DEBUG(sb);
}
return m_Interactable;
}
internal bool m_Down = false, m_Hold = false;
private bool ButtonPressed()
{
if (m_ActionsKeys == null) { return false; }
bool keyDown = false;
for (int i = 0; i < m_ActionsKeys.Count; i++)
{
var pressed = getButton(m_ActionsKeys[i]);
if (pressed)
{
sb.Clear().Append("ButtonPressed()").Append(m_ActionsKeys[i].name).Append(" is pressed.");
DEBUG(sb);
}
keyDown |= pressed;
}
m_Down = false;
if (!m_Hold) { m_Down |= keyDown; }
m_Hold = keyDown;
return m_Down;
}
protected override bool UseEyeData(out Vector3 direction)
{
bool isTracked = getTracked(m_EyePose);
InputTrackingState trackingState = getTrackingState(m_EyePose);
bool positionTracked = ((trackingState & InputTrackingState.Position) != 0);
bool rotationTracked = ((trackingState & InputTrackingState.Rotation) != 0);
bool useEye = m_EyeTracking
#if !UNITY_XR_OPENXR_1_6_0
&& isTracked // The isTracked value of Pose will always be flase in OpenXR 1.6.0
#endif
//&& positionTracked
&& rotationTracked;
getOrigin(m_EyePose);
direction = getDirection(m_EyePose);
if (printIntervalLog)
{
sb.Clear().Append("UseEyeData() m_EyeTracking: ").Append(m_EyeTracking)
.Append(", isTracked: ").Append(isTracked)
.Append(", trackingState: ").Append(trackingState)
.Append(", direction (").Append(direction.x).Append(", ").Append(direction.y).Append(", ").Append(direction.z).Append(")");
DEBUG(sb);
}
if (!useEye) { return base.UseEyeData(out direction); }
return useEye;
}
#region RaycastImpl Actions overrides
protected override bool OnDown()
{
if (m_InputEvent != GazeEvent.Down) { return false; }
bool down = false;
if (m_RingPercent >= 100 || m_KeyDown)
{
m_RingPercent = 0;
m_GazeOnTime = Time.unscaledTime;
down = true;
sb.Clear().Append("OnDown()"); DEBUG(sb);
}
return down;
}
protected override bool OnSubmit()
{
if (m_InputEvent != GazeEvent.Submit) { return false; }
bool submit = false;
if (m_RingPercent >= 100 || m_KeyDown)
{
m_RingPercent = 0;
m_GazeOnTime = Time.unscaledTime;
submit = true;
sb.Clear().Append("OnSubmit()"); DEBUG(sb);
}
return submit;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78ba47d171a9d604e87df3c8c0304fde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,285 @@
// Copyright HTC Corporation All Rights Reserved.
using UnityEngine;
using UnityEngine.XR;
using System.Text;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
namespace VIVE.OpenXR.Raycast
{
public class HandRaycastPointer : RaycastPointer
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.HandRaycastPointer ";
void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
#region Inspector
public bool IsLeft = false;
[Tooltip("To apply poses on the raycast pointer.")]
[SerializeField]
private bool m_UsePose = true;
public bool UsePose { get { return m_UsePose; } set { m_UsePose = value; } }
#if ENABLE_INPUT_SYSTEM
[SerializeField]
private InputActionReference m_AimPose = null;
public InputActionReference AimPose { get { return m_AimPose; } set { m_AimPose = value; } }
bool getAimTracked(InputActionReference actionReference)
{
bool tracked = false;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
tracked = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().isTracked;
#else
tracked = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().isTracked;
#endif
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimTracked(").Append(tracked).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimTracked() invalid input: ").Append(value);
DEBUG(sb);
}
}
return tracked;
}
InputTrackingState getAimTrackingState(InputActionReference actionReference)
{
InputTrackingState state = InputTrackingState.None;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
state = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().trackingState;
#else
state = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().trackingState;
#endif
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimTrackingState(").Append(state).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimTrackingState() invalid input: ").Append(value);
DEBUG(sb);
}
}
return state;
}
Vector3 getAimPosition(InputActionReference actionReference)
{
var position = Vector3.zero;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
position = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().position;
#else
position = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().position;
#endif
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimPosition(").Append(position.x).Append(", ").Append(position.y).Append(", ").Append(position.z).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimPosition() invalid input: ").Append(value);
DEBUG(sb);
}
}
return position;
}
Quaternion getAimRotation(InputActionReference actionReference)
{
var rotation = Quaternion.identity;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.InputSystem.XR.PoseState))
#else
if (actionReference.action.activeControl.valueType == typeof(UnityEngine.XR.OpenXR.Input.Pose))
#endif
{
#if USE_INPUT_SYSTEM_POSE_CONTROL // Scripting Define Symbol added by using OpenXR Plugin 1.6.0.
rotation = actionReference.action.ReadValue<UnityEngine.InputSystem.XR.PoseState>().rotation;
#else
rotation = actionReference.action.ReadValue<UnityEngine.XR.OpenXR.Input.Pose>().rotation;
#endif
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimRotation(").Append(rotation.x).Append(", ").Append(rotation.y).Append(", ").Append(rotation.z).Append(", ").Append(rotation.w).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getAimRotation() invalid input: ").Append(value);
DEBUG(sb);
}
}
return rotation;
}
[SerializeField]
private InputActionReference m_PinchStrength = null;
public InputActionReference PinchStrength { get => m_PinchStrength; set => m_PinchStrength = value; }
float getStrength(InputActionReference actionReference)
{
float strength = 0;
if (OpenXRHelper.VALIDATE(actionReference, out string value))
{
if (actionReference.action.activeControl.valueType == typeof(float))
{
strength = actionReference.action.ReadValue<float>();
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getStrength(").Append(strength).Append(")");
DEBUG(sb);
}
}
}
else
{
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("getStrength() invalid input: ").Append(value);
DEBUG(sb);
}
}
return strength;
}
#endif
[Tooltip("Pinch threshold to trigger events.")]
[SerializeField]
private float m_PinchThreshold = .5f;
public float PinchThreshold { get { return m_PinchThreshold; } set { m_PinchThreshold = value; } }
[SerializeField]
private bool m_AlwaysEnable = false;
public bool AlwaysEnable { get { return m_AlwaysEnable; } set { m_AlwaysEnable = value; } }
#endregion
#if ENABLE_INPUT_SYSTEM
protected override void Update()
{
base.Update();
if (!IsInteractable()) { return; }
pinchStrength = getStrength(m_PinchStrength);
if (m_UsePose)
{
transform.localPosition = getAimPosition(m_AimPose);
transform.localRotation = getAimRotation(m_AimPose);
}
}
private bool IsInteractable()
{
bool enabled = RaycastSwitch.Hand.Enabled;
bool isTracked = getAimTracked(m_AimPose);
InputTrackingState trackingState = getAimTrackingState(m_AimPose);
bool positionTracked = ((trackingState & InputTrackingState.Position) != 0);
bool rotationTracked = ((trackingState & InputTrackingState.Rotation) != 0);
m_Interactable = (m_AlwaysEnable || enabled)
#if !UNITY_XR_OPENXR_1_6_0
&& isTracked // The isTracked value of Pose will always be flase in OpenXR 1.6.0
#endif
&& positionTracked
&& rotationTracked;
if (printIntervalLog)
{
sb.Clear().Append(LOG_TAG).Append("IsInteractable() m_Interactable: ").Append(m_Interactable)
.Append(", enabled: ").Append(enabled)
.Append(", isTracked: ").Append(isTracked)
.Append(", positionTracked: ").Append(positionTracked)
.Append(", rotationTracked: ").Append(rotationTracked)
.Append(", m_AlwaysEnable: ").Append(m_AlwaysEnable);
DEBUG(sb);
}
return m_Interactable;
}
#endif
#region RaycastImpl Actions overrides
bool eligibleForClick = false;
float pinchStrength = 0;
protected override bool OnDown()
{
if (!eligibleForClick)
{
bool down = pinchStrength > m_PinchThreshold;
if (down)
{
eligibleForClick = true;
return true;
}
}
return false;
}
protected override bool OnHold()
{
bool hold = pinchStrength > m_PinchThreshold;
if (!hold)
eligibleForClick = false;
return hold;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d1efc78bbff2cc4eb930103b4111b67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
// Copyright HTC Corporation All Rights Reserved.
using UnityEngine;
using UnityEngine.EventSystems;
namespace VIVE.OpenXR.Raycast
{
public class RaycastEventData : PointerEventData
{
/// <summary> The actor sends an event. </summary>
public GameObject Actor { get { return m_Actor; } }
private GameObject m_Actor = null;
public RaycastEventData(EventSystem eventSystem, GameObject actor)
: base(eventSystem)
{
m_Actor = actor;
}
}
/// <summary>
/// The object which receives events should implement this interface.
/// </summary>
public interface IHoverHandler : IEventSystemHandler
{
void OnHover(RaycastEventData eventData);
}
/// <summary>
/// Objects will use
/// ExecuteEvents.Execute (GameObject, BaseEventData, RayastEvents.pointerXXXXHandler)
/// to send XXXX events.
/// </summary>
public static class RaycastEvents
{
#region Event Executor of Hover
/// Use ExecuteEvents.Execute (GameObject, BaseEventData, RaycastEvents.pointerHoverHandler)
private static void HoverExecutor(IHoverHandler handler, BaseEventData eventData)
{
handler.OnHover(ExecuteEvents.ValidateEventData<RaycastEventData>(eventData));
}
public static ExecuteEvents.EventFunction<IHoverHandler> pointerHoverHandler
{
get { return HoverExecutor; }
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34e90154ac6fdd64f85236ba9b967440
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,595 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace VIVE.OpenXR.Raycast
{
[DisallowMultipleComponent]
[RequireComponent(typeof(Camera))]
public class RaycastImpl : BaseRaycaster
{
#region Log
StringBuilder m_sb = null;
protected StringBuilder sb {
get {
if (m_sb == null) { m_sb = new StringBuilder(); }
return m_sb;
}
}
const string LOG_TAG = "VIVE.OpenXR.Raycast.RaycastImpl";
void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
#endregion
#region Inspector
[SerializeField]
private bool m_IgnoreReversedGraphics = false;
public bool IgnoreReversedGraphics { get { return m_IgnoreReversedGraphics; } set { m_IgnoreReversedGraphics = value; } }
[SerializeField]
private float m_PhysicsCastDistance = 100;
public float PhysicsCastDistance { get { return m_PhysicsCastDistance; } set { m_PhysicsCastDistance = value; } }
[SerializeField]
private LayerMask m_PhysicsEventMask = ~0;
public LayerMask PhysicsEventMask { get { return m_PhysicsEventMask; } set { m_PhysicsEventMask = value; } }
#endregion
private Camera m_Camera = null;
public override Camera eventCamera { get { return m_Camera; } }
#region MonoBehaviour overrides
protected override void OnEnable()
{
sb.Clear().Append("OnEnable()"); DEBUG(sb);
base.OnEnable();
/// 1. Set up the event camera.
m_Camera = GetComponent<Camera>();
m_Camera.stereoTargetEye = StereoTargetEyeMask.None;
m_Camera.enabled = false;
/// 2. Set up the EventSystem.
if (EventSystem.current == null)
{
var eventSystemObject = new GameObject("EventSystem");
eventSystemObject.AddComponent<EventSystem>();
}
}
protected override void OnDisable()
{
sb.Clear().Append("OnDisable()"); DEBUG(sb);
base.OnDisable();
}
int printFrame = 0;
protected bool printIntervalLog = false;
protected bool m_Interactable = true;
protected virtual void Update()
{
printFrame++;
printFrame %= 300;
printIntervalLog = (printFrame == 0);
if (!m_Interactable) return;
/// Use the event camera and EventSystem to reset PointerEventData.
ResetEventData();
/// Update the raycast results
resultAppendList.Clear();
Raycast(pointerData, resultAppendList);
pointerData.pointerCurrentRaycast = currentRaycastResult;
/// Send events
HandleRaycastEvent();
}
#endregion
#region Raycast Result Handling
static readonly Comparison<RaycastResult> rrComparator = RaycastResultComparator;
private RaycastResult GetFirstRaycastResult(List<RaycastResult> results)
{
RaycastResult rr = default;
results.Sort(rrComparator);
for (int i = 0; i < results.Count; i++)
{
if (results[i].isValid)
{
rr = results[i];
break;
}
}
return rr;
}
private static int RaycastResultComparator(RaycastResult lhs, RaycastResult rhs)
{
if (lhs.module != rhs.module)
{
if (lhs.module.eventCamera != null && rhs.module.eventCamera != null && lhs.module.eventCamera.depth != rhs.module.eventCamera.depth)
{
// need to reverse the standard compareTo
if (lhs.module.eventCamera.depth < rhs.module.eventCamera.depth) { return 1; }
if (lhs.module.eventCamera.depth == rhs.module.eventCamera.depth) { return 0; }
return -1;
}
if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
{
return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);
}
if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
{
return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
}
}
if (lhs.sortingLayer != rhs.sortingLayer)
{
// Uses the layer value to properly compare the relative order of the layers.
var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
return rid.CompareTo(lid);
}
if (lhs.sortingOrder != rhs.sortingOrder)
{
return rhs.sortingOrder.CompareTo(lhs.sortingOrder);
}
if (!Mathf.Approximately(lhs.distance, rhs.distance))
{
return lhs.distance.CompareTo(rhs.distance);
}
if (lhs.depth != rhs.depth)
{
return rhs.depth.CompareTo(lhs.depth);
}
return lhs.index.CompareTo(rhs.index);
}
#endregion
#if UNITY_EDITOR
bool drawDebugLine = false;
#endif
#region Raycast
protected virtual bool UseEyeData(out Vector3 direction)
{
direction = Vector3.forward;
return false;
}
protected PointerEventData pointerData = null;
protected Vector3 pointerLocalOffset = Vector3.forward;
private Vector3 physicsWorldPosition = Vector3.zero;
private Vector2 graphicScreenPosition = Vector2.zero;
private void UpdatePointerDataPosition()
{
/// 1. Calculate the pointer offset in "local" space.
pointerLocalOffset = Vector3.forward;
if (UseEyeData(out Vector3 direction))
{
pointerLocalOffset = direction;
// Revise the offset from World space to Local space.
// OpenXR always uses World space.
pointerLocalOffset = Quaternion.Inverse(transform.rotation) * pointerLocalOffset;
}
/// 2. Calculate the pointer position in "world" space.
Vector3 rotated_offset = transform.rotation * pointerLocalOffset;
physicsWorldPosition = transform.position + rotated_offset;
graphicScreenPosition = m_Camera.WorldToScreenPoint(physicsWorldPosition);
// The graphicScreenPosition.x should be equivalent to (0.5f * Screen.width);
// The graphicScreenPosition.y should be equivalent to (0.5f * Screen.height);
}
private void ResetEventData()
{
if (pointerData == null) { pointerData = new RaycastEventData(EventSystem.current, gameObject); }
UpdatePointerDataPosition();
pointerData.position = graphicScreenPosition;
}
List<RaycastResult> resultAppendList = new List<RaycastResult>();
private RaycastResult currentRaycastResult = default;
protected GameObject raycastObject = null;
protected List<GameObject> s_raycastObjects = new List<GameObject>();
protected GameObject raycastObjectEx = null;
protected List<GameObject> s_raycastObjectsEx = new List<GameObject>();
/**
* Call to
* GraphicRaycast(Canvas canvas, Camera eventCamera, Vector2 screenPosition, List<RaycastResult> resultAppendList)
* PhysicsRaycast(Ray ray, Camera eventCamera, List<RaycastResult> resultAppendList)
**/
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
// --------------- Previous Results ---------------
raycastObjectEx = raycastObject;
s_raycastObjects.Clear();
// --------------- Graphic Raycast ---------------
Canvas[] canvases = CanvasProvider.GetTargetCanvas();
if (canvases.Length <= 0) { canvases = FindObjectsOfType<Canvas>(); } // note: GC.Alloc
for (int i = 0; i < canvases.Length; i++)
{
GraphicRaycast(canvases[i], m_Camera, eventData.position, resultAppendList);
}
// --------------- Physics Raycast ---------------
Ray ray = new Ray(transform.position, (physicsWorldPosition - transform.position));
PhysicsRaycast(ray, m_Camera, resultAppendList);
currentRaycastResult = GetFirstRaycastResult(resultAppendList);
// --------------- Current Results ---------------
raycastObject = currentRaycastResult.gameObject;
GameObject raycastTarget = currentRaycastResult.gameObject;
while (raycastTarget != null)
{
s_raycastObjects.Add(raycastTarget);
raycastTarget = (raycastTarget.transform.parent != null ? raycastTarget.transform.parent.gameObject : null);
}
#if UNITY_EDITOR
if (drawDebugLine)
{
Vector3 end = transform.position + (transform.forward * 100);
Debug.DrawLine(transform.position, end, Color.red, 1);
}
#endif
}
Ray ray = new Ray();
protected virtual void GraphicRaycast(Canvas canvas, Camera eventCamera, Vector2 screenPosition, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
IList<Graphic> foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
if (foundGraphics == null || foundGraphics.Count == 0)
return;
int displayIndex = 0;
var currentEventCamera = eventCamera; // Property can call Camera.main, so cache the reference
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(screenPosition);
// Necessary for the event system
for (int i = 0; i < foundGraphics.Count; ++i)
{
Graphic graphic = foundGraphics[i];
// -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1) { continue; }
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, screenPosition, currentEventCamera)) { continue; }
if (currentEventCamera != null && currentEventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > currentEventCamera.farClipPlane) { continue; }
if (graphic.Raycast(screenPosition, currentEventCamera))
{
var go = graphic.gameObject;
bool appendGraphic = true;
if (m_IgnoreReversedGraphics)
{
if (currentEventCamera == null)
{
// If we dont have a camera we know that we should always be facing forward
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
// If we have a camera compare the direction against the cameras forward.
var cameraForward = currentEventCamera.transform.rotation * Vector3.forward * currentEventCamera.nearClipPlane;
appendGraphic = Vector3.Dot(go.transform.position - currentEventCamera.transform.position - cameraForward, go.transform.forward) >= 0;
}
}
if (appendGraphic)
{
float distance = 0;
Transform trans = go.transform;
Vector3 transForward = trans.forward;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
// http://geomalgorithms.com/a06-_intersect-2.html
distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));
// Check to see if the go is behind the camera.
if (distance < 0)
continue;
}
resultAppendList.Add(new RaycastResult
{
gameObject = go,
module = this,
distance = distance,
screenPosition = screenPosition,
displayIndex = displayIndex,
index = resultAppendList.Count,
depth = graphic.depth,
sortingLayer = canvas.sortingLayerID,
sortingOrder = canvas.sortingOrder,
worldPosition = ray.origin + ray.direction * distance,
worldNormal = -transForward
});
}
}
}
}
Vector3 hitScreenPos = Vector3.zero;
Vector2 hitScreenPos2D = Vector2.zero;
static readonly RaycastHit[] hits = new RaycastHit[255];
protected virtual void PhysicsRaycast(Ray ray, Camera eventCamera, List<RaycastResult> resultAppendList)
{
var hitCount = Physics.RaycastNonAlloc(ray, hits, m_PhysicsCastDistance, m_PhysicsEventMask);
for (int i = 0; i < hitCount; ++i)
{
hitScreenPos = eventCamera.WorldToScreenPoint(hits[i].point);
hitScreenPos2D.x = hitScreenPos.x;
hitScreenPos2D.y = hitScreenPos.y;
resultAppendList.Add(new RaycastResult
{
gameObject = hits[i].collider.gameObject,
module = this,
distance = hits[i].distance,
worldPosition = hits[i].point,
worldNormal = hits[i].normal,
screenPosition = hitScreenPos2D,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
});
}
}
#endregion
#region Event
private void CopyList(List<GameObject> src, List<GameObject> dst)
{
dst.Clear();
for (int i = 0; i < src.Count; i++)
dst.Add(src[i]);
}
private void ExitEnterHandler(ref List<GameObject> enterObjects, ref List<GameObject> exitObjects)
{
if (exitObjects.Count > 0)
{
for (int i = 0; i < exitObjects.Count; i++)
{
if (exitObjects[i] != null && !enterObjects.Contains(exitObjects[i]))
{
ExecuteEvents.Execute(exitObjects[i], pointerData, ExecuteEvents.pointerExitHandler);
sb.Clear().Append("ExitEnterHandler() Exit: ").Append(exitObjects[i].name); DEBUG(sb);
}
}
}
if (enterObjects.Count > 0)
{
for (int i = 0; i < enterObjects.Count; i++)
{
if (enterObjects[i] != null && !exitObjects.Contains(enterObjects[i]))
{
ExecuteEvents.Execute(enterObjects[i], pointerData, ExecuteEvents.pointerEnterHandler);
sb.Clear().Append("ExitEnterHandler() Enter: ").Append(enterObjects[i].name).Append(", camera: ").Append(pointerData.enterEventCamera); DEBUG(sb);
}
}
}
CopyList(enterObjects, exitObjects);
}
private void HoverHandler()
{
if (raycastObject != null && (raycastObject == raycastObjectEx))
{
if (printIntervalLog) { sb.Clear().Append("HoverHandler() Hover: ").Append(raycastObject.name); DEBUG(sb); }
ExecuteEvents.ExecuteHierarchy(raycastObject, pointerData, RaycastEvents.pointerHoverHandler);
}
}
private void DownHandler()
{
sb.Clear().Append("DownHandler()");DEBUG(sb);
if (raycastObject == null) { return; }
pointerData.pressPosition = pointerData.position;
pointerData.pointerPressRaycast = pointerData.pointerCurrentRaycast;
pointerData.pointerPress =
ExecuteEvents.ExecuteHierarchy(raycastObject, pointerData, ExecuteEvents.pointerDownHandler)
?? ExecuteEvents.GetEventHandler<IPointerClickHandler>(raycastObject);
sb.Clear().Append("DownHandler() Down: ").Append(pointerData.pointerPress).Append(", raycastObject: ").Append(raycastObject.name); DEBUG(sb);
// If Drag Handler exists, send initializePotentialDrag event.
pointerData.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(raycastObject);
if (pointerData.pointerDrag != null)
{
sb.Clear().Append("DownHandler() Send initializePotentialDrag to ").Append(pointerData.pointerDrag.name).Append(", current GameObject is ").Append(raycastObject.name); DEBUG(sb);
ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.initializePotentialDrag);
}
// press happened (even not handled) object.
pointerData.rawPointerPress = raycastObject;
// allow to send Pointer Click event
pointerData.eligibleForClick = true;
// reset the screen position of press, can be used to estimate move distance
pointerData.delta = Vector2.zero;
// current Down, reset drag state
pointerData.dragging = false;
pointerData.useDragThreshold = true;
// record the count of Pointer Click should be processed, clean when Click event is sent.
pointerData.clickCount = 1;
// set clickTime to current time of Pointer Down instead of Pointer Click.
// since Down & Up event should not be sent too closely. (< kClickInterval)
pointerData.clickTime = Time.unscaledTime;
}
private void UpHandler()
{
if (!pointerData.eligibleForClick && !pointerData.dragging)
{
// 1. no pending click
// 2. no dragging
// Mean user has finished all actions and do NOTHING in current frame.
return;
}
// raycastObject may be different with pointerData.pointerDrag so we don't check null
if (pointerData.pointerPress != null)
{
// In the frame of button is pressed -> unpressed, send Pointer Up
sb.Clear().Append("UpHandler() Send Pointer Up to ").Append(pointerData.pointerPress.name); DEBUG(sb);
ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerUpHandler);
}
if (pointerData.eligibleForClick)
{
GameObject objectToClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(raycastObject);
if (objectToClick != null)
{
if (objectToClick == pointerData.pointerPress)
{
// In the frame of button from being pressed to unpressed, send Pointer Click if Click is pending.
sb.Clear().Append("UpHandler() Send Pointer Click to ").Append(pointerData.pointerPress.name); DEBUG(sb);
ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerClickHandler);
}
else
{
sb.Clear().Append("UpHandler() pointer down object ").Append(pointerData.pointerPress).Append(" is different with click object ").Append(objectToClick.name); DEBUG(sb);
}
}
else
{
if (pointerData.dragging)
{
GameObject _pointerDrop = ExecuteEvents.GetEventHandler<IDropHandler>(raycastObject);
if (_pointerDrop == pointerData.pointerDrag)
{
// In next frame of button from being pressed to unpressed, send Drop and EndDrag if dragging.
sb.Clear().Append("UpHandler() Send Pointer Drop to ").Append(pointerData.pointerDrag); DEBUG(sb);
ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.dropHandler);
}
sb.Clear().Append("UpHandler() Send Pointer endDrag to ").Append(pointerData.pointerDrag); DEBUG(sb);
ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.endDragHandler);
pointerData.dragging = false;
}
}
}
// initializePotentialDrag was sent when IDragHandler exists.
pointerData.pointerDrag = null;
// Down of pending Click object.
pointerData.pointerPress = null;
// press happened (even not handled) object.
pointerData.rawPointerPress = null;
// clear pending state.
pointerData.eligibleForClick = false;
// Click is processed, clearcount.
pointerData.clickCount = 0;
// Up is processed thus clear the time limitation of Down event.
pointerData.clickTime = 0;
}
// After selecting an object over this duration, the drag action will be taken.
const float kTimeToDrag = 0.2f;
private void DragHandler()
{
if (Time.unscaledTime - pointerData.clickTime < kTimeToDrag) { return; }
if (pointerData.pointerDrag == null) { return; }
if (!pointerData.dragging)
{
sb.Clear().Append("DragHandler() Send BeginDrag to ").Append(pointerData.pointerDrag.name); DEBUG(sb);
ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.beginDragHandler);
pointerData.dragging = true;
}
else
{
ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.dragHandler);
}
}
private void SubmitHandler()
{
if (raycastObject == null) { return; }
sb.Clear().Append("SubmitHandler() Submit: ").Append(raycastObject.name); DEBUG(sb);
ExecuteEvents.ExecuteHierarchy(raycastObject, pointerData, ExecuteEvents.submitHandler);
}
// Do NOT allow event DOWN being sent multiple times during kClickInterval
// since UI element of Unity needs time to perform transitions.
const float kClickInterval = 0.2f;
private void HandleRaycastEvent()
{
ExitEnterHandler(ref s_raycastObjects, ref s_raycastObjectsEx);
HoverHandler();
bool submit = OnSubmit();
if (submit)
{
SubmitHandler();
return;
}
bool down = OnDown();
bool hold = OnHold();
if (!down && hold)
{
// Hold means to Drag.
DragHandler();
}
else if (Time.unscaledTime - pointerData.clickTime < kClickInterval)
{
// Delay new events until kClickInterval has passed.
}
else if (down && !pointerData.eligibleForClick)
{
// 1. Not Down -> Down
// 2. No pending Click should be procced.
DownHandler();
}
else if (!hold)
{
// 1. If Down before, send Up event and clear Down state.
// 2. If Dragging, send Drop & EndDrag event and clear Dragging state.
// 3. If no Down or Dragging state, do NOTHING.
UpHandler();
}
}
#endregion
#region Actions
protected virtual bool OnDown() { return false; }
protected virtual bool OnHold() { return false; }
protected virtual bool OnSubmit() { return false; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 74b5628be71247e4f80cb6ddc4d7025b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,210 @@
// Copyright HTC Corporation All Rights Reserved.
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
namespace VIVE.OpenXR.Raycast
{
[RequireComponent(typeof(LineRenderer))]
public class RaycastPointer : RaycastImpl
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.RaycastPointer";
void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
#region Inspector
[Tooltip("To show the ray which presents the casting direction.")]
[SerializeField]
private bool m_ShowRay = true;
public bool ShowRay { get { return m_ShowRay; } set { m_ShowRay = value; } }
[SerializeField]
private float m_RayStartWidth = 0.01f;
public float RayStartWidth { get { return m_RayStartWidth; } set { m_RayStartWidth = value; } }
[SerializeField]
private float m_RayEndWidth = 0.01f;
public float RayEndWidth { get { return m_RayEndWidth; } set { m_RayEndWidth = value; } }
[SerializeField]
private Material m_RayMaterial = null;
public Material RayMaterial { get { return m_RayMaterial; } set { m_RayMaterial = value; } }
[SerializeField]
private GameObject m_Pointer = null;
public GameObject Pointer { get { return m_Pointer; } set { m_Pointer = value; } }
#endregion
LineRenderer m_Ray = null;
Vector3 m_PointerScale = new Vector3(.15f, .15f, .15f);
#region MonoBehaviour overrides
protected override void OnEnable()
{
sb.Clear().Append("OnEnable()"); DEBUG(sb);
base.OnEnable();
if (m_Ray == null) { m_Ray = GetComponent<LineRenderer>(); }
if (m_Pointer != null)
{
m_PointerScale = m_Pointer.transform.localScale;
sb.Clear().Append("OnEnable() Get default pointer scale (").Append(m_PointerScale.x).Append(", ").Append(m_PointerScale.y).Append(", ").Append(m_PointerScale.z).Append(")"); DEBUG(sb);
}
}
protected override void OnDisable()
{
sb.Clear().Append("OnDisable()"); DEBUG(sb);
base.OnDisable();
ActivatePointer(false);
ActivateRay(false);
}
protected override void Update()
{
/// Raycast
base.Update();
if (printIntervalLog)
{
if (m_Ray != null)
sb.Clear().Append("Update() ").Append(gameObject.name).Append(", m_Ray enabled: ").Append(m_Ray.enabled); DEBUG(sb);
if (m_Pointer != null)
sb.Clear().Append("Update() ").Append(gameObject.name).Append(", m_Pointer enabled: ").Append(m_Pointer.activeSelf); DEBUG(sb);
}
if (!IsInteractable()) { return; }
/// Draw the ray and pointer.
DrawRayPointer();
}
#endregion
#region Ray and Pointer
private void ActivatePointer(bool active)
{
if (m_Pointer != null)
{
if (m_Pointer.activeSelf != active) { sb.Clear().Append("ActivatePointer() ").Append(gameObject.name).Append(" ").Append(active); DEBUG(sb); }
m_Pointer.SetActive(active);
}
}
private void ActivateRay(bool active)
{
if (m_Ray != null)
{
if (m_Ray.enabled != active) { sb.Clear().Append("ActivateRay() ").Append(gameObject.name).Append(" ").Append(active); DEBUG(sb); }
m_Ray.enabled = active;
}
}
private Vector3 GetIntersectionPosition(Camera cam, RaycastResult raycastResult)
{
if (cam == null)
return Vector3.zero;
float intersectionDistance = raycastResult.distance + cam.nearClipPlane;
Vector3 intersectionPosition = cam.transform.forward * intersectionDistance + cam.transform.position;
return intersectionPosition;
}
Vector3 rayStart = Vector3.zero, rayEnd = Vector3.zero;
const float kRayLengthMin = 0.5f;
private float m_RayLength = 10;
protected Vector3 pointerPosition = Vector3.zero;
private void DrawRayPointer()
{
Vector3 hit = GetIntersectionPosition(eventCamera, pointerData.pointerCurrentRaycast);
rayStart = transform.position;
if (raycastObject != null)
{
m_RayLength = Vector3.Distance(hit, rayStart);
m_RayLength = m_RayLength > kRayLengthMin ? m_RayLength : kRayLengthMin;
}
if (LockPointer())
{
Vector3 middle = new Vector3(0, 0, (m_RayLength - 0.2f) / 4);
DrawCurveRay(rayStart, middle, rayEnd, m_RayStartWidth, m_RayEndWidth, m_RayMaterial);
}
else
{
rayEnd = rayStart + (transform.forward * (m_RayLength - 0.2f));
pointerPosition = rayStart + (transform.forward * m_RayLength);
DrawRay(rayStart, rayEnd, m_RayStartWidth, m_RayEndWidth, m_RayMaterial);
}
DrawPointer(pointerPosition);
}
const float kPointerDistance = 10;
private void DrawPointer(Vector3 position)
{
if (m_Pointer == null) { return; }
m_Pointer.transform.position = position;
m_Pointer.transform.rotation = Camera.main.transform.rotation;
float distance = Vector3.Distance(position, Camera.main.transform.position);
m_Pointer.transform.localScale = m_PointerScale * (distance / kPointerDistance);
}
private void DrawRay(Vector3 start, Vector3 end, float startWidth, float endWidth, Material material)
{
if (m_Ray == null) { return; }
Vector3[] positions = new Vector3[] { start, end };
m_Ray.positionCount = positions.Length;
m_Ray.SetPositions(positions);
m_Ray.startWidth = startWidth;
m_Ray.endWidth = endWidth;
m_Ray.material = material;
m_Ray.useWorldSpace = true;
}
private void DrawCurveRay(Vector3 start, Vector3 middle, Vector3 end, float startWidth, float endWidth, Material material)
{
if (m_Ray == null) { m_Ray = GetComponent<LineRenderer>(); }
Vector3[] positions = GenerateBezierCurve3(50, start, middle, end);
m_Ray.positionCount = positions.Length;
m_Ray.SetPositions(positions);
m_Ray.startWidth = startWidth;
m_Ray.endWidth = endWidth;
m_Ray.material = material;
m_Ray.useWorldSpace = true;
}
Vector3[] GenerateBezierCurve2(int iteration, Vector3 start, Vector3 end)
{
Vector3[] points = new Vector3[iteration + 1];
for (int i = 0; i < iteration + 1; i++)
{
points.SetValue(start + ((end - start).normalized * (end - start).magnitude * i / iteration), i);
}
return points;
}
Vector3[] GenerateBezierCurve3(int iteration, Vector3 start, Vector3 middle, Vector3 end)
{
Vector3[] points1 = GenerateBezierCurve2(iteration, start, middle);
Vector3[] points2 = GenerateBezierCurve2(iteration, start, end);
Vector3[] points = new Vector3[iteration + 1];
for (int i = 0; i < iteration + 1; i++)
{
points.SetValue(points1[i] + ((points2[i] - points1[i]).normalized * (points2[i] - points1[i]).magnitude * i / iteration), i);
}
return points;
}
#endregion
private bool IsInteractable()
{
ActivatePointer(m_Interactable);
ActivateRay(m_Interactable && m_ShowRay);
return m_Interactable;
}
/// <summary> For DrawRayPointer(), controls whether locking the pointer or not. </summary>
protected virtual bool LockPointer()
{
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e8ff0b4ae49c1a4ea049f9e8afe0230
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,337 @@
// Copyright HTC Corporation All Rights Reserved.
using System.Text;
using UnityEngine;
namespace VIVE.OpenXR.Raycast
{
/// <summary>
/// To draw a ring pointer to indicate the gazed space.
/// </summary>
[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class RaycastRing : RaycastImpl
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.RaycastRing";
void DEBUG(StringBuilder msg) { Debug.LogFormat("{0} {1}", LOG_TAG, msg); }
public enum GazeEvent
{
Down = 0,
Submit = 1
}
#region Inspector
// ----------- Width of ring -----------
const float kRingWidthDefault = 0.005f;
const float kRingWidthMinimal = 0.001f;
[Tooltip("Set the width of the pointer's ring.")]
[SerializeField]
private float m_PointerRingWidth = kRingWidthDefault;
public float PointerRingWidth { get { return m_PointerRingWidth; } set { m_PointerRingWidth = value; } }
// ----------- Radius of inner circle -----------
const float kInnerCircleRadiusDefault = 0.005f;
const float kInnerCircleRadiusMinimal = 0.001f;
[Tooltip("Set the radius of the pointer's inner circle.")]
[SerializeField]
private float m_PointerCircleRadius = kInnerCircleRadiusDefault;
public float PointerCircleRadius { get { return m_PointerCircleRadius; } set { m_PointerCircleRadius = value; } }
// ----------- Z distance of ring -----------
const float kPointerDistanceDefault = 1;
const float kPointerDistanceMinimal = 0.1f;
[Tooltip("Set the z-coordinate of the pointer.")]
[SerializeField]
private float m_PointerDistance = kPointerDistanceDefault;
public float PointerDistance { get { return m_PointerDistance; } set { m_PointerDistance = value; } }
/// The offset from the pointer to the pointer-mounted object.
private Vector3 ringOffset = Vector3.zero;
/// The offset from the pointer to the pointer-mounted object in every frame.
private Vector3 ringFrameOffset = Vector3.zero;
/// The pointer world position.
private Vector3 ringWorldPosition = Vector3.zero;
// ----------- Color of ring -----------
/// Color of ring background.
[Tooltip("Set the ring background color.")]
[SerializeField]
private Color m_PointerColor = Color.white;
public Color PointerColor { get { return m_PointerColor; } set { m_PointerColor = value; } }
/// Color of ring foreground
[Tooltip("Set the ring foreground progess color.")]
[SerializeField]
private Color m_ProgressColor = new Color(0, 245, 255);
public Color ProgressColor { get { return m_ProgressColor; } set { m_ProgressColor = value; } }
// ----------- Material and Mesh -----------
private Mesh m_Mesh = null;
const string kPointerMaterial = "Materials/GazePointerDef";
[Tooltip("Empty for using the default material or set a customized material.")]
[SerializeField]
private Material m_PointerMaterial = null;
public Material PointerMaterial { get { return m_PointerMaterial; } set { m_PointerMaterial = value; } }
private Material pointerMaterialInstance = null;
private MeshFilter m_MeshFilter = null;
private MeshRenderer m_MeshRenderer = null;
const int kMaterialRenderQueueMin = 1000;
const int kMaterialRenderQueueMax = 5000;
/// The material's renderQueue.
[Tooltip("Set the Material's renderQueue.")]
[SerializeField]
private int m_PointerRenderQueue = kMaterialRenderQueueMax;
public int PointerRenderQueue { get { return m_PointerRenderQueue; } set { m_PointerRenderQueue = value; } }
/// The MeshRenderer's sortingOrder.
[Tooltip("Set the MeshRenderer's sortingOrder.")]
[SerializeField]
private int m_PointerSortingOrder = 32767;
public int PointerSortingOrder { get { return m_PointerSortingOrder; } set { m_PointerSortingOrder = value; } }
protected int m_RingPercent = 0;
public int RingPercent { get { return m_RingPercent; } set { m_RingPercent = value; } }
[Tooltip("Gaze timer to trigger gaze events.")]
[SerializeField]
private float m_TimeToGaze = 1.5f;
public float TimeToGaze { get { return m_TimeToGaze; } set { m_TimeToGaze = value; } }
private void ValidateParameters()
{
if (m_PointerRingWidth < kRingWidthMinimal)
m_PointerRingWidth = kRingWidthDefault;
if (m_PointerCircleRadius < kInnerCircleRadiusMinimal)
m_PointerCircleRadius = kInnerCircleRadiusDefault;
if (m_PointerDistance < kPointerDistanceMinimal)
m_PointerDistance = kPointerDistanceDefault;
if (m_PointerRenderQueue < kMaterialRenderQueueMin || m_PointerRenderQueue > kMaterialRenderQueueMax)
m_PointerRenderQueue = kMaterialRenderQueueMax;
}
#endregion
#region MonoBehaviour overrides
private bool mEnabled = false;
protected override void OnEnable()
{
base.OnEnable();
if (!mEnabled)
{
sb.Clear().Append("OnEnable()"); DEBUG(sb);
// 1. Texture or Mesh < Material < < MeshFilter < MeshRenderer, we don't use the texture.
if (m_Mesh == null)
m_Mesh = new Mesh();
if (m_Mesh != null)
m_Mesh.name = gameObject.name + " Mesh";
// 2. Load the Material RingUnlitTransparentMat.
if (m_PointerMaterial == null)
m_PointerMaterial = Resources.Load(kPointerMaterial) as Material;
if (m_PointerMaterial != null)
{
pointerMaterialInstance = Instantiate(m_PointerMaterial);
sb.Clear().Append("OnEnable() Loaded resource ").Append(pointerMaterialInstance.name); DEBUG(sb);
}
// 3. Get the MeshFilter.
m_MeshFilter = GetComponent<MeshFilter>();
// 4. Get the MeshRenderer.
m_MeshRenderer = GetComponent<MeshRenderer>();
m_MeshRenderer.sortingOrder = m_PointerSortingOrder;
m_MeshRenderer.material = pointerMaterialInstance;
m_MeshRenderer.material.renderQueue = PointerRenderQueue;
mEnabled = true;
}
}
protected override void OnDisable()
{
base.OnDisable();
if (mEnabled)
{
sb.Clear().Append("OnDisable()"); DEBUG(sb);
if (m_MeshFilter != null)
{
Mesh mesh = m_MeshFilter.mesh;
mesh.Clear();
}
Destroy(pointerMaterialInstance);
mEnabled = false;
}
}
protected override void Update()
{
base.Update();
if (!IsInteractable()) { return; }
ValidateParameters();
UpdatePointerOffset();
ringFrameOffset = ringOffset;
ringFrameOffset.z = ringFrameOffset.z < kPointerDistanceMinimal ? kPointerDistanceDefault : ringFrameOffset.z;
// Calculate the pointer world position
Vector3 rotated_direction = transform.rotation * ringFrameOffset;
ringWorldPosition = transform.position + rotated_direction;
//DEBUG("ringWorldPosition: " + ringWorldPosition.x + ", " + ringWorldPosition.y + ", " + ringWorldPosition.z);
float calcRingWidth = m_PointerRingWidth * (ringFrameOffset.z / kPointerDistanceDefault);
float calcInnerCircleRadius = m_PointerCircleRadius * (ringFrameOffset.z / kPointerDistanceDefault);
UpdateRingPercent();
DrawRingRoll(calcRingWidth + calcInnerCircleRadius, calcInnerCircleRadius, ringFrameOffset, m_RingPercent);
if (printIntervalLog)
{
sb.Clear().Append("Update() ")
.Append(gameObject.name).Append(" is ").Append(m_MeshRenderer.enabled ? "shown" : "hidden")
.Append(", ringFrameOffset (").Append(ringFrameOffset.x).Append(", ").Append(ringFrameOffset.y).Append(", ").Append(ringFrameOffset.z).Append(")");
DEBUG(sb);
}
}
#endregion
private bool IsInteractable()
{
ActivatePointer(m_Interactable);
return m_Interactable;
}
private void ActivatePointer(bool active)
{
if (m_MeshRenderer == null)
return;
if (m_MeshRenderer.enabled != active)
{
m_MeshRenderer.enabled = active;
sb.Clear().Append("ActivatePointer() ").Append(m_MeshRenderer.enabled); DEBUG(sb);
if (m_MeshRenderer.enabled)
{
m_MeshRenderer.sortingOrder = m_PointerSortingOrder;
if (pointerMaterialInstance != null)
{
m_MeshRenderer.material = pointerMaterialInstance;
m_MeshRenderer.material.renderQueue = PointerRenderQueue;
}
// The MeshFilter's mesh is updated in DrawRingRoll(), not here.
}
}
}
private void UpdatePointerOffset()
{
ringOffset = pointerLocalOffset;
// Moves the pointer onto the gazed object.
if (raycastObject != null)
{
Vector3 rotated_direction = pointerData.pointerCurrentRaycast.worldPosition - gameObject.transform.position;
ringOffset = Quaternion.Inverse(transform.rotation) * rotated_direction;
}
}
protected float m_GazeOnTime = 0;
private void UpdateRingPercent()
{
if (raycastObject != raycastObjectEx)
{
m_RingPercent = 0;
if (raycastObject != null)
m_GazeOnTime = Time.unscaledTime;
}
else
{
// Hovering
if (raycastObject != null)
m_RingPercent = (int)(((Time.unscaledTime - m_GazeOnTime) / m_TimeToGaze) * 100);
}
}
const int kRingVertexCount = 400; // 100 percents * 2 + 2, ex: 80% ring -> 80 * 2 + 2
private Vector3[] ringVert = new Vector3[kRingVertexCount];
private Color[] ringColor = new Color[kRingVertexCount];
const int kRingTriangleCount = 100 * 6; // 100 percents * 6, ex: 80% ring -> 80 * 6
private int[] ringTriangle = new int[kRingTriangleCount];
private Vector2[] ringUv = new Vector2[kRingVertexCount];
const float kPercentAngle = 3.6f; // 100% = 100 * 3.6f = 360 degrees.
private void DrawRingRoll(float radius, float innerRadius, Vector3 offset, int percent)
{
if (m_MeshFilter == null)
return;
percent = percent >= 100 ? 100 : percent;
// vertices and colors
float start_angle = 90; // Start angle of drawing ring.
for (int i = 0; i < kRingVertexCount; i += 2)
{
float radian_cur = start_angle * Mathf.Deg2Rad;
float cosA = Mathf.Cos(radian_cur);
float sinA = Mathf.Sin(radian_cur);
ringVert[i].x = offset.x + radius * cosA;
ringVert[i].y = offset.y + radius * sinA;
ringVert[i].z = offset.z;
ringColor[i] = (i <= (percent * 2) && i > 0) ? m_ProgressColor : m_PointerColor;
ringVert[i + 1].x = offset.x + innerRadius * cosA;
ringVert[i + 1].y = offset.y + innerRadius * sinA;
ringVert[i + 1].z = offset.z;
ringColor[i + 1] = (i <= (percent * 2) && i > 0) ? m_ProgressColor : m_PointerColor;
start_angle -= kPercentAngle;
}
// triangles
for (int i = 0, vi = 0; i < kRingTriangleCount; i += 6, vi += 2)
{
ringTriangle[i] = vi;
ringTriangle[i + 1] = vi + 3;
ringTriangle[i + 2] = vi + 1;
ringTriangle[i + 3] = vi + 2;
ringTriangle[i + 4] = vi + 3;
ringTriangle[i + 5] = vi;
}
// uv
for (int i = 0; i < kRingVertexCount; i++)
{
ringUv[i].x = ringVert[i].x / radius / 2 + 0.5f;
ringUv[i].y = ringVert[i].z / radius / 2 + 0.5f;
}
m_Mesh.Clear();
m_Mesh.vertices = ringVert;
m_Mesh.colors = ringColor;
m_Mesh.triangles = ringTriangle;
m_Mesh.uv = ringUv;
m_MeshFilter.mesh = m_Mesh;
}
#region External Functions
public Vector3 GetPointerPosition()
{
return ringWorldPosition;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 928994fe36775ae4782045d0503ba99f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,104 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using UnityEngine;
namespace VIVE.OpenXR.Raycast
{
[DisallowMultipleComponent]
public sealed class RaycastSwitch : MonoBehaviour
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.RaycastSwitch";
void DEBUG(string msg) { Debug.Log(LOG_TAG + " " + msg); }
[Serializable]
public class GazeSettings
{
public bool Enabled = false;
}
[SerializeField]
private GazeSettings m_GazeRaycast = new GazeSettings();
public GazeSettings GazeRaycast { get { return m_GazeRaycast; } set { m_GazeRaycast = value; } }
public static GazeSettings Gaze { get { return Instance.GazeRaycast; } }
[Serializable]
public class ControllerSettings
{
public bool Enabled = true;
}
[SerializeField]
private ControllerSettings m_ControllerRaycast = new ControllerSettings();
public ControllerSettings ControllerRaycast { get { return m_ControllerRaycast; } set { m_ControllerRaycast = value; } }
public static ControllerSettings Controller { get { return Instance.ControllerRaycast; } }
[Serializable]
public class HandSettings
{
public bool Enabled = true;
}
[SerializeField]
private HandSettings m_HandRaycast = new HandSettings();
public HandSettings HandRaycast { get { return m_HandRaycast; } set { m_HandRaycast = value; } }
public static HandSettings Hand { get { return Instance.HandRaycast; } }
private static RaycastSwitch m_Instance = null;
public static RaycastSwitch Instance
{
get
{
if (m_Instance == null)
{
var rs = new GameObject("RaycastSwitch");
m_Instance = rs.AddComponent<RaycastSwitch>();
// This object should survive all scene transitions.
DontDestroyOnLoad(rs);
}
return m_Instance;
}
}
private void Awake()
{
m_Instance = this;
}
private bool m_Enabled = false;
private void OnEnable()
{
if (!m_Enabled)
{
DEBUG("OnEnable()");
m_Enabled = true;
}
}
private void OnDisable()
{
if (m_Enabled)
{
DEBUG("OnDisable()");
m_Enabled = false;
}
}
int printFrame = 0;
bool printLog = false;
private void Update()
{
printFrame++;
printFrame %= 300;
printLog = (printFrame == 0);
CheckSettings();
if (printLog)
{
DEBUG("Update() Gaze.Enabled: " + GazeRaycast.Enabled
+ ", Controller.Enabled: " + ControllerRaycast.Enabled
+ ", Hand.Enabled: " + HandRaycast.Enabled);
}
}
/// <summary> Updates Gaze, Controller and Hand settings in runtime. </summary>
private void CheckSettings()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1caf898641806b3448ff86ad2e1b0727
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
// Copyright HTC Corporation All Rights Reserved.
using UnityEngine;
namespace VIVE.OpenXR.Raycast
{
public class TargetCanvas : MonoBehaviour
{
const string LOG_TAG = "VIVE.OpenXR.Raycast.TargetCanvas";
private void DEBUG(string msg) { Debug.Log(LOG_TAG + " " + gameObject.name + ", " + msg); }
Canvas m_Canvas = null;
private void Awake()
{
m_Canvas = GetComponent<Canvas>();
}
private void OnEnable()
{
DEBUG("OnEnable()");
if (m_Canvas != null)
{
DEBUG("OnEnable() RegisterTargetCanvas.");
CanvasProvider.RegisterTargetCanvas(m_Canvas);
}
}
private void OnDisable()
{
DEBUG("OnDisable()");
if (m_Canvas != null)
{
DEBUG("OnDisable() RemoveTargetCanvas.");
CanvasProvider.RemoveTargetCanvas(m_Canvas);
}
}
Canvas[] s_ChildrenCanvas = null;
private void Update()
{
Canvas[] canvases = GetComponentsInChildren<Canvas>();
if (canvases != null && canvases.Length > 0) // find children canvas
{
s_ChildrenCanvas = canvases;
for (int i = 0; i < s_ChildrenCanvas.Length; i++)
CanvasProvider.RegisterTargetCanvas(s_ChildrenCanvas[i]);
return;
}
if (s_ChildrenCanvas != null && s_ChildrenCanvas.Length > 0) // remove old children canvas
{
for (int i = 0; i < s_ChildrenCanvas.Length; i++)
CanvasProvider.RemoveTargetCanvas(s_ChildrenCanvas[i]);
s_ChildrenCanvas = null;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6fe9581cc98d0ff4fb8250646cc0359f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -5,7 +5,7 @@ using UnityEditor;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(HandMeshManager))]
public class HandMeshManagerEditor : Editor
public class HandMeshManagerEditor : UnityEditor.Editor
{
private HandMeshManager m_HandMesh;
private SerializedProperty m_Handedness, m_EnableCollider, m_HandJoints;

View File

@@ -67,7 +67,9 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
[SerializeField]
private Rigidbody m_Rigidbody = null;
public new Rigidbody rigidbody => m_Rigidbody;
#pragma warning disable
public Rigidbody rigidbody => m_Rigidbody;
#pragma warning enable
[SerializeField]
private List<GrabPose> m_GrabPoses = new List<GrabPose>();
@@ -82,15 +84,17 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
private bool m_ShowAllIndicator = false;
private List<Collider> allColliders = new List<Collider>();
private HandGrabInteractor closestGrabber = null;
private OnBeginGrabbed beginGrabbed;
private OnEndGrabbed endGrabbed;
[SerializeField]
private IOneHandContraintMovement m_OneHandContraintMovement;
public bool isContraint => m_OneHandContraintMovement != null;
public IOneHandContraintMovement oneHandContraintMovement { get { return m_OneHandContraintMovement; } set { m_OneHandContraintMovement = value; } }
public bool isContraint => m_OneHandContraintMovement != null;
#pragma warning disable
[SerializeField]
private int m_PreviewIndex = -1;
#pragma warning enable
private RaycastHit[] hitResults = new RaycastHit[10];
#region MonoBehaviour
private void Awake()
@@ -124,7 +128,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
handPose.GetPosition(JointType.Wrist, out Vector3 wristPos);
handPose.GetRotation(JointType.Wrist, out Quaternion wristRot);
UpdateBestGrabPose(handGrabber.isLeft, new Pose(wristPos, wristRot));
beginGrabbed?.Invoke(this);
m_OnBeginGrabbed?.Invoke(this);
DEBUG($"{transform.name} is grabbed by {handGrabber.name}");
}
@@ -132,7 +135,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
m_Grabber = null;
m_BestGrabPose = GrabPose.Identity;
endGrabbed?.Invoke(this);
m_OnEndGrabbed?.Invoke(this);
DEBUG($"{transform.name} is released.");
}
@@ -185,46 +187,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
return distacne > grabDistance ? 0 : 1 - (distacne / grabDistance);
}
/// <summary>
/// Add a listener for the event triggered when the grabbable object is grabbed.
/// </summary>
/// <param name="handler">The method to be called when the grabbable object is grabbed.</param>
[Obsolete("Please use onBeginGrabbed instead.")]
public void AddBeginGrabbedListener(OnBeginGrabbed handler)
{
beginGrabbed += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabbable object is grabbed.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onBeginGrabbed instead.")]
public void RemoveBeginGrabbedListener(OnBeginGrabbed handler)
{
beginGrabbed -= handler;
}
/// <summary>
/// Add a listener for the event triggered when the grabbable object is released.
/// </summary>
/// <param name="handler">The method to be called when the grabbable object is released.</param>
[Obsolete("Please use onEndGrabbed instead.")]
public void AddEndGrabbedListener(OnEndGrabbed handler)
{
endGrabbed += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabbable object is released.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onEndGrabbed instead.")]
public void RemoveEndGrabbedListener(OnEndGrabbed handler)
{
endGrabbed -= handler;
}
/// <summary>
/// Update the position and rotation of the self with the pose of the hand that is grabbing it.
/// </summary>
@@ -302,16 +264,19 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
Vector3 closestPoint = Vector3.zero;
float shortDistance = float.MaxValue;
foreach (var collider in allColliders)
for (int i = 0; i < allColliders.Count; i++)
{
Collider collider = allColliders[i];
Vector3 closePoint = collider.ClosestPointOnBounds(sourcePos);
float distance = Vector3.Distance(sourcePos, closePoint);
if (collider.bounds.Contains(closePoint))
{
Vector3 direction = (closePoint - sourcePos).normalized;
RaycastHit[] hits = Physics.RaycastAll(sourcePos, direction, distance);
foreach (var hit in hits)
Vector3 direction = closePoint - sourcePos;
direction.Normalize();
int hitCount = Physics.RaycastNonAlloc(sourcePos, direction, hitResults, distance);
for (int j = 0; j < hitCount; j++)
{
RaycastHit hit = hitResults[j];
if (hit.collider == collider)
{
float hitDistance = Vector3.Distance(sourcePos, hit.point);

View File

@@ -9,6 +9,7 @@
// specifications, and documentation provided by HTC to You."
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
@@ -34,8 +35,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
private void DEBUG(string msg) { Debug.Log($"{LOG_TAG}, {msg}"); }
private void WARNING(string msg) { Debug.LogWarning($"{LOG_TAG}, {msg}"); }
private void ERROR(string msg) { Debug.LogError($"{LOG_TAG}, {msg}"); }
int logFrame = 0;
bool printIntervalLog => logFrame == 0;
#endregion
@@ -79,8 +78,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
private GrabState m_State = GrabState.None;
private Pose wristPose = Pose.identity;
private Vector3[] fingerTipPosition = new Vector3[(int)FingerId.Count];
private OnBeginGrab beginGrabHandler;
private OnEndGrab endGrabHandler;
#region MonoBehaviour
private void Awake()
@@ -155,46 +152,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
}
return false;
}
/// <summary>
/// Add a listener for the event triggered when the grabber begins grabbing.
/// </summary>
/// <param name="handler">The method to be called when the grabber begins grabbing.</param>
[Obsolete("Please use onBeginGrab instead.")]
public void AddBeginGrabListener(OnBeginGrab handler)
{
beginGrabHandler += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabber begins grabbing.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onBeginGrab instead.")]
public void RemoveBeginGrabListener(OnBeginGrab handler)
{
beginGrabHandler -= handler;
}
/// <summary>
/// Add a listener for the event triggered when the grabber ends grabbing.
/// </summary>
/// <param name="handler">The method to be called when the grabber ends grabbing.</param>
[Obsolete("Please use onEndGrab instead.")]
public void AddEndGrabListener(OnEndGrab handler)
{
endGrabHandler += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabber ends grabbing.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onEndGrab instead.")]
public void RemoveEndGrabListener(OnEndGrab handler)
{
endGrabHandler -= handler;
}
#endregion
/// <summary>
@@ -231,13 +188,32 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
grabbable = null;
maxScore = 0f;
foreach (HandGrabInteractable interactable in GrabManager.handGrabbables)
{
interactable.ShowIndicator(false, this);
foreach (Vector3 tipPos in fingerTipPosition)
Collider[] nearColliders = Physics.OverlapSphere(wristPose.position, 0.5f);
List<HandGrabInteractable> nearHandGrabInteractables = new List<HandGrabInteractable>();
for (int i = 0; i < nearColliders.Length; i++)
{
HandGrabInteractable interactable = nearColliders[i].GetComponentInParent<HandGrabInteractable>();
if (interactable && !nearHandGrabInteractables.Contains(interactable))
{
float distanceScore = interactable.CalculateDistanceScore(tipPos, grabDistance);
nearHandGrabInteractables.Add(interactable);
continue;
}
interactable = nearColliders[i].GetComponentInChildren<HandGrabInteractable>();
if (interactable && !nearHandGrabInteractables.Contains(interactable))
{
nearHandGrabInteractables.Add(interactable);
continue;
}
}
for (int i = 0; i < nearHandGrabInteractables.Count; i++)
{
HandGrabInteractable interactable = nearHandGrabInteractables[i];
interactable.ShowIndicator(false, this);
for (int j = 0; j < fingerTipPosition.Length; j++)
{
float distanceScore = interactable.CalculateDistanceScore(fingerTipPosition[j], grabDistance);
if (distanceScore > maxScore)
{
maxScore = distanceScore;
@@ -280,7 +256,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
m_Grabbable = currentCandidate;
m_Grabbable.SetGrabber(this);
m_Grabbable.ShowIndicator(false, this);
beginGrabHandler?.Invoke(this);
onBeginGrab?.Invoke(this);
DEBUG($"The {(m_Handedness == Handedness.Left ? "left" : "right")} hand begins to grab the {m_Grabbable.name}");
@@ -296,7 +271,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
DEBUG($"The {(m_Handedness == Handedness.Left ? "left" : "right")} hand ends to grab the {m_Grabbable.name}");
endGrabHandler?.Invoke(this);
onEndGrab?.Invoke(this);
m_Grabbable.SetGrabber(null);
m_Grabbable = null;

View File

@@ -1,7 +1,7 @@
using System.Text;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public class OneGrabMoveConstraint : IOneHandContraintMovement
{
@@ -30,21 +30,28 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
[SerializeField]
private ConstraintInfo m_NegativeXMove = ConstraintInfo.Identity;
private float defaultNegativeXPos = 0.0f;
public float xNegativeBoundary => defaultNegativeXPos;
[SerializeField]
private ConstraintInfo m_PositiveXMove = ConstraintInfo.Identity;
private float defaultPositiveXPos = 0.0f;
public float xPositiveBoundary => defaultPositiveXPos;
[SerializeField]
private ConstraintInfo m_NegativeYMove = ConstraintInfo.Identity;
private float defaultNegativeYPos = 0.0f;
public float yNegativeBoundary => defaultNegativeYPos;
[SerializeField]
private ConstraintInfo m_PositiveYMove = ConstraintInfo.Identity;
private float defaultPositiveYPos = 0.0f;
public float yPositiveBoundary => defaultPositiveYPos;
[SerializeField]
private ConstraintInfo m_NegativeZMove = ConstraintInfo.Identity;
private float defaultNegativeZPos = 0.0f;
public float zNegativeBoundary => defaultNegativeZPos;
[SerializeField]
private ConstraintInfo m_PositiveZMove = ConstraintInfo.Identity;
private float defaultPositiveZPos = 0.0f;
public float zPositiveBoundary => defaultPositiveZPos;
private Pose previousHandPose = Pose.identity;
private GrabPose currentGrabPose = GrabPose.Identity;

View File

@@ -40,9 +40,12 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
private RotationAxis m_RotationAxis = RotationAxis.XAxis;
[SerializeField]
private ConstraintInfo m_ClockwiseAngle = ConstraintInfo.Identity;
public float clockwiseAngle => m_ClockwiseAngle.value;
[SerializeField]
private ConstraintInfo m_CounterclockwiseAngle = ConstraintInfo.Identity;
private float totalRotationAngle = 0.0f;
public float counterclockwiseAngle => m_CounterclockwiseAngle.value;
private float m_TotalDegrees = 0.0f;
public float totalDegrees => m_TotalDegrees;
private Pose previousHandPose = Pose.identity;
public override void Initialize(IGrabbable grabbable)
@@ -99,17 +102,17 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
float angleDelta = Vector3.Angle(previousVector, targetVector);
angleDelta *= Vector3.Dot(Vector3.Cross(previousVector, targetVector), worldAxis) > 0.0f ? 1.0f : -1.0f;
float previousAngle = totalRotationAngle;
totalRotationAngle += angleDelta;
float previousAngle = m_TotalDegrees;
m_TotalDegrees += angleDelta;
if (m_CounterclockwiseAngle.enableConstraint)
{
totalRotationAngle = Mathf.Max(totalRotationAngle, -m_CounterclockwiseAngle.value);
m_TotalDegrees = Mathf.Max(m_TotalDegrees, -m_CounterclockwiseAngle.value);
}
if (m_ClockwiseAngle.enableConstraint)
{
totalRotationAngle = Mathf.Min(totalRotationAngle, m_ClockwiseAngle.value);
m_TotalDegrees = Mathf.Min(m_TotalDegrees, m_ClockwiseAngle.value);
}
angleDelta = totalRotationAngle - previousAngle;
angleDelta = m_TotalDegrees - previousAngle;
m_Constraint.RotateAround(m_Pivot.position, worldAxis, angleDelta);
previousHandPose = handPose;

View File

@@ -13,11 +13,7 @@ using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_XR_HANDS
using UnityEngine.XR.Hands;
#endif
using VIVE.OpenXR.Toolkits.Common;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
@@ -60,31 +56,15 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
/// <summary>
/// This class is designed to update hand tracking data.
/// </summary>
#if UNITY_XR_HANDS
public static class DataWrapper
{
private static XRHandSubsystem handSubsystem = null;
private static List<XRHandSubsystem> s_XRHandSubsystems = new List<XRHandSubsystem>();
/// <summary>
/// Validate whether the hand tracking is active.
/// </summary>
/// <returns>True if the hand tracking is active; otherwise, false.</returns>
public static bool Validate()
{
if (handSubsystem == null || !handSubsystem.running)
{
SubsystemManager.GetSubsystems(s_XRHandSubsystems);
for (int i = 0; i < s_XRHandSubsystems.Count; i++)
{
if (handSubsystem != null)
{
handSubsystem = null;
}
handSubsystem = s_XRHandSubsystems[i];
}
}
return handSubsystem != null && handSubsystem.running;
return VIVEInput.IsHandValidate();
}
/// <summary>
@@ -94,11 +74,8 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
/// <returns>True if the hand tracking is successfully tracking; otherwise, false.</returns>
public static bool IsHandTracked(bool isLeft)
{
if (handSubsystem != null)
{
return isLeft ? handSubsystem.leftHand.isTracked : handSubsystem.rightHand.isTracked;
}
return false;
Common.Handedness handedness = isLeft ? Common.Handedness.Left : Common.Handedness.Right;
return VIVEInput.IsHandTracked(handedness);
}
/// <summary>
@@ -111,53 +88,17 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
/// <returns></returns>
public static bool GetJointPose(JointType jointType, ref Vector3 position, ref Quaternion rotation, bool isLeft)
{
if (IsHandTracked(isLeft))
Common.Handedness handedness = isLeft ? Common.Handedness.Left : Common.Handedness.Right;
if (IsHandTracked(isLeft) &&
VIVEInput.GetJointPose(handedness, (HandJointType)jointType, out Pose jointPose))
{
XRHand hand = isLeft ? handSubsystem.leftHand : handSubsystem.rightHand;
XRHandJoint xrHandJoint = hand.GetJoint(ConvertToXRHandJointID(jointType));
if (xrHandJoint.TryGetPose(out Pose pose))
{
position = pose.position;
rotation = pose.rotation;
return true;
}
position = jointPose.position;
rotation = jointPose.rotation;
return true;
}
return false;
}
private static XRHandJointID ConvertToXRHandJointID(JointType jointType)
{
int id = (int)jointType;
switch (id)
{
case 0:
return XRHandJointID.Palm;
case 1:
return XRHandJointID.Wrist;
default:
return (XRHandJointID)(id + 1);
}
}
}
#else
public static class DataWrapper
{
public static bool Validate()
{
return false;
}
public static bool IsHandTracked(bool isLeft)
{
return false;
}
public static bool GetJointPose(JointType jointType, ref Vector3 position, ref Quaternion rotation, bool isLeft)
{
return false;
}
}
#endif
/// <summary>
/// The enum is designed to define the IDs of joints.
@@ -242,28 +183,38 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public Vector3 direction = Vector3.zero;
public JointData[] joints = null;
public JointData joint0 {
get {
public JointData joint0
{
get
{
return joints[(Int32)JointId.Joint0];
}
}
public JointData joint1 {
get {
public JointData joint1
{
get
{
return joints[(Int32)JointId.Joint1];
}
}
public JointData joint2 {
get {
public JointData joint2
{
get
{
return joints[(Int32)JointId.Joint2];
}
}
public JointData joint3 {
get {
public JointData joint3
{
get
{
return joints[(Int32)JointId.Joint3];
}
}
public JointData tip {
get {
public JointData tip
{
get
{
return joints[(Int32)JointId.Tip];
}
}
@@ -692,28 +643,38 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
public JointData palm;
public JointData wrist;
public FingerData[] fingers = null; // size: FingerId.Count
public FingerData thumb {
get {
public FingerData thumb
{
get
{
return fingers[(Int32)FingerId.Thumb];
}
}
public FingerData index {
get {
public FingerData index
{
get
{
return fingers[(Int32)FingerId.Index];
}
}
public FingerData middle {
get {
public FingerData middle
{
get
{
return fingers[(Int32)FingerId.Middle];
}
}
public FingerData ring {
get {
public FingerData ring
{
get
{
return fingers[(Int32)FingerId.Ring];
}
}
public FingerData pinky {
get {
public FingerData pinky
{
get
{
return fingers[(Int32)FingerId.Pinky];
}
}
@@ -1026,13 +987,17 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.HandGrabState.FingerPinchState ";
StringBuilder m_sb = null;
StringBuilder sb {
get {
StringBuilder sb
{
get
{
if (m_sb == null) { m_sb = new StringBuilder(); }
return m_sb;
}
}
void DEBUG(StringBuilder msg) { Debug.Log(msg); }
void DEBUG(string msg) { Debug.Log($"{LOG_TAG}, {msg}"); }
bool printIntervalLog = false;
int logFrame = 0;
private bool isLeft = false;
private FingerData thumbData;
@@ -1107,10 +1072,10 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
fingerData.joint2.position};
float distance = float.PositiveInfinity;
foreach (var fingerJointPos in fingerPos)
for (int i = 0; i < fingerPos.Length; i++)
{
distance = Mathf.Min(distance, CalculateShortestDistance(fingerJointPos, thumbTip, thumbJoint2));
distance = Mathf.Min(distance, CalculateShortestDistance(fingerJointPos, thumbJoint2, thumbJoint1));
distance = Mathf.Min(distance, CalculateShortestDistance(fingerPos[i], thumbTip, thumbJoint2));
distance = Mathf.Min(distance, CalculateShortestDistance(fingerPos[i], thumbJoint2, thumbJoint1));
}
return distance;
}
@@ -1169,12 +1134,14 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
m_isPinching = true;
minDistance = distance;
sb.Clear().Append(LOG_TAG).Append(isLeft ? "Left " : "Right ").Append(m_finger.Name())
.Append(" UpdateState() pinch strength: ").Append(m_pinchStrength)
.Append(", pinch on threshold: ").Append(kPinchStrengthOnThreshold)
.Append(", is pinching: ").Append(m_isPinching)
.Append(", pinch distance: ").Append(minDistance);
DEBUG(sb);
if (printIntervalLog)
{
DEBUG($"{(isLeft ? "Left " : "Right ")} {finger.Name()}" +
$" UpdateState() pinch strength: {m_pinchStrength}" +
$", pinch on threshold: {kPinchStrengthOnThreshold}" +
$", is pinching: {m_isPinching}" +
$", pinch distance: {minDistance}");
}
updated = true;
}
@@ -1187,12 +1154,14 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
m_isPinching = false;
sb.Clear().Append(LOG_TAG).Append(isLeft ? "Left " : "Right ").Append(m_finger.Name())
.Append(" UpdateState() pinch strength: ").Append(m_pinchStrength)
.Append(", pinch off threshold: ").Append(kPinchStrengthOffThreshold)
.Append(", is pinching: ").Append(m_isPinching)
.Append(", pinch distance: ").Append(minDistance);
DEBUG(sb);
if (printIntervalLog)
{
DEBUG($"{(isLeft ? "Left " : "Right ")} {finger.Name()}" +
$" UpdateState() pinch strength: {m_pinchStrength}" +
$", pinch off threshold: {kPinchStrengthOffThreshold}" +
$", is pinching: {m_isPinching}" +
$", pinch distance: {minDistance}");
}
updated = true;
}
@@ -1207,6 +1176,9 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
/// <param name="finger">The FingerData of the finger.</param>
public void Update(FingerData thumb, FingerData finger)
{
logFrame++;
logFrame %= 300;
printIntervalLog = logFrame == 0;
if (!Validate()) { return; }
this.thumbData = thumb;
@@ -1901,15 +1873,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
[Serializable]
public class HandGrabbableEvent : UnityEvent<IGrabbable> { };
[Obsolete("Please use HandGrabberEvent instead.")]
public delegate void OnBeginGrab(IGrabber grabber);
[Obsolete("Please use HandGrabberEvent instead.")]
public delegate void OnEndGrab(IGrabber grabber);
[Obsolete("Please use HandGrabbableEvent instead.")]
public delegate void OnBeginGrabbed(IGrabbable grabbable);
[Obsolete("Please use HandGrabbableEvent instead.")]
public delegate void OnEndGrabbed(IGrabbable grabbable);
/// <summary>
/// Interface for objects capable of grabbing.
/// </summary>
@@ -1919,15 +1882,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
bool isGrabbing { get; }
HandGrabberEvent onBeginGrab { get; }
HandGrabberEvent onEndGrab { get; }
[Obsolete("Please use onBeginGrab instead.")]
void AddBeginGrabListener(OnBeginGrab handler);
[Obsolete("Please use onBeginGrab instead.")]
void RemoveBeginGrabListener(OnBeginGrab handler);
[Obsolete("Please use onEndGrab instead.")]
void AddEndGrabListener(OnEndGrab handler);
[Obsolete("Please use onEndGrab instead.")]
void RemoveEndGrabListener(OnEndGrab handler);
}
/// <summary>
@@ -1950,15 +1904,6 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
HandGrabbableEvent onBeginGrabbed { get; }
HandGrabbableEvent onEndGrabbed { get; }
void SetGrabber(IGrabber grabber);
[Obsolete("Please use onBeginGrabbed instead.")]
void AddBeginGrabbedListener(OnBeginGrabbed handler);
[Obsolete("Please use onBeginGrabbed instead.")]
void RemoveBeginGrabbedListener(OnBeginGrabbed handler);
[Obsolete("Please use onEndGrabbed instead.")]
void AddEndGrabbedListener(OnEndGrabbed handler);
[Obsolete("Please use onEndGrabbed instead.")]
void RemoveEndGrabbedListener(OnEndGrabbed handler);
}
/// <summary>

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VIVE.OpenXR.Toolkits.Common;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
@@ -50,6 +51,7 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
[SerializeField]
private Transform[] m_HandJoints = new Transform[k_JointCount];
private SkinnedMeshRenderer skinnedMeshRenderer = null;
private const int k_JointCount = (int)JointType.Count;
private const int k_RootId = (int)JointType.Wrist;
private bool updateRoot = false;
@@ -77,6 +79,8 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
MeshHandPose meshHandPose = transform.gameObject.AddComponent<MeshHandPose>();
meshHandPose.SetHandMeshRenderer(this);
skinnedMeshRenderer = transform.GetComponentInChildren<SkinnedMeshRenderer>();
}
private void OnDisable()
@@ -95,9 +99,9 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
private void Update()
{
HandData handData = CachedHand.Get(isLeft);
EnableHandModel(handData.isTracked);
if (!handData.isTracked) { return; }
bool isTracked = VIVEInput.IsHandTracked(isLeft ? Common.Handedness.Left : Common.Handedness.Right);
EnableHandModel(isTracked);
if (!isTracked) { return; }
//if (m_UseRuntimeModel || (!m_UseRuntimeModel && m_UseScale))
//{
@@ -118,22 +122,23 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
}
if (!updateRoot)
{
Vector3 rootPosition = Vector3.zero;
Quaternion rootRotation = Quaternion.identity;
handData.GetJointPosition((JointType)k_RootId, ref rootPosition);
handData.GetJointRotation((JointType)k_RootId, ref rootRotation);
m_HandJoints[k_RootId].position = m_HandJoints[k_RootId].parent.position + rootPosition;
m_HandJoints[k_RootId].rotation = m_HandJoints[k_RootId].parent.rotation * rootRotation;
VIVEInput.GetJointPose(isLeft ? Common.Handedness.Left : Common.Handedness.Right, HandJointType.Wrist, out Pose jointPose);
m_HandJoints[k_RootId].localPosition = jointPose.position;
m_HandJoints[k_RootId].localRotation = jointPose.rotation;
}
for (int i = 0; i < m_HandJoints.Length; i++)
{
if (m_HandJoints[i] == null || i == k_RootId) { continue; }
Quaternion jointRotation = Quaternion.identity;
handData.GetJointRotation((JointType)i, ref jointRotation);
m_HandJoints[i].rotation = m_HandJoints[k_RootId].parent.rotation * jointRotation;
VIVEInput.GetJointPose(isLeft ? Common.Handedness.Left : Common.Handedness.Right, (HandJointType)i, out Pose jointPose);
m_HandJoints[i].rotation = m_HandJoints[k_RootId].parent.rotation * jointPose.rotation;
}
if (VIVERig.Instance)
{
m_HandJoints[k_RootId].rotation = VIVERig.Instance.transform.rotation * m_HandJoints[k_RootId].localRotation;
m_HandJoints[k_RootId].position = VIVERig.Instance.transform.position + VIVERig.Instance.transform.rotation * m_HandJoints[k_RootId].localPosition;
}
if (isGrabbing)
@@ -343,6 +348,11 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
if (m_HandJoints[k_RootId].gameObject.activeSelf != enable)
{
m_HandJoints[k_RootId].gameObject.SetActive(enable);
if (skinnedMeshRenderer)
{
skinnedMeshRenderer.enabled = enable;
}
}
}

View File

@@ -83,11 +83,25 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
if (!HandPoseMap.ContainsKey(poseType))
{
UnityEngine.Object[] handObjects = UnityEngine.Object.FindObjectsOfType(typeof(RealHandPose));
for (int i = 0; i < handObjects.Length; i++)
{
UnityEngine.Object handObject = handObjects[i];
if (handObject is RealHandPose realHand &&
(realHand.isLeft ? poseType == HandPoseType.HAND_LEFT : poseType == HandPoseType.HAND_RIGHT))
{
realHand.SetType(poseType);
RegisterHandPose(poseType, realHand);
return realHand;
}
}
GameObject handPoseObject = new GameObject(poseName);
RealHandPose realHandPose = handPoseObject.AddComponent<RealHandPose>();
realHandPose.SetType(poseType);
RegisterHandPose(poseType, realHandPose);
return realHandPose;
RealHandPose newRealHand = handPoseObject.AddComponent<RealHandPose>();
newRealHand.SetType(poseType);
RegisterHandPose(poseType, newRealHand);
return newRealHand;
}
return HandPoseMap[poseType];
}

View File

@@ -7,7 +7,7 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[SerializeField]
private Handedness m_Handedness;
private bool isLeft => m_Handedness == Handedness.Left;
public bool isLeft => m_Handedness == Handedness.Left;
private bool keepUpdate = false;
protected override void OnEnable()
@@ -66,8 +66,8 @@ namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
if (handData.GetJointPosition((JointType)i, ref position) && handData.GetJointRotation((JointType)i, ref rotation))
{
m_Position[i] = position;
m_Rotation[i] = rotation;
m_Position[i] = transform.position + transform.rotation * position;
m_Rotation[i] = transform.rotation * rotation;
m_LocalPosition[i] = position;
m_LocalRotation[i] = rotation;
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1695418a852690c43b0206d5f2b54abd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3be1cbe32bf01284f9038c49e248393a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 67f99d00d315f5247a283247f1d1a046
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,82 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Android;
namespace VIVE.OpenXR.Toolkits.Spectator.Helper
{
public static class AndroidProcessHelper
{
private static AndroidJavaObject _activity;
public static AndroidJavaObject Activity
{
get
{
if (_activity != null)
{
return _activity;
}
var unityPlayer = new AndroidJavaClass(ANDROID_CLASS_UNITY_PLAYER);
_activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
return _activity;
}
}
// Reference: https://stackoverflow.com/q/58728596/10467387
public static IEnumerator RequestPermission(Dictionary<string, PermissionCallbacks> permissions)
{
var permissionGranted = Enumerable.Repeat(false, permissions.Count).ToList();
var permissionAsked = Enumerable.Repeat(false, permissions.Count).ToList();
var permissionAction = new List<Action>();
for (var i = 0; i < permissions.Count; i++)
{
int currentCount = i;
(string permission, PermissionCallbacks permissionCallbacks) = permissions.ElementAt(currentCount);
permissionAction.Add(() =>
{
permissionGranted[currentCount] = Permission.HasUserAuthorizedPermission(permission);
if (permissionGranted[currentCount] || permissionAsked[currentCount])
{
return;
}
Permission.RequestUserPermission(permission, permissionCallbacks);
permissionAsked[currentCount] = true;
});
}
for (var i = 0; i < permissions.Count;)
{
permissionAction[i].Invoke();
if (permissionAsked[i])
{
i++;
}
yield return new WaitForEndOfFrame();
}
}
public const string ANDROID_CLASS_UNITY_PLAYER = "com.unity3d.player.UnityPlayer";
public const string ANDROID_CLASS_MEDIA_STORE_IMAGE_MEDIA = "android.provider.MediaStore$Images$Media";
public const string ANDROID_CLASS_GRAPHICS_BITMAP_FACTORY = "android.graphics.BitmapFactory";
public const string ANDROID_CLASS_GRAPHICS_BITMAP_COMPRESS_FORMAT = "android.graphics.Bitmap$CompressFormat";
public const string ANDROID_CLASS_OS_ENVIRONMENT = "android.os.Environment";
public const string ANDROID_CLASS_OS_BUILD_VERSION = "android.os.Build$VERSION";
public const string ANDROID_CLASS_CONTENT_INTENT = "android.content.Intent";
public const string ANDROID_CLASS_CONTENT_VALUES = "android.content.ContentValues";
public const string ANDROID_CLASS_NET_URI = "android.net.Uri";
public const string JAVA_CLASS_IO_FILE = "java.io.File";
public const string JAVA_CLASS_IO_OUTPUTSTREAM = "java.io.OutputStream";
public const string JAVA_CLASS_IO_BYTEARRAYOUTPUTSTREAM = "java.io.ByteArrayOutputStream";
public const string JAVA_CLASS_IO_BYTEARRAYINPUTSTREAM = "java.io.ByteArrayInputStream";
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52cbc1ad9d288d348854b4e81d8aa57c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,63 @@
// Copyright HTC Corporation All Rights Reserved.
#if UNITY_EDITOR
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.Spectator.Helper
{
public static class EditorHelper
{
// Microsoft definition
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.conventions.backingfieldconvention?view=efcore-7.0#definition
private const string BackingFieldConventionPrefix = "<";
private const string BackingFieldConventionEndString = "k__BackingField";
public static void ShowDefaultInspector(SerializedObject obj)
{
EditorGUI.BeginChangeCheck();
obj.UpdateIfRequiredOrScript();
SerializedProperty iterator = obj.GetIterator();
for (bool enterChildren = true; iterator.NextVisible(enterChildren); enterChildren = false)
{
using (new EditorGUI.DisabledScope("m_Script" == iterator.propertyPath))
{
string originalLabelText = iterator.name;
if (originalLabelText.EndsWith(BackingFieldConventionEndString))
{
string fixLabelText = Regex.Replace(
originalLabelText.Substring(
1,
originalLabelText.Length - 1 - BackingFieldConventionPrefix.Length -
BackingFieldConventionEndString.Length),
"([a-z])([A-Z])",
"$1 $2");
EditorGUILayout.PropertyField(
property: iterator,
label: new GUIContent(fixLabelText),
includeChildren: true);
}
else
{
EditorGUILayout.PropertyField(iterator, true);
}
}
}
obj.ApplyModifiedProperties();
EditorGUI.EndChangeCheck();
}
public static string PropertyName(string propertyName)
{
// Microsoft definition
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.conventions.backingfieldconvention?view=efcore-7.0#definition
return $"<{propertyName}>k__BackingField";
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2fb164a4bcc8f04e9337299f4c030d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,163 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace VIVE.OpenXR.Toolkits.Spectator.Helper
{
public static class IOProcessHelper
{
#if UNITY_ANDROID
/// <summary>
/// Get the path of pictures folder in external storage in Android.
/// </summary>
/// <returns>Return Pictures folder directory if get successfully, otherwise return empty string</returns>
public static string GetAndroidExternalStoragePicturesDirectory()
{
Debug.Log("GetAndroidExternalStoragePicturesDirectory called");
string path = string.Empty;
try
{
// Init the class in java code
using (AndroidJavaClass environment =
new AndroidJavaClass(AndroidProcessHelper.ANDROID_CLASS_OS_ENVIRONMENT))
{
// Call static method to get the path of pictures folder in external storage
path = environment.CallStatic<AndroidJavaObject>("getExternalStoragePublicDirectory",
environment.GetStatic<string>("DIRECTORY_PICTURES"))
.Call<string>("getAbsolutePath");
}
}
catch (Exception e)
{
Debug.LogError($"Error on getting the path of pictures folder in external storage in Android: {e}");
}
Debug.Log($"Get path in GetAndroidExternalStoragePicturesDirectory: {path}");
return path;
}
public static string GetAndroidPrimaryExternalStorageDirectory()
{
Debug.Log("GetAndroidPrimaryExternalStorageDirectory called");
string path = string.Empty;
try
{
using (AndroidJavaClass environment =
new AndroidJavaClass(AndroidProcessHelper.ANDROID_CLASS_OS_ENVIRONMENT))
{
path = environment.CallStatic<AndroidJavaObject>("getExternalStorageDirectory")
.Call<string>("getAbsolutePath");
}
}
catch (Exception e)
{
Debug.LogError($"Error on getting the path of pictures folder in external storage in Android: {e}");
}
Debug.Log($"Get path in GetAndroidPrimaryExternalStorageDirectory: {path}");
return path;
}
public static Dictionary<ExternalStorageType, string> GetAndroidAllExternalStorageDirectory()
{
Debug.Log("GetAndroidAllExternalStorageDirectory called");
// Get all available external file directories (emulated or removable (aka sd card))
AndroidJavaObject[] externalFilesDirectories =
AndroidProcessHelper.Activity.Call<AndroidJavaObject[]>("getExternalFilesDirs", (object)null);
var result = new Dictionary<ExternalStorageType, string>();
using (var environment = new AndroidJavaClass(AndroidProcessHelper.ANDROID_CLASS_OS_ENVIRONMENT))
{
foreach (var item in externalFilesDirectories)
{
string directory = item.Call<string>("getAbsolutePath");
Debug.Log($"Find the path in GetAndroidExternalStorageDirectory: {directory}");
if (environment.CallStatic<bool>("isExternalStorageRemovable", item))
{
result.Add(ExternalStorageType.Removable, directory);
}
else if (environment.CallStatic<bool>("isExternalStorageEmulated", item))
{
result.Add(ExternalStorageType.Emulated, directory);
}
}
}
return result;
}
public enum ExternalStorageType
{
Removable,
Emulated
}
#endif
public static Task SaveByteDataToDisk(byte[] bytes, string saveDirectory, string fileNameWithFileExtension)
{
Directory.CreateDirectory(saveDirectory);
try
{
string fullPath = Path.Combine(saveDirectory, fileNameWithFileExtension);
System.IO.File.WriteAllBytes(fullPath, bytes);
}
catch (Exception e)
{
Debug.LogError($"Error on writing byte data to disk: {e}");
}
return Task.CompletedTask;
}
public static byte[] OpenFile(string path)
{
if (!File.Exists(path))
{
Debug.LogError("File not exist: " + path);
return null;
}
byte[] data = File.ReadAllBytes(path);
if (data.Length == 0)
{
Debug.LogError("File is empty: " + path);
return null;
}
return data;
}
public static byte[] OpenJpeg(string path)
{
byte[] data = OpenFile(path);
if (data == null)
{
Debug.LogError("Open Jpeg error");
return null;
}
if (data[0] != 0xFF || data[1] != 0xD8)
{
Debug.LogError("File is not JPEG: " + path);
return null;
}
return data;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d28163e4ec0b605478774041d1222262
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,102 @@
// Copyright HTC Corporation All Rights Reserved.
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif
namespace VIVE.OpenXR.Toolkits.Spectator.Helper
{
public static class LayerMaskHelper
{
public static bool HasLayer(this LayerMask layerMask, int layer)
{
if (layerMask == (layerMask | (1 << layer)))
{
return true;
}
return false;
}
public static bool[] HasLayers(this LayerMask layerMask)
{
var hasLayers = new bool[32];
for (int i = 0; i < 32; i++)
{
if (layerMask == (layerMask | (1 << i)))
{
Debug.Log($"LayerMask.LayerToName = {LayerMask.LayerToName(i)}");
hasLayers[i] = true;
}
}
return hasLayers;
}
#if UNITY_EDITOR
public static class LayerMaskDrawer
{
public static int LayerMaskField(string label, int layermask)
{
return FieldToLayerMask(EditorGUILayout.MaskField(label, LayerMaskToField(layermask),
InternalEditorUtility.layers));
}
public static int LayerMaskField(Rect position, string label, int layermask)
{
return FieldToLayerMask(EditorGUI.MaskField(position, label, LayerMaskToField(layermask),
InternalEditorUtility.layers));
}
/// <summary>
/// Converts field LayerMask values to in game LayerMask values
/// </summary>
/// <param name="field"></param>
/// <returns></returns>
private static int FieldToLayerMask(int field)
{
if (field == -1) return -1;
int mask = 0;
var layers = InternalEditorUtility.layers;
for (int c = 0; c < layers.Length; c++)
{
if ((field & (1 << c)) != 0)
{
mask |= 1 << LayerMask.NameToLayer(layers[c]);
}
else
{
mask &= ~(1 << LayerMask.NameToLayer(layers[c]));
}
}
return mask;
}
/// <summary>
/// Converts in game LayerMask values to field LayerMask values
/// </summary>
/// <param name="mask"></param>
/// <returns></returns>
private static int LayerMaskToField(int mask)
{
if (mask == -1) return -1;
int field = 0;
var layers = InternalEditorUtility.layers;
for (int c = 0; c < layers.Length; c++)
{
if ((mask & (1 << LayerMask.NameToLayer(layers[c]))) != 0)
{
field |= 1 << c;
}
}
return field;
}
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 05c260c7d911cff49b0333f5ba628420
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,516 @@
// Copyright HTC Corporation All Rights Reserved.
using System.IO;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.Spectator.Helper
{
public class SpectatorCameraHelper
{
# region Attribute value range
public const float VERTICAL_FOV_MIN = 10f;
public const float VERTICAL_FOV_MAX = 130f;
public const float FRUSTUM_LINE_WIDTH_MIN = .02f;
public const float FRUSTUM_LINE_WIDTH_MAX = .05f;
public const float FRUSTUM_CENTER_LINE_WIDTH_MIN = .01f;
public const float FRUSTUM_CENTER_LINE_WIDTH_MAX = .04f;
public const int PANORAMA_RESOLUTION_MIN = 512;
public const int PANORAMA_RESOLUTION_MAX = 4096;
public const int SMOOTH_CAMERA_MOVEMENT_MIN = 8;
public const int SMOOTH_CAMERA_MOVEMENT_MAX = 16;
public const float COMPARE_FLOAT_SUPER_SMALL_THRESHOLD = .001f;
public const float COMPARE_FLOAT_EXTRA_SMALL_THRESHOLD = .01f;
public const float COMPARE_FLOAT_SMALL_THRESHOLD = .1f;
public const float COMPARE_FLOAT_MEDIUM_THRESHOLD = 1f;
public const float COMPARE_FLOAT_LARGE_THRESHOLD = 10f;
public const float INTERVAL_SECOND_GET_SPECTATOR_HANDLER = .5f;
public const float MAX_SECOND_GET_SPECTATOR_HANDLER = 5f;
public const float INTERVAL_SECOND_INTERNAL_SPECTATOR_CAMERA = 1f;
public const float MAX_SECOND_GET_INTERNAL_SPECTATOR_CAMERA = 2f;
# endregion
public static readonly Vector3 SpectatorCameraSpherePrefabScaleDefault = new Vector3(.08f, .08f, .08f);
#region Attribute default value definition
public const int TEXTURE_WIDTH = 1920;
public const int TEXTURE_HEIGHT = 1080;
public const CameraSourceRef CAMERA_SOURCE_REF_DEFAULT = CameraSourceRef.Hmd;
public static readonly Vector3 PositionDefault = new Vector3(0f, 1.7f, 0f);
public static readonly Quaternion RotationDefault = Quaternion.identity;
public static readonly LayerMask LayerMaskDefault = -1;
public const bool IS_SMOOTH_CAMERA_MOVEMENT_DEFAULT = true;
public const int SMOOTH_CAMERA_MOVEMENT_SPEED_DEFAULT = 10;
public const bool IS_FRUSTUM_SHOWED_DEFAULT = false;
public const float VERTICAL_FOV_DEFAULT = 60f;
public const SpectatorCameraPanoramaResolution PANORAMA_RESOLUTION_DEFAULT =
SpectatorCameraPanoramaResolution._2048;
public const TextureProcessHelper.PictureOutputFormat PANORAMA_OUTPUT_FORMAT_DEFAULT =
TextureProcessHelper.PictureOutputFormat.PNG;
public const TextureProcessHelper.PanoramaType PANORAMA_TYPE_DEFAULT =
TextureProcessHelper.PanoramaType.Monoscopic;
public const float STEREO_SEPARATION_DEFAULT = 0.065f;
public const FrustumLineCount FRUSTUM_LINE_COUNT_DEFAULT =
FrustumLineCount.Four;
public const FrustumCenterLineCount FRUSTUM_CENTER_LINE_COUNT_DEFAULT =
FrustumCenterLineCount.Center;
public const float FRUSTUM_LINE_WIDTH_DEFAULT = .02f;
public const float FRUSTUM_CENTER_LINE_WIDTH_DEFAULT = .01f;
public const float FRUSTUM_LINE_BEGIN_DEFAULT = .3f;
// Define by Unity: https://docs.unity3d.com/ScriptReference/Camera.CalculateFrustumCorners.html
public const int FRUSTUM_OUT_CORNERS_COUNT = 4;
public static readonly Color LineColorDefault = Color.white;
public const string LINE_SHADER_NAME_DEFAULT = "UI/Default";
#if UNITY_EDITOR
public const bool IS_DEBUG_SPECTATOR_CAMERA = true;
#endif
#endregion
#region Frustum game object name definition
public const string FRUSTUM_LINE_ROOT_NAME_DEFAULT = "FrustumLines";
public const string FRUSTUM_LINE_NAME_PREFIX_DEFAULT = "Frustum";
public const string FRUSTUM_CENTER_LINE_ROOT_NAME_DEFAULT = "FrustumCenterLines";
public const string FRUSTUM_CENTER_LINE_NAME_PREFIX_DEFAULT = "FrustumCenter";
#endregion
#region Save file definition
public const string SAVE_PHOTO360_ALBUM_NAME = "Screenshots";
public const string ATTRIBUTE_FILE_PREFIX_NAME = "SpectatorCameraAttribute";
public const string ATTRIBUTE_FILE_EXTENSION = "json";
#endregion
/// <summary>
/// Load the attribute file from resource folder
/// </summary>
/// <param name="sceneName">The corresponding scene name of the attribute file</param>
/// <param name="gameObjectName">The corresponding gameObject name of the attribute file</param>
/// <param name="data">The attribute data which save in attribute file</param>
/// <returns>True if get the attribute data successfully. Otherwise, return false.</returns>
public static bool LoadAttributeFileFromResourcesFolder(
in string sceneName,
in string gameObjectName,
out SpectatorCameraAttribute data)
{
Debug.Log("Get spectator camera attribute at resources folder");
data = new SpectatorCameraAttribute();
TextAsset json;
// Name format: {PREFIX}_{SCENE NAME}_{GAME OBJECT NAME}.json
var fileName = GetSpectatorCameraAttributeFileNamePattern(sceneName, gameObjectName, false);
try
{
json = Resources.Load<TextAsset>(fileName);
}
catch (System.Exception e)
{
Debug.LogWarning("Get attribute data from resources folder fail:");
Debug.LogWarning($"{e}");
return false;
}
if (json == null)
{
Debug.LogWarning("The attribute data at resources folder is empty or null");
return false;
}
Debug.Log($"The attribute value is: {json.text}");
data = JsonUtility.FromJson<SpectatorCameraAttribute>(json.text);
if (data == null)
{
Debug.LogWarning("Convert attribute data from resources folder fail");
return false;
}
Debug.Log("Get attribute data from resources folder successful");
return true;
}
/// <summary>
/// Load the attribute file from full path
/// </summary>
/// <param name="fullPathWithFileNameAndExtension">Full path</param>
/// <param name="data">The attribute data which save in persistent folder</param>
/// <returns>True if get the attribute data successfully. Otherwise, return false.</returns>
public static bool LoadAttributeFileFromFolder(
string fullPathWithFileNameAndExtension,
out SpectatorCameraAttribute data)
{
Debug.Log($"Get spectator camera attribute at {fullPathWithFileNameAndExtension}");
return LoadAttributeData(fullPathWithFileNameAndExtension, out data);
}
/// <summary>
/// Load the attribute file from persistent folder
/// </summary>
/// <param name="sceneName">The corresponding scene name of the attribute file</param>
/// <param name="gameObjectName">The corresponding gameObject name of the attribute file</param>
/// <param name="data">The attribute data which save in persistent folder</param>
/// <returns>True if get the attribute data successfully. Otherwise, return false.</returns>
public static bool LoadAttributeFileFromPersistentFolder(
in string sceneName,
in string gameObjectName,
out SpectatorCameraAttribute data)
{
// Name format: {PREFIX}_{SCENE NAME}_{GAME OBJECT NAME}.json
var fileNameWithExtension = GetSpectatorCameraAttributeFileNamePattern(sceneName, gameObjectName);
var attributeFileDirectoryAndFileNameWithExtension =
Path.Combine(Application.persistentDataPath, fileNameWithExtension);
Debug.Log($"Get spectator camera attribute at {attributeFileDirectoryAndFileNameWithExtension}");
return LoadAttributeData(attributeFileDirectoryAndFileNameWithExtension, out data);
}
#if UNITY_EDITOR
/// <summary>
/// Save the spectator camera attribute as a JSON file to the resources folder located in the Unity project's assets folder.
/// </summary>
public static void SaveAttributeData2ResourcesFolder(
in string sceneName,
in string gameObjectName,
in SpectatorCameraAttribute data)
{
// Name format: {PREFIX}_{SCENE NAME}_{GAME OBJECT NAME}.json
var fileNameWithExtension = GetSpectatorCameraAttributeFileNamePattern(sceneName, gameObjectName);
string resourcesFolderPath = Path.Combine(Application.dataPath, "Resources");
SaveAttributeData(resourcesFolderPath, fileNameWithExtension, data);
}
#endif
/// <summary>
/// Save the spectator camera attribute as a JSON file to a persistent folder.
/// </summary>
public static void SaveAttributeData2PersistentFolder(
in string sceneName,
in string gameObjectName,
in SpectatorCameraAttribute data)
{
// Name format: {PREFIX}_{SCENE NAME}_{GAME OBJECT NAME}.json
var fileNameWithExtension = GetSpectatorCameraAttributeFileNamePattern(sceneName, gameObjectName);
SaveAttributeData(Application.persistentDataPath, fileNameWithExtension, data);
}
/// <summary>
/// Load the spectator camera attribute as a JSON file on disk.
/// </summary>
/// <param name="fullPathWithFileNameAndExtension">The path (string) that include directory, file name and extension</param>
/// <param name="data">The data of spectator camera attribute</param>
/// <returns>True if get the attribute data successfully. Otherwise, return false.</returns>
private static bool LoadAttributeData(
string fullPathWithFileNameAndExtension,
out SpectatorCameraAttribute data)
{
data = new SpectatorCameraAttribute();
string json;
try
{
json = File.ReadAllText(fullPathWithFileNameAndExtension);
}
catch (System.Exception e)
{
Debug.LogWarning($"Get attribute data from {fullPathWithFileNameAndExtension} failed: {e}");
return false;
}
if (string.IsNullOrEmpty(json))
{
Debug.LogWarning($"The attribute data at {fullPathWithFileNameAndExtension} is empty or null");
return false;
}
Debug.Log($"The attribute value is: {json}");
data = JsonUtility.FromJson<SpectatorCameraAttribute>(json);
if (data == null)
{
Debug.LogWarning($"Convert attribute data from {fullPathWithFileNameAndExtension} failed");
return false;
}
Debug.Log($"Get attribute data from {fullPathWithFileNameAndExtension} successful");
return true;
}
/// <summary>
/// Save the spectator camera attribute as a JSON file on disk.
/// </summary>
/// <param name="saveFileDirectory">The directory that the spectator camera attribute (JSON file) will save to</param>
/// <param name="saveFileNameWithExtension">The file name of the spectator camera attribute (JSON file)</param>
/// <param name="data">The data of spectator camera attribute</param>
private static void SaveAttributeData(
in string saveFileDirectory,
in string saveFileNameWithExtension,
in SpectatorCameraAttribute data)
{
if (string.IsNullOrEmpty(saveFileDirectory) || string.IsNullOrEmpty(saveFileNameWithExtension))
{
Debug.LogError("The saving file directory or name is null or empty");
return;
}
string fullPath = Path.Combine(saveFileDirectory, saveFileNameWithExtension);
// Convert to string format
string json = JsonUtility.ToJson(data);
// Make sure the file path is exist
if (!Directory.Exists(saveFileDirectory))
{
Directory.CreateDirectory(saveFileDirectory);
}
File.WriteAllText(fullPath, json);
Debug.Log($"The configuration save at {fullPath}");
}
public static string GetSpectatorCameraAttributeFileNamePattern(
in string sceneName,
in string gameObjectName,
bool withExtension = true)
{
var fileNamePattern = $"{ATTRIBUTE_FILE_PREFIX_NAME}_{sceneName}_{gameObjectName}";
if (withExtension)
{
fileNamePattern += $".{ATTRIBUTE_FILE_EXTENSION}";
}
return fileNamePattern;
}
/// <summary>
/// Check the panorama resolution is power of two or not. If not, convert to power of two.
/// </summary>
/// <param name="inputResolution">The panorama resolution</param>
/// <returns></returns>
public static int CheckAndConvertPanoramaResolution(in int inputResolution)
{
int result;
// Check is power of two
if ((inputResolution != 0) && ((inputResolution & (inputResolution - 1)) == 0))
{
result = Mathf.Clamp(inputResolution, PANORAMA_RESOLUTION_MIN, PANORAMA_RESOLUTION_MAX);
}
else
{
// If not power of two, convert to power of two
int clampInputValue = Mathf.Clamp(inputResolution, PANORAMA_RESOLUTION_MIN, PANORAMA_RESOLUTION_MAX);
var base2LogarithmOfClampInputValue = (int)System.Math.Log(clampInputValue, 2);
var base2LogarithmOfClampInputValueLowerStepValue =
(int)System.Math.Pow(2, base2LogarithmOfClampInputValue);
var base2LogarithmOfClampInputValueUpperStepValue =
(int)System.Math.Pow(2, base2LogarithmOfClampInputValue + 1);
// Check which one is closer
if (clampInputValue - base2LogarithmOfClampInputValueLowerStepValue <=
base2LogarithmOfClampInputValueUpperStepValue - clampInputValue)
{
result = base2LogarithmOfClampInputValueLowerStepValue;
}
else
{
result = base2LogarithmOfClampInputValueUpperStepValue;
}
}
return result;
}
#region Functions of changing camera culling mask
/// <summary>
/// Set the layer that the camera can watch
/// </summary>
/// <param name="targetCullingMask">The camera culling mask</param>
/// <param name="shownLayer">The layer number that you want to set the camera can watch</param>
/// <returns>The new camera culling mask after modify</returns>
public static LayerMask SetCameraVisualizationLayer(in LayerMask targetCullingMask, in int shownLayer)
{
LayerMask targetLayerMaskAfterModify = targetCullingMask;
targetLayerMaskAfterModify |= 1 << shownLayer;
return targetLayerMaskAfterModify;
}
/// <summary>
/// Set the layer that the camera cannot watch
/// </summary>
/// <param name="targetCullingMask">The camera culling mask</param>
/// <param name="hiddenLayer">The layer number that you want to set the camera cannot watch</param>
/// <returns>The new camera culling mask after modify</returns>
public static LayerMask SetCameraHiddenLayer(in LayerMask targetCullingMask, in int hiddenLayer)
{
LayerMask targetLayerMaskAfterModify = targetCullingMask;
targetLayerMaskAfterModify &= ~(1 << hiddenLayer);
return targetLayerMaskAfterModify;
}
/// <summary>
/// Inverse specific layer in camera culling mask
/// </summary>
/// <param name="targetCullingMask">The camera culling mask</param>
/// <param name="inverseLayer">The layer number that you want to inverse</param>
/// <returns>The new camera culling mask after modify</returns>
public static LayerMask InverseCameraLayer(in LayerMask targetCullingMask, in int inverseLayer)
{
LayerMask targetLayerMaskAfterModify = targetCullingMask;
targetLayerMaskAfterModify ^= 1 << inverseLayer;
return targetLayerMaskAfterModify;
}
#endregion
[System.Serializable]
public class SpectatorCameraAttribute
{
#region Serializable class field
public CameraSourceRef source;
public Vector3 position;
public Quaternion rotation;
public LayerMask layerMask;
public bool isSmoothCameraMovement;
public int smoothCameraMovementSpeed;
public bool isFrustumShowed;
public float verticalFov;
public SpectatorCameraPanoramaResolution panoramaResolution;
public TextureProcessHelper.PictureOutputFormat panoramaOutputFormat;
public TextureProcessHelper.PanoramaType panoramaOutputType;
public FrustumLineCount frustumLineCount;
public FrustumCenterLineCount frustumCenterLineCount;
public float frustumLineWidth;
public float frustumCenterLineWidth;
public Color frustumLineColor;
public Color frustumCenterLineColor;
#endregion
#region Constructor
public SpectatorCameraAttribute()
{
}
public SpectatorCameraAttribute(
CameraSourceRef source,
Vector3 position,
Quaternion rotation,
LayerMask layerMask,
bool isSmoothCameraMovement,
int smoothCameraMovementSpeed,
bool isFrustumShowed,
float verticalFov,
SpectatorCameraPanoramaResolution panoramaResolution,
TextureProcessHelper.PictureOutputFormat panoramaOutputFormat,
TextureProcessHelper.PanoramaType panoramaOutputType,
FrustumLineCount frustumLineCount,
FrustumCenterLineCount frustumCenterLineCount,
float frustumLineWidth,
float frustumCenterLineWidth,
Color frustumLineColor,
Color frustumCenterLineColor)
{
this.source = source;
this.position = position;
this.rotation = rotation;
this.layerMask = layerMask;
this.isSmoothCameraMovement = isSmoothCameraMovement;
this.smoothCameraMovementSpeed = smoothCameraMovementSpeed;
this.isFrustumShowed = isFrustumShowed;
this.verticalFov = verticalFov;
this.panoramaResolution = panoramaResolution;
this.panoramaOutputFormat = panoramaOutputFormat;
this.panoramaOutputType = panoramaOutputType;
this.frustumLineCount = frustumLineCount;
this.frustumCenterLineCount = frustumCenterLineCount;
this.frustumLineWidth = frustumLineWidth;
this.frustumCenterLineWidth = frustumCenterLineWidth;
this.frustumLineColor = frustumLineColor;
this.frustumCenterLineColor = frustumCenterLineColor;
}
#endregion
}
#region Enum definition
public enum CameraSourceRef
{
Hmd,
Tracker
}
public enum FrustumLineCount
{
None = 0,
Four = 4,
Eight = 8,
Sixteen = 16,
ThirtyTwo = 32,
OneTwentyEight = 128,
}
public enum FrustumCenterLineCount
{
None = 0,
Center = 1,
RuleOfThirds = 4,
CenterAndRuleOfThirds = 5,
}
public enum SpectatorCameraPanoramaResolution
{
_512 = 512,
_1024 = 1024,
_2048 = 2048,
_4096 = 4096
}
public enum AttributeFileLocation
{
ResourceFolder,
PersistentFolder
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3136b6864f4cb9c4da5234953d3a75b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09984648d0f364648af4f0ac5a03de55
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
// Copyright HTC Corporation All Rights Reserved.
using UnityEngine;
using VIVE.OpenXR.Toolkits.Spectator.Helper;
namespace VIVE.OpenXR.Toolkits.Spectator
{
/// <summary>
/// Name: ISpectatorCameraSetting.cs
/// Role: Contract
/// Responsibility: Define the setting attribute of the spectator camera.
/// </summary>
public interface ISpectatorCameraSetting
{
#region Property
/// <summary>
/// The struct UnityEngine.LayerMask defines which layer the camera can see or not.
/// </summary>
LayerMask LayerMask { get; set; }
/// <summary>
/// Whether or not to enable the feature of smoothing the spectator camera movement.
/// </summary>
bool IsSmoothCameraMovement { get; set; }
/// <summary>
/// The speed factor to control the smoothing impact.
/// </summary>
int SmoothCameraMovementSpeed { get; set; }
/// <summary>
/// True if visualize the spectator camera vertical FOV.
/// </summary>
bool IsFrustumShowed { get; set; }
/// <summary>
/// The spectator camera vertical FOV.
/// </summary>
float VerticalFov { get; set; }
/// <summary>
/// The panorama image resolution.
/// </summary>
SpectatorCameraHelper.SpectatorCameraPanoramaResolution PanoramaResolution { get; set; }
/// <summary>
/// The panorama image output format.
/// </summary>
TextureProcessHelper.PictureOutputFormat PanoramaOutputFormat { get; set; }
/// <summary>
/// The panorama types.
/// </summary>
TextureProcessHelper.PanoramaType PanoramaOutputType { get; set; }
/// <summary>
/// How many frustum lines will be shown?
/// </summary>
SpectatorCameraHelper.FrustumLineCount FrustumLineCount { get; set; }
/// <summary>
/// How many frustum center lines will be shown?
/// </summary>
SpectatorCameraHelper.FrustumCenterLineCount FrustumCenterLineCount { get; set; }
/// <summary>
/// Frustum line width.
/// </summary>
float FrustumLineWidth { get; set; }
/// <summary>
/// Frustum center line width.
/// </summary>
float FrustumCenterLineWidth { get; set; }
/// <summary>
/// Frustum line color.
/// </summary>
Color FrustumLineColor { get; set; }
/// <summary>
/// Frustum center line color.
/// </summary>
Color FrustumCenterLineColor { get; set; }
#endregion
#region Function
/// <summary>
/// Reset the spectator camera setting to the default value.
/// </summary>
void ResetSetting();
/// <summary>
/// Export the current spectator camera setting as a JSON file and then save it to the resource folder or persistent folder.
/// </summary>
/// <param name="attributeFileLocation">The enum SpectatorCameraHelper.AttributeFileLocation.</param>
void ExportSetting2JsonFile(in SpectatorCameraHelper.AttributeFileLocation attributeFileLocation);
/// <summary>
/// Load the setting (JSON) file via input full file path.
/// </summary>
/// <param name="jsonFilePath">The setting files full path (including file name and JSON extension).</param>
void LoadSettingFromJsonFile(in string jsonFilePath);
/// <summary>
/// Load the setting (JSON) file via input scene name, GameObject (hmd) name, and the file location (resource folder or persistent folder).
/// </summary>
/// <param name="sceneName">The scene name.</param>
/// <param name="gameObjectName">The GameObject name.</param>
/// <param name="attributeFileLocation"> The enum SpectatorCameraHelper.AttributeFileLocation.</param>
void LoadSettingFromJsonFile(
in string sceneName,
in string gameObjectName,
in SpectatorCameraHelper.AttributeFileLocation attributeFileLocation);
/// <summary>
/// Apply the spectator camera setting to the current component.
/// </summary>
/// <param name="data">The data you want to apply.</param>
void ApplyData(in SpectatorCameraHelper.SpectatorCameraAttribute data);
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 913ccea163eb50d4e911f0e8cadaadeb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,748 @@
// Copyright HTC Corporation All Rights Reserved.
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using VIVE.OpenXR.Toolkits.Spectator.Helper;
namespace VIVE.OpenXR.Toolkits.Spectator
{
/// <summary>
/// Name: SpectatorCameraManager.Editor.cs
/// Role: General script use in Unity Editor only
/// Responsibility: Display the SpectatorCameraManager.cs in Unity Inspector
/// </summary>
public partial class SpectatorCameraManager
{
[SerializeField] private Material spectatorCameraViewMaterial;
/// <summary>
/// Material that show the spectator camera view
/// </summary>
private Material SpectatorCameraViewMaterial
{
// get; private set;
get => spectatorCameraViewMaterial;
set
{
spectatorCameraViewMaterial = value;
if (SpectatorCameraBased && value)
{
SpectatorCameraBased.SpectatorCameraViewMaterial = value;
}
}
}
#if UNITY_EDITOR
[field: SerializeField] private bool IsShowHmdPart { get; set; }
[field: SerializeField] private bool IsShowTrackerPart { get; set; }
[field: SerializeField] private bool IsRequireLoadJsonFile { get; set; }
[CustomEditor(typeof(SpectatorCameraManager))]
public class SpectatorCameraManagerEditor : UnityEditor.Editor
{
private static readonly Color HighlightRegionBackgroundColor = new Color(.2f, .2f, .2f, .1f);
private SerializedProperty IsShowHmdPart { get; set; }
private SerializedProperty IsShowTrackerPart { get; set; }
private SerializedProperty IsRequireLoadJsonFile { get; set; }
private List<string> JsonFileList { get; set; }
private Vector2 JsonFileScrollViewVector { get; set; }
private SerializedProperty IsSmoothCameraMovement { get; set; }
private SerializedProperty SmoothCameraMovementSpeed { get; set; }
private SerializedProperty PanoramaResolution { get; set; }
private SerializedProperty PanoramaOutputFormat { get; set; }
private SerializedProperty PanoramaOutputType { get; set; }
private SerializedProperty SpectatorCameraPrefab { get; set; }
private void OnEnable()
{
IsShowHmdPart = serializedObject.FindProperty(EditorHelper.PropertyName("IsShowHmdPart"));
IsShowTrackerPart = serializedObject.FindProperty(EditorHelper.PropertyName("IsShowTrackerPart"));
IsRequireLoadJsonFile =
serializedObject.FindProperty(EditorHelper.PropertyName("IsRequireLoadJsonFile"));
JsonFileList = new List<string>();
JsonFileScrollViewVector = Vector2.zero;
IsSmoothCameraMovement =
serializedObject.FindProperty(EditorHelper.PropertyName("IsSmoothCameraMovement"));
SmoothCameraMovementSpeed =
serializedObject.FindProperty(EditorHelper.PropertyName("SmoothCameraMovementSpeed"));
PanoramaResolution = serializedObject.FindProperty(EditorHelper.PropertyName("PanoramaResolution"));
PanoramaOutputFormat = serializedObject.FindProperty(EditorHelper.PropertyName("PanoramaOutputFormat"));
PanoramaOutputType = serializedObject.FindProperty(EditorHelper.PropertyName("PanoramaOutputType"));
SpectatorCameraPrefab =
serializedObject.FindProperty(EditorHelper.PropertyName("SpectatorCameraPrefab"));
}
public override void OnInspectorGUI()
{
// Just return if not "SpectatorCameraManager" class
if (!(target is SpectatorCameraManager))
{
return;
}
serializedObject.Update();
DrawGUI();
serializedObject.ApplyModifiedProperties();
}
private void DrawGUI()
{
#region GUIStyle
var labelStyle = new GUIStyle()
{
richText = true,
alignment = TextAnchor.MiddleCenter,
normal = new GUIStyleState
{
textColor = EditorGUIUtility.isProSkin ? Color.green : Color.black
}
};
var resetButtonStyle = new GUIStyle(GUI.skin.button)
{
fontStyle = FontStyle.Bold,
normal = new GUIStyleState
{
textColor = EditorGUIUtility.isProSkin ? Color.yellow : Color.red
},
hover = new GUIStyleState
{
textColor = Color.red
},
active = new GUIStyleState
{
textColor = Color.cyan
},
};
var boldButtonStyle = new GUIStyle(GUI.skin.button)
{
fontStyle = FontStyle.Bold
};
#endregion
var script = (SpectatorCameraManager)target;
// Button for reset value
if (GUILayout.Button("Reset to default value", resetButtonStyle))
{
Undo.RecordObject(target, "Reset SpectatorCameraManager to default value");
EditorUtility.SetDirty(target);
script.ResetSetting();
}
// Button for export setting
if (GUILayout.Button("Export Spectator Camera HMD Setting", boldButtonStyle))
{
script.ExportSetting2JsonFile(SpectatorCameraHelper.AttributeFileLocation.ResourceFolder);
AssetDatabase.Refresh();
}
#region Load Setting From JSON File
GUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(IsRequireLoadJsonFile.boolValue);
if (GUILayout.Button("Load Setting From JSON File in Resources Folder", boldButtonStyle) ||
IsRequireLoadJsonFile.boolValue)
{
IsRequireLoadJsonFile.boolValue = true;
var searchPattern =
$"{SpectatorCameraHelper.ATTRIBUTE_FILE_PREFIX_NAME}*.{SpectatorCameraHelper.ATTRIBUTE_FILE_EXTENSION}";
if (JsonFileList == null)
{
JsonFileList = new List<string>();
}
JsonFileList.Clear();
var dir = new DirectoryInfo(Path.Combine(Application.dataPath, "Resources"));
var files = dir.GetFiles(searchPattern);
foreach (var item in files)
{
JsonFileList.Add(item.Name);
}
if (JsonFileList.Count == 0)
{
Debug.Log(
"Can't find any JSON file related to the spectator camera setting in the Resources folder.");
IsRequireLoadJsonFile.boolValue = false;
}
}
EditorGUI.EndDisabledGroup();
if (IsRequireLoadJsonFile.boolValue)
{
if (GUILayout.Button("Cancel"))
{
IsRequireLoadJsonFile.boolValue = false;
}
}
GUILayout.EndHorizontal();
if (IsRequireLoadJsonFile.boolValue)
{
Rect r = EditorGUILayout.BeginVertical();
JsonFileScrollViewVector = EditorGUILayout.BeginScrollView(JsonFileScrollViewVector,
GUILayout.Width(r.width),
GUILayout.Height(80));
for (int i = 0; i < JsonFileList.Count; i++)
{
if (GUILayout.Button(JsonFileList[i]))
{
var path = Path.Combine(
System.IO.Path.Combine(Application.dataPath, "Resources"),
JsonFileList[i]);
Undo.RecordObject(target, $"Load {JsonFileList[i]} setting to {target.name} SpectatorCameraManager");
EditorUtility.SetDirty(target);
script.LoadSettingFromJsonFile(path);
}
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
#endregion
EditorGUILayout.LabelField("\n");
// Spectator camera prefab
EditorGUILayout.PropertyField(SpectatorCameraPrefab, new GUIContent("Spectator Camera Prefab"));
if (SpectatorCameraPrefab.objectReferenceValue != null &&
PrefabUtility.GetPrefabAssetType(SpectatorCameraPrefab.objectReferenceValue) ==
PrefabAssetType.NotAPrefab)
{
// The assign object is scene object
Debug.Log("Please assign the object as prefab only.");
SpectatorCameraPrefab.objectReferenceValue = null;
}
EditorGUILayout.LabelField("\n");
EditorGUILayout.LabelField("<b>[ General Setting ]</b>", labelStyle);
// Setting of spectator camera reference source
// EditorGUILayout.PropertyField(CameraSourceRef, new GUIContent("Camera Source"));
EditorGUI.BeginChangeCheck();
var currentCameraSourceRef = (SpectatorCameraHelper.CameraSourceRef)
EditorGUILayout.EnumPopup("Camera Source", script.CameraSourceRef);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager CameraSourceRef");
EditorUtility.SetDirty(target);
script.CameraSourceRef = currentCameraSourceRef;
}
#region Tracker Region
if (script.CameraSourceRef == SpectatorCameraHelper.CameraSourceRef.Tracker)
{
EditorGUI.BeginChangeCheck();
var currentFollowSpectatorCameraTracker = EditorGUILayout.ObjectField(
"Tracker",
script.FollowSpectatorCameraTracker,
typeof(SpectatorCameraTracker),
true) as SpectatorCameraTracker;
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FollowSpectatorCameraTracker");
EditorUtility.SetDirty(target);
script.FollowSpectatorCameraTracker = currentFollowSpectatorCameraTracker;
}
if (script.FollowSpectatorCameraTracker == null)
{
// The assign object is null
EditorGUILayout.HelpBox("Please assign the SpectatorCameraTracker", MessageType.Info, false);
}
else if (PrefabUtility.GetPrefabAssetType(script.FollowSpectatorCameraTracker) !=
PrefabAssetType.NotAPrefab)
{
// Don't allow assign object is prefab
Debug.Log("Please assign the scene object.");
script.FollowSpectatorCameraTracker = null;
}
else
{
// The assign object is scene object => ok
EditorGUILayout.LabelField("\n");
IsShowTrackerPart.boolValue =
EditorGUILayout.Foldout(IsShowTrackerPart.boolValue, "Tracker Setting");
if (IsShowTrackerPart.boolValue)
{
// If show the tracker setting
Rect r = EditorGUILayout.BeginVertical();
SpectatorCameraTracker trackerObject = script.FollowSpectatorCameraTracker;
if (trackerObject != null)
{
EditorGUILayout.HelpBox(
$"You are now editing the tracker setting in \"{trackerObject.gameObject.name}\" GameObject",
MessageType.Info,
true);
EditorGUI.BeginChangeCheck();
var currentTrackerObjectLayerMask =
LayerMaskHelper.LayerMaskDrawer.LayerMaskField("Camera Layer Mask",
trackerObject.LayerMask);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} LayerMask");
EditorUtility.SetDirty(trackerObject);
trackerObject.LayerMask = currentTrackerObjectLayerMask;
}
EditorGUI.BeginChangeCheck();
var currentTrackerObjectIsSmoothCameraMovement=
EditorGUILayout.Toggle("Enable Smoothing Camera Movement",
trackerObject.IsSmoothCameraMovement);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} IsSmoothCameraMovement");
EditorUtility.SetDirty(trackerObject);
trackerObject.IsSmoothCameraMovement = currentTrackerObjectIsSmoothCameraMovement;
}
if (trackerObject.IsSmoothCameraMovement)
{
EditorGUILayout.LabelField("\n");
EditorGUILayout.LabelField("<b>[ Smooth Camera Movement Speed Setting ]</b>",
labelStyle);
EditorGUI.BeginChangeCheck();
var currentTrackerObjectSmoothCameraMovementSpeed =
EditorGUILayout.IntSlider(
new GUIContent("Speed of Smoothing Camera Movement"),
trackerObject.SmoothCameraMovementSpeed,
SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_MIN,
SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} SmoothCameraMovementSpeed");
EditorUtility.SetDirty(trackerObject);
trackerObject.SmoothCameraMovementSpeed = currentTrackerObjectSmoothCameraMovementSpeed;
}
EditorGUILayout.LabelField("\n");
}
EditorGUI.BeginChangeCheck();
// Spectator camera frustum show/hide
var currentTrackerObjectIsFrustumShowed =
EditorGUILayout.Toggle("Enable Camera FOV Frustum", trackerObject.IsFrustumShowed);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} IsFrustumShowed");
EditorUtility.SetDirty(trackerObject);
trackerObject.IsFrustumShowed = currentTrackerObjectIsFrustumShowed;
}
EditorGUILayout.LabelField("\n");
#region VerticalFov
EditorGUILayout.LabelField("<b>[ Vertical FOV Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
var currentTrackerObjectVerticalFov = EditorGUILayout.Slider(
"Vertical FOV",
trackerObject.VerticalFov,
SpectatorCameraHelper.VERTICAL_FOV_MIN,
SpectatorCameraHelper.VERTICAL_FOV_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} VerticalFov");
EditorUtility.SetDirty(trackerObject);
trackerObject.VerticalFov = currentTrackerObjectVerticalFov;
}
#endregion
EditorGUILayout.LabelField("\n");
#region Setting related to panorama capturing of spectator camera
// Panorama resolution
EditorGUILayout.LabelField("<b>[ Panorama Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
// Panorama output resolution
var currentTrackerObjectPanoramaResolution =
(SpectatorCameraHelper.SpectatorCameraPanoramaResolution)
EditorGUILayout.EnumPopup("Resolution", trackerObject.PanoramaResolution);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} PanoramaResolution");
EditorUtility.SetDirty(trackerObject);
trackerObject.PanoramaResolution = currentTrackerObjectPanoramaResolution;
}
EditorGUI.BeginChangeCheck();
// Panorama output format
var currentTrackerObjectPanoramaOutputFormat = (TextureProcessHelper.PictureOutputFormat)
EditorGUILayout.EnumPopup("Output Format", trackerObject.PanoramaOutputFormat);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} PanoramaOutputFormat");
EditorUtility.SetDirty(trackerObject);
trackerObject.PanoramaOutputFormat = currentTrackerObjectPanoramaOutputFormat;
}
EditorGUI.BeginChangeCheck();
// Panorama output type
var currentTrackerObjectPanoramaOutputType = (TextureProcessHelper.PanoramaType)
EditorGUILayout.EnumPopup("Output Type", trackerObject.PanoramaOutputType);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} PanoramaOutputType");
EditorUtility.SetDirty(trackerObject);
trackerObject.PanoramaOutputType = currentTrackerObjectPanoramaOutputType;
}
#endregion
EditorGUILayout.LabelField("\n");
#region Setting related to frustum
if (trackerObject.IsFrustumShowed)
{
EditorGUILayout.LabelField("<b>[ Frustum Setting ]</b>",
labelStyle);
#region Count of frustum and frustum center line
EditorGUI.BeginChangeCheck();
var currentTrackerObjectFrustumLineCount = (SpectatorCameraHelper.FrustumLineCount)
EditorGUILayout.EnumPopup("Frustum Line Total", trackerObject.FrustumLineCount);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} FrustumLineCount");
EditorUtility.SetDirty(trackerObject);
trackerObject.FrustumLineCount = currentTrackerObjectFrustumLineCount;
}
EditorGUI.BeginChangeCheck();
var currentTrackerObjectFrustumCenterLineCount =
(SpectatorCameraHelper.FrustumCenterLineCount)
EditorGUILayout.EnumPopup("Frustum Center Line Total",
trackerObject.FrustumCenterLineCount);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} FrustumCenterLineCount");
EditorUtility.SetDirty(trackerObject);
trackerObject.FrustumCenterLineCount = currentTrackerObjectFrustumCenterLineCount;
}
#endregion
EditorGUILayout.LabelField("\n");
#region Width of frustum and frustum center line
EditorGUI.BeginChangeCheck();
var currentTrackerObjectFrustumLineWidth =
EditorGUILayout.Slider(
"Frustum Line Width",
trackerObject.FrustumLineWidth,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} FrustumLineWidth");
EditorUtility.SetDirty(trackerObject);
trackerObject.FrustumLineWidth = currentTrackerObjectFrustumLineWidth;
}
EditorGUI.BeginChangeCheck();
var currentTrackerObjectFrustumCenterLineWidth =
EditorGUILayout.Slider(
"Frustum Center Line Width",
trackerObject.FrustumCenterLineWidth,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} FrustumCenterLineWidth");
EditorUtility.SetDirty(trackerObject);
trackerObject.FrustumCenterLineWidth = currentTrackerObjectFrustumCenterLineWidth;
}
#endregion
EditorGUILayout.LabelField("\n");
#region Material of frustum and frustum center line
EditorGUI.BeginChangeCheck();
var currentTrackerObjectFrustumLineColor = EditorGUILayout.ColorField(
"Frustum Line Color", trackerObject.FrustumLineColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} FrustumLineColor");
EditorUtility.SetDirty(trackerObject);
trackerObject.FrustumLineColor = currentTrackerObjectFrustumLineColor;
}
EditorGUI.BeginChangeCheck();
var currentTrackerObjectFrustumCenterLineColor = EditorGUILayout.ColorField(
"Frustum Center Line Color",
trackerObject.FrustumCenterLineColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(trackerObject, $"Modified {trackerObject.name} FrustumCenterLineColor");
EditorUtility.SetDirty(trackerObject);
trackerObject.FrustumCenterLineColor = currentTrackerObjectFrustumCenterLineColor;
}
#endregion
}
#endregion
EditorGUILayout.EndVertical();
r = new Rect(r.x, r.y, r.width, r.height);
EditorGUI.DrawRect(r, HighlightRegionBackgroundColor);
EditorGUILayout.LabelField("\n");
}
}
}
}
#endregion
#region HMD Region
IsShowHmdPart.boolValue = EditorGUILayout.Foldout(IsShowHmdPart.boolValue, "HMD Setting");
if (IsShowHmdPart.boolValue)
{
Rect r = EditorGUILayout.BeginVertical();
EditorGUI.BeginChangeCheck();
// Setting of spectator camera layer mask
var currentLayerMask =
LayerMaskHelper.LayerMaskDrawer.LayerMaskField("Camera Layer Mask", script.LayerMask);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager LayerMask");
EditorUtility.SetDirty(target);
script.LayerMask = currentLayerMask;
}
// Setting of smooth spectator camera movement
EditorGUILayout.PropertyField(IsSmoothCameraMovement,
new GUIContent("Enable Smoothing Camera Movement"));
if (IsSmoothCameraMovement.boolValue)
{
EditorGUILayout.LabelField("\n");
EditorGUILayout.LabelField("<b>[ Smooth Camera Movement Speed Setting ]</b>", labelStyle);
// Setting of smooth spectator camera movement speed
EditorGUILayout.IntSlider(
SmoothCameraMovementSpeed,
SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_MIN,
SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_MAX,
"Speed of Smoothing Camera Movement");
EditorGUILayout.LabelField("\n");
}
EditorGUI.BeginChangeCheck();
// Spectator camera frustum show/hide
var currentIsFrustumShowed =
EditorGUILayout.Toggle("Enable Camera FOV Frustum", script.IsFrustumShowed);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager IsFrustumShowed");
EditorUtility.SetDirty(target);
script.IsFrustumShowed = currentIsFrustumShowed;
}
EditorGUILayout.LabelField("\n");
#region VerticalFov
EditorGUILayout.LabelField("<b>[ Vertical FOV Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
// FOV
var currentVerticalFov = EditorGUILayout.Slider(
"Vertical FOV",
script.VerticalFov,
SpectatorCameraHelper.VERTICAL_FOV_MIN,
SpectatorCameraHelper.VERTICAL_FOV_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager VerticalFov");
EditorUtility.SetDirty(target);
script.VerticalFov = currentVerticalFov;
}
#endregion
EditorGUILayout.LabelField("\n");
#region Setting related to panorama capturing of spectator camera
// Panorama resolution
EditorGUILayout.LabelField("<b>[ Panorama Setting ]</b>", labelStyle);
EditorGUILayout.PropertyField(PanoramaResolution, new GUIContent("Resolution"));
// Panorama output format
EditorGUILayout.PropertyField(PanoramaOutputFormat, new GUIContent("Output Format"));
// Panorama output type
EditorGUILayout.PropertyField(PanoramaOutputType, new GUIContent("Output Type"));
#endregion
#region Setting related to frustum
if (script.IsFrustumShowed)
{
EditorGUILayout.LabelField("\n");
EditorGUILayout.LabelField("<b>[ Frustum Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
// Count of frustum line
var currentFrustumLineCount = (SpectatorCameraHelper.FrustumLineCount)
EditorGUILayout.EnumPopup("Frustum Line Total", script.FrustumLineCount);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FrustumLineCount");
EditorUtility.SetDirty(target);
script.FrustumLineCount = currentFrustumLineCount;
}
EditorGUI.BeginChangeCheck();
// Count of frustum center line
var currentFrustumCenterLineCount = (SpectatorCameraHelper.FrustumCenterLineCount)
EditorGUILayout.EnumPopup("Frustum Center Line Total", script.FrustumCenterLineCount);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FrustumCenterLineCount");
EditorUtility.SetDirty(target);
script.FrustumCenterLineCount = currentFrustumCenterLineCount;
}
EditorGUILayout.LabelField("\n");
EditorGUI.BeginChangeCheck();
// Width of frustum line
var currentFrustumLineWidth =
EditorGUILayout.Slider(
"Frustum Line Width",
script.FrustumLineWidth,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FrustumLineWidth");
EditorUtility.SetDirty(target);
script.FrustumLineWidth = currentFrustumLineWidth;
}
EditorGUI.BeginChangeCheck();
// Width of frustum center line
var currentFrustumCenterLineWidth =
EditorGUILayout.Slider(
"Frustum Center Line Width",
script.FrustumCenterLineWidth,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FrustumCenterLineWidth");
EditorUtility.SetDirty(target);
script.FrustumCenterLineWidth = currentFrustumCenterLineWidth;
}
EditorGUILayout.LabelField("\n");
EditorGUI.BeginChangeCheck();
// Color of frustum line
var currentFrustumLineColor = EditorGUILayout.ColorField(
"Frustum Line Color", script.FrustumLineColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FrustumLineColor");
EditorUtility.SetDirty(target);
script.FrustumLineColor = currentFrustumLineColor;
}
EditorGUI.BeginChangeCheck();
// Color of frustum center line
var currentFrustumCenterLineColor = EditorGUILayout.ColorField(
"Frustum Center Line Color",
script.FrustumCenterLineColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager FrustumCenterLineColor");
EditorUtility.SetDirty(target);
script.FrustumCenterLineColor = currentFrustumCenterLineColor;
}
}
#endregion
EditorGUILayout.EndVertical();
r = new Rect(r.x, r.y, r.width, r.height);
EditorGUI.DrawRect(r, HighlightRegionBackgroundColor);
}
#endregion
EditorGUILayout.LabelField("\n");
#region Test 360 Output
EditorGUILayout.LabelField("<b>[ Debug Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
var currentSpectatorCameraViewMaterial = EditorGUILayout.ObjectField(
"Spectator Camera View Material",
script.SpectatorCameraViewMaterial,
typeof(Material),
false) as Material;
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Modified SpectatorCameraManager SpectatorCameraViewMaterial");
EditorUtility.SetDirty(target);
script.SpectatorCameraViewMaterial = currentSpectatorCameraViewMaterial;
}
EditorGUILayout.HelpBox("Test - Output 360 photo", MessageType.Info, true);
if (GUILayout.Button("Test - Output 360 photo"))
{
script.CaptureSpectatorCamera360Photo();
}
#endregion
}
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5a03d623c31c2344a1f99005572762a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fda46617f9e447e47863938d3006d5bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,374 @@
// Copyright HTC Corporation All Rights Reserved.
#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using VIVE.OpenXR.Toolkits.Spectator.Helper;
namespace VIVE.OpenXR.Toolkits.Spectator
{
/// <summary>
/// Name: SpectatorCameraTracker.Editor.cs
/// Role: General script use in Unity Editor only
/// Responsibility: Display the SpectatorCameraTracker.cs in Unity Inspector
/// </summary>
public partial class SpectatorCameraTracker
{
[field: SerializeField] private bool IsRequireLoadJsonFile { get; set; }
[CustomEditor(typeof(SpectatorCameraTracker))]
public class SpectatorCameraTrackerSettingEditor : UnityEditor.Editor
{
private SerializedProperty IsRequireLoadJsonFile { get; set; }
private List<string> JsonFileList { get; set; }
private Vector2 JsonFileScrollViewVector { get; set; }
private SerializedProperty IsSmoothCameraMovement { get; set; }
private SerializedProperty SmoothCameraMovementSpeed { get; set; }
private SerializedProperty PanoramaResolution { get; set; }
private SerializedProperty PanoramaOutputFormat { get; set; }
private SerializedProperty PanoramaOutputType { get; set; }
private void OnEnable()
{
IsRequireLoadJsonFile =
serializedObject.FindProperty(EditorHelper.PropertyName("IsRequireLoadJsonFile"));
JsonFileList = new List<string>();
JsonFileScrollViewVector = Vector2.zero;
IsSmoothCameraMovement =
serializedObject.FindProperty(EditorHelper.PropertyName("IsSmoothCameraMovement"));
SmoothCameraMovementSpeed =
serializedObject.FindProperty(EditorHelper.PropertyName("SmoothCameraMovementSpeed"));
PanoramaResolution = serializedObject.FindProperty(EditorHelper.PropertyName("PanoramaResolution"));
PanoramaOutputFormat = serializedObject.FindProperty(EditorHelper.PropertyName("PanoramaOutputFormat"));
PanoramaOutputType = serializedObject.FindProperty(EditorHelper.PropertyName("PanoramaOutputType"));
}
public override void OnInspectorGUI()
{
// Just return if not "SpectatorCameraTrackerSetting" class
if (!(target is SpectatorCameraTracker))
{
return;
}
serializedObject.Update();
DrawGUI();
serializedObject.ApplyModifiedProperties();
}
private void DrawGUI()
{
#region GUIStyle
var labelStyle = new GUIStyle()
{
richText = true,
alignment = TextAnchor.MiddleCenter,
normal = new GUIStyleState
{
textColor = EditorGUIUtility.isProSkin ? Color.green : Color.black
}
};
var resetButtonStyle = new GUIStyle(GUI.skin.button)
{
fontStyle = FontStyle.Bold,
normal = new GUIStyleState
{
textColor = EditorGUIUtility.isProSkin ? Color.yellow : Color.red
},
hover = new GUIStyleState
{
textColor = Color.red
},
active = new GUIStyleState
{
textColor = Color.cyan
},
};
var boldButtonStyle = new GUIStyle(GUI.skin.button)
{
fontStyle = FontStyle.Bold
};
#endregion
var script = (SpectatorCameraTracker)target;
// Button for reset value
if (GUILayout.Button("Reset to default value", resetButtonStyle))
{
Undo.RecordObject(target, $"Reset {target.name} SpectatorCameraTracker to default value");
EditorUtility.SetDirty(target);
script.ResetSetting();
}
// Button for export setting
if (GUILayout.Button("Export Spectator Camera Tracker Setting", boldButtonStyle))
{
script.ExportSetting2JsonFile(SpectatorCameraHelper.AttributeFileLocation.ResourceFolder);
AssetDatabase.Refresh();
}
#region Load setting from JSON file
GUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(IsRequireLoadJsonFile.boolValue);
if (GUILayout.Button("Load Setting From JSON File in Resources Folder", boldButtonStyle) ||
IsRequireLoadJsonFile.boolValue)
{
IsRequireLoadJsonFile.boolValue = true;
var searchPattern =
$"{SpectatorCameraHelper.ATTRIBUTE_FILE_PREFIX_NAME}*.{SpectatorCameraHelper.ATTRIBUTE_FILE_EXTENSION}";
if (JsonFileList == null)
{
JsonFileList = new List<string>();
}
JsonFileList.Clear();
var dir = new DirectoryInfo(Path.Combine(Application.dataPath, "Resources"));
var files = dir.GetFiles(searchPattern);
foreach (var item in files)
{
JsonFileList.Add(item.Name);
}
if (JsonFileList.Count == 0)
{
Debug.Log(
"Can't find any JSON file related to the spectator camera setting in the Resources folder.");
IsRequireLoadJsonFile.boolValue = false;
}
}
EditorGUI.EndDisabledGroup();
if (IsRequireLoadJsonFile.boolValue)
{
if (GUILayout.Button("Cancel"))
{
IsRequireLoadJsonFile.boolValue = false;
}
}
GUILayout.EndHorizontal();
if (IsRequireLoadJsonFile.boolValue)
{
Rect r = EditorGUILayout.BeginVertical();
JsonFileScrollViewVector = EditorGUILayout.BeginScrollView(JsonFileScrollViewVector,
GUILayout.Width(r.width),
GUILayout.Height(80));
for (int i = 0; i < JsonFileList.Count; i++)
{
if (GUILayout.Button(JsonFileList[i]))
{
var path = Path.Combine(
Path.Combine(Application.dataPath, "Resources"),
JsonFileList[i]);
Undo.RecordObject(target, $"Load {JsonFileList[i]} setting to {target.name} SpectatorCameraTracker");
EditorUtility.SetDirty(target);
script.LoadSettingFromJsonFile(path);
}
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
#endregion
EditorGUILayout.LabelField("\n");
#region Setting related to spectator camera and its frustum show/hide
EditorGUILayout.LabelField("<b>[ General Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
// Setting of spectator camera layer mask
var currentLayerMask =
LayerMaskHelper.LayerMaskDrawer.LayerMaskField("Camera Layer Mask", script.LayerMask);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker LayerMask");
EditorUtility.SetDirty(target);
script.LayerMask = currentLayerMask;
}
// Setting of smooth spectator camera movement
EditorGUILayout.PropertyField(IsSmoothCameraMovement,
new GUIContent("Enable Smoothing Camera Movement"));
if (IsSmoothCameraMovement.boolValue)
{
EditorGUILayout.LabelField("\n");
EditorGUILayout.LabelField("<b>[ Smooth Camera Movement Speed Setting ]</b>", labelStyle);
EditorGUILayout.IntSlider(
SmoothCameraMovementSpeed,
SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_MIN,
SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_MAX,
"Speed of Smoothing Camera Movement");
EditorGUILayout.LabelField("\n");
}
EditorGUI.BeginChangeCheck();
// Spectator camera frustum show/hide
var currentIsFrustumShowed =
EditorGUILayout.Toggle("Enable Camera FOV Frustum", script.IsFrustumShowed);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker IsFrustumShowed");
EditorUtility.SetDirty(target);
script.IsFrustumShowed = currentIsFrustumShowed;
}
#endregion
EditorGUILayout.LabelField("\n");
#region VerticalFov
EditorGUILayout.LabelField("<b>[ Vertical FOV Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
// FOV
var currentVerticalFov = EditorGUILayout.Slider(
"Vertical FOV",
script.VerticalFov,
SpectatorCameraHelper.VERTICAL_FOV_MIN,
SpectatorCameraHelper.VERTICAL_FOV_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker VerticalFov");
EditorUtility.SetDirty(target);
script.VerticalFov = currentVerticalFov;
}
#endregion
EditorGUILayout.LabelField("\n");
#region Setting related to panorama capturing of spectator camera
// Panorama resolution
EditorGUILayout.LabelField("<b>[ Panorama Setting ]</b>", labelStyle);
EditorGUILayout.PropertyField(PanoramaResolution, new GUIContent("Resolution"));
// Panorama output format
EditorGUILayout.PropertyField(PanoramaOutputFormat, new GUIContent("Output Format"));
// Panorama output type
EditorGUILayout.PropertyField(PanoramaOutputType, new GUIContent("Output Type"));
#endregion
EditorGUILayout.LabelField("\n");
#region Setting related to frustum
if (script.IsFrustumShowed)
{
EditorGUILayout.LabelField("<b>[ Frustum Setting ]</b>", labelStyle);
EditorGUI.BeginChangeCheck();
// Count of frustum line
var currentFrustumLineCount = (SpectatorCameraHelper.FrustumLineCount)
EditorGUILayout.EnumPopup("Frustum Line Total", script.FrustumLineCount);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker FrustumLineCount");
EditorUtility.SetDirty(target);
script.FrustumLineCount = currentFrustumLineCount;
}
EditorGUI.BeginChangeCheck();
// Count of frustum center line
var currentFrustumCenterLineCount = (SpectatorCameraHelper.FrustumCenterLineCount)
EditorGUILayout.EnumPopup("Frustum Center Line Total", script.FrustumCenterLineCount);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker FrustumCenterLineCount");
EditorUtility.SetDirty(target);
script.FrustumCenterLineCount = currentFrustumCenterLineCount;
}
EditorGUILayout.LabelField("\n");
EditorGUI.BeginChangeCheck();
// Width of frustum line
var currentFrustumLineWidth =
EditorGUILayout.Slider(
"Frustum Line Width",
script.FrustumLineWidth,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker FrustumLineWidth");
EditorUtility.SetDirty(target);
script.FrustumLineWidth = currentFrustumLineWidth;
}
EditorGUI.BeginChangeCheck();
// Width of frustum center line
var currentFrustumCenterLineWidth =
EditorGUILayout.Slider(
"Frustum Center Line Width",
script.FrustumCenterLineWidth,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MAX);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker FrustumCenterLineWidth");
EditorUtility.SetDirty(target);
script.FrustumCenterLineWidth = currentFrustumCenterLineWidth;
}
EditorGUILayout.LabelField("\n");
EditorGUI.BeginChangeCheck();
// Color of frustum line
var currentFrustumLineColor = EditorGUILayout.ColorField(
"Frustum Line Color", script.FrustumLineColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker FrustumLineColor");
EditorUtility.SetDirty(target);
script.FrustumLineColor = currentFrustumLineColor;
}
EditorGUI.BeginChangeCheck();
// Color of frustum center line
var currentFrustumCenterLineColor = EditorGUILayout.ColorField(
"Frustum Center Line Color",
script.FrustumCenterLineColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, $"Modified {target.name} SpectatorCameraTracker FrustumCenterLineColor");
EditorUtility.SetDirty(target);
script.FrustumCenterLineColor = currentFrustumCenterLineColor;
}
EditorGUILayout.LabelField("\n");
}
#endregion
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 523f24465def2a246b2f27dca663a530
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,479 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using VIVE.OpenXR.Toolkits.Spectator.Helper;
namespace VIVE.OpenXR.Toolkits.Spectator
{
/// <summary>
/// Name: SpectatorCameraTracker.cs
/// Role: General script
/// Responsibility: To implement the spectator camera tracker setting and I/O function
/// </summary>
public partial class SpectatorCameraTracker : MonoBehaviour, ISpectatorCameraSetting
{
private const int TryAddTrackerIntervalSecond = 1;
private SpectatorCameraManager SpectatorCameraManager => SpectatorCameraManager.Instance;
public SpectatorCameraHelper.CameraSourceRef CameraSourceRef => SpectatorCameraHelper.CameraSourceRef.Tracker;
public Vector3 Position
{
get => transform.position;
set => transform.position = value;
}
public Quaternion Rotation
{
get => transform.rotation;
set => transform.rotation = value;
}
[SerializeField] private LayerMask layerMask = SpectatorCameraHelper.LayerMaskDefault;
public LayerMask LayerMask
{
get => layerMask;
set
{
if (layerMask == value)
{
return;
}
layerMask = value;
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SpectatorCameraBased.SpectatorCamera.cullingMask = layerMask;
}
}
}
[field: SerializeField]
public bool IsSmoothCameraMovement { get; set; } = SpectatorCameraHelper.IS_SMOOTH_CAMERA_MOVEMENT_DEFAULT;
[field: SerializeField]
public int SmoothCameraMovementSpeed { get; set; } = SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_SPEED_DEFAULT;
[SerializeField] private bool isFrustumShowed = SpectatorCameraHelper.IS_FRUSTUM_SHOWED_DEFAULT;
public bool IsFrustumShowed
{
get => isFrustumShowed;
set
{
if (isFrustumShowed == value)
{
return;
}
isFrustumShowed = value;
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustum();
}
}
}
[SerializeField] private float verticalFov = SpectatorCameraHelper.VERTICAL_FOV_DEFAULT;
public float VerticalFov
{
get => verticalFov;
set
{
if (Math.Abs(verticalFov - value) < SpectatorCameraHelper.COMPARE_FLOAT_MEDIUM_THRESHOLD)
{
return;
}
verticalFov = Mathf.Clamp(
value,
SpectatorCameraHelper.VERTICAL_FOV_MIN,
SpectatorCameraHelper.VERTICAL_FOV_MAX);
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SpectatorCameraBased.SpectatorCamera.fieldOfView = verticalFov;
SpectatorCameraManager.SetupFrustum();
}
}
}
#region Panorama properties
[field: SerializeField]
public SpectatorCameraHelper.SpectatorCameraPanoramaResolution PanoramaResolution { get; set; } =
SpectatorCameraHelper.PANORAMA_RESOLUTION_DEFAULT;
[field: SerializeField]
public TextureProcessHelper.PictureOutputFormat PanoramaOutputFormat { get; set; } =
SpectatorCameraHelper.PANORAMA_OUTPUT_FORMAT_DEFAULT;
[field: SerializeField]
public TextureProcessHelper.PanoramaType PanoramaOutputType { get; set; } =
SpectatorCameraHelper.PANORAMA_TYPE_DEFAULT;
#endregion
[SerializeField] private SpectatorCameraHelper.FrustumLineCount frustumLineCount =
SpectatorCameraHelper.FRUSTUM_LINE_COUNT_DEFAULT;
public SpectatorCameraHelper.FrustumLineCount FrustumLineCount
{
get => frustumLineCount;
set
{
if (frustumLineCount == value)
{
return;
}
frustumLineCount = value;
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustumLine();
}
}
}
[SerializeField] private SpectatorCameraHelper.FrustumCenterLineCount frustumCenterLineCount =
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_COUNT_DEFAULT;
public SpectatorCameraHelper.FrustumCenterLineCount FrustumCenterLineCount
{
get => frustumCenterLineCount;
set
{
if (frustumCenterLineCount == value)
{
return;
}
frustumCenterLineCount = value;
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustumCenterLine();
}
}
}
[SerializeField] private float frustumLineWidth = SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_DEFAULT;
public float FrustumLineWidth
{
get => frustumLineWidth;
set
{
if (Math.Abs(frustumLineWidth - value) < SpectatorCameraHelper.COMPARE_FLOAT_SUPER_SMALL_THRESHOLD)
{
return;
}
frustumLineWidth = Mathf.Clamp(
value,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_MAX);
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustumLine();
}
}
}
[SerializeField] private float frustumCenterLineWidth = SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_DEFAULT;
public float FrustumCenterLineWidth
{
get => frustumCenterLineWidth;
set
{
if (Math.Abs(frustumCenterLineWidth - value) <
SpectatorCameraHelper.COMPARE_FLOAT_SUPER_SMALL_THRESHOLD)
{
return;
}
frustumCenterLineWidth = Mathf.Clamp(value, SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MIN,
SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_MAX);
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustumCenterLine();
}
}
}
[SerializeField] private Color frustumLineColor = SpectatorCameraHelper.LineColorDefault;
public Color FrustumLineColor
{
get => frustumLineColor;
set
{
if (frustumLineColor == value)
{
return;
}
frustumLineColor = value;
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustumLine();
}
}
}
[SerializeField] private Color frustumCenterLineColor = SpectatorCameraHelper.LineColorDefault;
public Color FrustumCenterLineColor
{
get => frustumCenterLineColor;
set
{
if (frustumCenterLineColor == value)
{
return;
}
frustumCenterLineColor = value;
if (Application.isPlaying &&
SpectatorCameraManager != null &&
SpectatorCameraManager.IsCameraSourceAsTracker() &&
SpectatorCameraManager.IsFollowTrackerEqualTo(this))
{
SpectatorCameraManager.SetupFrustumCenterLine();
}
}
}
public void ResetSetting()
{
LayerMask = SpectatorCameraHelper.LayerMaskDefault;
IsSmoothCameraMovement = SpectatorCameraHelper.IS_SMOOTH_CAMERA_MOVEMENT_DEFAULT;
SmoothCameraMovementSpeed = SpectatorCameraHelper.SMOOTH_CAMERA_MOVEMENT_SPEED_DEFAULT;
IsFrustumShowed = SpectatorCameraHelper.IS_FRUSTUM_SHOWED_DEFAULT;
VerticalFov = SpectatorCameraHelper.VERTICAL_FOV_DEFAULT;
PanoramaResolution = SpectatorCameraHelper.PANORAMA_RESOLUTION_DEFAULT;
PanoramaOutputFormat = SpectatorCameraHelper.PANORAMA_OUTPUT_FORMAT_DEFAULT;
PanoramaOutputType = SpectatorCameraHelper.PANORAMA_TYPE_DEFAULT;
FrustumLineCount = SpectatorCameraHelper.FRUSTUM_LINE_COUNT_DEFAULT;
FrustumCenterLineCount = SpectatorCameraHelper.FRUSTUM_CENTER_LINE_COUNT_DEFAULT;
FrustumLineWidth = SpectatorCameraHelper.FRUSTUM_LINE_WIDTH_DEFAULT;
FrustumCenterLineWidth = SpectatorCameraHelper.FRUSTUM_CENTER_LINE_WIDTH_DEFAULT;
FrustumLineColor = SpectatorCameraHelper.LineColorDefault;
FrustumCenterLineColor = SpectatorCameraHelper.LineColorDefault;
}
public void ExportSetting2JsonFile(in SpectatorCameraHelper.AttributeFileLocation attributeFileLocation)
{
#if !UNITY_EDITOR
if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.ResourceFolder)
{
Debug.LogError("It's not allowed to save setting to resource folder in runtime mode");
return;
}
#endif
var data = new SpectatorCameraHelper.SpectatorCameraAttribute(
CameraSourceRef,
Position,
Rotation,
LayerMask,
IsSmoothCameraMovement,
SmoothCameraMovementSpeed,
IsFrustumShowed,
VerticalFov,
PanoramaResolution,
PanoramaOutputFormat,
PanoramaOutputType,
FrustumLineCount,
FrustumCenterLineCount,
FrustumLineWidth,
FrustumCenterLineWidth,
FrustumLineColor,
FrustumCenterLineColor);
#if UNITY_EDITOR
if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.ResourceFolder)
{
SpectatorCameraHelper.SaveAttributeData2ResourcesFolder(
SceneManager.GetActiveScene().name,
gameObject.name,
data);
}
else if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.PersistentFolder)
{
SpectatorCameraHelper.SaveAttributeData2PersistentFolder(
SceneManager.GetActiveScene().name,
gameObject.name,
data);
}
#else
SpectatorCameraHelper.SaveAttributeData2PersistentFolder(
SceneManager.GetActiveScene().name,
gameObject.name,
data);
#endif
}
public void LoadSettingFromJsonFile(in string jsonFilePath)
{
bool loadSuccess = SpectatorCameraHelper.LoadAttributeFileFromFolder(
jsonFilePath,
out SpectatorCameraHelper.SpectatorCameraAttribute data);
if (loadSuccess)
{
ApplyData(data);
}
else
{
Debug.Log($"Load setting from {jsonFilePath} file to scene gameObject {gameObject.name} failed.");
}
}
public void LoadSettingFromJsonFile(
in string sceneName,
in string trackerName,
in SpectatorCameraHelper.AttributeFileLocation attributeFileLocation)
{
if (string.IsNullOrEmpty(sceneName) || string.IsNullOrEmpty(trackerName))
{
Debug.LogError("sceneName or trackerName is null or empty");
return;
}
var loadSuccess = false;
SpectatorCameraHelper.SpectatorCameraAttribute data = new SpectatorCameraHelper.SpectatorCameraAttribute();
if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.ResourceFolder)
{
loadSuccess = SpectatorCameraHelper.LoadAttributeFileFromResourcesFolder(
sceneName,
trackerName,
out data);
}
else if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.PersistentFolder)
{
loadSuccess = SpectatorCameraHelper.LoadAttributeFileFromPersistentFolder(
sceneName,
trackerName,
out data);
}
if (loadSuccess)
{
ApplyData(data);
}
else
{
var fileDirectory = string.Empty;
if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.ResourceFolder)
{
fileDirectory = System.IO.Path.Combine(Application.dataPath, "Resources");
}
else if (attributeFileLocation is SpectatorCameraHelper.AttributeFileLocation.PersistentFolder)
{
fileDirectory = Application.persistentDataPath;
}
var fileName =
SpectatorCameraHelper.GetSpectatorCameraAttributeFileNamePattern(sceneName, trackerName);
Debug.Log(
$"Load setting from {fileDirectory}/{fileName} file to scene gameObject {gameObject.name} failed.");
}
}
public void ApplyData(in SpectatorCameraHelper.SpectatorCameraAttribute data)
{
Transform gameObjectTransform = transform;
gameObjectTransform.position = data.position;
gameObjectTransform.rotation = data.rotation;
LayerMask = data.layerMask;
IsSmoothCameraMovement = data.isSmoothCameraMovement;
SmoothCameraMovementSpeed = data.smoothCameraMovementSpeed;
IsFrustumShowed = data.isFrustumShowed;
VerticalFov = data.verticalFov;
PanoramaResolution = data.panoramaResolution;
PanoramaOutputFormat = data.panoramaOutputFormat;
PanoramaOutputType = data.panoramaOutputType;
FrustumLineCount = data.frustumLineCount;
FrustumCenterLineCount = data.frustumCenterLineCount;
FrustumLineWidth = data.frustumLineWidth;
FrustumCenterLineWidth = data.frustumCenterLineWidth;
FrustumLineColor = data.frustumLineColor;
FrustumCenterLineColor = data.frustumCenterLineColor;
}
private void Start()
{
if (SpectatorCameraManager != null)
{
SpectatorCameraManager.AddSpectatorCameraTracker(this);
}
else
{
InvokeRepeating(nameof(AddTracker), TryAddTrackerIntervalSecond, TryAddTrackerIntervalSecond);
}
}
private void OnEnable()
{
if (SpectatorCameraManager != null)
{
SpectatorCameraManager.AddSpectatorCameraTracker(this);
}
}
private void OnDisable()
{
if (SpectatorCameraManager != null)
{
SpectatorCameraManager.RemoveSpectatorCameraTracker(this);
}
}
private void AddTracker()
{
if (SpectatorCameraManager != null)
{
SpectatorCameraManager.AddSpectatorCameraTracker(this);
CancelInvoke(nameof(AddTracker));
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f0b61457a6a405c49b000f7a3ed895bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: