[FREE] Stochastic Screen Space Reflection

This is a a new screen space reflection effect I made.

It is based on the “Stochastic Screen-Space Reflections” presentation by Tomasz Stachowiak and Yasin Uludag.

This implementation is not perfect but, it’s open source so anyone can contribute to it and make it better.

You can get it here and here . You can also try it here (WASD to move and hold right click to rotate camera): Windows Build

20 Likes

Awesome! more reading here (and why isn’t it baseline in Unity?!)

2 Likes

Thanks @hippocoder !

I’ve made small update and also change the git structure.

Now it’s a project with a sample scene.

There’s also a package containing the effect only.

Hey Charkes, seems you like the SSR effect a lot ;), glad to see you have new SSR for unity.
However I tested on my mac in unity editor 5.4.2 and it doesn’t work as expected. In editor mode, there have no SSR effect, in play mode it rendered completely black. I got neither script or shader error in the editor, so i don’t have a clue why it is not working.
Here is the screenshot.

very interesting, thanks for sharing!!!

1 Like

Yeah I like SSR :).

About mac support, it’s expected.

I’ve made an update just a minute ago, let me know if it works.

EDIT : Editor mode will be added later.

Thank you very much for your work Charkes.

This should be included in the new unity effect stack.

1 Like

I see you added #pragma glsl in the shader, unlike unity 4.x, it is no longer used in unity 5. So I got same result.

I wasn’t aware of that. I don’t have a mac around for testing, it’s going to be tricky. What happend if you disable these options : Ray reuse, mipmap and temporal ?

It seems disabled the mipmap that made more obvious different than other option in combine debug pass, which less black area. However disabled mipmap did not make too much different in other debug pass. in general that disabled Ray reuse and temporal mades only small different. The artifact is still there.
Here is the animated gif to show the debug passes:
2851777--208387--Stochastic_SSR_Mac.gif

My old macbook can only do openGL3.3, I don’t know it is because of my graphic card that is too old. Not sure if it happens in other newer macs.

From what I see, it looks like the previous frame reprojection don’t work well here (Not used in debug pass). I’ll figured out why.

Can you try this :

//The MIT License(MIT)

//Copyright(c) 2016 Charles Greivelding Thomas

//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.

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

namespace cCharkes
{
    [System.Serializable]
    public enum ResolutionMode
    {
        halfRes = 2,
        fullRes = 1,
    };

    [System.Serializable]
    public enum SSRDebugPass
    {
        Combine,
        Reflection,
        Cubemap,
        ReflectionAndCubemap,
        SSRMask,
        CombineNoCubemap,
        RayCast,
        Jitter,
    };

    [RequireComponent(typeof(Camera))]
    [AddComponentMenu("cCharkes/Image Effects/Rendering/Stochastic Screen Space Reflection")]
    public class StochasticSSR : MonoBehaviour
    {
        Vector4 debug;

        [Header("RayCast")]
        [SerializeField]
        ResolutionMode depthMode = ResolutionMode.halfRes;

        [SerializeField]
        ResolutionMode rayMode = ResolutionMode.halfRes;

        //[SerializeField]
        FilterMode rayFilterMode = FilterMode.Bilinear;

        [Range(1, 100)]
        [SerializeField]
        int rayDistance = 70;

        [Range(0.0f, 1.0f)]
        [SerializeField]
        float BRDFBias = 0.7f;

        //[SerializeField]
        Texture noise;

        [Header("Resolve")]
        [SerializeField]
        ResolutionMode resolveMode = ResolutionMode.fullRes;

        [SerializeField]
        bool rayReuse = true;

        [SerializeField]
        bool normalization = true;

        [SerializeField]
        bool reduceFireflies = true;

        [SerializeField]
        bool useMipMap = true;

        //[SerializeField]
        int maxMipMap = 5;

        [Header("Temporal")]
        [SerializeField]
        bool useTemporal = true;

        [SerializeField]
        float scale = 2.0f;

        [Range(0.0f, 1.0f)]
        [SerializeField]
        float minResponse = 0.85f;

        [Range(0.0f, 1.0f)]
        [SerializeField]
        float maxResponse = 0.95f;

        [Header("General")]
        [SerializeField]
        bool useFresnel = true;

