/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using Meta.XR.Util; using Unity.Collections; using Unity.Jobs; using UnityEngine; /// /// A that has a 2D bounds associated with it. /// /// /// Examples of scene anchors that would be associated with this component include Wall, Floor, and Ceiling. /// /// and associated classes are deprecated (v65), please use [MR Utility Kit](https://developer.oculus.com/documentation/unity/unity-mr-utility-kit-overview)" instead. /// [DisallowMultipleComponent] [RequireComponent(typeof(OVRSceneAnchor))] [HelpURL("https://developer.oculus.com/documentation/unity/unity-scene-use-scene-anchors/#further-scene-model-unity-components")] [Obsolete(OVRSceneManager.DeprecationMessage)] [Feature(Feature.Scene)] public class OVRScenePlane : MonoBehaviour, IOVRSceneComponent { /// /// The plane's width (in the local X-direction), in meters. /// public float Width { get; private set; } /// /// The plane's height (in the local Y-direction), in meters. /// public float Height { get; private set; } /// /// The offset of the plane with respect to the anchor's pivot as a Vector2. /// /// /// The offset is mostly zero, as objects have the anchor's pivot /// aligned with centroid of the plane. /// /// The Offset is provided in the local coordinate space of the /// children. See to see the /// transformation of Unity and OpenXR coordinate systems. public Vector2 Offset { get; private set; } /// /// The dimensions of the plane as a Vector2. /// /// /// This property corresponds to a Vector whose components are /// (, ). /// public Vector2 Dimensions => new Vector2(Width, Height); /// /// The vertices of the 2D plane boundary as list of Vector2. /// /// /// The vertices are provided in clockwise order and in plane-space (relative to the /// plane's local space). The X and Y coordinates of the 2D coordinates are the same as the 3D /// coordinates. To map the 2D vertices (x, y) to 3D, set the Z coordinate to zero: (x, y, 0). /// public IReadOnlyList Boundary => _boundary; /// /// Whether the child transforms will be scaled according to the dimensions of this plane. /// /// If set to True, all the child transforms will be scaled to the dimensions of this plane immediately. /// And, if it's set to False, dimensions of this plane will no longer affect the child transforms, and child /// transforms will retain their current scale. This can be controlled further by using a /// . Note: if the current game object also contains /// a , then the volume's scale will take precedence. public bool ScaleChildren { get => _scaleChildren; set { _scaleChildren = value; if (_scaleChildren && _sceneAnchor.Space.Valid) { SetChildScale(); } } } /// /// Whether the child transforms will be offset according to the offset of this plane. /// /// If set to True, all the child transforms will be offset to the offset of this plane immediately. /// And, if it's set to False, offsets of this plane will no longer affect the child transforms, and child /// transforms will retain their current offset. This can be controlled further by using a /// . Note: if the current game object also contains /// a , then the volume's offset will take precedence. public bool OffsetChildren { get => _offsetChildren; set { _offsetChildren = value; if (_offsetChildren && _sceneAnchor.Space.Valid) { SetChildOffset(); } } } [Tooltip("When enabled, scales the child transforms according to the dimensions of this plane. " + "If both Volume and Plane components exist on the game object, the volume takes precedence.")] [SerializeField] internal bool _scaleChildren = true; [Tooltip("When enabled, offsets the child transforms according to the offset of this plane. " + "If both Volume and Plane components exist on the game object, the volume takes precedence.")] [SerializeField] internal bool _offsetChildren = true; internal JobHandle? _jobHandle; private NativeArray _previousBoundary; private NativeArray _boundaryLength; private NativeArray _boundaryBuffer; private bool _boundaryRequested; private OVRSceneAnchor _sceneAnchor; private readonly List _boundary = new List(); private void SetChildScale() { var hasVolume = TryGetComponent(out var volume); // we scale the child if we have the TransformType.Plane // or if the sibling volume component will not scale for (var i = 0; i < transform.childCount; i++) { var child = transform.GetChild(i); if (child.TryGetComponent(out var transformType)) { if (transformType.TransformType != OVRSceneObjectTransformType.Transformation.Plane) continue; } else { // if there's no TransformType, then we only don't apply transform // if the volume will take care of it instead if (hasVolume && volume.ScaleChildren) continue; } child.localScale = new Vector3(Width, Height, child.localScale.z); } } private void SetChildOffset() { var hasVolume = TryGetComponent(out var volume); // we offset the child if we have the TransformType.Plane // or if the sibling volume component will not offset for (var i = 0; i < transform.childCount; i++) { var child = transform.GetChild(i); if (child.TryGetComponent(out var transformType)) { if (transformType.TransformType != OVRSceneObjectTransformType.Transformation.Plane) continue; } else { // if there's no TransformType, then we only don't apply transform // if the volume will take care of it instead if (hasVolume && volume.OffsetChildren) continue; } child.localPosition = new Vector3(Offset.x, Offset.y, 0); } } internal void UpdateTransform() { if (OVRPlugin.GetSpaceBoundingBox2D(GetComponent().Space, out var rect)) { Width = rect.Size.w; Height = rect.Size.h; Vector2 planePivot = transform.TransformPoint( rect.Pos.FromVector2f() + (rect.Size.FromSizef() / 2)); var anchorPivot = new Vector2(transform.position.x, transform.position.y); Offset = planePivot - anchorPivot; OVRSceneManager.Development.Log(nameof(OVRScenePlane), $"[{_sceneAnchor.Uuid}] Plane has dimensions {Dimensions} " + $"and offset {Offset}.", gameObject); if (ScaleChildren) SetChildScale(); if (OffsetChildren) SetChildOffset(); } else { OVRSceneManager.Development.LogError(nameof(OVRScenePlane), $"[{GetComponent().Uuid}] Failed to retrieve plane's information.", gameObject); } } private void Awake() { _sceneAnchor = GetComponent(); if (_sceneAnchor.Space.Valid) { ((IOVRSceneComponent)this).Initialize(); } } private void Start() { RequestBoundary(); } void IOVRSceneComponent.Initialize() { UpdateTransform(); } internal void ScheduleGetLengthJob() { // Don't schedule if already running if (_jobHandle != null) return; if (!OVRPlugin.GetSpaceComponentStatus(_sceneAnchor.Space, OVRPlugin.SpaceComponentType.Bounded2D, out var isEnabled, out var isChangePending)) { return; } if (!isEnabled || isChangePending) return; // Scratch buffer to store single value on the heap _boundaryLength = new NativeArray(1, Allocator.TempJob); // Two-call idiom: first call gets the length _jobHandle = new GetBoundaryLengthJob { Length = _boundaryLength, Space = _sceneAnchor.Space }.Schedule(); _boundaryRequested = false; } internal void RequestBoundary() { _boundaryRequested = true; if (enabled) { // If enabled, we can go ahead and start right away ScheduleGetLengthJob(); } } private void Update() { if (_jobHandle?.IsCompleted == true) { _jobHandle.Value.Complete(); _jobHandle = null; } else { return; } if (_boundaryLength.IsCreated) { var length = _boundaryLength[0]; _boundaryLength.Dispose(); if (length < 3) { // This means that we failed to get the boundary length, so try again ScheduleGetLengthJob(); return; } using (new OVRProfilerScope("Schedule " + nameof(GetBoundaryJob))) { _boundaryBuffer = new NativeArray(length, Allocator.TempJob); if (!_previousBoundary.IsCreated) { _previousBoundary = new NativeArray(length, Allocator.Persistent); } _jobHandle = new GetBoundaryJob { Space = _sceneAnchor.Space, Boundary = _boundaryBuffer, PreviousBoundary = _previousBoundary, }.Schedule(); } } else if (_boundaryBuffer.IsCreated) { using (new OVRProfilerScope("Copy boundary")) { if (_previousBoundary.Length == 0 || float.IsNaN(_previousBoundary[0].x)) { if (_previousBoundary.IsCreated) { _previousBoundary.Dispose(); } _previousBoundary = new NativeArray(_boundaryBuffer.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); _previousBoundary.CopyFrom(_boundaryBuffer); // Finally, copy it to the publicly accessible list. _boundary.Clear(); foreach (var vertex in _previousBoundary) { _boundary.Add(new Vector2(-vertex.x, vertex.y)); } } } _boundaryBuffer.Dispose(); if (TryGetComponent(out var planeMeshFilter)) { // Notify mesh filter that there's a new boundary planeMeshFilter.RequestMeshGeneration(); } } else if (_boundaryRequested) { ScheduleGetLengthJob(); } } private void OnDisable() { // Job completed but we may not yet have consumed the data if (_boundaryLength.IsCreated) _boundaryLength.Dispose(_jobHandle ?? default); if (_boundaryBuffer.IsCreated) _boundaryBuffer.Dispose(_jobHandle ?? default); if (_previousBoundary.IsCreated) _previousBoundary.Dispose(_jobHandle ?? default); _previousBoundary = default; _boundaryBuffer = default; _boundaryLength = default; _jobHandle = null; } private struct GetBoundaryLengthJob : IJob { /// This is an internal member. public OVRSpace Space; /// This is an internal member. [WriteOnly] public NativeArray Length; /// This is an internal member. public void Execute() => Length[0] = OVRPlugin.GetSpaceBoundary2DCount(Space, out var count) ? count : 0; } private struct GetBoundaryJob : IJob { /// This is an internal member. public OVRSpace Space; /// This is an internal member. public NativeArray Boundary; /// This is an internal member. public NativeArray PreviousBoundary; private bool HasBoundaryChanged() { if (!PreviousBoundary.IsCreated) return true; if (Boundary.Length != PreviousBoundary.Length) return true; var length = Boundary.Length; for (var i = 0; i < length; i++) { if (Vector2.SqrMagnitude(Boundary[i] - PreviousBoundary[i]) > 1e-6f) return true; } return false; } private static void SetNaN(NativeArray array) { // Set a NaN to indicate failure if (array.Length > 0) { array[0] = new Vector2(float.NaN, float.NaN); } } /// This is an internal member. public void Execute() { if (OVRPlugin.GetSpaceBoundary2D(Space, Boundary) && HasBoundaryChanged()) { // Invalid old boundary SetNaN(PreviousBoundary); } else { // Invalid boundary SetNaN(Boundary); } } } }