Mirror Reflections in VR

Hej there,

I’m currently working on a little VR project that needs to have mirror reflections. I’m using this script: http://wiki.unity3d.com/index.php/MirrorReflection4 for the reflections.

using UnityEngine;
using System.Collections;

// This is in fact just the Water script from Pro Standard Assets,
// just with refraction stuff removed.

[ExecuteInEditMode] // Make mirror live-update even when not in play mode
public class MirrorReflection : MonoBehaviour {
    public bool m_DisablePixelLights = true;
    public int m_TextureSize = 256;
    public float m_ClipPlaneOffset = 0.07f;

    public LayerMask m_ReflectLayers = -1;

    private Hashtable m_ReflectionCameras = new Hashtable(); // Camera -> Camera table

    private RenderTexture m_ReflectionTexture = null;
    private int m_OldReflectionTextureSize = 0;

    private static bool s_InsideRendering = false;

    // This is called when it's known that the object will be rendered by some
    // camera. We render reflections and do other updates here.
    // Because the script executes in edit mode, reflections for the scene view
    // camera will just work!
    public void OnWillRenderObject() {
        var rend = GetComponent<Renderer>();
        if (!enabled || !rend || !rend.sharedMaterial || !rend.enabled)
            return;

        Camera cam = Camera.current;
        if (!cam)
            return;

        // Safeguard from recursive reflections.       
        if (s_InsideRendering)
            return;
        s_InsideRendering = true;

        Camera reflectionCamera;
        CreateMirrorObjects(cam, out reflectionCamera);

        // find out the reflection plane: position and normal in world space
        Vector3 pos = transform.position;
        Vector3 normal = transform.up;

        // Optionally disable pixel lights for reflection
        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = 0;

        UpdateCameraModes(cam, reflectionCamera);

        // Render reflection
        // Reflect camera around reflection plane
        float d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;
        Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

        Matrix4x4 reflection = Matrix4x4.zero;
        CalculateReflectionMatrix(ref reflection, reflectionPlane);
        Vector3 oldpos = cam.transform.position;
        Vector3 newpos = reflection.MultiplyPoint(oldpos);
        reflectionCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;

        // Setup oblique projection matrix so that near plane is our reflection
        // plane. This way we clip everything below/above it for free.
        Vector4 clipPlane = CameraSpacePlane(reflectionCamera, pos, normal, 1.0f);
        //Matrix4x4 projection = cam.projectionMatrix;
        Matrix4x4 projection = cam.CalculateObliqueMatrix(clipPlane);
        reflectionCamera.projectionMatrix = projection;

        reflectionCamera.cullingMask = ~(1 << 4) & m_ReflectLayers.value; // never render water layer
        reflectionCamera.targetTexture = m_ReflectionTexture;
        GL.invertCulling = true;
        reflectionCamera.transform.position = newpos;
        Vector3 euler = cam.transform.eulerAngles;
        reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);
        reflectionCamera.Render();
        reflectionCamera.transform.position = oldpos;
        GL.invertCulling = false;
        Material[] materials = rend.sharedMaterials;
        foreach (Material mat in materials) {
            if (mat.HasProperty("_ReflectionTex"))
                mat.SetTexture("_ReflectionTex", m_ReflectionTexture);
        }

        // Restore pixel light count
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = oldPixelLightCount;

