SurfaceReflection

From Unify Community Wiki
Jump to: navigation, search


Contents

Description

This is a shader/script to add realtime reflections to flat surfaces. It uses a RenderCamera to do this, thus requiring Unity Pro.

Features:

  • Creates realtime reflections based on actual scene objects instead of cubemaps. (meaning it works on its own and is dynamic)
  • Supports the adding of a base texture to be drawn underneath, meaning you can use this script to add reflections to surfaces with existing textures.
  • Has opacity settings for both the base texture and the reflections, letting you customize the overall surface opacity as well as the visibility ratio between base texture and reflections.
  • Has a tint color option for changing the brightness of the surface or adding color.

This shader/script is based on the MirrorReflection2 shader/script written by Aras Pranckevicius.

With its additional features/options, this shader/script can be used to add reflections without removing the surface's transparency or existing texture, letting you create reflective windows and textured mirrors.

Usage

Prerequisites: This technique requires Unity Pro, version 2 or newer.

  • Create a material that uses the shader below (FX/Surface Reflection).
  • Apply the material to a plane-like (i.e. flat) object.
  • Attach the SurfaceReflection.cs script to the object.
  • Set the object's layer to 'Water'. (optional but recommended)

Notes:

  • The reflection happens along the object's 'up' direction by default (green axis in the scene view). (e.g. the built-in plane object is suitable for use as a mirror) If you experience weird reflections, try enabling the script's "Normals From Mesh" option. If the problem persists, try correcting the surface's orientation/axis-direction manually.
  • If you use this on multiple surfaces, you have to create a separate material for each one, otherwise their reflections will be disrupted when both in view.

Shader - SurfaceReflection.shader

Shader "FX/Surface Reflection"
{ 
    Properties
    {
        _MainAlpha("MainAlpha", Range(0, 1)) = 1
        _ReflectionAlpha("ReflectionAlpha", Range(0, 1)) = 1
        _TintColor ("Tint Color (RGB)", Color) = (1,1,1)
        _MainTex ("MainTex (RGBA)", 2D) = ""
        _ReflectionTex ("ReflectionTex", 2D) = "white" { TexGen ObjectLinear }
    }
 
    //Two texture cards: full thing
    Subshader
    { 
        Tags {Queue = Transparent}
        ZWrite Off
        Colormask RGBA
        Color [_TintColor]
        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            SetTexture[_ReflectionTex] { constantColor(0,0,0, [_ReflectionAlpha]) matrix [_ProjMatrix] combine texture * previous, constant} 
        }
        Pass
        {
            SetTexture[_MainTex] { constantColor(0,0,0, [_MainAlpha]) combine texture * primary, texture * constant}
        }
    }
 
    //Fallback: just main texture
    Subshader
    {
        Pass
        {
            SetTexture [_MainTex] { combine texture }
        }
    }
}

Script - SurfaceReflection.cs

using UnityEngine;
using System.Collections;
 
//This is in fact just the Water script from Pro Standard Assets,
//just with refraction stuff removed.
 
[ExecuteInEditMode] //Make reflection live-update even when not in play mode
public class SurfaceReflection : MonoBehaviour
{
    public bool m_DisablePixelLights = true;
    public int m_TextureSize = 256;
    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;
 
    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]);
 
         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;
 
        //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 = 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);
        }
 
        //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.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_TextureSize)
        {
            if(m_ReflectionTexture)
                DestroyImmediate(m_ReflectionTexture);
            m_ReflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);
            m_ReflectionTexture.name = "__SurfaceReflection" + 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("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;
    }
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Tools