        [Range(0.0f, 1.0f)]
        [SerializeField]
        float screenFadeSize = 0.25f;

        [Header("Debug")]

        [Range(0.0f, 1.0f)]
        [SerializeField]
        float smoothnessRange = 1.0f;

        public SSRDebugPass debugPass = SSRDebugPass.Combine;

        Camera m_camera;

        private Matrix4x4 projectionMatrix;
        private Matrix4x4 viewProjectionMatrix;
        private Matrix4x4 inverseViewProjectionMatrix;
        private Matrix4x4 worldToCameraMatrix;
        private Matrix4x4 cameraToWorldMatrix;

        private Matrix4x4 prevViewProjectionMatrix;

        RenderTexture temporalBuffer;

        RenderTexture mainBuffer0, mainBuffer1;
        RenderTexture mipMapBuffer0, mipMapBuffer1, mipMapBuffer2;

        RenderBuffer[] renderBuffer = new RenderBuffer[2];

        float[] dist = new float[5] { 1.0f / 1024.0f, 1.0f / 256.0f, 1.0f / 128.0f, 1.0f / 64.0f, 1.0f / 32.0f };

        int[] mipLevel = new int[5] { 0, 2, 3, 4, 5 };

        void Awake()
        {

            noise = Resources.Load("tex_BlueNoise_256x256_UNI") as Texture2D;
            m_camera = GetComponent<Camera>();
            m_camera.depthTextureMode |= DepthTextureMode.Depth | DepthTextureMode.MotionVectors;
        }

        static Material m_rendererMaterial = null;
        protected Material rendererMaterial
        {
            get
            {
                if (m_rendererMaterial == null)
                {
                    m_rendererMaterial = new Material(Shader.Find("Hidden/Stochastic SSR"));
                    m_rendererMaterial.hideFlags = HideFlags.DontSave;
                }
                return m_rendererMaterial;
            }
        }

        public static RenderTexture CreateRenderTexture(int w, int h, int d, RenderTextureFormat f, bool useMipMap, bool generateMipMap, FilterMode filterMode)
        {
            RenderTexture r = new RenderTexture(w, h, d, f);
            r.filterMode = filterMode;
            r.useMipMap = useMipMap;
            r.generateMips = generateMipMap;
            r.Create();
            return r;
        }

        void OnDestroy()
        {
            Object.DestroyImmediate(rendererMaterial);
        }

        void OnDisable()
        {
            ReleaseRenderTargets();
        }

        void ReleaseRenderTargets()
        {

            if (temporalBuffer != null)
            {
                temporalBuffer.Release();
                temporalBuffer = null;
            }

            if (mainBuffer0 != null || mainBuffer1 != null)
            {
                mainBuffer0.Release();
                mainBuffer0 = null;
                mainBuffer1.Release();
                mainBuffer1 = null;
            }

            if (mipMapBuffer0 != null)
            {
                mipMapBuffer0.Release();
                mipMapBuffer0 = null;
            }
        }

        void UpdateRenderTargets(int width, int height)
        {
            if (temporalBuffer != null && temporalBuffer.width != width)
            {
                ReleaseRenderTargets();
            }

            if (temporalBuffer == null || !temporalBuffer.IsCreated())
            {
                temporalBuffer = CreateRenderTexture(width, height, 0, RenderTextureFormat.ARGBHalf, false, false, FilterMode.Bilinear);

            }

            if (mainBuffer0 == null || !mainBuffer0.IsCreated())
            {
                mainBuffer0 = CreateRenderTexture(width, height, 0, RenderTextureFormat.DefaultHDR, false, false, FilterMode.Bilinear);
                mainBuffer1 = CreateRenderTexture(width, height, 0, RenderTextureFormat.DefaultHDR, false, false, FilterMode.Bilinear);
            }

            if (mipMapBuffer0 == null || !mipMapBuffer0.IsCreated())
            {
                mipMapBuffer0 = CreateRenderTexture(1024, 1024, 0, RenderTextureFormat.DefaultHDR, true, true, FilterMode.Bilinear); // Need to be power of two
                mipMapBuffer1 = CreateRenderTexture(1024, 1024, 0, RenderTextureFormat.DefaultHDR, true, true, FilterMode.Bilinear); // Need to be power of two
                mipMapBuffer2 = CreateRenderTexture(1024, 1024, 0, RenderTextureFormat.DefaultHDR, true, false, FilterMode.Bilinear); // Need to be power of two
            }
        }