        s_InsideRendering = false;
    }


    // Cleanup all the objects we possibly have created
    void OnDisable() {
        if (m_ReflectionTexture) {
            DestroyImmediate(m_ReflectionTexture);
            m_ReflectionTexture = null;
        }
        foreach (DictionaryEntry kvp in m_ReflectionCameras)
            DestroyImmediate(((Camera)kvp.Value).gameObject);
        m_ReflectionCameras.Clear();
    }


    private void UpdateCameraModes(Camera src, Camera dest) {
        if (dest == null)
            return;
        // set camera to clear the same way as current camera
        dest.clearFlags = src.clearFlags;
        dest.backgroundColor = src.backgroundColor;
        if (src.clearFlags == CameraClearFlags.Skybox) {
            Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;
            Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;
            if (!sky || !sky.material) {
                mysky.enabled = false;
            } else {
                mysky.enabled = true;
                mysky.material = sky.material;
            }
        }
        // update other values to match current camera.
        // even if we are supplying custom camera&projection matrices,
        // some of values are used elsewhere (e.g. skybox uses far plane)
        dest.farClipPlane = src.farClipPlane;
        dest.nearClipPlane = src.nearClipPlane;
        dest.orthographic = src.orthographic;
        dest.fieldOfView = src.fieldOfView;
        dest.aspect = src.aspect;
        dest.orthographicSize = src.orthographicSize;
    }

    // On-demand create any objects we need
    private void CreateMirrorObjects(Camera currentCamera, out Camera reflectionCamera) {
        reflectionCamera = null;

        // Reflection render texture
        if (!m_ReflectionTexture || m_OldReflectionTextureSize != m_TextureSize) {
            if (m_ReflectionTexture)
                DestroyImmediate(m_ReflectionTexture);
            m_ReflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);
            m_ReflectionTexture.name = "__MirrorReflection" + GetInstanceID();
            m_ReflectionTexture.isPowerOfTwo = true;
            m_ReflectionTexture.hideFlags = HideFlags.DontSave;
            m_OldReflectionTextureSize = m_TextureSize;
        }

        // Camera for reflection
        reflectionCamera = m_ReflectionCameras[currentCamera] as Camera;
        if (!reflectionCamera) // catch both not-in-dictionary and in-dictionary-but-deleted-GO
        {
            GameObject go = new GameObject("Mirror Refl Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
            reflectionCamera = go.GetComponent<Camera>();
            reflectionCamera.enabled = false;
            reflectionCamera.transform.position = transform.position;
            reflectionCamera.transform.rotation = transform.rotation;
            reflectionCamera.gameObject.AddComponent<FlareLayer>();
            go.hideFlags = HideFlags.HideAndDontSave;
            m_ReflectionCameras[currentCamera] = reflectionCamera;
        }
    }

    // Extended sign: returns -1, 0 or 1 based on sign of a
    private static float sgn(float a) {
        if (a > 0.0f) return 1.0f;
        if (a < 0.0f) return -1.0f;
        return 0.0f;
    }

    // Given position/normal of the plane, calculates plane in camera space.
    private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign) {
        Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;
        Matrix4x4 m = cam.worldToCameraMatrix;
        Vector3 cpos = m.MultiplyPoint(offsetPos);
        Vector3 cnormal = m.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }

    // Calculates reflection matrix around the given plane
    private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane) {
        reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMat.m01 = (-2F * plane[0] * plane[1]);
        reflectionMat.m02 = (-2F * plane[0] * plane[2]);
        reflectionMat.m03 = (-2F * plane[3] * plane[0]);

        reflectionMat.m10 = (-2F * plane[1] * plane[0]);
        reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMat.m12 = (-2F * plane[1] * plane[2]);
        reflectionMat.m13 = (-2F * plane[3] * plane[1]);

        reflectionMat.m20 = (-2F * plane[2] * plane[0]);
        reflectionMat.m21 = (-2F * plane[2] * plane[1]);
        reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMat.m23 = (-2F * plane[3] * plane[2]);

        reflectionMat.m30 = 0F;
        reflectionMat.m31 = 0F;
        reflectionMat.m32 = 0F;
        reflectionMat.m33 = 1F;
    }
}

Everything is working fine until i use my HTC Vive. The Reflections in the mirror seem to drift apart depending on my SteamVR’s CameraPosition. The further I move away from the mirror the more the reflections are drifting apart and vise versa. I googled for a while and read something about the vergence of the eyes but i don’t know how to implement such things in the script.

-Any help is appreciated

1 Like

for stereo mirror reflection see https://www.assetstore.unity3d.com/en/#!/content/71255

3 Likes

Since this was the first post about VR mirror I found when Googling this issue.
Here is modified http://wiki.unity3d.com/index.php/MirrorReflection4 that works acceptably in VR, it uses ideas from: VR-Mirrors/Assets/MirrorEffect/MirrorCamera.cs at master · AADProductions/VR-Mirrors · GitHub

ShaderLab - Mirror.shader

// original source from: http://wiki.unity3d.com/index.php/MirrorReflection4
Shader "FX/MirrorReflection"
{
    Properties
    {
        _MainTex ("_MainTex", 2D) = "white" {}
        _ReflectionTexLeft ("_ReflectionTexLeft", 2D) = "white" {}
        _ReflectionTexRight ("_ReflectionTexRight", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 refl : TEXCOORD1;
                float4 pos : SV_POSITION;
            };
            float4 _MainTex_ST;
            v2f vert(float4 pos : POSITION, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (pos);
                o.uv = TRANSFORM_TEX(uv, _MainTex);
                o.refl = ComputeScreenPos (o.pos);
                return o;
            }
            sampler2D _MainTex;
            sampler2D _ReflectionTexLeft;
            sampler2D _ReflectionTexRight;
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 tex = tex2D(_MainTex, i.uv);
                fixed4 refl;
                if (unity_StereoEyeIndex == 0) refl = tex2Dproj(_ReflectionTexLeft, UNITY_PROJ_COORD(i.refl));
                else refl = tex2Dproj(_ReflectionTexRight, UNITY_PROJ_COORD(i.refl));
                return tex * refl;
            }
            ENDCG
        }
    }
}

