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