/* * 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. */ #if USING_XR_MANAGEMENT && (USING_XR_SDK_OCULUS || USING_XR_SDK_OPENXR) #define USING_XR_SDK #endif using System; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Rendering; using UnityEngine.Serialization; using UnityEngine.XR; [RequireComponent(typeof(RectTransform))] [ExecuteAlways] public class OVROverlayCanvas : OVRRayTransformer { public enum DrawMode { Opaque = 0, OpaqueWithClip = 1, Transparent = 2, #if UNITY_2020_1_OR_NEWER [Obsolete("Deprecated. Use Transparent", false)] #endif TransparentDefaultAlpha = 2, #if UNITY_2020_1_OR_NEWER [Obsolete("Deprecated. Use Transparent", false)] #endif TransparentCorrectAlpha = 3, AlphaToMask = 4, } public enum CanvasShape { Flat, Curved } // The optimal resolution for the display is approximately 2x the initial eye texture resolution private const float kOptimalResolutionScale = 2.0f; private int CanvasRenderLayer => OVROverlayCanvasSettings.Instance.CanvasRenderLayer; private Camera _camera; private OVROverlay _overlay; private MeshRenderer _meshRenderer; private OVROverlayMeshGenerator _meshGenerator; private RenderTexture _renderTexture; private Material _imposterMaterial; private bool _optimalResolutionInitialized; private float _optimalResolutionWidth; private float _optimalResolutionHeight; private int _lastPixelWidth; private int _lastPixelHeight; private Vector2 _imposterTextureOffset; private Vector2 _imposterTextureScale; private bool _frameIsReady; private bool _useTempRT; [SerializeField] internal bool _enableMipmapping = false; [SerializeField] internal bool _dynamicResolution = true; [SerializeField] internal int _redrawResolutionThreshold = int.MaxValue; private bool ShouldScaleViewport => _dynamicResolution; public RectTransform rectTransform; [FormerlySerializedAs("MaxTextureSize")] public int maxTextureSize = 2048; public bool manualRedraw = false; [FormerlySerializedAs("DrawRate")] public int renderInterval = 1; [FormerlySerializedAs("DrawFrameOffset")] public int renderIntervalFrameOffset = 0; [FormerlySerializedAs("Expensive")] public bool expensive = false; [FormerlySerializedAs("Layer")] public int layer = 5; [FormerlySerializedAs("Opacity")] public DrawMode opacity = DrawMode.Transparent; public CanvasShape shape = CanvasShape.Flat; public float curveRadius = 1.0f; public bool overlapMask = false; public OVROverlay.OverlayType overlayType = OVROverlay.OverlayType.Underlay; [SerializeField] internal bool _overlayEnabled = true; private static readonly Plane[] _FrustumPlanes = new Plane[6]; private static readonly Vector3[] _Corners = new Vector3[4]; private bool _nonUniformScaleWarningShown; private (int frameCount, float? score) _lastViewPriorityScore = (-1, null); public bool IsCanvasPriority => OVROverlayCanvasManager.Instance?.IsCanvasPriority(this) is true; public bool ShouldShowImposter => !IsCanvasPriority || !overlayEnabled || overlayType is OVROverlay.OverlayType.Underlay; public bool overlayEnabled { get { return _overlayEnabled; } set { if (_overlay && Application.isPlaying) { _overlay.enabled = value; // Update our impostor color to switch between visible and punch-a-hole _imposterMaterial.color = value ? Color.black : Color.white; } _overlayEnabled = value; } } // Start is called before the first frame update void Start() { if (rectTransform == null) { rectTransform = GetComponent(); } Debug.Assert( rectTransform.gameObject == gameObject, $"{nameof(rectTransform)} must be the same GameObject as the {nameof(OVROverlayCanvas)}"); HideFlags hideFlags = HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInHierarchy; GameObject overlayCamera = new GameObject(name + " Overlay Camera") { hideFlags = hideFlags }; overlayCamera.transform.SetParent(transform, false); _camera = overlayCamera.AddComponent(); _camera.stereoTargetEye = StereoTargetEyeMask.None; _camera.transform.position = transform.position - _camera.transform.forward; _camera.orthographic = true; _camera.enabled = false; _camera.clearFlags = CameraClearFlags.SolidColor; _camera.backgroundColor = Color.clear; _camera.nearClipPlane = 0.99f; _camera.farClipPlane = 1.01f; GameObject imposter = new GameObject(name + " Imposter") { hideFlags = hideFlags }; imposter.transform.SetParent(transform, false); imposter.AddComponent(); _meshRenderer = imposter.AddComponent(); _meshGenerator = imposter.AddComponent(); GameObject overlay = new GameObject(name + " Overlay") { hideFlags = hideFlags }; overlay.transform.SetParent(transform, false); _overlay = overlay.AddComponent(); _overlay.enabled = false; _overlay.isDynamic = true; UpdateOverlaySettings(); // On mobile we need to use a temporary copy texture for best performance // on versions without the viewport flag _useTempRT = Application.isMobilePlatform; InitializeRenderTexture(); } public void UpdateOverlaySettings() { InitializeRenderTexture(); _meshRenderer.enabled = ShouldShowImposter; _overlay.noDepthBufferTesting = ShouldShowImposter; _overlay.isAlphaPremultiplied = true; _overlay.currentOverlayType = overlayType; _overlay.enabled = overlayEnabled; } private void InitializeRenderTexture() { if (rectTransform == null) { rectTransform = GetComponent(); } float rectWidth = rectTransform.rect.width; float rectHeight = rectTransform.rect.height; float aspectX = rectWidth >= rectHeight ? 1 : rectWidth / rectHeight; float aspectY = rectHeight >= rectWidth ? 1 : rectHeight / rectWidth; // if we are scaling the viewport we don't need to add a border int pixelBorder = ShouldScaleViewport ? 0 : 8; int innerWidth = Mathf.CeilToInt(aspectX * (maxTextureSize - pixelBorder * 2)); int innerHeight = Mathf.CeilToInt(aspectY * (maxTextureSize - pixelBorder * 2)); int width = innerWidth + pixelBorder * 2; int height = innerHeight + pixelBorder * 2; float paddedWidth = rectWidth * (width / (float)innerWidth); float paddedHeight = rectHeight * (height / (float)innerHeight); if (_renderTexture == null || _renderTexture.width != width || _renderTexture.height != height) { if (_renderTexture != null) { DestroyImmediate(_renderTexture); } RenderTextureDescriptor descriptor = new RenderTextureDescriptor(width, height, GraphicsFormat.R8G8B8A8_SRGB, GraphicsFormat.D24_UNorm_S8_UInt); // if we can't scale the viewport, generate mipmaps instead descriptor.autoGenerateMips = descriptor.useMipMap = _enableMipmapping; _renderTexture = new RenderTexture(descriptor); _renderTexture.filterMode = FilterMode.Trilinear; _renderTexture.name = name; } _camera.orthographicSize = 0.5f * paddedHeight * GetRectTransformScale().y; _camera.targetTexture = _renderTexture; _camera.cullingMask = 1 << CanvasRenderLayer; Shader shader = OVROverlayCanvasSettings.Instance.GetShader(opacity); if (_imposterMaterial == null) { _imposterMaterial = new Material(shader); } else { _imposterMaterial.shader = shader; } if (opacity == DrawMode.OpaqueWithClip) { _imposterMaterial.EnableKeyword("WITH_CLIP"); } else { _imposterMaterial.DisableKeyword("WITH_CLIP"); } #if !UNITY_2020_1_OR_NEWER if (opacity == DrawMode.TransparentDefaultAlpha) { _imposterMaterial.EnableKeyword("ALPHA_SQUARED"); } else { _imposterMaterial.DisableKeyword("ALPHA_SQUARED"); } #endif if (expensive) { _imposterMaterial.EnableKeyword("EXPENSIVE"); } else { _imposterMaterial.DisableKeyword("EXPENSIVE"); } if (opacity == DrawMode.AlphaToMask) { _imposterMaterial.EnableKeyword("ALPHA_TO_MASK"); _imposterMaterial.SetInt("_AlphaToMask", 1); } else { _imposterMaterial.DisableKeyword("ALPHA_TO_MASK"); _imposterMaterial.SetInt("_AlphaToMask", 0); } if (overlayEnabled && overlapMask) { _imposterMaterial.EnableKeyword("OVERLAP_MASK"); } else { _imposterMaterial.DisableKeyword("OVERLAP_MASK"); } _imposterMaterial.mainTexture = _renderTexture; _imposterMaterial.color = CalcImposterColor(); _imposterMaterial.mainTextureOffset = _imposterTextureOffset; _imposterMaterial.mainTextureScale = _imposterTextureScale; _meshRenderer.sharedMaterial = _imposterMaterial; _meshRenderer.gameObject.layer = layer; if (shape == CanvasShape.Flat) { _meshRenderer.transform.localPosition = _overlay.transform.localPosition = Vector3.zero; _meshRenderer.transform.localScale = new Vector3(rectWidth, rectHeight, 1); _overlay.transform.localScale = new Vector3(paddedWidth, paddedHeight, 1); } else { _meshRenderer.transform.localPosition = _overlay.transform.localPosition = new Vector3(0, 0, -curveRadius / transform.lossyScale.z); _meshRenderer.transform.localScale = new Vector3(rectWidth, rectHeight, curveRadius / transform.lossyScale.z); _overlay.transform.localScale = new Vector3(paddedWidth, paddedHeight, curveRadius / transform.lossyScale.z); } _overlay.textures[0] = _renderTexture; _overlay.currentOverlayShape = shape == CanvasShape.Flat ? OVROverlay.OverlayShape.Quad : OVROverlay.OverlayShape.Cylinder; _overlay.hidden = !IsCanvasPriority; _overlay.useExpensiveSuperSample = expensive; _overlay.enabled = Application.isPlaying && _overlayEnabled; // always turn on autofiltering _overlay.useAutomaticFiltering = true; _meshGenerator.SetOverlay(_overlay); OVROverlayCanvasSettings.Instance.ApplyGlobalSettings(); } private Color CalcImposterColor() { return overlayEnabled && IsCanvasPriority && overlayType is OVROverlay.OverlayType.Underlay ? Color.black : Color.white; } private void OnDestroy() { if (Application.isPlaying) { Destroy(_imposterMaterial); Destroy(_renderTexture); } else { DestroyImmediate(_imposterMaterial); DestroyImmediate(_renderTexture); } } private void OnEnable() { OVROverlayCanvasManager.AddCanvas(this); if (_overlay) { _meshRenderer.enabled = ShouldShowImposter; _overlay.enabled = Application.isPlaying && _overlayEnabled; } } private void OnDisable() { OVROverlayCanvasManager.RemoveCanvas(this); if (_overlay) { _overlay.enabled = false; _meshRenderer.enabled = false; } } protected virtual bool ShouldRender() { if (manualRedraw && _frameIsReady) { // Check if the resolution has changed enough to trigger a redraw if (_dynamicResolution && _redrawResolutionThreshold != int.MaxValue && CalculateScaledResolution() is var (width, height)) { return width - _lastPixelWidth >= _redrawResolutionThreshold || height - _lastPixelHeight >= _redrawResolutionThreshold; } else { return false; } } if (renderInterval > 1) { if (Time.frameCount % renderInterval != renderIntervalFrameOffset % renderInterval && _frameIsReady) { return false; } } // Always render in the editor if (Application.isEditor) { return true; } return IsInFrustum(); } private bool IsInFrustum() { var mainCamera = OVRManager.FindMainCamera(); if (mainCamera != null) { // Perform Frustum culling #if USING_XR_SDK XRDisplaySubsystem currentDisplaySubsystem = OVRManager.GetCurrentDisplaySubsystem(); if (currentDisplaySubsystem != null && currentDisplaySubsystem.GetRenderPassCount() > 0) { for (int i = 0; i < currentDisplaySubsystem.GetRenderPassCount(); i++) { currentDisplaySubsystem.GetRenderPass(i, out var renderPass); currentDisplaySubsystem.GetCullingParameters(mainCamera, renderPass.cullingPassIndex, out var cullingParameters); var mat = cullingParameters.stereoProjectionMatrix * cullingParameters.stereoViewMatrix; GeometryUtility.CalculateFrustumPlanes(mat, _FrustumPlanes); if (GeometryUtility.TestPlanesAABB(_FrustumPlanes, _meshRenderer.bounds)) { return true; } } } else #endif if (mainCamera.stereoEnabled) { for (int i = 0; i < 2; i++) { var eye = (Camera.StereoscopicEye)i; var mat = mainCamera.GetStereoProjectionMatrix(eye) * mainCamera.GetStereoViewMatrix(eye); GeometryUtility.CalculateFrustumPlanes(mat, _FrustumPlanes); if (GeometryUtility.TestPlanesAABB(_FrustumPlanes, _meshRenderer.bounds)) { return true; } } } else { var mat = mainCamera.projectionMatrix * mainCamera.worldToCameraMatrix; GeometryUtility.CalculateFrustumPlanes(mat, _FrustumPlanes); if (GeometryUtility.TestPlanesAABB(_FrustumPlanes, _meshRenderer.bounds)) { return true; } } return false; } return true; } private void Update() { UpdateOverlaySettings(); var shouldRender = ShouldRender(); _overlay.isDynamic = shouldRender; if (!shouldRender) return; ApplyViewportScale(); _frameIsReady = true; RenderCamera(); } private void LateUpdate() { // Update our impostor color to switch between visible and punch-a-hole _imposterMaterial.color = CalcImposterColor(); // Update the scale and offset each frame to avoid a bug where Unity likes to reset them for some reason _imposterMaterial.mainTextureScale = _imposterTextureScale; _imposterMaterial.mainTextureOffset = _imposterTextureOffset; } public float? GetViewPriorityScore() { var frameCount = Time.renderedFrameCount; if (_lastViewPriorityScore.frameCount != frameCount) { _lastViewPriorityScore = (frameCount, score: GetViewPriorityScoreImpl()); } return _lastViewPriorityScore.score; } private float? GetViewPriorityScoreImpl() { var mainCamera = OVRManager.FindMainCamera(); if (mainCamera == null) return null; if (!_overlayEnabled) return null; rectTransform.GetWorldCorners(_Corners); for (var i = 0; i != 4; ++i) { var anchor = mainCamera.WorldToViewportPoint(_Corners[i]); anchor.x = Mathf.Clamp01(anchor.x) - 0.5f; anchor.y = Mathf.Clamp01(anchor.y) - 0.5f; // if it's behind the camera, use NaN to flag it // otherwise, ignore Z anchor.z = anchor.z < 0 ? float.NaN : 0; _Corners[i] = anchor; } var area = TriangleArea(_Corners[0], _Corners[1], _Corners[2]) + TriangleArea(_Corners[1], _Corners[2], _Corners[3]); var midpoint = (_Corners[0] + _Corners[1] + _Corners[2] + _Corners[3]) * 0.25f; var score = area / Mathf.Max(midpoint.magnitude, 0.01f); // divide by distance from center to prioritize center return float.IsNaN(area) ? null : score; } private static float TriangleArea(Vector3 a, Vector3 b, Vector3 c) => Vector3.Cross(b - a, c - a).magnitude * 0.5f; private void OnValidate() { #if UNITY_EDITOR if (Meta.XR.Editor.Callbacks.InitializeOnLoad.EditorReady) { UnityEngine.Assertions.Assert.IsNotNull(OVROverlayCanvasSettings.Instance); } #endif } private Vector3 GetRectTransformScale() { // Allow the rect transform to scale non-uniformly (often z scale may be different than x and y) Vector3 localScale = rectTransform.localScale; Vector3 parentScale = rectTransform.parent != null ? rectTransform.parent.lossyScale : Vector3.one; // Check that the parent scale is uniform (otherwise our lossy scale might produce unexpected results) if (!Mathf.Approximately(parentScale.x, parentScale.y) || !Mathf.Approximately(parentScale.y, parentScale.z)) { if (!_nonUniformScaleWarningShown) { Debug.LogWarning($"[OVROverlayCanvas][{name}] Non Uniform Parent Scale. This will result in unexpected behavior!", this); _nonUniformScaleWarningShown = true; } } return new Vector3(parentScale.x * localScale.x, parentScale.y * localScale.y, parentScale.z * localScale.z); } private Matrix4x4 GetWorldToViewportMatrix(Camera mainCamera) { #if USING_XR_SDK XRDisplaySubsystem currentDisplaySubsystem = OVRManager.GetCurrentDisplaySubsystem(); if (currentDisplaySubsystem != null && currentDisplaySubsystem.GetRenderPassCount() > 0) { currentDisplaySubsystem.GetRenderPass(0, out var renderPass); renderPass.GetRenderParameter(mainCamera, 0, out var renderParameter); return renderParameter.projection * mainCamera.worldToCameraMatrix; } else #endif { return mainCamera.projectionMatrix * mainCamera.worldToCameraMatrix; } } private (int pixelWidth, int pixelHeight)? CalculateScaledResolution() { #if UNITY_EDITOR if (!ShouldScaleViewport || !Application.isPlaying) #else if (!ShouldScaleViewport) #endif { return (_renderTexture.width, _renderTexture.height); } if (!IsInFrustum()) { return (32, 32); } var mainCamera = OVRManager.FindMainCamera(); if (mainCamera == null) return null; if (!_optimalResolutionInitialized && UnityEngine.XR.XRSettings.isDeviceActive) { // Calculate Optimal resolution relative to the default resolution _optimalResolutionWidth = UnityEngine.XR.XRSettings.eyeTextureWidth * kOptimalResolutionScale / UnityEngine.XR.XRSettings.eyeTextureResolutionScale; _optimalResolutionHeight = UnityEngine.XR.XRSettings.eyeTextureHeight * kOptimalResolutionScale / UnityEngine.XR.XRSettings.eyeTextureResolutionScale; // Don't consider the resolution initialized until the resolution is greater than zero _optimalResolutionInitialized = _optimalResolutionWidth > 0 && _optimalResolutionHeight > 0; } rectTransform.GetLocalCorners(_Corners); var localToWorldMatrix = rectTransform.localToWorldMatrix; if (shape == CanvasShape.Curved) { // for curve, the world corners aren't a great way to determine texture scale. // To get more accurate results, apply billboard rotation to the rect based on the curve // so that our corners better approximate the resolution needed. localToWorldMatrix *= CalculateCurveViewBillboardMatrix(mainCamera); } var worldToViewport = GetWorldToViewportMatrix(mainCamera); var viewportToTexture = Matrix4x4.Scale(new Vector3(0.5f * _optimalResolutionWidth, 0.5f * _optimalResolutionHeight, 0.0f)); var rectToTexture = viewportToTexture * worldToViewport * localToWorldMatrix; // Calculate Clip Pos for our quad for (int i = 0; i < 4; i++) { _Corners[i] = rectToTexture.MultiplyPoint(_Corners[i]); } // Because our quad might be rotated, we find the raw max pixel length of each quad side int height = Mathf.RoundToInt(Mathf.Max((_Corners[1] - _Corners[0]).magnitude, (_Corners[3] - _Corners[2]).magnitude)); int width = Mathf.RoundToInt(Mathf.Max((_Corners[2] - _Corners[1]).magnitude, (_Corners[3] - _Corners[0]).magnitude)); // round to the nearest even pixel size, with 2 pixels of padding on all sides int pixelHeight = ((height + 1) / 2) * 2 * (expensive ? 2 : 1) + 4; int pixelWidth = ((width + 1) / 2) * 2 * (expensive ? 2 : 1) + 4; // clamp our viewport to the texture size pixelHeight = Mathf.Clamp(pixelHeight, 32, _renderTexture.height); pixelWidth = Mathf.Clamp(pixelWidth, 32, _renderTexture.width); return (pixelWidth, pixelHeight); } private void ApplyViewportScale() { if (CalculateScaledResolution() is not var (pixelWidth, pixelHeight)) return; // Don't change texture sizes unless our image would change more than four pixels to avoid judder if (Math.Abs(pixelHeight - _lastPixelHeight) < 4 && Math.Abs(pixelWidth - _lastPixelWidth) < 4) { pixelWidth = _lastPixelWidth; pixelHeight = _lastPixelHeight; } else { _lastPixelHeight = pixelHeight; _lastPixelWidth = pixelWidth; } // subtract the two pixels from all sides int innerPixelHeight = pixelHeight - 4; int innerPixelWidth = pixelWidth - 4; var rectTransformScale = GetRectTransformScale(); float orthoHeight = rectTransform.rect.height * rectTransformScale.y * pixelHeight / (float)innerPixelHeight; float orthoWidth = rectTransform.rect.width * rectTransformScale.x * pixelWidth / (float)innerPixelWidth; _camera.orthographicSize = (0.5f * orthoHeight); _camera.aspect = (orthoWidth / orthoHeight); float sizeX = pixelWidth / (float)_renderTexture.width; float sizeY = pixelHeight / (float)_renderTexture.height; float innerSizeX = innerPixelWidth / (float)_renderTexture.width; float innerSizeY = innerPixelHeight / (float)_renderTexture.height; // scale the camera rect _camera.rect = new Rect((1 - sizeX) / 2, ((1 - sizeY) / 2), sizeX, sizeY); Rect src = new Rect(0.5f - (0.5f * innerSizeX), (0.5f - (0.5f * innerSizeY)), innerSizeX, innerSizeY); Rect dst = new Rect(0, 0, 1, 1); // update the overlay to use this same size _overlay.overrideTextureRectMatrix = true; _overlay.SetSrcDestRects(src, src, dst, dst); // Update our material offset and scale var pixelBorder = ShouldScaleViewport ? 0 : 8; var res = new Vector2(pixelWidth, pixelHeight); var pixelSize = new Vector2(1.0f / pixelWidth, 1.0f / pixelHeight); _imposterTextureOffset = (src.min * res + Vector2.one * pixelBorder) * pixelSize; _imposterTextureScale = (src.size * res - Vector2.one * pixelBorder * 2) * pixelSize; } public struct ScopedCallback : IDisposable { public event Action OnDispose; void IDisposable.Dispose() => OnDispose?.Invoke(); } private void RenderCamera() { _camera.transform.position = transform.position - _camera.transform.forward; var rect = _camera.rect; int pixWidth = (int)(rect.width * _renderTexture.width); int pixHeight = (int)(rect.height * _renderTexture.height); using var scopedCallback = new ScopedCallback(); if (GraphicsSettings.defaultRenderPipeline == null) { // switch all targeted renderers to another layer, so we don't render things outside of this object _camera.cullingMask = 1 << CanvasRenderLayer; var targetLayer = gameObject.layer; var transforms = GetComponentsInChildren(); foreach (var tf in transforms) if (tf.gameObject.layer == targetLayer) tf.gameObject.layer = CanvasRenderLayer; scopedCallback.OnDispose += () => { // revert to the original layers foreach (var tf in transforms) if (tf.gameObject.layer == CanvasRenderLayer) tf.gameObject.layer = targetLayer; }; } else { _camera.cullingMask = 1 << gameObject.layer; } if (_useTempRT && (pixWidth < _renderTexture.width || pixHeight < _renderTexture.height)) { RenderTextureDescriptor descriptor = new RenderTextureDescriptor(pixWidth, pixHeight, GraphicsFormat.R8G8B8A8_SRGB, GraphicsFormat.D24_UNorm_S8_UInt, 0); var tempRT = RenderTexture.GetTemporary(descriptor); tempRT.Create(); // override render texture with the temporary one _camera.targetTexture = tempRT; _camera.rect = new Rect(0, 0, 1, 1); _camera.Render(); // Copy to our original render texture, then release the temporary texture Graphics.CopyTexture(tempRT, 0, 0, 0, 0, pixWidth, pixHeight, _renderTexture, 0, 0, (int)(rect.x * _renderTexture.width), (int)(rect.y * _renderTexture.height)); RenderTexture.ReleaseTemporary(tempRT); // restore original target rect and texture _camera.rect = rect; _camera.targetTexture = _renderTexture; } else { _camera.Render(); } } // Calculate a billboard rotation of our rect based on the curve parameters and the current camera position private Matrix4x4 CalculateCurveViewBillboardMatrix(Camera mainCamera) { var relativeViewPos = Quaternion.Inverse(rectTransform.rotation) * (mainCamera.transform.position - rectTransform.position); // calculate the billboard angle based on the x and z positions float angle = Mathf.Atan2(-relativeViewPos.x, -relativeViewPos.z); Vector3 lossyScale = GetRectTransformScale(); float fullAngleWidth = rectTransform.rect.width * lossyScale.x / curveRadius; // clamp angle to the maximum curvature angle = Mathf.Clamp(angle, -0.5f * fullAngleWidth, 0.5f * fullAngleWidth); // calculate the tangent point to keep our billboard touching the curve surface var tangentPoint = new Vector3(angle * curveRadius, 0, 0); // Set the pivot point to the curve center var pivotPoint = new Vector3(0, 0, curveRadius); // calculate our offset rotation matrix, which consists of first applying x and y scale, // offsetting to the pivot location minus the tangent point, rotating, reversing the pivot offset, then // reversing the scale multiplication. Note that z scale is removed entirely. return Matrix4x4.Scale(new Vector3(1.0f / lossyScale.x, 1.0f / lossyScale.y, 1.0f / lossyScale.z)) * Matrix4x4.Translate(-pivotPoint) * Matrix4x4.Rotate(Quaternion.AngleAxis(Mathf.Rad2Deg * angle, Vector3.up)) * Matrix4x4.Translate(pivotPoint - tangentPoint) * Matrix4x4.Scale(new Vector3(lossyScale.x, lossyScale.y, 1.0f)); } public override Ray TransformRay(Ray ray) { if (shape != CanvasShape.Curved) { return ray; } // Transform from 3D space to Curved UI Space var localPoint = transform.InverseTransformPoint(ray.origin); var localDirection = transform.InverseTransformDirection(ray.direction); var localRadius = curveRadius / transform.lossyScale.z; var localCenter = new Vector3(0, 0, -localRadius); // find xz intersect with circle if (!LineCircleIntersection(new Vector2(localPoint.x, localPoint.z), new Vector2(localDirection.x, localDirection.z), new Vector2(localCenter.x, localCenter.z), localRadius, out float distance)) { // the ray misses, return a ray parallel to the canvas return new Ray(ray.origin, transform.right); } Vector3 localIntersection = localPoint + localDirection * distance; // convert circle coordinates to plane coordinates by getting angle float angle = Mathf.Atan2(localIntersection.x, localIntersection.z + localRadius); float xPos = angle * localRadius; float yPos = localIntersection.y; // return a ray going directly into our calculated intersection point return new Ray(transform.TransformPoint(new Vector3(xPos, yPos, -1)), transform.forward); } private static bool LineCircleIntersection(Vector2 p1, Vector2 dp, Vector2 center, float radius, out float distance) { // Find intersection with circle using quadratic equation var a = dp.sqrMagnitude; var b = 2 * Vector2.Dot(dp, p1 - center); var c = center.sqrMagnitude; c += p1.sqrMagnitude; c -= 2 * Vector2.Dot(center, p1); c -= radius * radius; var bb4ac = b * b - 4 * a * c; if (Mathf.Abs(a) < float.Epsilon || bb4ac < 0) { // line does not intersect distance = default; return false; } var mu1 = (-b - Mathf.Sqrt(bb4ac)) / (2 * a); var mu2 = (-b + Mathf.Sqrt(bb4ac)) / (2 * a); distance = mu1 >= 0 ? mu1 : mu2; return true; } public OVROverlay Overlay => _overlay; public void SetFrameDirty() => _frameIsReady = false; public void SetCanvasLayer(int layer, bool forceUpdate) { SetLayerRecursive(gameObject, layer, gameObject.layer, forceUpdate); } private static void SetLayerRecursive(GameObject gameObject, int layer, int previousLayer, bool forceUpdate) { if (gameObject.layer == previousLayer || forceUpdate) { gameObject.layer = layer; } for (int i = 0; i < gameObject.transform.childCount; i++) { var c = gameObject.transform.GetChild(i).gameObject; if ((c.hideFlags &= HideFlags.DontSave) != 0) { continue; } SetLayerRecursive(c, layer, previousLayer, forceUpdate); } } }