C# - MirrorReflection.cs

// original source from: http://wiki.unity3d.com/index.php/MirrorReflection4

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

// This is in fact just the Water script from Pro Standard Assets,
// just with refraction stuff removed.

[ExecuteInEditMode] // Make mirror live-update even when not in play mode
public class MirrorReflection : MonoBehaviour
{
    public bool m_DisablePixelLights = true;
    public int m_TextureSize = 256;
    public float m_ClipPlaneOffset = 0.07f;
    public int m_framesNeededToUpdate = 0;

    public LayerMask m_ReflectLayers = -1;

    private Dictionary<Camera, Camera> m_ReflectionCameras = new Dictionary<Camera, Camera>();

    private RenderTexture m_ReflectionTextureLeft = null;
    private RenderTexture m_ReflectionTextureRight = null;
    private int m_OldReflectionTextureSize = 0;

    private int m_frameCounter = 0;

    private static bool s_InsideRendering = false;

    // This is called when it's known that the object will be rendered by some
    // camera. We render reflections and do other updates here.
    // Because the script executes in edit mode, reflections for the scene view
    // camera will just work!
    public void OnWillRenderObject()
    {
        if (m_frameCounter > 0)
        {
            m_frameCounter--;
            return;
        }

        var rend = GetComponent<Renderer>();
        if (!enabled || !rend || !rend.sharedMaterial || !rend.enabled)
            return;

        Camera cam = Camera.current;
        if (!cam)
            return;

        // Safeguard from recursive reflections.     
        if (s_InsideRendering)
            return;
        s_InsideRendering = true;

        m_frameCounter = m_framesNeededToUpdate;

        RenderCamera(cam, rend, Camera.StereoscopicEye.Left, ref m_ReflectionTextureLeft);
        if (cam.stereoEnabled)
            RenderCamera(cam, rend, Camera.StereoscopicEye.Right, ref m_ReflectionTextureRight);
    }

    private void RenderCamera(Camera cam, Renderer rend, Camera.StereoscopicEye eye, ref RenderTexture reflectionTexture)
    {
        Camera reflectionCamera;
        CreateMirrorObjects(cam, eye, out reflectionCamera, ref reflectionTexture);

        // find out the reflection plane: position and normal in world space
        Vector3 pos = transform.position;
        Vector3 normal = transform.up;

        // Optionally disable pixel lights for reflection
        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = 0;

        CopyCameraProperties(cam, reflectionCamera);

        // Render reflection
        // Reflect camera around reflection plane
        float d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;
        Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

        Matrix4x4 reflection = Matrix4x4.zero;
        CalculateReflectionMatrix(ref reflection, reflectionPlane);

        Vector3 oldEyePos;
        Matrix4x4 worldToCameraMatrix;
        if (cam.stereoEnabled)
        {
            worldToCameraMatrix = cam.GetStereoViewMatrix(eye) * reflection;

            var eyeOffset = SteamVR.instance.eyes[(int)eye].pos;
            eyeOffset.z = 0.0f;
            oldEyePos = cam.transform.position + cam.transform.TransformVector(eyeOffset);
        }
        else
        {
            worldToCameraMatrix = cam.worldToCameraMatrix * reflection;
            oldEyePos = cam.transform.position;
        }

        Vector3 newEyePos = reflection.MultiplyPoint(oldEyePos);
        reflectionCamera.transform.position = newEyePos;

        reflectionCamera.worldToCameraMatrix = worldToCameraMatrix;

        // Setup oblique projection matrix so that near plane is our reflection
        // plane. This way we clip everything below/above it for free.
        Vector4 clipPlane = CameraSpacePlane(worldToCameraMatrix, pos, normal, 1.0f);

        Matrix4x4 projectionMatrix;
        if (cam.stereoEnabled) projectionMatrix = HMDMatrix4x4ToMatrix4x4(SteamVR.instance.hmd.GetProjectionMatrix((Valve.VR.EVREye)eye, cam.nearClipPlane, cam.farClipPlane));
        else projectionMatrix = cam.projectionMatrix;
     
        //projectionMatrix = cam.CalculateObliqueMatrix(clipPlane);
        MakeProjectionMatrixOblique(ref projectionMatrix, clipPlane);

        reflectionCamera.projectionMatrix = projectionMatrix;
        reflectionCamera.cullingMask = m_ReflectLayers.value;
        reflectionCamera.targetTexture = reflectionTexture;
        GL.invertCulling = true;
        //Vector3 euler = cam.transform.eulerAngles;
        //reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);
        reflectionCamera.transform.rotation = cam.transform.rotation;
        reflectionCamera.Render();
        //reflectionCamera.transform.position = oldEyePos;
        GL.invertCulling = false;
        Material[] materials = rend.sharedMaterials;
        string property = "_ReflectionTex" + eye.ToString();
        foreach (Material mat in materials)
        {
            if (mat.HasProperty(property))
                mat.SetTexture(property, reflectionTexture);
        }

        // Restore pixel light count
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = oldPixelLightCount;

        s_InsideRendering = false;
    }


