/*
* 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;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
///
/// A component to apply a Colored vignette effect to the camera
///
[RequireComponent(typeof(Camera))]
[ExecuteInEditMode]
public class OVRVignette : MonoBehaviour {
///
/// Controls the number of triangles in the vignette mesh.
///
public enum MeshComplexityLevel
{
VerySimple,
Simple,
Normal,
Detailed,
VeryDetailed
}
///
/// Controls the falloff appearance.
///
public enum FalloffType
{
Linear,
Quadratic
}
private static readonly string QUADRATIC_FALLOFF = "QUADRATIC_FALLOFF";
[SerializeField]
[HideInInspector]
private Shader VignetteShader;
// These are only used at startup.
[SerializeField]
[Tooltip("Controls the number of triangles used for the vignette mesh." +
" Normal is best for most purposes.")]
private MeshComplexityLevel MeshComplexity = MeshComplexityLevel.Normal;
[SerializeField]
[Tooltip("Controls how the falloff looks.")]
private FalloffType Falloff = FalloffType.Linear;
// These can be controlled dynamically at runtime
[Tooltip("The Vertical FOV of the vignette")]
public float VignetteFieldOfView = 60;
[Tooltip("The Aspect ratio of the vignette controls the " +
"Horizontal FOV. (Larger numbers are wider)")]
public float VignetteAspectRatio = 1f;
[Tooltip("The width of the falloff for the vignette in degrees")]
public float VignetteFalloffDegrees = 10f;
[ColorUsage(false)]
[Tooltip("The color of the vignette. Alpha value is ignored")]
public Color VignetteColor;
private Camera _Camera;
private MeshFilter _OpaqueMeshFilter;
private MeshFilter _TransparentMeshFilter;
private MeshRenderer _OpaqueMeshRenderer;
private MeshRenderer _TransparentMeshRenderer;
private Mesh _OpaqueMesh;
private Mesh _TransparentMesh;
private Material _OpaqueMaterial;
private Material _TransparentMaterial;
private int _ShaderScaleAndOffset0Property;
private int _ShaderScaleAndOffset1Property;
private Vector4[] _TransparentScaleAndOffset0 = new Vector4[2];
private Vector4[] _TransparentScaleAndOffset1 = new Vector4[2];
private Vector4[] _OpaqueScaleAndOffset0 = new Vector4[2];
private Vector4[] _OpaqueScaleAndOffset1 = new Vector4[2];
private bool _OpaqueVignetteVisible = false;
private bool _TransparentVignetteVisible = false;
#if UNITY_EDITOR
// in the editor, allow these to be changed at runtime
private MeshComplexityLevel _InitialMeshComplexity;
private FalloffType _InitialFalloff;
#endif
private int GetTriangleCount()
{
switch(MeshComplexity)
{
case MeshComplexityLevel.VerySimple: return 32;
case MeshComplexityLevel.Simple: return 64;
case MeshComplexityLevel.Normal: return 128;
case MeshComplexityLevel.Detailed: return 256;
case MeshComplexityLevel.VeryDetailed: return 512;
default: return 128;
}
}
private void BuildMeshes()
{
#if UNITY_EDITOR
_InitialMeshComplexity = MeshComplexity;
#endif
int triangleCount = GetTriangleCount();
Vector3[] innerVerts = new Vector3[triangleCount];
Vector2[] innerUVs = new Vector2[triangleCount];
Vector3[] outerVerts = new Vector3[triangleCount];
Vector2[] outerUVs = new Vector2[triangleCount];
int[] tris = new int[triangleCount * 3];
for (int i = 0; i < triangleCount; i += 2)
{
float angle = 2 * i * Mathf.PI / triangleCount;
float x = Mathf.Cos(angle);
float y = Mathf.Sin(angle);
outerVerts[i] = new Vector3(x, y, 0);
outerVerts[i + 1] = new Vector3(x, y, 0);
outerUVs[i] = new Vector2(0, 1);
outerUVs[i + 1] = new Vector2(1, 1);
innerVerts[i] = new Vector3(x, y, 0);
innerVerts[i + 1] = new Vector3(x, y, 0);
innerUVs[i] = new Vector2(0, 1);
innerUVs[i + 1] = new Vector2(1, 0);
int ti = i * 3;
tris[ti] = i;
tris[ti + 1] = i + 1;
tris[ti + 2] = (i + 2) % triangleCount;
tris[ti + 3] = i + 1;
tris[ti + 4] = (i + 3) % triangleCount;
tris[ti + 5] = (i + 2) % triangleCount;
}
if (_OpaqueMesh != null)
{
DestroyImmediate(_OpaqueMesh);
}
if (_TransparentMesh != null)
{
DestroyImmediate(_TransparentMesh);
}
_OpaqueMesh = new Mesh()
{
name = "Opaque Vignette Mesh",
hideFlags = HideFlags.HideAndDontSave
};
_TransparentMesh = new Mesh()
{
name = "Transparent Vignette Mesh",
hideFlags = HideFlags.HideAndDontSave
};
_OpaqueMesh.vertices = outerVerts;
_OpaqueMesh.uv = outerUVs;
_OpaqueMesh.triangles = tris;
_OpaqueMesh.UploadMeshData(true);
_OpaqueMesh.bounds = new Bounds(Vector3.zero, Vector3.one * 10000);
_OpaqueMeshFilter.sharedMesh = _OpaqueMesh;
_TransparentMesh.vertices = innerVerts;
_TransparentMesh.uv = innerUVs;
_TransparentMesh.triangles = tris;
_TransparentMesh.UploadMeshData(true);
_TransparentMesh.bounds = new Bounds(Vector3.zero, Vector3.one * 10000);
_TransparentMeshFilter.sharedMesh = _TransparentMesh;
}
private void BuildMaterials()
{
#if UNITY_EDITOR
_InitialFalloff = Falloff;
#endif
if (VignetteShader == null)
{
VignetteShader = Shader.Find("Oculus/OVRVignette");
}
if (VignetteShader == null)
{
Debug.LogError("Could not find Vignette Shader! Vignette will not be drawn!");
return;
}
if (_OpaqueMaterial == null)
{
_OpaqueMaterial = new Material(VignetteShader)
{
name = "Opaque Vignette Material",
hideFlags = HideFlags.HideAndDontSave,
renderQueue = (int)RenderQueue.Background
};
_OpaqueMaterial.SetFloat("_BlendSrc", (float)BlendMode.One);
_OpaqueMaterial.SetFloat("_BlendDst", (float)BlendMode.Zero);
_OpaqueMaterial.SetFloat("_ZWrite", 1);
}
_OpaqueMeshRenderer.sharedMaterial = _OpaqueMaterial;
if (_TransparentMaterial == null)
{
_TransparentMaterial = new Material(VignetteShader)
{
name = "Transparent Vignette Material",
hideFlags = HideFlags.HideAndDontSave,
renderQueue = (int)RenderQueue.Overlay
};
_TransparentMaterial.SetFloat("_BlendSrc", (float)BlendMode.SrcAlpha);
_TransparentMaterial.SetFloat("_BlendDst", (float)BlendMode.OneMinusSrcAlpha);
_TransparentMaterial.SetFloat("_ZWrite", 0);
}
if (Falloff == FalloffType.Quadratic)
{
_TransparentMaterial.EnableKeyword(QUADRATIC_FALLOFF);
}
else
{
_TransparentMaterial.DisableKeyword(QUADRATIC_FALLOFF);
}
_TransparentMeshRenderer.sharedMaterial = _TransparentMaterial;
}
private void OnEnable()
{
#if UNITY_2019_1_OR_NEWER
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
#elif UNITY_2018_1_OR_NEWER
UnityEngine.Experimental.Rendering.RenderPipeline.beginCameraRendering += OnBeginCameraRendering;
#endif
}
private void OnDisable()
{
#if UNITY_2019_1_OR_NEWER
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
#elif UNITY_2018_1_OR_NEWER
UnityEngine.Experimental.Rendering.RenderPipeline.beginCameraRendering -= OnBeginCameraRendering;
#endif
DisableRenderers();
}
private void Awake()
{
_Camera = GetComponent();
_ShaderScaleAndOffset0Property = Shader.PropertyToID("_ScaleAndOffset0");
_ShaderScaleAndOffset1Property = Shader.PropertyToID("_ScaleAndOffset1");
GameObject opaqueObject = new GameObject("Opaque Vignette") { hideFlags = HideFlags.HideAndDontSave };
opaqueObject.transform.SetParent(_Camera.transform, false);
_OpaqueMeshFilter = opaqueObject.AddComponent();
_OpaqueMeshRenderer = opaqueObject.AddComponent();
_OpaqueMeshRenderer.receiveShadows = false;
_OpaqueMeshRenderer.shadowCastingMode = ShadowCastingMode.Off;
_OpaqueMeshRenderer.lightProbeUsage = LightProbeUsage.Off;
_OpaqueMeshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
_OpaqueMeshRenderer.allowOcclusionWhenDynamic = false;
_OpaqueMeshRenderer.enabled = false;
GameObject transparentObject = new GameObject("Transparent Vignette") { hideFlags = HideFlags.HideAndDontSave };
transparentObject.transform.SetParent(_Camera.transform, false);
_TransparentMeshFilter = transparentObject.AddComponent();
_TransparentMeshRenderer = transparentObject.AddComponent();
_TransparentMeshRenderer.receiveShadows = false;
_TransparentMeshRenderer.shadowCastingMode = ShadowCastingMode.Off;
_TransparentMeshRenderer.lightProbeUsage = LightProbeUsage.Off;
_TransparentMeshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
_TransparentMeshRenderer.allowOcclusionWhenDynamic = false;
_TransparentMeshRenderer.enabled = false;
BuildMeshes();
BuildMaterials();
}
private void GetTanFovAndOffsetForStereoEye(Camera.StereoscopicEye eye, out float tanFovX, out float tanFovY, out float offsetX, out float offsetY)
{
var pt = _Camera.GetStereoProjectionMatrix(eye).transpose;
var right = pt * new Vector4(-1, 0, 0, 1);
var left = pt * new Vector4(1, 0, 0, 1);
var up = pt * new Vector4(0, -1, 0, 1);
var down = pt * new Vector4(0, 1, 0, 1);
float rightTanFovX = right.z / right.x;
float leftTanFovX = left.z / left.x;
float upTanFovY = up.z / up.y;
float downTanFovY = down.z / down.y;
offsetX = -(rightTanFovX + leftTanFovX) / 2;
offsetY = -(upTanFovY + downTanFovY) / 2;
tanFovX = (rightTanFovX - leftTanFovX) / 2;
tanFovY = (upTanFovY - downTanFovY) / 2;
}
private void GetTanFovAndOffsetForMonoEye(out float tanFovX, out float tanFovY, out float offsetX, out float offsetY)
{
// When calculating from Unity's camera fields, this is the calculation used.
// We can't use this for stereo eyes because VR projection matrices are usually asymmetric.
tanFovY = Mathf.Tan(Mathf.Deg2Rad * _Camera.fieldOfView * 0.5f);
tanFovX = tanFovY * _Camera.aspect;
offsetX = 0f;
offsetY = 0f;
}
private bool VisibilityTest(float scaleX, float scaleY, float offsetX, float offsetY)
{
// because the corners of our viewport are the furthest from the center of our vignette,
// we only need to test that the farthest corner is outside the vignette ring.
return new Vector2((1 + Mathf.Abs(offsetX)) / scaleX, (1 + Mathf.Abs(offsetY)) / scaleY).sqrMagnitude > 1.0f;
}
private void Update()
{
#if UNITY_EDITOR
if (MeshComplexity != _InitialMeshComplexity)
{
// rebuild meshes
BuildMeshes();
}
if(Falloff != _InitialFalloff)
{
// rebuild materials
BuildMaterials();
}
#endif
// The opaque material could not be created, so just return
if (_OpaqueMaterial == null)
{
return;
}
float tanInnerFovY = Mathf.Tan(VignetteFieldOfView * Mathf.Deg2Rad * 0.5f);
float tanInnerFovX = tanInnerFovY * VignetteAspectRatio;
float tanMiddleFovX = Mathf.Tan((VignetteFieldOfView + VignetteFalloffDegrees) * Mathf.Deg2Rad * 0.5f);
float tanMiddleFovY = tanMiddleFovX * VignetteAspectRatio;
_TransparentVignetteVisible = false;
_OpaqueVignetteVisible = false;
for (int i = 0; i < 2; i++)
{
float tanFovX, tanFovY, offsetX, offsetY;
if (_Camera.stereoEnabled)
{
GetTanFovAndOffsetForStereoEye((Camera.StereoscopicEye)i, out tanFovX, out tanFovY, out offsetX, out offsetY);
}
else
{
GetTanFovAndOffsetForMonoEye(out tanFovX, out tanFovY, out offsetX, out offsetY);
}
float borderScale = new Vector2((1 + Mathf.Abs(offsetX)) / VignetteAspectRatio, 1 + Mathf.Abs(offsetY)).magnitude * 1.01f;
float innerScaleX = tanInnerFovX / tanFovX;
float innerScaleY = tanInnerFovY / tanFovY;
float middleScaleX = tanMiddleFovX / tanFovX;
float middleScaleY = tanMiddleFovY / tanFovY;
float outerScaleX = borderScale * VignetteAspectRatio;
float outerScaleY = borderScale;
// test for visibility.
_TransparentVignetteVisible |= VisibilityTest(innerScaleX, innerScaleY, offsetX, offsetY);
_OpaqueVignetteVisible |= VisibilityTest(middleScaleX, middleScaleY, offsetX, offsetY);
_OpaqueScaleAndOffset0[i] = new Vector4(outerScaleX, outerScaleY, offsetX, offsetY);
_OpaqueScaleAndOffset1[i] = new Vector4(middleScaleX, middleScaleY, offsetX, offsetY);
_TransparentScaleAndOffset0[i] = new Vector4(middleScaleX, middleScaleY, offsetX, offsetY);
_TransparentScaleAndOffset1[i] = new Vector4(innerScaleX, innerScaleY, offsetX, offsetY);
}
// if the vignette falloff is less than or equal to zero, we don't need to draw
// the transparent mesh.
_TransparentVignetteVisible &= VignetteFalloffDegrees > 0.0f;
_OpaqueMaterial.SetVectorArray(_ShaderScaleAndOffset0Property, _OpaqueScaleAndOffset0);
_OpaqueMaterial.SetVectorArray(_ShaderScaleAndOffset1Property, _OpaqueScaleAndOffset1);
_OpaqueMaterial.color = VignetteColor;
_TransparentMaterial.SetVectorArray(_ShaderScaleAndOffset0Property, _TransparentScaleAndOffset0);
_TransparentMaterial.SetVectorArray(_ShaderScaleAndOffset1Property, _TransparentScaleAndOffset1);
_TransparentMaterial.color = VignetteColor;
}
private void EnableRenderers()
{
_OpaqueMeshRenderer.enabled = _OpaqueVignetteVisible;
_TransparentMeshRenderer.enabled = _TransparentVignetteVisible;
}
private void DisableRenderers()
{
_OpaqueMeshRenderer.enabled = false;
_TransparentMeshRenderer.enabled = false;
}
// Objects are enabled on pre cull and disabled on post render so they only draw in this camera
private void OnPreCull()
{
EnableRenderers();
}
private void OnPostRender()
{
DisableRenderers();
}
#if UNITY_2019_1_OR_NEWER
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
#else
private void OnBeginCameraRendering(Camera camera)
#endif
{
if (camera == _Camera)
{
EnableRenderers();
}
else
{
DisableRenderers();
}
}
}