/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.Rendering; /// /// When attached to a GameObject with an OVROverlay component, OVROverlayMeshGenerator will use a mesh renderer /// to preview the appearance of the OVROverlay as it would appear as a TimeWarp overlay on a headset. /// [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] [ExecuteInEditMode] [HelpURL("https://developer.oculus.com/reference/unity/latest/class_o_v_r_overlay_mesh_generator")] public class OVROverlayMeshGenerator : MonoBehaviour { private readonly List _Tris = new List(); private readonly List _UV = new List(); private readonly List _CubeUV = new List(); private readonly List _Verts = new List(); private Transform _CameraRoot; private Rect _LastDestRectLeft; private Rect _LastDestRectRight; private Vector3 _LastPosition; private Quaternion _LastRotation; private Vector3 _LastScale; private TextureDimension _LastTextureDimension; private OVROverlay.OverlayShape _LastShape; private Rect _LastSrcRectLeft; private Mesh _Mesh; private MeshCollider _MeshCollider; private MeshFilter _MeshFilter; private MeshRenderer _MeshRenderer; private OVROverlay _Overlay; private Transform _Transform; protected void OnEnable() { Initialize(); #if UNITY_EDITOR UnityEditor.EditorApplication.update += Update; #endif } protected void OnDisable() { #if UNITY_EDITOR UnityEditor.EditorApplication.update -= Update; #endif } protected void OnDestroy() { #if UNITY_EDITOR UnityEditor.EditorApplication.update -= Update; #endif if (_Mesh != null) { DestroyImmediate(_Mesh); } } #if UNITY_EDITOR protected void Update() { if (!_Overlay) { return; } if (_Transform == null) { _Transform = transform; } OVROverlay.OverlayShape shape = _Overlay.currentOverlayShape; Vector3 position = _CameraRoot ? _Transform.position - _CameraRoot.position : _Transform.position; Quaternion rotation = _Transform.rotation; Vector3 scale = _Transform.lossyScale; Rect destRectLeft = _Overlay.overrideTextureRectMatrix ? _Overlay.destRectLeft : new Rect(0, 0, 1, 1); Rect destRectRight = _Overlay.overrideTextureRectMatrix ? _Overlay.destRectRight : new Rect(0, 0, 1, 1); Rect srcRectLeft = _Overlay.overrideTextureRectMatrix ? _Overlay.srcRectLeft : new Rect(0, 0, 1, 1); Texture texture = _Overlay.textures[0]; TextureDimension dimension = texture != null ? texture.dimension : TextureDimension.None; // Re-generate the mesh if necessary if (_LastShape != shape || _LastPosition != position || _LastRotation != rotation || _LastScale != scale || _LastDestRectLeft != destRectLeft || _LastDestRectRight != destRectRight || _LastTextureDimension != dimension) { UpdateMesh(shape, position, rotation, scale, GetBoundingRect(destRectLeft, destRectRight), dimension == TextureDimension.Cube); } // Generate the material and update textures if necessary if (_MeshRenderer) { if (_MeshRenderer.sharedMaterial == null || dimension != _LastTextureDimension) { if (_MeshRenderer.sharedMaterial != null) { DestroyImmediate(_MeshRenderer.sharedMaterial); } Material previewMat = null; switch (dimension) { case TextureDimension.Tex2D: previewMat = new Material(Shader.Find("Unlit/Transparent")); break; case TextureDimension.Cube: previewMat = new Material(Shader.Find("Hidden/CubeCopy")); break; } if (previewMat != null) { previewMat.mainTexture = texture; } _MeshRenderer.sharedMaterial = previewMat; } if (_LastSrcRectLeft != srcRectLeft) { _MeshRenderer.sharedMaterial.mainTextureOffset = srcRectLeft.position; _MeshRenderer.sharedMaterial.mainTextureScale = srcRectLeft.size; } } _LastShape = shape; _LastPosition = position; _LastRotation = rotation; _LastScale = scale; _LastDestRectLeft = destRectLeft; _LastDestRectRight = destRectRight; _LastSrcRectLeft = srcRectLeft; _LastTextureDimension = dimension; } #endif private void Initialize() { _MeshFilter = GetComponent(); _MeshRenderer = GetComponent(); _Transform = transform; if (Camera.main && Camera.main.transform.parent) { _CameraRoot = Camera.main.transform.parent; } if (_Overlay) { CreateMesh(); } } public void SetOverlay(OVROverlay overlay) { _Overlay = overlay; CreateMesh(); } public static Rect GetBoundingRect(Rect a, Rect b) { float xMin = Mathf.Min(a.x, b.x); float xMax = Mathf.Max(a.x + a.width, b.x + b.width); float yMin = Mathf.Min(a.y, b.y); float yMax = Mathf.Max(a.y + a.height, b.y + b.height); return new Rect(xMin, yMin, xMax - xMin, yMax - yMin); } private void CreateMesh() { if (_Mesh != null) { DestroyImmediate(_Mesh); } _Mesh = new Mesh { name = "Overlay" }; _Mesh.hideFlags = HideFlags.DontSaveInBuild | HideFlags.DontSaveInEditor; if (_MeshFilter) { _MeshFilter.sharedMesh = _Mesh; } if (_MeshCollider) { _MeshCollider.sharedMesh = _Mesh; } } private void UpdateMesh(OVROverlay.OverlayShape shape, Vector3 position, Quaternion rotation, Vector3 scale, Rect rect, bool cubemap = false) { _Verts.Clear(); _UV.Clear(); _CubeUV.Clear(); _Tris.Clear(); GenerateMesh(_Verts, _UV, _CubeUV, _Tris, shape, position, rotation, scale, rect); _Mesh.Clear(false); _Mesh.SetVertices(_Verts); if (cubemap) { _Mesh.SetUVs(0, _CubeUV); } else { _Mesh.SetUVs(0, _UV); } _Mesh.SetTriangles(_Tris, 0); _Mesh.UploadMeshData(false); } public static void GenerateMesh(List verts, List uvs, List cubeUVs, List tris, OVROverlay.OverlayShape shape, Vector3 position, Quaternion rotation, Vector3 scale, Rect rect) { switch (shape) { case OVROverlay.OverlayShape.Equirect: BuildSphere(verts, uvs, tris, position, rotation, scale, rect); break; case OVROverlay.OverlayShape.Cubemap: case OVROverlay.OverlayShape.OffcenterCubemap: BuildCube(verts, uvs, cubeUVs, tris, position, rotation, scale); break; case OVROverlay.OverlayShape.Quad: BuildQuad(verts, uvs, tris, rect); break; case OVROverlay.OverlayShape.Cylinder: BuildHemicylinder(verts, uvs, tris, scale, rect); break; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector3 InverseTransformVert(in Vector3 vert, in Vector3 position, in Vector3 scale, float worldScale) { return new Vector3( (worldScale * vert.x - position.x) / scale.x, (worldScale * vert.y - position.y) / scale.y, (worldScale * vert.z - position.z) / scale.z); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2 GetSphereUV(float theta, float phi, float expandScale) { float thetaU = expandScale * (theta / (2 * Mathf.PI) - 0.5f) + 0.5f; float phiV = expandScale * phi / Mathf.PI + 0.5f; return new Vector2(thetaU, phiV); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector3 GetSphereVert(float theta, float phi) { return new Vector3(-Mathf.Sin(theta) * Mathf.Cos(phi), Mathf.Sin(phi), -Mathf.Cos(theta) * Mathf.Cos(phi)); } public static void BuildSphere(List verts, List uv, List triangles, Vector3 position, Quaternion rotation, Vector3 scale, Rect rect, float worldScale = 800, int latitudes = 128, int longitudes = 128, float expandCoefficient = 1.0f) { position = Quaternion.Inverse(rotation) * position; latitudes = Mathf.CeilToInt(latitudes * rect.height); longitudes = Mathf.CeilToInt(longitudes * rect.width); float minTheta = Mathf.PI * 2.0f * rect.x; float minPhi = Mathf.PI * (0.5f - rect.y - rect.height); float thetaScale = Mathf.PI * 2.0f * rect.width / longitudes; float phiScale = Mathf.PI * rect.height / latitudes; float expandScale = 1.0f / expandCoefficient; for (int j = 0; j < latitudes + 1; j += 1) { for (int k = 0; k < longitudes + 1; k++) { float theta = minTheta + k * thetaScale; float phi = minPhi + j * phiScale; Vector2 suv = GetSphereUV(theta, phi, expandScale); uv.Add(new Vector2((suv.x - rect.x) / rect.width, (suv.y - rect.y) / rect.height)); Vector3 vert = GetSphereVert(theta, phi); verts.Add(InverseTransformVert(in vert, in position, in scale, worldScale)); } } for (int j = 0; j < latitudes; j++) { for (int k = 0; k < longitudes; k++) { triangles.Add(j * (longitudes + 1) + k); triangles.Add((j + 1) * (longitudes + 1) + k); triangles.Add((j + 1) * (longitudes + 1) + k + 1); triangles.Add((j + 1) * (longitudes + 1) + k + 1); triangles.Add(j * (longitudes + 1) + k + 1); triangles.Add(j * (longitudes + 1) + k); } } } private enum CubeFace { Bottom, Front, Back, Right, Left, Top, COUNT } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2 GetCubeUV(CubeFace face, float sideU, float sideV, float expandScale, float expandOffset) { sideU = sideU * expandScale + expandOffset; sideV = sideV * expandScale + expandOffset; switch (face) { case CubeFace.Bottom: return new Vector2(sideU / 3.0f, sideV / 2.0f); case CubeFace.Front: return new Vector2((1.0f + sideU) / 3.0f, sideV / 2.0f); case CubeFace.Back: return new Vector2((2.0f + sideU) / 3.0f, sideV / 2.0f); case CubeFace.Right: return new Vector2(sideU / 3.0f, (1.0f + sideV) / 2.0f); case CubeFace.Left: return new Vector2((1.0f + sideU) / 3.0f, (1.0f + sideV) / 2.0f); case CubeFace.Top: return new Vector2((2.0f + sideU) / 3.0f, (1.0f + sideV) / 2.0f); default: return Vector2.zero; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector3 GetCubeVert(CubeFace face, float sideU, float sideV) { switch (face) { case CubeFace.Bottom: return new Vector3(0.5f - sideU, -0.5f, 0.5f - sideV); case CubeFace.Front: return new Vector3(0.5f - sideU, -0.5f + sideV, -0.5f); case CubeFace.Back: return new Vector3(-0.5f + sideU, -0.5f + sideV, 0.5f); case CubeFace.Right: return new Vector3(-0.5f, -0.5f + sideV, -0.5f + sideU); case CubeFace.Left: return new Vector3(0.5f, -0.5f + sideV, 0.5f - sideU); case CubeFace.Top: return new Vector3(0.5f - sideU, 0.5f, -0.5f + sideV); default: return Vector3.zero; } } public static void BuildCube(List verts, List uv, List cubeUV, List triangles, Vector3 position, Quaternion rotation, Vector3 scale, float worldScale = 800, int subQuads = 1, float expandCoefficient = 1.01f) { position = Quaternion.Inverse(rotation) * position; int vertsPerSide = (subQuads + 1) * (subQuads + 1); float expandScale = 1.0f / expandCoefficient; float expandOffset = 0.5f - 0.5f / expandCoefficient; for (int i = 0; i < (int)CubeFace.COUNT; i++) { for (int j = 0; j < subQuads + 1; j++) { for (int k = 0; k < subQuads + 1; k++) { float sideU = j / (float)subQuads; float sideV = k / (float)subQuads; uv.Add(GetCubeUV((CubeFace)i, sideU, sideV, expandScale, expandOffset)); Vector3 vert = GetCubeVert((CubeFace)i, sideU, sideV); verts.Add(InverseTransformVert(in vert, in position, in scale, worldScale)); cubeUV.Add(vert.normalized); } } for (int j = 0; j < subQuads; j++) { for (int k = 0; k < subQuads; k++) { triangles.Add(vertsPerSide * i + (j + 1) * (subQuads + 1) + k); triangles.Add(vertsPerSide * i + j * (subQuads + 1) + k); triangles.Add(vertsPerSide * i + (j + 1) * (subQuads + 1) + k + 1); triangles.Add(vertsPerSide * i + (j + 1) * (subQuads + 1) + k + 1); triangles.Add(vertsPerSide * i + j * (subQuads + 1) + k); triangles.Add(vertsPerSide * i + j * (subQuads + 1) + k + 1); } } } } public static void BuildQuad(List verts, List uv, List triangles, Rect rect) { verts.Add(new Vector3(rect.x - 0.5f, 1.0f - rect.y - rect.height - 0.5f, 0.0f)); verts.Add(new Vector3(rect.x - 0.5f, 1.0f - rect.y - 0.5f, 0.0f)); verts.Add(new Vector3(rect.x + rect.width - 0.5f, 1.0f - rect.y - 0.5f, 0.0f)); verts.Add(new Vector3(rect.x + rect.width - 0.5f, 1.0f - rect.y - rect.height - 0.5f, 0.0f)); uv.Add(new Vector2(0.0f, 0.0f)); uv.Add(new Vector2(0.0f, 1.0f)); uv.Add(new Vector2(1.0f, 1.0f)); uv.Add(new Vector2(1.0f, 0.0f)); triangles.Add(0); triangles.Add(1); triangles.Add(2); triangles.Add(2); triangles.Add(3); triangles.Add(0); } public static void BuildHemicylinder(List verts, List uv, List triangles, Vector3 scale, Rect rect, int longitudes = 128) { float height = Mathf.Abs(scale.y) * rect.height; float radius = scale.z; float arcLength = scale.x * rect.width; float arcAngle = arcLength / radius; float minAngle = scale.x * (-0.5f + rect.x) / radius; int columns = Mathf.CeilToInt(longitudes * arcAngle / (2.0f * Mathf.PI)); // we don't want super tall skinny triangles because that can lead to artifacting. // make triangles no more than 2x taller than wide float triangleWidth = arcLength / columns; float ratio = height / triangleWidth; int rows = Mathf.CeilToInt(ratio / 2.0f); for (int j = 0; j < rows + 1; j++) { for (int k = 0; k < columns + 1; k++) { uv.Add(new Vector2(k / (float)columns, 1.0f - j / (float)rows)); verts.Add(new Vector3( // because the scale is used to control the parameters, we need // to reverse multiply by scale to appear correctly Mathf.Sin(minAngle + k * arcAngle / columns) * radius / scale.x, 0.5f - rect.y - rect.height + rect.height * (1.0f - j / (float)rows), Mathf.Cos(minAngle + k * arcAngle / columns) * radius / scale.z)); } } for (int j = 0; j < rows; j++) { for (int k = 0; k < columns; k++) { triangles.Add(j * (columns + 1) + k); triangles.Add((j + 1) * (columns + 1) + k + 1); triangles.Add((j + 1) * (columns + 1) + k); triangles.Add((j + 1) * (columns + 1) + k + 1); triangles.Add(j * (columns + 1) + k); triangles.Add(j * (columns + 1) + k + 1); } } } }