    // Cleanup all the objects we possibly have created
    void OnDisable()
    {
        if (m_ReflectionTextureLeft)
        {
            DestroyImmediate(m_ReflectionTextureLeft);
            m_ReflectionTextureLeft = null;
        }
        if (m_ReflectionTextureRight)
        {
            DestroyImmediate(m_ReflectionTextureRight);
            m_ReflectionTextureRight = null;
        }
        foreach (var kvp in m_ReflectionCameras)
            DestroyImmediate(((Camera)kvp.Value).gameObject);
        m_ReflectionCameras.Clear();
    }


    private void CopyCameraProperties(Camera src, Camera dest)
    {
        if (dest == null)
            return;
        // set camera to clear the same way as current camera
        dest.clearFlags = src.clearFlags;
        dest.backgroundColor = src.backgroundColor;
        if (src.clearFlags == CameraClearFlags.Skybox)
        {
            Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;
            Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;
            if (!sky || !sky.material)
            {
                mysky.enabled = false;
            }
            else
            {
                mysky.enabled = true;
                mysky.material = sky.material;
            }
        }
        // update other values to match current camera.
        // even if we are supplying custom camera&projection matrices,
        // some of values are used elsewhere (e.g. skybox uses far plane)
        dest.farClipPlane = src.farClipPlane;
        dest.nearClipPlane = src.nearClipPlane;
        dest.orthographic = src.orthographic;
        dest.fieldOfView = src.fieldOfView;
        dest.aspect = src.aspect;
        dest.orthographicSize = src.orthographicSize;
    }

    // On-demand create any objects we need
    private void CreateMirrorObjects(Camera currentCamera, Camera.StereoscopicEye eye, out Camera reflectionCamera, ref RenderTexture reflectionTexture)
    {
        reflectionCamera = null;


        // Reflection render texture
        if (!reflectionTexture || m_OldReflectionTextureSize != m_TextureSize)
        {
            if (reflectionTexture)
                DestroyImmediate(reflectionTexture);
            reflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);
            reflectionTexture.name = "__MirrorReflection" + eye.ToString() + GetInstanceID();
            reflectionTexture.isPowerOfTwo = true;
            reflectionTexture.hideFlags = HideFlags.DontSave;
            m_OldReflectionTextureSize = m_TextureSize;
        }

        // Camera for reflection
        if (!m_ReflectionCameras.TryGetValue(currentCamera, out reflectionCamera)) // catch both not-in-dictionary and in-dictionary-but-deleted-GO
        {
            GameObject go = new GameObject("Mirror Reflection Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
            reflectionCamera = go.GetComponent<Camera>();
            reflectionCamera.enabled = false;
            reflectionCamera.transform.position = transform.position;
            reflectionCamera.transform.rotation = transform.rotation;
            reflectionCamera.gameObject.AddComponent<FlareLayer>();
            go.hideFlags = HideFlags.DontSave;
            m_ReflectionCameras.Add(currentCamera, reflectionCamera);
        }
    }

    // Extended sign: returns -1, 0 or 1 based on sign of a
    private static float sgn(float a)
    {
        if (a > 0.0f) return 1.0f;
        if (a < 0.0f) return -1.0f;
        return 0.0f;
    }

    // Given position/normal of the plane, calculates plane in camera space.
    private Vector4 CameraSpacePlane(Matrix4x4 worldToCameraMatrix, Vector3 pos, Vector3 normal, float sideSign)
    {
        Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;
        Vector3 cpos = worldToCameraMatrix.MultiplyPoint(offsetPos);
        Vector3 cnormal = worldToCameraMatrix.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }

