// Copyright HTC Corporation All Rights Reserved. #if UNITY_EDITOR #define FAKE_DATA #endif using System; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.XR.OpenXR; using UnityEngine.XR.OpenXR.Features; using VIVE.OpenXR.Feature; #if UNITY_EDITOR using UnityEditor; using UnityEditor.XR.OpenXR.Features; #endif namespace VIVE.OpenXR.PlaneDetection { #if UNITY_EDITOR [OpenXRFeature(UiName = "VIVE XR PlaneDetection", Desc = "VIVE's implementaion of the XR_EXT_plane_detection.", Company = "HTC", DocumentationLink = "..\\Documentation", OpenxrExtensionStrings = kOpenxrExtensionString, Version = "1.0.0", BuildTargetGroups = new[] { BuildTargetGroup.Android }, FeatureId = featureId )] #endif public class VivePlaneDetection : OpenXRFeature { public const string kOpenxrExtensionString = "XR_EXT_plane_detection"; /// /// The feature id string. This is used to give the feature a well known id for reference. /// public const string featureId = "vive.wave.openxr.feature.planedetection"; private bool m_XrInstanceCreated = false; private XrInstance m_XrInstance = 0; private bool m_XrSessionCreated = false; private XrSession session = 0; private XrSystemId m_XrSystemId = 0; #region struct, enum, const of this extensions /// /// See XrPlaneDetectorOrientationEXT /// public enum XrPlaneDetectorOrientationEXT { HORIZONTAL_UPWARD_EXT = 0, HORIZONTAL_DOWNWARD_EXT = 1, VERTICAL_EXT = 2, ARBITRARY_EXT = 3, MAX_ENUM_EXT = 0x7FFFFFFF } /// /// See XrPlaneDetectorSemanticTypeEXT /// public enum XrPlaneDetectorSemanticTypeEXT { UNDEFINED_EXT = 0, CEILING_EXT = 1, FLOOR_EXT = 2, WALL_EXT = 3, PLATFORM_EXT = 4, MAX_ENUM_EXT = 0x7FFFFFFF } /// /// See XrPlaneDetectionStateEXT /// public enum XrPlaneDetectionStateEXT { NONE_EXT = 0, PENDING_EXT = 1, // Try get plane detection state again DONE_EXT = 2, // Ready to get result ERROR_EXT = 3, // Can try begin again FATAL_EXT = 4, // Should destroy the plane detector MAX_ENUM_EXT = 0x7FFFFFFF } //XrFlags64 XrPlaneDetectionCapabilityFlagsEXT; // Flag bits for XrPlaneDetectionCapabilityFlagsEXT public static XrFlags64 CAPABILITY_PLANE_DETECTION_BIT_EXT = 0x00000001; public static XrFlags64 CAPABILITY_PLANE_HOLES_BIT_EXT = 0x00000002; public static XrFlags64 CAPABILITY_SEMANTIC_CEILING_BIT_EXT = 0x00000004; public static XrFlags64 CAPABILITY_SEMANTIC_FLOOR_BIT_EXT = 0x00000008; public static XrFlags64 CAPABILITY_SEMANTIC_WALL_BIT_EXT = 0x00000010; public static XrFlags64 CAPABILITY_SEMANTIC_PLATFORM_BIT_EXT = 0x00000020; public static XrFlags64 CAPABILITY_ORIENTATION_BIT_EXT = 0x00000040; //XrFlags64 XrPlaneDetectorFlagsEXT; // Flag bits for XrPlaneDetectorFlagsEXT public static XrFlags64 XR_PLANE_DETECTOR_ENABLE_CONTOUR_BIT_EXT = 0x00000001; /// /// See XrSystemPlaneDetectionPropertiesEXT /// public struct XrSystemPlaneDetectionPropertiesEXT { public XrStructureType type; public IntPtr next; public XrFlags64 supportedFeatures; // XrPlaneDetectionCapabilityFlagsEXT } /// /// See XrPlaneDetectorCreateInfoEXT /// public struct XrPlaneDetectorCreateInfoEXT { public XrStructureType type; public IntPtr next; public XrFlags64 flags; // XrPlaneDetectorFlagsEXT } /// /// See XrExtent3DfEXT /// public struct XrExtent3DfEXT { public float width; public float height; public float depth; public XrExtent3DfEXT(float width, float height, float depth) { this.width = width; this.height = height; this.depth = depth; } public XrExtent3DfEXT One => new XrExtent3DfEXT(1, 1, 1); } /// /// See XrPlaneDetectorBeginInfoEXT /// public struct XrPlaneDetectorBeginInfoEXT { public XrStructureType type; public IntPtr next; public XrSpace baseSpace; public XrTime time; public uint orientationCount; public IntPtr orientations; // XrPlaneDetectorOrientationEXT[] public uint semanticTypeCount; public IntPtr semanticTypes; // XrPlaneDetectorSemanticTypeEXT[] public uint maxPlanes; public float minArea; public XrPosef boundingBoxPose; public XrExtent3DfEXT boundingBoxExtent; } /// /// See XrPlaneDetectorGetInfoEXT /// public struct XrPlaneDetectorGetInfoEXT { public XrStructureType type; public IntPtr next; public XrSpace baseSpace; public XrTime time; } /// /// See XrPlaneDetectorLocationEXT /// public struct XrPlaneDetectorLocationEXT { public XrStructureType type; public IntPtr next; public ulong planeId; public XrSpaceLocationFlags locationFlags; public XrPosef pose; public XrExtent2Df extents; public XrPlaneDetectorOrientationEXT orientation; public XrPlaneDetectorSemanticTypeEXT semanticType; public uint polygonBufferCount; } /// /// See XrPlaneDetectorLocationsEXT /// public struct XrPlaneDetectorLocationsEXT { public XrStructureType type; public IntPtr next; public uint planeLocationCapacityInput; public uint planeLocationCountOutput; public IntPtr planeLocations; // XrPlaneDetectorLocationEXT[] } /// /// See XrPlaneDetectorPolygonBufferEXT /// public struct XrPlaneDetectorPolygonBufferEXT { public XrStructureType type; public IntPtr next; public uint vertexCapacityInput; public uint vertexCountOutput; public IntPtr vertices; // XrVector2f[] } #endregion #region delegates and delegate instances delegate XrResult DelegateXrCreatePlaneDetectorEXT(XrSession session, ref XrPlaneDetectorCreateInfoEXT createInfo, ref IntPtr/*XrPlaneDetectorEXT*/ planeDetector); delegate XrResult DelegateXrDestroyPlaneDetectorEXT(IntPtr/*XrPlaneDetectorEXT*/ planeDetector); delegate XrResult DelegateXrBeginPlaneDetectionEXT(IntPtr/*XrPlaneDetectorEXT*/ planeDetector, ref XrPlaneDetectorBeginInfoEXT beginInfo); delegate XrResult DelegateXrGetPlaneDetectionStateEXT(IntPtr/*XrPlaneDetectorEXT*/planeDetector, ref XrPlaneDetectionStateEXT state); delegate XrResult DelegateXrGetPlaneDetectionsEXT(IntPtr/*XrPlaneDetectorEXT*/planeDetector, ref XrPlaneDetectorGetInfoEXT info, ref XrPlaneDetectorLocationsEXT locations); delegate XrResult DelegateXrGetPlanePolygonBufferEXT(IntPtr/*XrPlaneDetectorEXT*/planeDetector, ulong planeId, uint polygonBufferIndex, ref XrPlaneDetectorPolygonBufferEXT polygonBuffer); DelegateXrCreatePlaneDetectorEXT XrCreatePlaneDetectorEXT; DelegateXrDestroyPlaneDetectorEXT XrDestroyPlaneDetectorEXT; DelegateXrBeginPlaneDetectionEXT XrBeginPlaneDetectionEXT; DelegateXrGetPlaneDetectionStateEXT XrGetPlaneDetectionStateEXT; DelegateXrGetPlaneDetectionsEXT XrGetPlaneDetectionsEXT; DelegateXrGetPlanePolygonBufferEXT XrGetPlanePolygonBufferEXT; #endregion delegates and delegate instances #region override functions protected override IntPtr HookGetInstanceProcAddr(IntPtr func) { return ViveInterceptors.Instance.HookGetInstanceProcAddr(func); } /// protected override bool OnInstanceCreate(ulong xrInstance) { //Debug.Log("VIVEPD OnInstanceCreate() "); if (!OpenXRRuntime.IsExtensionEnabled(kOpenxrExtensionString)) { Debug.LogWarning("OnInstanceCreate() " + kOpenxrExtensionString + " is NOT enabled."); return false; } m_XrInstanceCreated = true; m_XrInstance = xrInstance; //Debug.Log("OnInstanceCreate() " + m_XrInstance); CommonWrapper.Instance.OnInstanceCreate(xrInstance, xrGetInstanceProcAddr); SpaceWrapper.Instance.OnInstanceCreate(xrInstance, CommonWrapper.Instance.GetInstanceProcAddr); return GetXrFunctionDelegates(m_XrInstance); } protected override void OnInstanceDestroy(ulong xrInstance) { CommonWrapper.Instance.OnInstanceDestroy(); SpaceWrapper.Instance.OnInstanceDestroy(); } /// protected override void OnSessionCreate(ulong xrSession) { Debug.Log("VIVEPD OnSessionCreate() "); // here's one way you can grab the session Debug.Log($"EXT: Got xrSession: {xrSession}"); session = xrSession; m_XrSessionCreated = true; } /// protected override void OnSessionBegin(ulong xrSession) { Debug.Log("VIVEPD OnSessionBegin() "); Debug.Log($"EXT: xrBeginSession: {xrSession}"); } /// protected override void OnSessionEnd(ulong xrSession) { Debug.Log("VIVEPD OnSessionEnd() "); Debug.Log($"EXT: about to xrEndSession: {xrSession}"); } // XXX Every millisecond the AppSpace switched from one space to another space. I don't know what is going on. //private ulong appSpace; //protected override void OnAppSpaceChange(ulong space) //{ // Debug.Log($"VIVEPD OnAppSpaceChange({appSpace} -> {space})"); // appSpace = space; //} protected override void OnSystemChange(ulong xrSystem) { m_XrSystemId = xrSystem; Debug.Log("OnSystemChange() " + m_XrSystemId); } #endregion override functions private bool GetXrFunctionDelegates(XrInstance xrInstance) { Debug.Log("VIVEPD GetXrFunctionDelegates() "); bool ret = true; IntPtr funcPtr = IntPtr.Zero; OpenXRHelper.xrGetInstanceProcAddrDelegate GetAddr = CommonWrapper.Instance.GetInstanceProcAddr; // shorter name ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrCreatePlaneDetectorEXT", out XrCreatePlaneDetectorEXT); ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrDestroyPlaneDetectorEXT", out XrDestroyPlaneDetectorEXT); ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrBeginPlaneDetectionEXT", out XrBeginPlaneDetectionEXT); ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrGetPlaneDetectionStateEXT", out XrGetPlaneDetectionStateEXT); ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrGetPlaneDetectionsEXT", out XrGetPlaneDetectionsEXT); ret &= OpenXRHelper.GetXrFunctionDelegate(GetAddr, xrInstance, "xrGetPlanePolygonBufferEXT", out XrGetPlanePolygonBufferEXT); return ret; } #region functions of extension /// /// Helper function to get this feature' properties. /// See xrGetSystemProperties /// public XrResult GetProperties(out XrSystemPlaneDetectionPropertiesEXT properties) { properties = new XrSystemPlaneDetectionPropertiesEXT(); properties.type = XrStructureType.XR_TYPE_SYSTEM_PLANE_DETECTION_PROPERTIES_EXT; #if FAKE_DATA if (Application.isEditor) { properties.supportedFeatures = CAPABILITY_PLANE_DETECTION_BIT_EXT; return XrResult.XR_SUCCESS; } #endif if (!m_XrSessionCreated) { Debug.LogError("GetProperties() XR_ERROR_SESSION_LOST."); return XrResult.XR_ERROR_SESSION_LOST; } if (!m_XrInstanceCreated) { Debug.LogError("GetProperties() XR_ERROR_INSTANCE_LOST."); return XrResult.XR_ERROR_INSTANCE_LOST; } return CommonWrapper.Instance.GetProperties(m_XrInstance, m_XrSystemId, ref properties); } /// /// Create a PlaneDetector with . XrSession is implied. The output handle need be destroyed. /// See xrCreatePlaneDetectorEXT /// /// Fill flags for detection engine. /// The output detector's handle. /// public XrResult CreatePlaneDetector(XrPlaneDetectorCreateInfoEXT createInfo, out IntPtr/*XrPlaneDetectorEXT*/ planeDetector) { planeDetector = IntPtr.Zero; #if FAKE_DATA if (Application.isEditor) return XrResult.XR_SUCCESS; #endif return XrCreatePlaneDetectorEXT(session, ref createInfo, ref planeDetector); } /// /// Destroy the PlaneDetector handle. /// See xrDestroyPlaneDetectorEXT /// /// The detector's handle to be destroyed. /// public XrResult DestroyPlaneDetector(IntPtr/*XrPlaneDetectorEXT*/ planeDetector) { #if FAKE_DATA if (Application.isEditor) return XrResult.XR_SUCCESS; #endif return XrDestroyPlaneDetectorEXT(planeDetector); } /// /// Let Detector start to work. /// See xrBeginPlaneDetectionEXT /// /// The detector's handle to be begined. /// Fill flags for detection engine. You can use the result of MakeGetAllXrPlaneDetectorBeginInfoEXT. /// public XrResult BeginPlaneDetection(IntPtr/*XrPlaneDetectorEXT*/ planeDetector, XrPlaneDetectorBeginInfoEXT beginInfo) { #if FAKE_DATA if (Application.isEditor) return XrResult.XR_SUCCESS; #endif return XrBeginPlaneDetectionEXT(planeDetector, ref beginInfo); } /// /// Check PlaneDetector's state. If the state is DONE_EXT, you can get the result by GetPlaneDetections. /// See xrGetPlaneDetectionStateEXT /// /// The detector's state to be check. /// Fill flags for detection engine. You can use the result of MakeGetAllXrPlaneDetectorBeginInfoEXT. public XrResult GetPlaneDetectionState(IntPtr/*XrPlaneDetectorEXT*/ planeDetector, ref XrPlaneDetectionStateEXT state) { #if FAKE_DATA if (Application.isEditor) { state = XrPlaneDetectionStateEXT.DONE_EXT; return XrResult.XR_SUCCESS; } #endif return XrGetPlaneDetectionStateEXT(planeDetector, ref state); } /// /// Get the result of PlaneDetector. /// See xrGetPlaneDetectionsEXT /// /// The detector's state to be check. /// Use info to specify the data's space. /// The output data. public XrResult GetPlaneDetections(IntPtr/*XrPlaneDetectorEXT*/ planeDetector, ref XrPlaneDetectorGetInfoEXT info, ref XrPlaneDetectorLocationsEXT locations) { #if FAKE_DATA if (Application.isEditor) { locations.planeLocationCountOutput = 1; if (locations.planeLocationCapacityInput == 0) return XrResult.XR_SUCCESS; if (locations.planeLocationCapacityInput < 1 || locations.planeLocations == IntPtr.Zero) return XrResult.XR_ERROR_SIZE_INSUFFICIENT; locations.planeLocationCountOutput = 1; XrPlaneDetectorLocationEXT location = new XrPlaneDetectorLocationEXT(); location.planeId = 1; location.extents = new XrExtent2Df(1, 1); location.locationFlags = XrSpaceLocationFlags.XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XrSpaceLocationFlags.XR_SPACE_LOCATION_POSITION_VALID_BIT; location.semanticType = XrPlaneDetectorSemanticTypeEXT.FLOOR_EXT; location.polygonBufferCount = 1; location.pose = new XrPosef(XrQuaternionf.Identity, new XrVector3f(0, 1, -1)); // This plane will face the Z axis location.orientation = XrPlaneDetectorOrientationEXT.VERTICAL_EXT; Marshal.StructureToPtr(location, locations.planeLocations, false); return XrResult.XR_SUCCESS; } #endif return XrGetPlaneDetectionsEXT(planeDetector, ref info, ref locations); } /// /// Get the vertex buffer of a plane. /// See GetPlanePolygonBuffer /// /// The detector's state to be check. /// The target plane's planeId. Get it from . /// The buffer index in the plane. Get it from . Currently VIVE will only return 1 buffer for each plane. /// The output data. public XrResult GetPlanePolygonBuffer(IntPtr/*XrPlaneDetectorEXT*/ planeDetector, ulong planeId, uint polygonBufferIndex, ref XrPlaneDetectorPolygonBufferEXT polygonBuffer) { #if FAKE_DATA if (Application.isEditor) { if (planeId != 1) return XrResult.XR_ERROR_NAME_INVALID; if (polygonBufferIndex != 0) return XrResult.XR_ERROR_INDEX_OUT_OF_RANGE; polygonBuffer.vertexCountOutput = 4; if (polygonBuffer.vertexCapacityInput == 0) return XrResult.XR_SUCCESS; if (polygonBuffer.vertexCapacityInput != 4 || polygonBuffer.vertices == IntPtr.Zero) return XrResult.XR_ERROR_SIZE_INSUFFICIENT; XrVector2f[] vertices = new XrVector2f[4]; // Make a plane's contour vertices[0] = new XrVector2f(-0.5f, -0.5f); vertices[1] = new XrVector2f( 0.5f, -0.5f); vertices[2] = new XrVector2f( 0.5f, 0.5f); vertices[3] = new XrVector2f(-0.5f, 0.5f); MemoryTools.CopyToRawMemory(polygonBuffer.vertices, vertices); return XrResult.XR_SUCCESS; } #endif return XrGetPlanePolygonBufferEXT(planeDetector, planeId, polygonBufferIndex, ref polygonBuffer); } #endregion #region tools for user /// /// A helper function to generate XrPlaneDetectorBeginInfoEXT. VIVE didn't implement all possible features of this extension. /// Hardcode some parameters here. /// /// public XrPlaneDetectorBeginInfoEXT MakeGetAllXrPlaneDetectorBeginInfoEXT() { XrPlaneDetectorBeginInfoEXT beginInfo = new XrPlaneDetectorBeginInfoEXT { type = XrStructureType.XR_TYPE_PLANE_DETECTOR_BEGIN_INFO_EXT, baseSpace = new XrSpace(GetCurrentAppSpace()), // Cannot depend on GetCurrentAppSpace... //baseSpace = GetTrackingSpace(), time = ViveInterceptors.Instance.GetPredictTime(), orientationCount = 0, // Any orientation orientations = IntPtr.Zero, // XrPlaneDetectorOrientationEXT[] semanticTypeCount = 0, // Any semantic type semanticTypes = IntPtr.Zero, // XrPlaneDetectorSemanticTypeEXT[] maxPlanes = 10000, // Hopefully enough minArea = 0.1f, // 10cm^2 boundingBoxPose = XrPosef.Identity, boundingBoxExtent = new XrExtent3DfEXT(1000, 1000, 1000), }; return beginInfo; } /// /// The time here is only used for info of GetPlaneDetections. Not the real predictTime of XrWaitFrame. /// /// public XrTime GetPredictTime() { return ViveInterceptors.Instance.GetPredictTime(); } /// /// According to XRInputSubsystem's tracking origin mode, return the corresponding XrSpace. /// /// public XrSpace GetTrackingSpace() { return GetCurrentAppSpace(); } #endregion } }