3
$\begingroup$

Ok guys, tricky one!

I'm trying to modify this Unity script + shader to include normal mapping. The tl;dr is, it uses a single texture rather than a cubemap as it is only for reflecting in a planar surface. So it doesn't use the standard ray bounce onto an axis aligned cube. Instead it :

  • creates a reflection matrix from the plane (pos and normal)
  • reflects the camera position and view matrix through the plane
  • renders to texture from the reflected camera (with oblique near clipping plane set to cull everything behind the mirror)
  • creates a projection matrix and sends it as uniform to the shader (basically just a standard MVP matrix but with scale cancelled, and biased from [-1, 1] to [0, 1]
  • multiplies the object space coordinate by the projection matrix to get a texcoord and then looks up the mirror texture with this

So there are no normals actually used in the shader at all at present. I know how to construct a TBN matrix and use this to warp the per-pixel normals, but I have no idea how this translates into a texture offset in this case. I have a feeling I might need screen-space normals like in the question here but pretty stumped beyond that. Any ideas?

EDIT -- Here's the script

using UnityEngine;
using System.Collections;

[ExecuteInEditMode()]
public class PlanarRealtimeReflection : MonoBehaviour
{
    public bool m_DisablePixelLights = true;
    public int m_TextureResolution = 1024;
    public float m_clipPlaneOffset = 0.07f;
    private float m_finalClipPlaneOffset = 0.0f;
    public bool m_NormalsFromMesh = false;
    public bool m_BaseClipOffsetFromMesh = false;
    public bool m_BaseClipOffsetFromMeshInverted = false;
    private Vector3 m_calculatedNormal = Vector3.zero;
    private Vector3 m_calculatedTangent = Vector4.zero;
    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()
    {
        if(!enabled || !renderer || !renderer.sharedMaterial || !renderer.enabled)
            return;

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

        if(m_NormalsFromMesh && GetComponent<MeshFilter>() != null)
            m_calculatedNormal = transform.TransformDirection(GetComponent<MeshFilter>().sharedMesh.normals[0]);
            m_calculatedTangent = transform.TransformDirection(GetComponent<MeshFilter>().sharedMesh.tangents[0]);

        if(m_BaseClipOffsetFromMesh && GetComponent<MeshFilter>() != null)
            m_finalClipPlaneOffset = (transform.position - transform.TransformPoint(GetComponent<MeshFilter>().sharedMesh.vertices[0])).magnitude + m_clipPlaneOffset;
        else if(m_BaseClipOffsetFromMeshInverted && GetComponent<MeshFilter>() != null)
            m_finalClipPlaneOffset = -(transform.position - transform.TransformPoint(GetComponent<MeshFilter>().sharedMesh.vertices[0])).magnitude + m_clipPlaneOffset;
        else
            m_finalClipPlaneOffset = m_clipPlaneOffset;

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

        Camera reflectionCamera;
        CreateSurfaceObjects(cam, out reflectionCamera);

        //Find out the reflection plane: position and normal in world space
        Vector3 pos = transform.position;
        Vector3 normal = m_NormalsFromMesh && GetComponent<MeshFilter>() != null ? m_calculatedNormal : transform.up;
        Vector3 tangent = m_NormalsFromMesh && GetComponent<MeshFilter>() != null ? m_calculatedTangent : transform.right;
        Vector3 bitangent = Vector3.Cross(normal, tangent);

        //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_finalClipPlaneOffset;
        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;
        CalculateObliqueMatrix (ref projection, clipPlane);
        reflectionCamera.projectionMatrix = projection;

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

        //Set matrix on the shader that transforms UVs from object space into screen
        //space. We want to just project reflection texture on screen.
        Matrix4x4 scaleOffset = Matrix4x4.TRS(
            new Vector3(0.5f,0.5f,0.5f), Quaternion.identity, new Vector3(0.5f,0.5f,0.5f));
        Vector3 scale = transform.localScale;//.lossyScale;
        Matrix4x4 mtx = transform.localToWorldMatrix * Matrix4x4.Scale(new Vector3(1.0f/scale.x, 1.0f/scale.y, 1.0f/scale.z));
        mtx = scaleOffset *  cam.projectionMatrix * cam.worldToCameraMatrix * mtx;
        foreach(Material mat in materials)
            mat.SetMatrix("_ProjMatrix", mtx);
        //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 CreateSurfaceObjects(Camera currentCamera, out Camera reflectionCamera)
    {
        reflectionCamera = null;

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

        //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("Surface Refl Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
            reflectionCamera = go.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_finalClipPlaneOffset;
        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));
    }

    //Adjusts the given projection matrix so that near plane is the given clipPlane
    //clipPlane is given in camera space. See article in Game Programming Gems 5 and
    //http://aras-p.info/texts/obliqueortho.html
    private static void CalculateObliqueMatrix (ref Matrix4x4 projection, Vector4 clipPlane)
    {
        Vector4 q = projection.inverse * new Vector4(
            sgn(clipPlane.x),
            sgn(clipPlane.y),
            1.0f,
            1.0f
            );
        Vector4 c = clipPlane * (2.0F / (Vector4.Dot (clipPlane, q)));
        //third row = clip plane - fourth row
        projection[2] = c.x - projection[3];
        projection[6] = c.y - projection[7];
        projection[10] = c.z - projection[11];
        projection[14] = c.w - projection[15];
    }

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

And the shader so far

Shader "Realtime Reflections/New Shader"
{
    Properties {        
        _MainAlpha("MainAlpha", Range(0, 1)) = 1
        _MapAmount("Distortion Amount", Range(0, 0.05)) = 0
        _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
        _ReflectionTex ("Reflection", 2D) = "white" {}
        _NormalMap ("Normal Map", 2D) = "white" {}
        _RefColor("Color",Color) = (1,1,1,1)
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"}
        LOD 100
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform float4x4    _ProjMatrix;
            uniform float       _RefType;
            uniform float4      _Normal;
            uniform float4      _Tangent;
            uniform float4      _Bitangent;
            sampler2D           _ReflectionTex;
            sampler2D           _MainTex;
            sampler2D           _NormalMap;
            float4              _MainTex_ST;
            float4              _NormalMap_ST;       
            float4              _RefColor;
            float               _MainAlpha;
            float               _MapAmount;

            struct outvertex {
                float4 pos :        SV_POSITION;
                float2 colorUV :    TEXCOORD0;
                float2 bumpUV :     TEXCOORD1;
                float4 varyingPos : TEXCOORD2;
            };

            outvertex vert(appdata_full v) {

                outvertex o;
                o.pos = mul (UNITY_MATRIX_MVP,v.vertex);
                o.colorUV = TRANSFORM_TEX(v.texcoord.xy,_MainTex);
                o.bumpUV = TRANSFORM_TEX(v.texcoord1.xy,_NormalMap);
                o.varyingPos = v.vertex;    
                o. color = fixed4(mul(float3x3(UNITY_MATRIX_IT_MV),v.normal),1);
                return o;
            }

            float4 frag(outvertex i) : COLOR {          
                float4 posProj = mul(_ProjMatrix, i.varyingPos);      
                float2 normPosProj = posProj.xy / posProj.w;
                float3 bump = (float3(tex2D(_NormalMap, i.bumpUV));
                //normPosProj += ?????????
                half4 flecol = tex2D(_ReflectionTex, normPosProj);  
                half4 maincol =  tex2D(_MainTex, i.colorUV);                                    
                half4 outcolor = half4(1,1,1,1);                
                outcolor = maincol*_MainAlpha+flecol*(1-_MainAlpha);
                return outcolor*_RefColor;
            }
            ENDCG
        }
    }
}

I had the idea to get the normal in screen space (so z is always toward camera) then simply find the delta between the regular normal and the warped one in x,y and use this as the texture offset, but I have no idea if this is feasible or how to do it.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.