    // Calculates reflection matrix around the given plane
    private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
    {
        reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMat.m01 = (-2F * plane[0] * plane[1]);
        reflectionMat.m02 = (-2F * plane[0] * plane[2]);
        reflectionMat.m03 = (-2F * plane[3] * plane[0]);

        reflectionMat.m10 = (-2F * plane[1] * plane[0]);
        reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMat.m12 = (-2F * plane[1] * plane[2]);
        reflectionMat.m13 = (-2F * plane[3] * plane[1]);

        reflectionMat.m20 = (-2F * plane[2] * plane[0]);
        reflectionMat.m21 = (-2F * plane[2] * plane[1]);
        reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMat.m23 = (-2F * plane[3] * plane[2]);

        reflectionMat.m30 = 0F;
        reflectionMat.m31 = 0F;
        reflectionMat.m32 = 0F;
        reflectionMat.m33 = 1F;
    }

    // taken from http://www.terathon.com/code/oblique.html
    private static void MakeProjectionMatrixOblique(ref Matrix4x4 matrix, Vector4 clipPlane)
    {
        Vector4 q;

        // Calculate the clip-space corner point opposite the clipping plane
        // as (sgn(clipPlane.x), sgn(clipPlane.y), 1, 1) and
        // transform it into camera space by multiplying it
        // by the inverse of the projection matrix

        q.x = (sgn(clipPlane.x) + matrix[8]) / matrix[0];
        q.y = (sgn(clipPlane.y) + matrix[9]) / matrix[5];
        q.z = -1.0F;
        q.w = (1.0F + matrix[10]) / matrix[14];

        // Calculate the scaled plane vector
        Vector4 c = clipPlane * (2.0F / Vector3.Dot(clipPlane, q));

        // Replace the third row of the projection matrix
        matrix[2] = c.x;
        matrix[6] = c.y;
        matrix[10] = c.z + 1.0F;
        matrix[14] = c.w;
    }

    protected Matrix4x4 HMDMatrix4x4ToMatrix4x4(Valve.VR.HmdMatrix44_t input)
    {
        var m = Matrix4x4.identity;

        m[0, 0] = input.m0;
        m[0, 1] = input.m1;
        m[0, 2] = input.m2;
        m[0, 3] = input.m3;

        m[1, 0] = input.m4;
        m[1, 1] = input.m5;
        m[1, 2] = input.m6;
        m[1, 3] = input.m7;

        m[2, 0] = input.m8;
        m[2, 1] = input.m9;
        m[2, 2] = input.m10;
        m[2, 3] = input.m11;

        m[3, 0] = input.m12;
        m[3, 1] = input.m13;
        m[3, 2] = input.m14;
        m[3, 3] = input.m15;

        return m;
    }

}
5 Likes

Version for Oculus Rift:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.XR;


// This is in fact just the Water script from Pro Standard Assets,
// just with refraction stuff removed.

[ExecuteInEditMode] // Make mirror live-update even when not in play mode
public class MirrorReflection : MonoBehaviour
{
    public bool m_DisablePixelLights = true;
    public int m_TextureSize = 256;
    public float m_ClipPlaneOffset = 0.07f;
    public int m_framesNeededToUpdate = 0;

    public LayerMask m_ReflectLayers = -1;

    private Dictionary<Camera, Camera> m_ReflectionCameras = new Dictionary<Camera, Camera>();

    private RenderTexture m_ReflectionTextureLeft = null;
    private RenderTexture m_ReflectionTextureRight = null;
    private int m_OldReflectionTextureSize = 0;

    private int m_frameCounter = 0;

    private static bool s_InsideRendering = false;

    // This is called when it's known that the object will be rendered by some
    // camera. We render reflections and do other updates here.
    // Because the script executes in edit mode, reflections for the scene view
    // camera will just work!
    public void OnWillRenderObject()
    {
        if (m_frameCounter > 0)
        {
            m_frameCounter--;
            return;
        }

        var rend = GetComponent<Renderer>();
        if (!enabled || !rend || !rend.sharedMaterial || !rend.enabled)
            return;

        Camera cam = Camera.current;
        if (!cam)
            return;

        // Safeguard from recursive reflections.  
        if (s_InsideRendering)
            return;
        s_InsideRendering = true;

        m_frameCounter = m_framesNeededToUpdate;

        RenderCamera(cam, rend, Camera.StereoscopicEye.Left, ref m_ReflectionTextureLeft);
        if (cam.stereoEnabled)
            RenderCamera(cam, rend, Camera.StereoscopicEye.Right, ref m_ReflectionTextureRight);
    }

