Reflection probe renders only back face of planar objects

I am trying to make an interior scene and make the floor reflective using the kMirror method (based on texture rendering with a second camera) since I was not able to achieve correct reflections with reflection probes. In addition I would like to have the walls for example, which are planar objects (not closed geometry), to be affected by reflection probes.
I managed to do the reflective floor, but I noticed a glitch: Since the kMirror script has been added in the scene, a reflection probe placed in the same scene does not render the front face of the planar the objects, i.e. the walls or a plane GO (strangely enough, the “3d” objects render fine. I figured that if I set the render face property of the material on the planar object to render the back face (or both) or, of course, if I rotate a plane’s back face to “look” at the camera the reflection probe render is accurate.
I know I am asking for a wild guess here, but do you have any thoughts of when this odd behaviour can happen?
It’s hard for me to figure out the source of the issue (reflection probe’s camera confusion?, mess with normals?), so I share the mirror script in hope it can help (it’s a public project, so I guess it’s alright to post it here too):

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace kTools.Mirrors
{
    /// <summary>
    /// Mirror Object component.
    /// </summary>
    [AddComponentMenu("kTools/Mirror"), ExecuteInEditMode]
    [RequireComponent(typeof(Camera), typeof(UniversalAdditionalCameraData))]
    public class Mirror : MonoBehaviour
    {
#region Enumerations
        /// <summary>
        /// Camera override enumeration for Mirror properties
        /// <summary>
        public enum MirrorCameraOverride
        {
            UseSourceCameraSettings,
            Off,
        }

        /// <summary>
        /// Scope enumeration for Mirror output destination
        /// <summary>
        public enum OutputScope
        {
            Global,
            Local,
        }
#endregion

#region Serialized Fields
        [SerializeField]
        float m_Offset;

        [SerializeField]
        int m_LayerMask;

        [SerializeField]
        OutputScope m_Scope;

        [SerializeField]
        List<Renderer> m_Renderers;

        [SerializeField]
        float m_TextureScale;

        [SerializeField]
        MirrorCameraOverride m_AllowHDR;

        [SerializeField]
        MirrorCameraOverride m_AllowMSAA;
#endregion

#region Fields
        const string kGizmoPath = "Packages/com.kink3d.mirrors/Gizmos/Mirror.png";
        Camera m_ReflectionCamera;
        UniversalAdditionalCameraData m_CameraData;
        RenderTexture m_RenderTexture;
        RenderTextureDescriptor m_PreviousDescriptor;
#endregion

#region Constructors
        public Mirror()
        {
            // Set data
            m_Offset = 0.01f;
            m_LayerMask = -1;
            m_Scope = OutputScope.Global;
            m_Renderers = new List<Renderer>();
            m_TextureScale = 1.0f;
            m_AllowHDR = MirrorCameraOverride.UseSourceCameraSettings;
            m_AllowMSAA = MirrorCameraOverride.UseSourceCameraSettings;
        }
#endregion

#region Properties
        /// <summary>Offset value for oplique near clip plane.</summary>
        public float offest
        {
            get => m_Offset;
            set => m_Offset = value;
        }

        /// <summary>Which layers should the Mirror render.</summary>
        public LayerMask layerMask
        {
            get => m_LayerMask;
            set => m_LayerMask = value;
        }

        /// <summary>
        /// Global output renders to the global texture. Only one Mirror can be global.
        /// Local output renders to one texture per Mirror, this is set on all elements of the Renderers list.
        /// </summary>
        public OutputScope scope
        {
            get => m_Scope;
            set => m_Scope = value;
        }

        /// <summary>Renderers to set the reflection texture on.</summary>
        public List<Renderer> renderers
        {
            get => m_Renderers;
            set => m_Renderers = value;
        }

        /// <summary>Scale value applied to the size of the source camera texture.</summary>
        public float textureScale
        {
            get => m_TextureScale;
            set => m_TextureScale = value;
        }

        /// <summary>Should reflections be rendered in HDR.</summary>
        public MirrorCameraOverride allowHDR
        {
            get => m_AllowHDR;
            set => m_AllowHDR = value;
        }

        /// <summary>Should reflections be resolved with MSAA.</summary>
        public MirrorCameraOverride allowMSAA
        {
            get => m_AllowMSAA;
            set => m_AllowMSAA = value;
        }

        Camera reflectionCamera
        {
            get
            {
                if(m_ReflectionCamera == null)
                    m_ReflectionCamera = GetComponent<Camera>();
                return m_ReflectionCamera;
            }
        }

        UniversalAdditionalCameraData cameraData
        {
            get
            {
                if(m_CameraData == null)
                    m_CameraData = GetComponent<UniversalAdditionalCameraData>();
                return m_CameraData;
            }
        }
#endregion

#region State
        void OnEnable()
        {
            // Callbacks
            RenderPipelineManager.beginCameraRendering += BeginCameraRendering;
    
            // Initialize Components
            InitializeCamera();
        }

        void OnDisable()
        {
            // Callbacks
            RenderPipelineManager.beginCameraRendering -= BeginCameraRendering;

            // Dispose RenderTexture
            SafeDestroyObject(m_RenderTexture);
        }
#endregion

#region Initialization
        void InitializeCamera()
        {
            // Setup Camera
            reflectionCamera.cameraType = CameraType.Reflection;
            reflectionCamera.targetTexture = m_RenderTexture;

            // Setup AdditionalCameraData
            cameraData.renderShadows = false;
            cameraData.requiresColorOption = CameraOverrideOption.Off;
            cameraData.requiresDepthOption = CameraOverrideOption.Off;
        }
#endregion

#region RenderTexture
        RenderTextureDescriptor GetDescriptor(Camera camera)
        {
            // Get scaled Texture size
            var width = (int)Mathf.Max(camera.pixelWidth * textureScale, 4);
            var height = (int)Mathf.Max(camera.pixelHeight * textureScale, 4);

            // Get Texture format
            var hdr = allowHDR == MirrorCameraOverride.UseSourceCameraSettings ? camera.allowHDR : false;
            var renderTextureFormat = hdr ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
            return new RenderTextureDescriptor(width, height, renderTextureFormat, 16) { autoGenerateMips = true, useMipMap = true };
        }
#endregion

#region Rendering
        void BeginCameraRendering(ScriptableRenderContext context, Camera camera)
        {
            // Never render Mirrors for URP Overlay Camera
            var camData = camera.GetUniversalAdditionalCameraData();
            if (camData.renderType == CameraRenderType.Overlay)
                return;

            // Profiling command
            CommandBuffer cmd = CommandBufferPool.Get($"Mirror {gameObject.GetInstanceID()}");
            using (new ProfilingScope(cmd, new ProfilingSampler($"Mirror {gameObject.GetInstanceID()}")))
            {
                ExecuteCommand(context, cmd);

                // Test for Descriptor changes
                var descriptor = GetDescriptor(camera);
                if (!descriptor.Equals(m_PreviousDescriptor))
                {
                    // Dispose RenderTexture
                    if (m_RenderTexture != null)
                    {
                        SafeDestroyObject(m_RenderTexture);
                    }

                    // Create new RenderTexture
                    m_RenderTexture = new RenderTexture(descriptor);
                    m_PreviousDescriptor = descriptor;
                    reflectionCamera.targetTexture = m_RenderTexture;
                }

                // Execute
                RenderMirror(context, camera);
                SetShaderUniforms(context, m_RenderTexture, cmd);
            }

            ExecuteCommand(context, cmd);
        }

        void RenderMirror(ScriptableRenderContext context, Camera camera)
        {
            // Mirror the view matrix
            var mirrorMatrix = GetMirrorMatrix();
            reflectionCamera.worldToCameraMatrix = camera.worldToCameraMatrix * mirrorMatrix;

            // Make oplique projection matrix where near plane is mirror plane
            var mirrorPlane = GetMirrorPlane(reflectionCamera);
            var projectionMatrix = camera.CalculateObliqueMatrix(mirrorPlane);
            reflectionCamera.projectionMatrix = projectionMatrix;
    
            // Miscellanious camera settings
            reflectionCamera.allowHDR = allowHDR == MirrorCameraOverride.UseSourceCameraSettings ? camera.allowHDR : false;
            reflectionCamera.allowMSAA = allowMSAA == MirrorCameraOverride.UseSourceCameraSettings ? camera.allowMSAA : false;
            reflectionCamera.enabled = false;

            // Render reflection camera with inverse culling
            GL.invertCulling = true;
            UniversalRenderPipeline.RenderSingleCamera(context, reflectionCamera);
            GL.invertCulling = false;
        }
#endregion

#region Projection
        Matrix4x4 GetMirrorMatrix()
        {
            // Setup
            var position = transform.position;
            var normal = transform.forward;
            var depth = -Vector3.Dot(normal, position) - offest;

            // Create matrix
            var mirrorMatrix = new Matrix4x4()
            {
                m00 = (1f - 2f * normal.x  * normal.x),
                m01 = (-2f     * normal.x  * normal.y),
                m02 = (-2f     * normal.x  * normal.z),
                m03 = (-2f     * depth     * normal.x),
                m10 = (-2f     * normal.y  * normal.x),
                m11 = (1f - 2f * normal.y  * normal.y),
                m12 = (-2f     * normal.y  * normal.z),
                m13 = (-2f     * depth     * normal.y),
                m20 = (-2f     * normal.z  * normal.x),
                m21 = (-2f     * normal.z  * normal.y),
                m22 = (1f - 2f * normal.z  * normal.z),
                m23 = (-2f     * depth     * normal.z),
                m30 = 0f,
                m31 = 0f,
                m32 = 0f,
                m33 = 1f,
            };
            return mirrorMatrix;
        }
 
        Vector4 GetMirrorPlane(Camera camera)
        {
            // Calculate mirror plane in camera space.
            var pos = transform.position - Vector3.forward * 0.1f;
            var normal = transform.forward;
            var offsetPos = pos + normal * offest;
            var cpos = camera.worldToCameraMatrix.MultiplyPoint(offsetPos);
            var cnormal = camera.worldToCameraMatrix.MultiplyVector(normal).normalized;
            return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
        }
#endregion

#region Output
        void SetShaderUniforms(ScriptableRenderContext context, RenderTexture renderTexture, CommandBuffer cmd)
        {
            var block = new MaterialPropertyBlock();
            switch(scope)
            {
                case OutputScope.Global:
                    // Globals
                    cmd.SetGlobalTexture("_ReflectionMap", renderTexture);
                    ExecuteCommand(context, cmd);

                    // Property Blocm
                    block.SetFloat("_LocalMirror", 0.0f);
                    foreach(var renderer in renderers)
                    {
                        renderer.SetPropertyBlock(block);
                    }
                    break;
                case OutputScope.Local:
                    // Keywords
                    Shader.EnableKeyword("_BLEND_MIRRORS");

                    // Property Block
                    block.SetTexture("_LocalReflectionMap", renderTexture);
                    block.SetFloat("_LocalMirror", 1.0f);
                    foreach(var renderer in renderers)
                    {
                        renderer.SetPropertyBlock(block);
                    }
                    break;
            }
        }
#endregion

#region CommandBufer
        void ExecuteCommand(ScriptableRenderContext context, CommandBuffer cmd)
        {
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
        }
#endregion

#region Object
        void SafeDestroyObject(Object obj)
        {
            if(obj == null)
                return;
    
            #if UNITY_EDITOR
            DestroyImmediate(obj);
            #else
            Destroy(obj);
            #endif
        }
#endregion

#region AssetMenu
#if UNITY_EDITOR
        // Add a menu item to Mirrors
        [UnityEditor.MenuItem("GameObject/kTools/Mirror", false, 10)]
        static void CreateMirrorObject(UnityEditor.MenuCommand menuCommand)
        {
            // Create Mirror
            GameObject go = new GameObject("New Mirror", typeof(Mirror));
    
            // Transform
            UnityEditor.GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
    
            // Undo and Selection
            UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Create " + go.name);
            UnityEditor.Selection.activeObject = go;
        }
#endif
#endregion

#region Gizmos
#if UNITY_EDITOR
        void OnDrawGizmos()
        {
            // Setup
            var bounds = new Vector3(1.0f, 1.0f, 0.0f);
            var color = new Color32(0, 120, 255, 255);
            var selectedColor = new Color32(255, 255, 255, 255);
            var isSelected = UnityEditor.Selection.activeObject == gameObject;

            // Draw Gizmos
            Gizmos.matrix = transform.localToWorldMatrix;
            Gizmos.color = isSelected ? selectedColor : color;
            Gizmos.DrawIcon(transform.position, kGizmoPath, true);
            Gizmos.DrawWireCube(Vector3.zero, bounds);
        }
#endif
#endregion
    }
}
1 Like

I have posted about a similar problem here:
Reflection Probe rendered from wrong direction
I am not sure if it is specific to URP only or more general and also happens in other pipelines.
I have fixed this by changing material from one to two sided.

Hello and thanks for the response! It seems to be a similar issue. I also changed the material to 2-sided, but this is not so convenient in my case for another reason. Since I am working on a building interior, having planar objects for walls, with non-2-sided mat, helps with the view in the scene, because the back face is culled and I can see “inside” the building easier. I also did not find any easy way for culling the back face of objects in scene view, either a single or a 2-sided material is used. I guess, by design, the cameras (included the one in the scene) only render the objects based on their material/shader properties as they apply on geometry and not on their geometry properties (normals) directly.

I am doing the same in case of typical interior scenes.
But my scene this time includes both interior and exterior with walls covered with separate materials on outside and inside and I am probing only the inside with the probe.

But the problem might arise because as I guess we both might be using a script with similar code using beginCameraRendering and endCameraRendering functions, and manipulate with Camera.projectionMatrix. The problem disappears when the script gets detached from camera. The script affects also the lens flares, which get occluded the wrong way.

1 Like

I wrote a flip camera render feature, basing on this article:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@15.0/manual/renderer-features/how-to-fullscreen-blit.html
And all the problems have gone.

Well done! However, and please correct me if I am mistaken, isn’t this method creates an extra draw call to flip the already flipped faces?
Wouldn’t it be more efficient if the scripts/shaders that gave us the odd behaviour in the first place, were “fixed”?
I am currently trying to find another (free) method for planar reflections, preferably on shader level if possible, without a camera object involved. I am yet too amateur though to figure out enough.

Correction on my first post: It was my confusion, since for every object the back faces are rendered (by the reflection probe), unless the material is set to 2-sided, without differentiation among planar or volumetric/“3d” objects, which makes of course more sense.

Yes, it does create an extra draw call, but in my case I am doing this as an alternative.

This behaviour is not anything odd, because both my previous script and your solution from github use GL.invertCulling = true; tellig GL to cull the other way round.

Possibly my solution could also work for you, but I am not simulating mirror, but flipping 3d scenes, and then I like the rendering probe to update properly, when user puts textures or colors on these exhibition stand models.
https://fairs.d2.pl/stoiskaStandard/galleries/

I appreciate the reply and am happy that this solution works for you. You have been helpful already and I don’t wish to bother you further, but you (or anyone else) may be able to explain me in a simple manner some of the following?

From the theoretical point of view,
I find it hard to understand at which level the GL api is involved. Without digging much I found this picture online (copied from this site https://www3.ntu.edu.sg):


My question is how/where the GL.invertCulling = true; command affects the rendering pipeline?
How is it possible that the main camera view (not the one used for the reflections) is correct, but the reflection probe is affected?

From the practical point of view,
and generally speaking, what should be done, in order the invertCulling flag to affect the geometry only as processed by the reflection camera and not by the rest of transformations (eg. by the reflection probe)? Is it correct to say that a completely other approach is needed, because the scripts we have used will have this outcome by design and no easy (script) editing will fix the issue?

My guess is the reflection probes - if they are realtime - might get rendered for both cameras. Possibly you could block them from being rendered when the mirror camera is being rendered, and use the texture generated for the normal camera for that time, with
GL.invertCulling = false;


Do you also use the lens flares? They get occluded the wrong way for me. I was hoping this might be a result of using GL.invertCulling = true;, but I was wrong. The lens flares get occluded the other way round in builds (webgl and android), the right way in editor only, unaffected by any invertCulling changes. (I don’t like to bother you too much with this, either). https://discussions.unity.com/t/903180/7

Either real time or baked the same result.

Not sure how to apply this, but thanks anyway! I don’t currently use post processing at all.
I will have a look into the other link you posted for extra info.

I have found that the beginCameraRendering event is being fired for each camera and rendering probe.
Means if - as an example - I have two cameras and a reflection probe in the scene it is fired 10 times:

  • 6 times for 6 sides of reflection cubemap

  • scene view camera

  • scene camera

  • 2 game cameras

The code including GL.invertCulling = false; should get executed for the mirror camera only.

So I would replace this code:

            if (camData.renderType == CameraRenderType.Overlay)
                return;

from the lines 207, 208 from the script in your first post
with:

            if (camData.renderType != reflectionCamera)
                return;

You can also take a look at a similar script I was using from here:
https://answers.unity.com/questions/20337/flipmirror-camera.html?page=1&pageSize=5&sort=votes
and adding if (camera != Camera) return; to beginCameraRendering and endCameraRendering functions fixed the issues.

1 Like

You are amazing @tomekkie2 !! Your persistence to provide help really moves me! Adding to my appreciation is the fact that your simple and straightforward solution has fixed the issue!
Now, to be specific, I am not sure if I have set everything right yet, but the probe bakes fine. The if (camData.renderType != reflectionCamera) is not compiling right away and I had to do a small, although irrational I believe, edit. I will explain more when I’ll have a better understanding, but I did not want to be late before I thank you. And of course, I cannot thank you enough!
I’m back soon with more details…

1 Like