        void UpdateVariable()
        {
            rendererMaterial.SetTexture("_Noise", noise);
            rendererMaterial.SetVector("_NoiseSize", new Vector2(noise.width, noise.height));
            rendererMaterial.SetFloat("_BRDFBias", BRDFBias);
            rendererMaterial.SetFloat("_SmoothnessRange", smoothnessRange);
            rendererMaterial.SetFloat("_EdgeFactor", screenFadeSize);
            rendererMaterial.SetInt("_NumSteps", rayDistance);

            if (!rayReuse)
                rendererMaterial.SetInt("_RayReuse", 0);
            else
                rendererMaterial.SetInt("_RayReuse", 1);

            if (!normalization)
                rendererMaterial.SetInt("_UseNormalization", 0);
            else
                rendererMaterial.SetInt("_UseNormalization", 1);

            if (!useFresnel)
                rendererMaterial.SetInt("_UseFresnel", 0);
            else
                rendererMaterial.SetInt("_UseFresnel", 1);

            if (!useTemporal)
                rendererMaterial.SetInt("_UseTemporal", 0);
            else
                rendererMaterial.SetInt("_UseTemporal", 1);

            if (!reduceFireflies)
                rendererMaterial.SetInt("_Fireflies", 0);
            else
                rendererMaterial.SetInt("_Fireflies", 1);

            switch (debugPass)
            {
                case SSRDebugPass.Combine:
                    rendererMaterial.SetInt("_DebugPass", 0);
                    break;
                case SSRDebugPass.Reflection:
                    rendererMaterial.SetInt("_DebugPass", 1);
                    break;
                case SSRDebugPass.Cubemap:
                    rendererMaterial.SetInt("_DebugPass", 2);
                    break;
                case SSRDebugPass.ReflectionAndCubemap:
                    rendererMaterial.SetInt("_DebugPass", 3);
                    break;
                case SSRDebugPass.SSRMask:
                    rendererMaterial.SetInt("_DebugPass", 4);
                    break;
                case SSRDebugPass.CombineNoCubemap:
                    rendererMaterial.SetInt("_DebugPass", 5);
                    break;
                case SSRDebugPass.RayCast:
                    rendererMaterial.SetInt("_DebugPass", 6);
                    break;
                case SSRDebugPass.Jitter:
                    rendererMaterial.SetInt("_DebugPass", 7);
                    break;
            }
        }

        void UpdatePrevMatrices(RenderTexture source, RenderTexture destination)
        {
            worldToCameraMatrix = m_camera.worldToCameraMatrix;
            cameraToWorldMatrix = worldToCameraMatrix.inverse;

            projectionMatrix = GL.GetGPUProjectionMatrix(m_camera.projectionMatrix, false);
            viewProjectionMatrix = projectionMatrix * worldToCameraMatrix;
            inverseViewProjectionMatrix = viewProjectionMatrix.inverse;

            rendererMaterial.SetMatrix("_ProjectionMatrix", projectionMatrix);
            rendererMaterial.SetMatrix("_ViewProjectionMatrix", viewProjectionMatrix);
            rendererMaterial.SetMatrix("_InverseProjectionMatrix", projectionMatrix.inverse);
            rendererMaterial.SetMatrix("_InverseViewProjectionMatrix", inverseViewProjectionMatrix);
            rendererMaterial.SetMatrix("_WorldToCameraMatrix", worldToCameraMatrix);
            rendererMaterial.SetMatrix("_CameraToWorldMatrix", cameraToWorldMatrix);

            rendererMaterial.SetMatrix("_PrevViewProjectionMatrix", prevViewProjectionMatrix);
            rendererMaterial.SetMatrix("_PrevInverseViewProjectionMatrix", prevViewProjectionMatrix * Matrix4x4.Inverse(viewProjectionMatrix));
        }

        RenderTexture CreateTempBuffer(int x, int y, int depth, RenderTextureFormat format)
        {
            return RenderTexture.GetTemporary(x, y, depth, format);
        }

        void ReleaseTempBuffer(RenderTexture rt)
        {
            RenderTexture.ReleaseTemporary(rt);
        }