    private void RenderCamera(Camera cam, Renderer rend, Camera.StereoscopicEye eye, ref RenderTexture reflectionTexture)
    {
        Camera reflectionCamera;
        CreateMirrorObjects(cam, eye, out reflectionCamera, ref reflectionTexture);

        // find out the reflection plane: position and normal in world space
        Vector3 pos = transform.position;
        Vector3 normal = transform.up;

        // Optionally disable pixel lights for reflection
        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = 0;

        CopyCameraProperties(cam, reflectionCamera);

        // Render reflection
        // Reflect camera around reflection plane
        float d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;
        Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

        Matrix4x4 reflection = Matrix4x4.zero;
        CalculateReflectionMatrix(ref reflection, reflectionPlane);

        Vector3 oldEyePos;
        Matrix4x4 worldToCameraMatrix;
        if (cam.stereoEnabled)
        {
            worldToCameraMatrix = cam.GetStereoViewMatrix(eye) * reflection;
            Vector3 eyeOffset;
            if (eye == Camera.StereoscopicEye.Left)
                eyeOffset = InputTracking.GetLocalPosition(XRNode.LeftEye);
            else
                eyeOffset = InputTracking.GetLocalPosition(XRNode.RightEye);
            eyeOffset.z = 0.0f;
            oldEyePos = cam.transform.position + cam.transform.TransformVector(eyeOffset);
        }
        else
        {
            worldToCameraMatrix = cam.worldToCameraMatrix * reflection;
            oldEyePos = cam.transform.position;
        }

        Vector3 newEyePos = reflection.MultiplyPoint(oldEyePos);
        reflectionCamera.transform.position = newEyePos;

        reflectionCamera.worldToCameraMatrix = worldToCameraMatrix;

        // Setup oblique projection matrix so that near plane is our reflection
        // plane. This way we clip everything below/above it for free.
        Vector4 clipPlane = CameraSpacePlane(worldToCameraMatrix, pos, normal, 1.0f);

        Matrix4x4 projectionMatrix;



        //if (cam.stereoEnabled) projectionMatrix = HMDMatrix4x4ToMatrix4x4(cam.GetStereoProjectionMatrix(Camera.StereoscopicEye.Left));
        //else
        //if (cam.stereoEnabled)
        //    projectionMatrix = HMDMatrix4x4ToMatrix4x4(SteamVR.instance.hmd.GetProjectionMatrix((Valve.VR.EVREye)eye, cam.nearClipPlane, cam.farClipPlane));
        //else
        if (cam.stereoEnabled)
            projectionMatrix = cam.GetStereoProjectionMatrix(eye);
        else
            projectionMatrix = cam.projectionMatrix;
        //projectionMatrix = cam.CalculateObliqueMatrix(clipPlane);
        MakeProjectionMatrixOblique(ref projectionMatrix, clipPlane);

        reflectionCamera.projectionMatrix = projectionMatrix;
        reflectionCamera.cullingMask = m_ReflectLayers.value;
        reflectionCamera.targetTexture = reflectionTexture;
        GL.invertCulling = true;
        //Vector3 euler = cam.transform.eulerAngles;
        //reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);
        reflectionCamera.transform.rotation = cam.transform.rotation;
        reflectionCamera.Render();
        //reflectionCamera.transform.position = oldEyePos;
        GL.invertCulling = false;
        Material[] materials = rend.sharedMaterials;
        string property = "_ReflectionTex" + eye.ToString();
        foreach (Material mat in materials)
        {
            if (mat.HasProperty(property))
                mat.SetTexture(property, reflectionTexture);
        }

        // Restore pixel light count
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = oldPixelLightCount;

        s_InsideRendering = false;
    }


    // Cleanup all the objects we possibly have created
    void OnDisable()
    {
        if (m_ReflectionTextureLeft)
        {
            DestroyImmediate(m_ReflectionTextureLeft);
            m_ReflectionTextureLeft = null;
        }
        if (m_ReflectionTextureRight)
        {
            DestroyImmediate(m_ReflectionTextureRight);
            m_ReflectionTextureRight = null;
        }
        foreach (var kvp in m_ReflectionCameras)
            DestroyImmediate(((Camera)kvp.Value).gameObject);
        m_ReflectionCameras.Clear();
    }


