/* * 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 UnityEngine; using System; using System.Collections; using System.Runtime.InteropServices; using System.Collections.Generic; /// /// Add OVROverlay script to an object with an optional mesh primitive /// rendered as a TimeWarp overlay instead by drawing it into the eye buffer. /// This will take full advantage of the display resolution and avoid double /// resampling of the texture. /// /// We support 3 types of Overlay shapes right now /// 1. Quad : This is most common overlay type , you render a quad in Timewarp space. /// 2. Cylinder: [Mobile Only][Experimental], Display overlay as partial surface of a cylinder /// * The cylinder's center will be your game object's center /// * We encoded the cylinder's parameters in transform.scale, /// **[scale.z] is the radius of the cylinder /// **[scale.y] is the height of the cylinder /// **[scale.x] is the length of the arc of cylinder /// * Limitations /// **Only the half of the cylinder can be displayed, which means the arc angle has to be smaller than 180 degree, [scale.x] / [scale.z] <= PI /// **Your camera has to be inside of the inscribed sphere of the cylinder, the overlay will be faded out automatically when the camera is close to the inscribed sphere's surface. /// **Translation only works correctly with vrDriver 1.04 or above /// 3. Cubemap: Display overlay as a cube map /// 4. OffcenterCubemap: [Mobile Only] Display overlay as a cube map with a texture coordinate offset /// * The actually sampling will looks like [color = texture(cubeLayerSampler, normalize(direction) + offset)] instead of [color = texture( cubeLayerSampler, direction )] /// * The extra center offset can be feed from transform.position /// * Note: if transform.position's magnitude is greater than 1, which will cause some cube map pixel always invisible /// Which is usually not what people wanted, we don't kill the ability for developer to do so here, but will warn out. /// 5. Equirect: Display overlay as a 360-degree equirectangular skybox. /// [ExecuteInEditMode] [HelpURL("https://developer.oculus.com/reference/unity/latest/class_o_v_r_overlay")] public class OVROverlay : MonoBehaviour { #region Interface /// /// Determines the on-screen appearance of a layer. /// public enum OverlayShape { Quad = OVRPlugin.OverlayShape.Quad, Cylinder = OVRPlugin.OverlayShape.Cylinder, Cubemap = OVRPlugin.OverlayShape.Cubemap, OffcenterCubemap = OVRPlugin.OverlayShape.OffcenterCubemap, Equirect = OVRPlugin.OverlayShape.Equirect, ReconstructionPassthrough = OVRPlugin.OverlayShape.ReconstructionPassthrough, SurfaceProjectedPassthrough = OVRPlugin.OverlayShape.SurfaceProjectedPassthrough, Fisheye = OVRPlugin.OverlayShape.Fisheye, KeyboardHandsPassthrough = OVRPlugin.OverlayShape.KeyboardHandsPassthrough, KeyboardMaskedHandsPassthrough = OVRPlugin.OverlayShape.KeyboardMaskedHandsPassthrough, } /// /// Whether the layer appears behind or infront of other content in the scene. /// public enum OverlayType { None, Underlay, Overlay, }; /// /// Specify overlay's type /// [Tooltip("Specify overlay's type")] public OverlayType currentOverlayType = OverlayType.Overlay; /// /// If true, the texture's content is copied to the compositor each frame. /// [Tooltip("If true, the texture's content is copied to the compositor each frame.")] public bool isDynamic = false; /// /// If true, the layer would be used to present protected content (e.g. HDCP), /// the content won't be shown in screenshots or recordings. /// [Tooltip("If true, the layer would be used to present protected content (e.g. HDCP), " + "the content won't be shown in screenshots or recordings.")] public bool isProtectedContent = false; //Source and dest rects public Rect srcRectLeft = new Rect(0, 0, 1, 1); public Rect srcRectRight = new Rect(0, 0, 1, 1); public Rect destRectLeft = new Rect(0, 0, 1, 1); public Rect destRectRight = new Rect(0, 0, 1, 1); // Used to support legacy behavior where the top left was considered the origin public bool invertTextureRects = false; private OVRPlugin.TextureRectMatrixf textureRectMatrix = OVRPlugin.TextureRectMatrixf.zero; public bool overrideTextureRectMatrix = false; public bool overridePerLayerColorScaleAndOffset = false; public Vector4 colorScale = Vector4.one; public Vector4 colorOffset = Vector4.zero; //Warning: Developers should only use this supersample setting if they absolutely have the budget and need for it. //It is extremely expensive, and will not be relevant for most developers. public bool useExpensiveSuperSample = false; //Warning: Developers should only use this sharpening setting if they absolutely have the budget and need for it. //It is extremely expensive, and will not be relevant for most developers. public bool useExpensiveSharpen = false; //Property that can hide overlays when required. Should be false when present, true when hidden. public bool hidden = false; /// /// If true, the layer will be created as an external surface. externalSurfaceObject contains /// the Surface object. It's effective only on Android. /// [Tooltip("If true, the layer will be created as an external surface. externalSurfaceObject contains " + "the Surface object. It's effective only on Android.")] public bool isExternalSurface = false; /// /// The width which will be used to create the external surface. It's effective only on Android. /// [Tooltip("The width which will be used to create the external surface. It's effective only on Android.")] public int externalSurfaceWidth = 0; /// /// The height which will be used to create the external surface. It's effective only on Android. /// [Tooltip("The height which will be used to create the external surface. It's effective only on Android.")] public int externalSurfaceHeight = 0; /// /// The compositionDepth defines the order of the OVROverlays in composition. The overlay/underlay with smaller /// compositionDepth would be composited in the front of the overlay/underlay with larger compositionDepth. /// [Tooltip("The compositionDepth defines the order of the OVROverlays in composition. The overlay/underlay with " + "smaller compositionDepth would be composited in the front of the overlay/underlay with larger compositionDepth.")] public int compositionDepth = 0; private int layerCompositionDepth = 0; /// /// The noDepthBufferTesting will stop layer's depth buffer compositing even if the engine has /// "Depth buffer sharing" enabled on Rift. /// [Tooltip("The noDepthBufferTesting will stop layer's depth buffer compositing even if the engine has " + "\"Shared Depth Buffer\" enabled. The layer's ordering will be used instead which is determined by it's " + "composition depth and overlay/underlay type.")] public bool noDepthBufferTesting = true; //Format corresponding to the source texture for this layer. sRGB by default, but can be modified if necessary public OVRPlugin.EyeTextureFormat layerTextureFormat = OVRPlugin.EyeTextureFormat.R8G8B8A8_sRGB; /// /// Specify overlay's shape /// [Tooltip("Specify overlay's shape")] public OverlayShape currentOverlayShape = OverlayShape.Quad; private OverlayShape prevOverlayShape = OverlayShape.Quad; /// /// The left- and right-eye Textures to show in the layer. /// \note If you need to change the texture on a per-frame basis, please use OverrideOverlayTextureInfo(..) /// to avoid caching issues. /// [Tooltip("The left- and right-eye Textures to show in the layer.")] public Texture[] textures = new Texture[] { null, null }; [Tooltip("When checked, the texture is treated as if the alpha was already premultiplied")] public bool isAlphaPremultiplied = false; [Tooltip("When checked, the layer will use bicubic filtering")] public bool useBicubicFiltering = false; [Tooltip("When checked, the cubemap will retain the legacy rotation which was rotated 180 degrees around " + "the Y axis comapred to Unity's definition of cubemaps. This setting will be deprecated in the near future, " + "therefore it is recommended to fix the cubemap texture instead.")] public bool useLegacyCubemapRotation = false; [Tooltip("When checked, the layer will use efficient super sampling")] public bool useEfficientSupersample = false; [Tooltip( "When checked, the layer will use efficient sharpen.")] public bool useEfficientSharpen = false; [Tooltip( "When checked, The runtime automatically chooses the appropriate sharpening or super sampling filter")] public bool useAutomaticFiltering = false; /// /// Preview the overlay in the editor using a mesh renderer. /// public bool previewInEditor { get { return _previewInEditor; } set { if (_previewInEditor != value) { _previewInEditor = value; SetupEditorPreview(); } } } [SerializeField] internal bool _previewInEditor = false; #if UNITY_EDITOR private GameObject previewObject; #endif protected IntPtr[] texturePtrs = new IntPtr[] { IntPtr.Zero, IntPtr.Zero }; /// /// The Surface object (Android only). /// public System.IntPtr externalSurfaceObject; public delegate void ExternalSurfaceObjectCreated(); /// /// Will be triggered after externalSurfaceTextueObject get created. /// public ExternalSurfaceObjectCreated externalSurfaceObjectCreated; /// /// Use this function to set texture and texNativePtr when app is running /// GetNativeTexturePtr is a slow behavior, the value should be pre-cached /// public void OverrideOverlayTextureInfo(Texture srcTexture, IntPtr nativePtr, UnityEngine.XR.XRNode node) { int index = (node == UnityEngine.XR.XRNode.RightEye) ? 1 : 0; if (textures.Length <= index) return; textures[index] = srcTexture; texturePtrs[index] = nativePtr; isOverridePending = true; } protected bool isOverridePending; internal const int maxInstances = 15; public static OVROverlay[] instances = new OVROverlay[maxInstances]; public int layerId { get; private set; } = 0; // The layer's internal handle in the compositor. #endregion private static Material tex2DMaterial; private static Material cubeMaterial; private OVRPlugin.LayerLayout layout { get { #if UNITY_ANDROID && !UNITY_EDITOR if (textures.Length == 2 && textures[1] != null && textures[1] != textures[0]) return OVRPlugin.LayerLayout.Stereo; #endif return OVRPlugin.LayerLayout.Mono; } } private struct LayerTexture { public Texture appTexture; public IntPtr appTexturePtr; public Texture[] swapChain; public IntPtr[] swapChainPtr; }; private LayerTexture[] layerTextures; private OVRPlugin.LayerDesc layerDesc; private int stageCount = -1; private int layerIndex = -1; // Controls the composition order based on wake-up time. private GCHandle layerIdHandle; private IntPtr layerIdPtr = IntPtr.Zero; private int frameIndex = 0; private int prevFrameIndex = -1; private Renderer rend; private int texturesPerStage { get { return (layout == OVRPlugin.LayerLayout.Stereo) ? 2 : 1; } } private static bool NeedsTexturesForShape(OverlayShape shape) { return !IsPassthroughShape(shape); } private bool CreateLayer(int mipLevels, int sampleCount, OVRPlugin.EyeTextureFormat etFormat, int flags, OVRPlugin.Sizei size, OVRPlugin.OverlayShape shape) { if (!layerIdHandle.IsAllocated || layerIdPtr == IntPtr.Zero) { layerIdHandle = GCHandle.Alloc(layerId, GCHandleType.Pinned); layerIdPtr = layerIdHandle.AddrOfPinnedObject(); } if (layerIndex == -1) { for (int i = 0; i < maxInstances; ++i) { if (instances[i] == null || instances[i] == this) { layerIndex = i; instances[i] = this; break; } } } bool needsSetup = ( isOverridePending || layerDesc.MipLevels != mipLevels || layerDesc.SampleCount != sampleCount || layerDesc.Format != etFormat || layerDesc.Layout != layout || layerDesc.LayerFlags != flags || !layerDesc.TextureSize.Equals(size) || layerDesc.Shape != shape || layerCompositionDepth != compositionDepth) ; if (!needsSetup) return false; OVRPlugin.LayerDesc desc = OVRPlugin.CalculateLayerDesc(shape, layout, size, mipLevels, sampleCount, etFormat, flags); OVRPlugin.EnqueueSetupLayer(desc, compositionDepth, layerIdPtr); layerId = (int)layerIdHandle.Target; if (layerId > 0) { layerDesc = desc; layerCompositionDepth = compositionDepth; if (isExternalSurface) { stageCount = 1; } else { stageCount = OVRPlugin.GetLayerTextureStageCount(layerId); } } isOverridePending = false; return true; } private bool CreateLayerTextures(bool useMipmaps, OVRPlugin.Sizei size, bool isHdr) { if (isExternalSurface) { if (externalSurfaceObject == System.IntPtr.Zero) { externalSurfaceObject = OVRPlugin.GetLayerAndroidSurfaceObject(layerId); if (externalSurfaceObject != System.IntPtr.Zero) { Debug.LogFormat("GetLayerAndroidSurfaceObject returns {0}", externalSurfaceObject); if (externalSurfaceObjectCreated != null) { externalSurfaceObjectCreated(); } } } return false; } bool needsCopy = false; if (stageCount <= 0) return false; // For newer SDKs, blit directly to the surface that will be used in compositing. if (layerTextures == null) layerTextures = new LayerTexture[texturesPerStage]; for (int eyeId = 0; eyeId < texturesPerStage; ++eyeId) { if (layerTextures[eyeId].swapChain == null) layerTextures[eyeId].swapChain = new Texture[stageCount]; if (layerTextures[eyeId].swapChainPtr == null) layerTextures[eyeId].swapChainPtr = new IntPtr[stageCount]; for (int stage = 0; stage < stageCount; ++stage) { Texture sc = layerTextures[eyeId].swapChain[stage]; IntPtr scPtr = layerTextures[eyeId].swapChainPtr[stage]; if (sc != null && scPtr != IntPtr.Zero && size.w == sc.width && size.h == sc.height) continue; if (scPtr == IntPtr.Zero) scPtr = OVRPlugin.GetLayerTexture(layerId, stage, (OVRPlugin.Eye)eyeId); if (scPtr == IntPtr.Zero) continue; var txFormat = (isHdr) ? TextureFormat.RGBAHalf : TextureFormat.RGBA32; if (currentOverlayShape != OverlayShape.Cubemap && currentOverlayShape != OverlayShape.OffcenterCubemap) sc = Texture2D.CreateExternalTexture(size.w, size.h, txFormat, useMipmaps, true, scPtr); else sc = Cubemap.CreateExternalTexture(size.w, txFormat, useMipmaps, scPtr); layerTextures[eyeId].swapChain[stage] = sc; layerTextures[eyeId].swapChainPtr[stage] = scPtr; needsCopy = true; } } return needsCopy; } private void DestroyLayerTextures() { if (isExternalSurface) { return; } for (int eyeId = 0; layerTextures != null && eyeId < texturesPerStage; ++eyeId) { if (layerTextures[eyeId].swapChain != null) { for (int stage = 0; stage < stageCount; ++stage) DestroyImmediate(layerTextures[eyeId].swapChain[stage]); } } layerTextures = null; } private void DestroyLayer() { if (layerIndex != -1) { // Turn off the overlay if it was on. OVRPlugin.EnqueueSubmitLayer(true, false, false, IntPtr.Zero, IntPtr.Zero, -1, 0, OVRPose.identity.ToPosef_Legacy(), Vector3.one.ToVector3f(), layerIndex, (OVRPlugin.OverlayShape)prevOverlayShape); instances[layerIndex] = null; layerIndex = -1; } if (layerIdPtr != IntPtr.Zero) { OVRPlugin.EnqueueDestroyLayer(layerIdPtr); layerIdPtr = IntPtr.Zero; layerIdHandle.Free(); layerId = 0; } layerDesc = new OVRPlugin.LayerDesc(); frameIndex = 0; prevFrameIndex = -1; } /// /// Sets the source and dest rects for both eyes. Source explains what portion of the source texture is used, and /// dest is what portion of the destination texture is rendered into. /// public void SetSrcDestRects(Rect srcLeft, Rect srcRight, Rect destLeft, Rect destRight) { srcRectLeft = srcLeft; srcRectRight = srcRight; destRectLeft = destLeft; destRectRight = destRight; } public void UpdateTextureRectMatrix() { // External surfaces are encoded with reversed UV's, so our texture rects are also inverted Rect srcRectLeftConverted = new Rect(srcRectLeft.x, isExternalSurface ^ invertTextureRects ? 1 - srcRectLeft.y - srcRectLeft.height : srcRectLeft.y, srcRectLeft.width, srcRectLeft.height); Rect srcRectRightConverted = new Rect(srcRectRight.x, isExternalSurface ^ invertTextureRects ? 1 - srcRectRight.y - srcRectRight.height : srcRectRight.y, srcRectRight.width, srcRectRight.height); Rect destRectLeftConverted = new Rect(destRectLeft.x, isExternalSurface ^ invertTextureRects ? 1 - destRectLeft.y - destRectLeft.height : destRectLeft.y, destRectLeft.width, destRectLeft.height); Rect destRectRightConverted = new Rect(destRectRight.x, isExternalSurface ^ invertTextureRects ? 1 - destRectRight.y - destRectRight.height : destRectRight.y, destRectRight.width, destRectRight.height); textureRectMatrix.leftRect = srcRectLeftConverted; textureRectMatrix.rightRect = srcRectRightConverted; // Fisheye layer requires a 0.5f offset for texture to be centered on the fisheye projection if (currentOverlayShape == OverlayShape.Fisheye) { destRectLeftConverted.x -= 0.5f; destRectLeftConverted.y -= 0.5f; destRectRightConverted.x -= 0.5f; destRectRightConverted.y -= 0.5f; } float leftWidthFactor = srcRectLeft.width / destRectLeft.width; float leftHeightFactor = srcRectLeft.height / destRectLeft.height; textureRectMatrix.leftScaleBias = new Vector4(leftWidthFactor, leftHeightFactor, srcRectLeftConverted.x - destRectLeftConverted.x * leftWidthFactor, srcRectLeftConverted.y - destRectLeftConverted.y * leftHeightFactor); float rightWidthFactor = srcRectRight.width / destRectRight.width; float rightHeightFactor = srcRectRight.height / destRectRight.height; textureRectMatrix.rightScaleBias = new Vector4(rightWidthFactor, rightHeightFactor, srcRectRightConverted.x - destRectRightConverted.x * rightWidthFactor, srcRectRightConverted.y - destRectRightConverted.y * rightHeightFactor); } public void SetPerLayerColorScaleAndOffset(Vector4 scale, Vector4 offset) { colorScale = scale; colorOffset = offset; } private bool LatchLayerTextures() { if (isExternalSurface) { return true; } for (int i = 0; i < texturesPerStage; ++i) { if (textures[i] != layerTextures[i].appTexture || layerTextures[i].appTexturePtr == IntPtr.Zero) { if (textures[i] != null) { #if UNITY_EDITOR var assetPath = UnityEditor.AssetDatabase.GetAssetPath(textures[i]); var importer = UnityEditor.AssetImporter.GetAtPath(assetPath) as UnityEditor.TextureImporter; if (importer != null && importer.textureType != UnityEditor.TextureImporterType.Default) { Debug.LogError("Need Default Texture Type for overlay"); return false; } #endif var rt = textures[i] as RenderTexture; if (rt && !rt.IsCreated()) rt.Create(); layerTextures[i].appTexturePtr = (texturePtrs[i] != IntPtr.Zero) ? texturePtrs[i] : textures[i].GetNativeTexturePtr(); if (layerTextures[i].appTexturePtr != IntPtr.Zero) layerTextures[i].appTexture = textures[i]; } } if (currentOverlayShape == OverlayShape.Cubemap) { if (textures[i] as Cubemap == null) { Debug.LogError("Need Cubemap texture for cube map overlay"); return false; } } } #if !UNITY_ANDROID || UNITY_EDITOR if (currentOverlayShape == OverlayShape.OffcenterCubemap) { Debug.LogWarning("Overlay shape " + currentOverlayShape + " is not supported on current platform"); return false; } #endif if (layerTextures[0].appTexture == null || layerTextures[0].appTexturePtr == IntPtr.Zero) return false; return true; } private OVRPlugin.LayerDesc GetCurrentLayerDesc() { OVRPlugin.Sizei textureSize = new OVRPlugin.Sizei() { w = 0, h = 0 }; if (isExternalSurface) { textureSize.w = externalSurfaceWidth; textureSize.h = externalSurfaceHeight; } else if (NeedsTexturesForShape(currentOverlayShape)) { if (textures[0] == null) { Debug.LogWarning("textures[0] hasn't been set"); } textureSize.w = textures[0] ? textures[0].width : 0; textureSize.h = textures[0] ? textures[0].height : 0; } OVRPlugin.LayerDesc newDesc = new OVRPlugin.LayerDesc() { Format = layerTextureFormat, LayerFlags = isExternalSurface ? 0 : (int)OVRPlugin.LayerFlags.TextureOriginAtBottomLeft, Layout = layout, MipLevels = 1, SampleCount = 1, Shape = (OVRPlugin.OverlayShape)currentOverlayShape, TextureSize = textureSize }; var tex2D = textures[0] as Texture2D; if (tex2D != null) { if (tex2D.format == TextureFormat.RGBAHalf || tex2D.format == TextureFormat.RGBAFloat) newDesc.Format = OVRPlugin.EyeTextureFormat.R16G16B16A16_FP; newDesc.MipLevels = tex2D.mipmapCount; } var texCube = textures[0] as Cubemap; if (texCube != null) { if (texCube.format == TextureFormat.RGBAHalf || texCube.format == TextureFormat.RGBAFloat) newDesc.Format = OVRPlugin.EyeTextureFormat.R16G16B16A16_FP; newDesc.MipLevels = texCube.mipmapCount; } var rt = textures[0] as RenderTexture; if (rt != null) { newDesc.SampleCount = rt.antiAliasing; if (rt.format == RenderTextureFormat.ARGBHalf || rt.format == RenderTextureFormat.ARGBFloat || rt.format == RenderTextureFormat.RGB111110Float) newDesc.Format = OVRPlugin.EyeTextureFormat.R16G16B16A16_FP; } if (isProtectedContent) { newDesc.LayerFlags |= (int)OVRPlugin.LayerFlags.ProtectedContent; } if (isExternalSurface) { newDesc.LayerFlags |= (int)OVRPlugin.LayerFlags.AndroidSurfaceSwapChain; } if (useBicubicFiltering) { newDesc.LayerFlags |= (int)OVRPlugin.LayerFlags.BicubicFiltering; } return newDesc; } private Rect GetBlitRect(int eyeId) { if (texturesPerStage == 2) { return eyeId == 0 ? srcRectLeft : srcRectRight; } else { // Get intersection of both rects if we use the same texture for both eyes float minX = Mathf.Min(srcRectLeft.x, srcRectRight.x); float minY = Mathf.Min(srcRectLeft.y, srcRectRight.y); float maxX = Mathf.Max(srcRectLeft.x + srcRectLeft.width, srcRectRight.x + srcRectRight.width); float maxY = Mathf.Max(srcRectLeft.y + srcRectLeft.height, srcRectRight.y + srcRectRight.height); return new Rect(minX, minY, maxX - minX, maxY - minY); } } // A blit method that only draws into the specified rect by setting the viewport. private void BlitSubImage(Texture src, RenderTexture dst, Material mat, Rect rect, bool invertRect = false) { var p = RenderTexture.active; RenderTexture.active = dst; if (invertRect) { // our rects are inverted rect.y = 1 - rect.y - rect.height; } // Round our rect to the bounding pixel rect var viewport = new Rect( Mathf.Floor(dst.width * rect.x), Mathf.Floor(dst.height * rect.y), Mathf.Ceil(dst.width * rect.xMax) - Mathf.Floor(dst.width * rect.x), Mathf.Ceil(dst.height * rect.yMax) - Mathf.Floor(dst.height * rect.y)); // do our blit using GL ops GL.PushMatrix(); GL.Viewport(viewport); GL.LoadPixelMatrix(viewport.x, viewport.x + viewport.width, viewport.y, viewport.y + viewport.height); tex2DMaterial.mainTexture = src; tex2DMaterial.SetPass(0); GL.Begin(GL.TRIANGLES); GL.TexCoord(new Vector2(viewport.x / dst.width, viewport.y / dst.height)); GL.Vertex3(viewport.x, viewport.y, 0); GL.TexCoord(new Vector2(viewport.x / dst.width, (viewport.y + viewport.height * 2) / dst.height)); GL.Vertex3(viewport.x, viewport.y + viewport.height * 2, 0); GL.TexCoord(new Vector2((viewport.x + viewport.width * 2) / dst.width, viewport.y / dst.height)); GL.Vertex3(viewport.x + viewport.width * 2, viewport.y, 0); GL.End(); GL.Flush(); GL.PopMatrix(); RenderTexture.active = p; } private bool PopulateLayer(int mipLevels, bool isHdr, OVRPlugin.Sizei size, int sampleCount, int stage) { if (isExternalSurface) { return true; } bool ret = false; RenderTextureFormat rtFormat = (isHdr) ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB32; for (int eyeId = 0; eyeId < texturesPerStage; ++eyeId) { Texture et = layerTextures[eyeId].swapChain[stage]; if (et == null) continue; ret = true; // PC requries premultiplied Alpha, premultiply it unless its already premultiplied bool premultiplyAlpha = !Application.isMobilePlatform && !isAlphaPremultiplied; // OpenGL does not support copy texture between different format bool isOpenGL = SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3 || SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLES2; // Graphics.CopyTexture only works when textures are same size and same mipmap count bool isSameSize = et.width == textures[eyeId].width && et.height == textures[eyeId].height; bool sameMipMap = textures[eyeId].mipmapCount == et.mipmapCount; bool bypassBlit = Application.isMobilePlatform && !isOpenGL && isSameSize && sameMipMap; if (bypassBlit) { Graphics.CopyTexture(textures[eyeId], et); continue; } // Need to run the blit shader for premultiply Alpha for (int mip = 0; mip < mipLevels; ++mip) { RenderTexture tempRTDst = null; int width = size.w >> mip; if (width < 1) width = 1; int height = size.h >> mip; if (height < 1) height = 1; RenderTextureDescriptor descriptor = new RenderTextureDescriptor(width, height, rtFormat, 0); descriptor.msaaSamples = sampleCount; descriptor.useMipMap = true; descriptor.autoGenerateMips = false; descriptor.sRGB = true; tempRTDst = RenderTexture.GetTemporary(descriptor); if (!tempRTDst.IsCreated()) { tempRTDst.Create(); } tempRTDst.DiscardContents(); Material blitMat = null; if (currentOverlayShape != OverlayShape.Cubemap && currentOverlayShape != OverlayShape.OffcenterCubemap) { blitMat = tex2DMaterial; } else { blitMat = cubeMaterial; } blitMat.SetInt("_premultiply", premultiplyAlpha ? 1 : 0); if (currentOverlayShape != OverlayShape.Cubemap && currentOverlayShape != OverlayShape.OffcenterCubemap) { blitMat.SetInt("_flip", OVRPlugin.nativeXrApi == OVRPlugin.XrApi.OpenXR ? 1 : 0); if (overrideTextureRectMatrix) { BlitSubImage(textures[eyeId], tempRTDst, tex2DMaterial, GetBlitRect(eyeId), invertTextureRects); } else { Graphics.Blit(textures[eyeId], tempRTDst, tex2DMaterial); } //Resolve, decompress, swizzle, etc not handled by simple CopyTexture. Graphics.CopyTexture(tempRTDst, 0, 0, et, 0, mip); } else // Cubemap { for (int face = 0; face < 6; ++face) { cubeMaterial.SetInt("_face", face); //Resolve, decompress, swizzle, etc not handled by simple CopyTexture. Graphics.Blit(textures[eyeId], tempRTDst, cubeMaterial); Graphics.CopyTexture(tempRTDst, 0, 0, et, face, mip); } } if (tempRTDst != null) { RenderTexture.ReleaseTemporary(tempRTDst); } } } return ret; } private bool SubmitLayer(bool overlay, bool headLocked, bool noDepthBufferTesting, OVRPose pose, Vector3 scale, int frameIndex) { int rightEyeIndex = (texturesPerStage >= 2) ? 1 : 0; if (overrideTextureRectMatrix) { UpdateTextureRectMatrix(); } bool internalUseEfficientSharpen = useEfficientSharpen; bool internalUseEfficientSupersample = useEfficientSupersample; // No sharpening or supersampling method was selected, defaulting to efficient supersampling and efficient sharpening. if (useAutomaticFiltering && !(useEfficientSharpen || useEfficientSupersample || useExpensiveSharpen || useExpensiveSuperSample)) { internalUseEfficientSharpen = true; internalUseEfficientSupersample = true; } if (!useAutomaticFiltering && ((useEfficientSharpen && useEfficientSupersample) || (useExpensiveSharpen && useExpensiveSuperSample) || (useEfficientSharpen && useExpensiveSuperSample) || (useExpensiveSharpen && useEfficientSupersample))) { Debug.LogError("Warning-XR sharpening and supersampling cannot be enabled simultaneously, either enable autofiltering or disable one of the options"); return false; } bool noTextures = isExternalSurface || !NeedsTexturesForShape(currentOverlayShape); bool isOverlayVisible = OVRPlugin.EnqueueSubmitLayer(overlay, headLocked, noDepthBufferTesting, noTextures ? System.IntPtr.Zero : layerTextures[0].appTexturePtr, noTextures ? System.IntPtr.Zero : layerTextures[rightEyeIndex].appTexturePtr, layerId, frameIndex, pose.flipZ().ToPosef_Legacy(), scale.ToVector3f(), layerIndex, (OVRPlugin.OverlayShape)currentOverlayShape, overrideTextureRectMatrix, textureRectMatrix, overridePerLayerColorScaleAndOffset, colorScale, colorOffset, useExpensiveSuperSample, useBicubicFiltering, internalUseEfficientSupersample, internalUseEfficientSharpen, useExpensiveSharpen, hidden, isProtectedContent, useAutomaticFiltering ); prevOverlayShape = currentOverlayShape; return isOverlayVisible; } private void SetupEditorPreview() { #if UNITY_EDITOR if (previewInEditor) { if (previewObject == null) { previewObject = new GameObject(); previewObject.hideFlags = HideFlags.HideAndDontSave; previewObject.transform.SetParent(this.transform, false); OVROverlayMeshGenerator generator = previewObject.AddComponent(); generator.SetOverlay(this); } previewObject.SetActive(true); } else if (previewObject != null) { previewObject.SetActive(false); DestroyImmediate(previewObject); previewObject = null; } #endif } public void ResetEditorPreview() { previewInEditor = false; previewInEditor = true; } public static bool IsPassthroughShape(OverlayShape shape) { return OVRPlugin.IsPassthroughShape((OVRPlugin.OverlayShape)shape); } #region Unity Messages void Awake() { if (Application.isPlaying) { if (tex2DMaterial == null) tex2DMaterial = new Material(Shader.Find("Oculus/Texture2D Blit")); if (cubeMaterial == null) cubeMaterial = new Material(Shader.Find("Oculus/Cubemap Blit")); } rend = GetComponent(); if (textures.Length == 0) textures = new Texture[] { null }; // Backward compatibility if (rend != null && textures[0] == null) textures[0] = rend.sharedMaterial.mainTexture; SetupEditorPreview(); } static public string OpenVROverlayKey { get { return "unity:" + Application.companyName + "." + Application.productName; } } private ulong OpenVROverlayHandle = OVR.OpenVR.OpenVR.k_ulOverlayHandleInvalid; void OnEnable() { if (OVRManager.OVRManagerinitialized) InitOVROverlay(); SetupEditorPreview(); } void InitOVROverlay() { #if USING_XR_SDK_OPENXR if (!OVRPlugin.UnityOpenXR.Enabled) { #endif if (!OVRManager.isHmdPresent) { enabled = false; return; } #if USING_XR_SDK_OPENXR } #endif constructedOverlayXRDevice = OVRManager.XRDevice.Unknown; if (OVRManager.loadedXRDevice == OVRManager.XRDevice.OpenVR) { OVR.OpenVR.CVROverlay overlay = OVR.OpenVR.OpenVR.Overlay; if (overlay != null) { OVR.OpenVR.EVROverlayError error = overlay.CreateOverlay(OpenVROverlayKey + transform.name, gameObject.name, ref OpenVROverlayHandle); if (error != OVR.OpenVR.EVROverlayError.None) { enabled = false; return; } } else { enabled = false; return; } } constructedOverlayXRDevice = OVRManager.loadedXRDevice; xrDeviceConstructed = true; } void OnDisable() { #if UNITY_EDITOR if (previewObject != null) { previewObject.SetActive(false); } #endif if ((gameObject.hideFlags & HideFlags.DontSaveInBuild) != 0) return; if (!OVRManager.OVRManagerinitialized) return; if (OVRManager.loadedXRDevice != constructedOverlayXRDevice) return; if (OVRManager.loadedXRDevice == OVRManager.XRDevice.Oculus) { DestroyLayerTextures(); DestroyLayer(); } else if (OVRManager.loadedXRDevice == OVRManager.XRDevice.OpenVR) { if (OpenVROverlayHandle != OVR.OpenVR.OpenVR.k_ulOverlayHandleInvalid) { OVR.OpenVR.CVROverlay overlay = OVR.OpenVR.OpenVR.Overlay; if (overlay != null) { overlay.DestroyOverlay(OpenVROverlayHandle); } OpenVROverlayHandle = OVR.OpenVR.OpenVR.k_ulOverlayHandleInvalid; } } constructedOverlayXRDevice = OVRManager.XRDevice.Unknown; xrDeviceConstructed = false; } void OnDestroy() { DestroyLayerTextures(); DestroyLayer(); #if UNITY_EDITOR if (previewObject != null) { GameObject.DestroyImmediate(previewObject); } #endif } void ComputePoseAndScale(ref OVRPose pose, ref Vector3 scale, ref bool overlay, ref bool headLocked) { Camera headCamera = Camera.main; overlay = (currentOverlayType == OverlayType.Overlay); headLocked = false; for (var t = transform; t != null && !headLocked; t = t.parent) headLocked |= (t == headCamera.transform); pose = (headLocked) ? transform.ToHeadSpacePose(headCamera) : transform.ToTrackingSpacePose(headCamera); scale = transform.lossyScale; for (int i = 0; i < 3; ++i) scale[i] /= headCamera.transform.lossyScale[i]; if (currentOverlayShape == OverlayShape.Cubemap) { if (useLegacyCubemapRotation) { #if UNITY_ANDROID && !UNITY_EDITOR pose.orientation = pose.orientation * Quaternion.AngleAxis(180, Vector3.up); #endif } else { #if UNITY_STANDALONE_WIN || UNITY_EDITOR pose.orientation = pose.orientation * Quaternion.AngleAxis(180, Vector3.up); #endif } pose.position = headCamera.transform.position; } } bool ComputeSubmit(ref OVRPose pose, ref Vector3 scale, ref bool overlay, ref bool headLocked) { ComputePoseAndScale(ref pose, ref scale, ref overlay, ref headLocked); // Pack the offsetCenter directly into pose.position for offcenterCubemap if (currentOverlayShape == OverlayShape.OffcenterCubemap) { pose.position = transform.position; if (pose.position.magnitude > 1.0f) { Debug.LogWarning("Your cube map center offset's magnitude is greater than 1, " + "which will cause some cube map pixel always invisible ."); return false; } } // Cylinder overlay sanity checking when not using OpenXR if (OVRPlugin.nativeXrApi != OVRPlugin.XrApi.OpenXR && currentOverlayShape == OverlayShape.Cylinder) { float arcAngle = scale.x / scale.z / (float)Math.PI * 180.0f; if (arcAngle > 180.0f) { Debug.LogWarning("Cylinder overlay's arc angle has to be below 180 degree, current arc angle is " + arcAngle + " degree."); return false; } } if (OVRPlugin.nativeXrApi == OVRPlugin.XrApi.OpenXR && currentOverlayShape == OverlayShape.Fisheye) { Debug.LogWarning("Fisheye overlay shape is not support on OpenXR"); return false; } return true; } void OpenVROverlayUpdate(Vector3 scale, OVRPose pose) { OVR.OpenVR.CVROverlay overlayRef = OVR.OpenVR.OpenVR.Overlay; if (overlayRef == null) return; Texture overlayTex = textures[0]; if (overlayTex != null) { OVR.OpenVR.EVROverlayError error = overlayRef.ShowOverlay(OpenVROverlayHandle); if (error == OVR.OpenVR.EVROverlayError.InvalidHandle || error == OVR.OpenVR.EVROverlayError.UnknownOverlay) { if (overlayRef.FindOverlay(OpenVROverlayKey + transform.name, ref OpenVROverlayHandle) != OVR.OpenVR.EVROverlayError.None) return; } OVR.OpenVR.Texture_t tex = new OVR.OpenVR.Texture_t(); tex.handle = overlayTex.GetNativeTexturePtr(); tex.eType = SystemInfo.graphicsDeviceVersion.StartsWith("OpenGL") ? OVR.OpenVR.ETextureType.OpenGL : OVR.OpenVR.ETextureType.DirectX; tex.eColorSpace = OVR.OpenVR.EColorSpace.Auto; overlayRef.SetOverlayTexture(OpenVROverlayHandle, ref tex); OVR.OpenVR.VRTextureBounds_t textureBounds = new OVR.OpenVR.VRTextureBounds_t(); textureBounds.uMin = (0 + OpenVRUVOffsetAndScale.x) * OpenVRUVOffsetAndScale.z; textureBounds.vMin = (1 + OpenVRUVOffsetAndScale.y) * OpenVRUVOffsetAndScale.w; textureBounds.uMax = (1 + OpenVRUVOffsetAndScale.x) * OpenVRUVOffsetAndScale.z; textureBounds.vMax = (0 + OpenVRUVOffsetAndScale.y) * OpenVRUVOffsetAndScale.w; overlayRef.SetOverlayTextureBounds(OpenVROverlayHandle, ref textureBounds); OVR.OpenVR.HmdVector2_t vecMouseScale = new OVR.OpenVR.HmdVector2_t(); vecMouseScale.v0 = OpenVRMouseScale.x; vecMouseScale.v1 = OpenVRMouseScale.y; overlayRef.SetOverlayMouseScale(OpenVROverlayHandle, ref vecMouseScale); overlayRef.SetOverlayWidthInMeters(OpenVROverlayHandle, scale.x); Matrix4x4 mat44 = Matrix4x4.TRS(pose.position, pose.orientation, Vector3.one); OVR.OpenVR.HmdMatrix34_t pose34 = mat44.ConvertToHMDMatrix34(); overlayRef.SetOverlayTransformAbsolute(OpenVROverlayHandle, OVR.OpenVR.ETrackingUniverseOrigin.TrackingUniverseStanding, ref pose34); } } private Vector4 OpenVRUVOffsetAndScale = new Vector4(0, 0, 1.0f, 1.0f); private Vector2 OpenVRMouseScale = new Vector2(1, 1); private OVRManager.XRDevice constructedOverlayXRDevice; private bool xrDeviceConstructed = false; void LateUpdate() { if (!OVRManager.OVRManagerinitialized || !OVRPlugin.userPresent) return; if (!xrDeviceConstructed) { InitOVROverlay(); } if (OVRManager.loadedXRDevice != constructedOverlayXRDevice) { Debug.LogError("Warning-XR Device was switched during runtime with overlays still enabled. " + "When doing so, all overlays constructed with the previous XR device must first be disabled."); return; } // The overlay must be specified every eye frame, because it is positioned relative to the // current head location. If frames are dropped, it will be time warped appropriately, // just like the eye buffers. bool requiresTextures = !isExternalSurface && NeedsTexturesForShape(currentOverlayShape); if (currentOverlayType == OverlayType.None || (requiresTextures && (textures.Length < texturesPerStage || textures[0] == null))) { return; } OVRPose pose = OVRPose.identity; Vector3 scale = Vector3.one; bool overlay = false; bool headLocked = false; if (!ComputeSubmit(ref pose, ref scale, ref overlay, ref headLocked)) return; if (OVRManager.loadedXRDevice == OVRManager.XRDevice.OpenVR) { if (currentOverlayShape == OverlayShape.Quad) OpenVROverlayUpdate(scale, pose); //No more Overlay processing is required if we're on OpenVR return; } OVRPlugin.LayerDesc newDesc = GetCurrentLayerDesc(); bool isHdr = (newDesc.Format == OVRPlugin.EyeTextureFormat.R16G16B16A16_FP); // If the layer and textures are created but sizes differ, force re-creating them. // If the layer needed textures but does not anymore (or vice versa), re-create as well. bool textureSizesDiffer = !layerDesc.TextureSize.Equals(newDesc.TextureSize) && layerId > 0; bool needsTextures = NeedsTexturesForShape(currentOverlayShape); bool needsTextureChanged = NeedsTexturesForShape(prevOverlayShape) != needsTextures; if (textureSizesDiffer || needsTextureChanged) { DestroyLayerTextures(); DestroyLayer(); } bool createdLayer = CreateLayer(newDesc.MipLevels, newDesc.SampleCount, newDesc.Format, newDesc.LayerFlags, newDesc.TextureSize, newDesc.Shape); if (layerIndex == -1 || layerId <= 0) { if (createdLayer) { // Propagate the current shape and avoid the permanent state of "needs texture changed" prevOverlayShape = currentOverlayShape; } return; } if (needsTextures) { bool useMipmaps = (newDesc.MipLevels > 1); createdLayer |= CreateLayerTextures(useMipmaps, newDesc.TextureSize, isHdr); if (!isExternalSurface && (layerTextures[0].appTexture as RenderTexture != null)) isDynamic = true; if (!LatchLayerTextures()) return; // Don't populate the same frame image twice. if (frameIndex > prevFrameIndex) { int stage = frameIndex % stageCount; if (!PopulateLayer(newDesc.MipLevels, isHdr, newDesc.TextureSize, newDesc.SampleCount, stage)) return; } } bool isOverlayVisible = SubmitLayer(overlay, headLocked, noDepthBufferTesting, pose, scale, frameIndex); prevFrameIndex = frameIndex; if (isDynamic) ++frameIndex; // Backward compatibility: show regular renderer if overlay isn't visible. if (rend) rend.enabled = !isOverlayVisible; } #endregion }