        [ImageEffectOpaque]
        void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            int width = m_camera.pixelWidth;
            int height = m_camera.pixelHeight;

            UpdatePrevMatrices(source, destination);
            UpdateRenderTargets(width, height);
            UpdateVariable();

            int rayWidth = width / (int)rayMode;
            int rayHeight = height / (int)rayMode;
            debug = new Vector4(width, height, m_camera.nearClipPlane / (m_camera.nearClipPlane - m_camera.farClipPlane), 0.0f);
            rendererMaterial.SetVector("_Project", debug);
            rendererMaterial.SetVector("_RayCastSize", new Vector2((float)rayWidth, (float)rayHeight));

            RenderTexture rayCast = CreateTempBuffer(rayWidth, rayHeight, 0, RenderTextureFormat.ARGBHalf);
            RenderTexture rayCastMask = CreateTempBuffer(rayWidth, rayHeight, 0, RenderTextureFormat.RHalf);
            RenderTexture depthBuffer = CreateTempBuffer(width / (int)depthMode, height / (int)depthMode, 0, RenderTextureFormat.RFloat);
            rayCast.filterMode = rayFilterMode;
            depthBuffer.filterMode = FilterMode.Point;

            rendererMaterial.SetTexture("_RayCast", rayCast);
            rendererMaterial.SetTexture("_RayCastMask", rayCastMask);
            rendererMaterial.SetTexture("_CameraDepthBuffer", depthBuffer);

            // Depth Buffer
            Graphics.SetRenderTarget(depthBuffer);
            rendererMaterial.SetPass(4);
            DrawFullScreenQuad();
            ReleaseTempBuffer(depthBuffer);
            //

            switch (debugPass)
            {
                case SSRDebugPass.Reflection:
                case SSRDebugPass.Cubemap:
                case SSRDebugPass.CombineNoCubemap:
                case SSRDebugPass.RayCast:
                case SSRDebugPass.ReflectionAndCubemap:
                case SSRDebugPass.SSRMask:
                case SSRDebugPass.Jitter:
                case SSRDebugPass.Combine:
                    Graphics.Blit(source, mainBuffer0, rendererMaterial, 1);
                    break;
                /*case SSRDebugPass.Combine:
                    Graphics.Blit(mainBuffer1, mainBuffer0, rendererMaterial, 8);
                    break;*/
            }

            // Raycast pass
            renderBuffer[0] = rayCast.colorBuffer;
            renderBuffer[1] = rayCastMask.colorBuffer;
            Graphics.SetRenderTarget(renderBuffer, rayCast.depthBuffer);
            rendererMaterial.SetPass(3);
            DrawFullScreenQuad();
            //

            int resolveWidth = width / (int)resolveMode;
            int resolveHeight = height / (int)resolveMode;

            RenderTexture resolvePass = CreateTempBuffer(resolveWidth, resolveHeight, 0, RenderTextureFormat.ARGBHalf);

            rendererMaterial.SetVector("_BufferSize", new Vector2((float)rayWidth, (float)rayHeight));
            rendererMaterial.SetInt("_MaxMipMap", maxMipMap);

            if (useMipMap)
            {
                Graphics.Blit(mainBuffer0, mipMapBuffer0); // Copy the source frame buffer to the mip map buffer

                for (int i = 0; i < maxMipMap; i++)
                {
                    rendererMaterial.SetVector("_GaussianDir", dist[i] * new Vector2(1.0f, 0.0f));
                    rendererMaterial.SetInt("_MipMapCount", mipLevel[i]);
                    Graphics.Blit(mipMapBuffer0, mipMapBuffer1, rendererMaterial, 6);

                    rendererMaterial.SetVector("_GaussianDir", dist[i] * new Vector2(0.0f, 1.0f));
                    rendererMaterial.SetInt("_MipMapCount", mipLevel[i]);
                    Graphics.Blit(mipMapBuffer1, mipMapBuffer0, rendererMaterial, 6);

                    Graphics.SetRenderTarget(mipMapBuffer2, i);
                    DrawFullScreenQuad();
                }

                Graphics.Blit(mipMapBuffer2, resolvePass, rendererMaterial, 0); // Resolve pass using mip map buffer
            }
            else
            {
                Graphics.Blit(mainBuffer0, resolvePass, rendererMaterial, 0); // Resolve pass without mip map buffer
            }