    private void CopyCameraProperties(Camera src, Camera dest)
    {
        if (dest == null)
            return;
        // set camera to clear the same way as current camera
        dest.clearFlags = src.clearFlags;
        dest.backgroundColor = src.backgroundColor;
        if (src.clearFlags == CameraClearFlags.Skybox)
        {
            Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;
            Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;
            if (!sky || !sky.material)
            {
                mysky.enabled = false;
            }
            else
            {
                mysky.enabled = true;
                mysky.material = sky.material;
            }
        }
        // update other values to match current camera.
        // even if we are supplying custom camera&projection matrices,
        // some of values are used elsewhere (e.g. skybox uses far plane)
        dest.farClipPlane = src.farClipPlane;
        dest.nearClipPlane = src.nearClipPlane;
        dest.orthographic = src.orthographic;
        dest.fieldOfView = src.fieldOfView;
        dest.aspect = src.aspect;
        dest.orthographicSize = src.orthographicSize;
    }

    // On-demand create any objects we need
    private void CreateMirrorObjects(Camera currentCamera, Camera.StereoscopicEye eye, out Camera reflectionCamera, ref RenderTexture reflectionTexture)
    {
        reflectionCamera = null;


        // Reflection render texture
        if (!reflectionTexture || m_OldReflectionTextureSize != m_TextureSize)
        {
            if (reflectionTexture)
                DestroyImmediate(reflectionTexture);
            reflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);
            reflectionTexture.name = "__MirrorReflection" + eye.ToString() + GetInstanceID();
            reflectionTexture.isPowerOfTwo = true;
            reflectionTexture.hideFlags = HideFlags.DontSave;
            m_OldReflectionTextureSize = m_TextureSize;
        }

        // Camera for reflection
        if (!m_ReflectionCameras.TryGetValue(currentCamera, out reflectionCamera)) // catch both not-in-dictionary and in-dictionary-but-deleted-GO
        {
            GameObject go = new GameObject("Mirror Reflection Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
            reflectionCamera = go.GetComponent<Camera>();
            reflectionCamera.enabled = false;
            reflectionCamera.transform.position = transform.position;
            reflectionCamera.transform.rotation = transform.rotation;
            reflectionCamera.gameObject.AddComponent<FlareLayer>();
            go.hideFlags = HideFlags.DontSave;
            m_ReflectionCameras.Add(currentCamera, reflectionCamera);
        }
    }

    // Extended sign: returns -1, 0 or 1 based on sign of a
    private static float sgn(float a)
    {
        if (a > 0.0f) return 1.0f;
        if (a < 0.0f) return -1.0f;
        return 0.0f;
    }

    // Given position/normal of the plane, calculates plane in camera space.
    private Vector4 CameraSpacePlane(Matrix4x4 worldToCameraMatrix, Vector3 pos, Vector3 normal, float sideSign)
    {
        Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;
        Vector3 cpos = worldToCameraMatrix.MultiplyPoint(offsetPos);
        Vector3 cnormal = worldToCameraMatrix.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }

    // Calculates reflection matrix around the given plane
    private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
    {
        reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMat.m01 = (-2F * plane[0] * plane[1]);
        reflectionMat.m02 = (-2F * plane[0] * plane[2]);
        reflectionMat.m03 = (-2F * plane[3] * plane[0]);

        reflectionMat.m10 = (-2F * plane[1] * plane[0]);
        reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMat.m12 = (-2F * plane[1] * plane[2]);
        reflectionMat.m13 = (-2F * plane[3] * plane[1]);

        reflectionMat.m20 = (-2F * plane[2] * plane[0]);
        reflectionMat.m21 = (-2F * plane[2] * plane[1]);
        reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMat.m23 = (-2F * plane[3] * plane[2]);

        reflectionMat.m30 = 0F;
        reflectionMat.m31 = 0F;
        reflectionMat.m32 = 0F;
        reflectionMat.m33 = 1F;
    }

    // taken from http://www.terathon.com/code/oblique.html
    private static void MakeProjectionMatrixOblique(ref Matrix4x4 matrix, Vector4 clipPlane)
    {
        Vector4 q;

        // Calculate the clip-space corner point opposite the clipping plane
        // as (sgn(clipPlane.x), sgn(clipPlane.y), 1, 1) and
        // transform it into camera space by multiplying it
        // by the inverse of the projection matrix

        q.x = (sgn(clipPlane.x) + matrix[8]) / matrix[0];
        q.y = (sgn(clipPlane.y) + matrix[9]) / matrix[5];
        q.z = -1.0F;
        q.w = (1.0F + matrix[10]) / matrix[14];

        // Calculate the scaled plane vector
        Vector4 c = clipPlane * (2.0F / Vector3.Dot(clipPlane, q));

        // Replace the third row of the projection matrix
        matrix[2] = c.x;
        matrix[6] = c.y;
        matrix[10] = c.z + 1.0F;
        matrix[14] = c.w;
    }

}
9 Likes

