/*
* 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);
}
}
}
}