            rendererMaterial.SetTexture("_ReflectionBuffer", resolvePass);

            ReleaseTempBuffer(rayCast);
            ReleaseTempBuffer(rayCastMask);

            if (useTemporal)
            {
                rendererMaterial.SetFloat("_TScale", scale);
                rendererMaterial.SetFloat("_TMinResponse", minResponse);
                rendererMaterial.SetFloat("_TMaxResponse", maxResponse);

                RenderTexture temporalBuffer0 = CreateTempBuffer(width, height, 0, RenderTextureFormat.ARGBHalf);

                rendererMaterial.SetTexture("_PreviousBuffer", temporalBuffer);

                Graphics.Blit(resolvePass, temporalBuffer0, rendererMaterial, 5); // Temporal pass

                rendererMaterial.SetTexture("_ReflectionBuffer", temporalBuffer0);

                Graphics.Blit(temporalBuffer0, temporalBuffer);

                ReleaseTempBuffer(temporalBuffer0);
            }

            switch(debugPass)
            {
                case SSRDebugPass.Reflection:
                case SSRDebugPass.Cubemap:
                case SSRDebugPass.CombineNoCubemap:
                case SSRDebugPass.RayCast:
                case SSRDebugPass.ReflectionAndCubemap:
                case SSRDebugPass.SSRMask:
                case SSRDebugPass.Jitter:
                case SSRDebugPass.Combine:
                    Graphics.Blit(source, destination, rendererMaterial, 2);
                    break;
                /*case SSRDebugPass.Combine:
                    Graphics.Blit(source, mainBuffer1, rendererMaterial, 2);
                    Graphics.Blit(mainBuffer1, destination);
                    break;*/
            }

            ReleaseTempBuffer(resolvePass);

            prevViewProjectionMatrix = viewProjectionMatrix;
        }

        public void DrawFullScreenQuad()
        {
            GL.PushMatrix();
            GL.LoadOrtho();

            GL.Begin(GL.QUADS);
            GL.MultiTexCoord2(0, 0.0f, 0.0f);
            GL.Vertex3(0.0f, 0.0f, 0.0f); // BL

            GL.MultiTexCoord2(0, 1.0f, 0.0f);
            GL.Vertex3(1.0f, 0.0f, 0.0f); // BR

            GL.MultiTexCoord2(0, 1.0f, 1.0f);
            GL.Vertex3(1.0f, 1.0f, 0.0f); // TR

            GL.MultiTexCoord2(0, 0.0f, 1.0f);
            GL.Vertex3(0.0f, 1.0f, 0.0f); // TL

            GL.End();
            GL.PopMatrix();
        }
    }
}

Copy past this in StochasticSSR.cs (replace everything), it will disable the first free bounce.

Let me know if it’s working.

After replaced the codes, It seems working much better, right now it just got some little black dots/noise when temporal is on, turn off the temporal seems removed all that little black dots. Good job!

Ok great. For black dots, it must be a nan I left somewhere. Thanks !

i agree with @hippocoder , this is actually better and more performant than the current built in ssr imo.
ping @Chman :smile:

On second thought, performance are the same, but still looks better especially for reflection in rough surfaces

2 Likes

@Reanimate_L glad you liked it. I’ve made some updates on GitHub and BitBucket.

Change Log

Linear for bluenoise
Fixed cone for mip map selection
Added scene view support (has issues)

Known issues :

No MAC support.
Not working in scene view if DepthTextureMode.MotionVectors is used by any script attached to the camera.
Half res depth shows artefact with a low clipping plane.
Blue noise animation pattern too visible.

2 Likes

Nice, btw it seems your SSR doesn’t work quite well with post process stack.
Sometimes i’m getting black screen randomly

Damn I thought I fixed that, I’m not sure exactly what is causing this.

Was it with my test scene or yours ?

Both, seems happen more often if the eye adaptation effect from PPstack are active.

I may have found the problem, something related to some temp RT I’m using, they got lost rendomly. Will update in a few minutes.

EDIT : I’ve updated the repo, hope it’s fixed, it is on my side.

2 Likes

See some lovely reflection shots :

5 Likes