I got this error:

Assets/Scripts/MirrorReflection.cs(305,49): error CS0246: The type or namespace name `Valve’ could not be found. Are you missing an assembly reference?

Any clue?

1 Like

This namespace from OpenVR api plugin - https://assetstore.unity.com/packages/tools/particles-effects/vive-stereo-rendering-toolkit-71255

But for Oculus this is not need. Simple remove this method…

1 Like

Thanks, I think I just used the wrong script :confused:

Has anyone been able to get this reflection script working in Single-Pass?

2 Likes

I have another question: this solution seems to work by rendering reflections twice, once for each eye, which may be computationally expensive. In the use case I am working with, most of the reflected objects are very far away, making the stereo parallax between L/R negligible. In this case the second render would be redundant and making a mono reflection sufficient in theory. However, when I try to modify the script/shader so that the same rendertexture is applied to both eyes, the reflections appear incorrect when seen from inside the Oculus Rift (the two images appear offset wrong, especially when tilting your head to the right/left). This seems to specifically occur in Unity 2017.4.11 and later (The release notes for this version mention asymmetric FOV support for Rift; perhaps this is related). Is there a way to make mono reflections appear OK?

The shader offsets the UVs for the reflection based on the current Stereo Eye Index, because of the assumption that the images are different. You could try removing the offset altogether, or changing it to always use the offset of whatever eye you chose to use twice.

I later found out that the asymmetric FOV rendering feature is indeed the cause, as it is specifically mentioned in Oculus documentation that some shaders will break: https://developer.oculus.com/blog/tech-note-asymmetric-field-of-view-faq/ Unfortunately, unlike some other shader-breaking optimizations like single pass stereo, I cannot find a way to disable asymmetric FOV.

Pardon my not being very bright with shader code, but is it correct to assume the “UVs for the reflection” you mention is i.refl in the Mirror.shader code? Or is it some other value? I experimented around modifying the i.refl.xy values, but they never seem to appear right.

oh, my mistake! I’ve been using my own variant of this shader for a while now and I didn’t realize that I had changed my versikon of the shader a bunch. The version of this shader that i’ve been using renders a single double-wide texture for the reflection and adjusts the UVs for single-pass stereo. The only issue is for some reason it’s unable to reflect the skybox correctly, so I’ve been using a gigantic sphere for my skybox instead. Asymmetric FOV should only be needed for multi-pass, right?

Ideally, everyone should benefit if it works both in single and multi-pass, but for the time being, the project I am working on is in multi-pass since more stuff begins to break if I switch it to single-pass.

Is there an example of the working Single-pass version?

I’m having trouble locating the project files from when i did single-pass stereo mirrors, but I’m going to try to do it again in the next few days! If I do, I’ll share the code here.

1 Like

You are an absolute hero, please do share your findings!

Did anyone have a single pass version of mirrors working?

I do, somewhere! check back here again after the weekend, i’m hoping to re-do it by then.

1 Like

I don’t really undestand what you are trying to achieve, but can’t you simply use a Camera with appropriate culling to render to a RenderTexture ? At least that’s what I’m doing and it’s working fine

update: I’ve been extremely busy! I’ll do the thing… at some point. Sorry fam ;-;

The normal method of planar reflections - with an oblique near clip plane on a second camera - works just fine for Multi-pass VR and non-VR. Multi-pass is the easiest and least performant of the stereo rendering modes; it renders the whole scene for left eye, and then again for the right eye, so each eye renders like a normal scene.

In single-pass stereo, however, the left and right eye are done at the same time, using a double wide render texture containing the image for both eyes. This is nearly twice as fast - or multi-pass is twice as slow - making single-pass stereo the only real option for performance-constrained VR games, especially those on the Oculus Quest, or any PC VR that aims for a high graphical fidelity and realtime lighting.
The problem is, the unusual projection required for that double-wide texture needs to be built into custom shaders and image effects manually. It’s not too bad for most shaders, but for ray marching, reflection, projection, and pretty much anything that moves pixels around in screen-space, it’s difficult or impossible to implement. Lots of effects don’t work without being made specifically for single-pass stereo rendering, and planar reflections are one of them, due both the positioning of the reflection camera and the later projection of the reflected render texture.

3 Likes