﻿/// <summary>
/// Shiny SSRR - Screen Space Raytraced Reflections - (c) 2021 Kronnect
/// </summary>

using UnityEngine;
using UnityEngine.Rendering;

namespace ShinySSRR {

    public enum OutputMode {
        Final,
        OnlyReflections,
        SideBySideComparison
    }

    public enum RaytracingPreset {
        Fast = 10,
        Medium = 20,
        High = 30,
        Superb = 35,
        Ultra = 40
    }

    [ExecuteInEditMode]
    [RequireComponent(typeof(Camera))]
    [ImageEffectAllowedInSceneView]
    public class ShinySSRR : MonoBehaviour {

        class SSRPass {

            enum Pass {
                CopyExact = 0,
                SSRSurf = 1,
                Resolve = 2,
                BlurHoriz = 3,
                BlurVert = 4,
                Debug = 5,
                Combine = 6,
                CombineWithCompare = 7,
                GBuffPass = 8,
                Copy = 9
            }

            const string SHINY_CBUFNAME = "Shiny_SSRR";
            const float GOLDEN_RATIO = 0.618033989f;
            const int MIP_COUNT = 5;
            Material sMat;
            Texture noiseTex;
            ShinySSRR settings;
            readonly Plane[] frustumPlanes = new Plane[6];
            CommandBuffer cmd;
            int[] rtPyramid;

            public void Setup(ShinySSRR settings, bool deferred) {
                this.settings = settings;
                if (cmd == null) {
                    cmd = new CommandBuffer {
                        name = SHINY_CBUFNAME
                    };
                }
                if (sMat == null) {
                    Shader shader = Shader.Find("Hidden/Kronnect/SSR");
                    sMat = new Material(shader);
                }
                if (noiseTex == null) {
                    noiseTex = Resources.Load<Texture>("SSR/blueNoiseSSR64");
                }
                sMat.SetTexture(ShaderParams.NoiseTex, noiseTex);

                // set global settings
                sMat.SetVector(ShaderParams.SSRSettings2, new Vector4(settings.jitter, settings.contactHardening, settings.reflectionsMultiplier, settings.vignetteSize));
                sMat.SetVector(ShaderParams.SSRSettings4, new Vector4(settings.separationPos, settings.reflectionsMinIntensity, settings.reflectionsMaxIntensity, settings.specularSoftenPower));
                sMat.SetVector(ShaderParams.SSRBlurStrength, new Vector4(settings.blurStrength.x, settings.blurStrength.y, 0, 0));
                if (settings.specularControl) {
                    sMat.EnableKeyword(ShaderParams.SKW_DENOISE);
                } else {
                    sMat.DisableKeyword(ShaderParams.SKW_DENOISE);
                }
                sMat.SetFloat(ShaderParams.MinimumBlur, settings.minimumBlur);

                if (deferred) {
                    if (settings.jitter > 0) {
                        sMat.EnableKeyword(ShaderParams.SKW_JITTER);
                    } else {
                        sMat.DisableKeyword(ShaderParams.SKW_JITTER);
                    }
                    if (settings.refineThickness) {
                        sMat.EnableKeyword(ShaderParams.SKW_REFINE_THICKNESS);
                        sMat.SetFloat(ShaderParams.SSRSettings5, settings.thicknessFine * settings.thickness);
                    } else {
                        sMat.DisableKeyword(ShaderParams.SKW_REFINE_THICKNESS);
                    }
                    sMat.SetVector(ShaderParams.SSRSettings, new Vector4(settings.thickness, settings.sampleCount, settings.binarySearchIterations, settings.maxRayLength));
                    sMat.SetVector(ShaderParams.MaterialData, new Vector4(0, settings.fresnel, settings.fuzzyness, settings.decay));
                }

                if (rtPyramid == null || rtPyramid.Length != MIP_COUNT) {
                    rtPyramid = new int[MIP_COUNT];
                }
                for (int k = 0; k < rtPyramid.Length; k++) {
                    rtPyramid[k] = Shader.PropertyToID("_BlurRTMip" + k);
                }
            }

            public void Execute(RenderTexture source, RenderTexture destination, Camera cam) {
                ExecuteInternal(source, destination, cam);
                Graphics.Blit(source, destination, sMat, (int)Pass.CopyExact);
            }

            void ExecuteInternal(RenderTexture source, RenderTexture renderTexture, Camera cam) {
                // ignore SceneView depending on setting
                if (cam.cameraType == CameraType.SceneView) {
                    if (!settings.showInSceneView) return;
                } else {
                    // ignore any camera other than GameView
                    if (cam.cameraType != CameraType.Game) return;
                }

                RenderTextureDescriptor sourceDesc = source.descriptor;
                sourceDesc.colorFormat = settings.lowPrecision ? RenderTextureFormat.ARGB32 : RenderTextureFormat.ARGBHalf;
                sourceDesc.width /= settings.downsampling;
                sourceDesc.height /= settings.downsampling;
                sourceDesc.msaaSamples = 1;

                float goldenFactor = GOLDEN_RATIO;
                if (settings.animatedJitter) {
                    goldenFactor *= (Time.frameCount % 480);
                }
                Shader.SetGlobalVector(ShaderParams.SSRSettings3, new Vector4(sourceDesc.width, sourceDesc.height, goldenFactor, settings.depthBias));

                if (settings.isDeferredActive) {
                    // init command buffer
                    cmd.Clear();

                    // pass UNITY_MATRIX_V
                    sMat.SetMatrix(ShaderParams.WorldToViewDir, cam.worldToCameraMatrix);

                    // prepare ssr target
                    cmd.GetTemporaryRT(ShaderParams.RayCast, sourceDesc, FilterMode.Point);

                    // raytrace using gbuffers
                    cmd.Blit(source, ShaderParams.RayCast, sMat, (int)Pass.GBuffPass);

                } else {

                    // early exit if no reflection objects
                    int count = Reflections.instances.Count;
                    if (count == 0) return;

                    bool firstSSR = true;

                    GeometryUtility.CalculateFrustumPlanes(cam, frustumPlanes);

                    for (int k = 0; k < count; k++) {
                        Reflections go = Reflections.instances[k];
                        if (go == null) continue;
                        int rendererCount = go.ssrRenderers.Count;
                        for (int j = 0; j < rendererCount; j++) {
                            Reflections.SSR_Renderer ssrRenderer = go.ssrRenderers[j];
                            Renderer goRenderer = ssrRenderer.renderer;

                            if (goRenderer == null || !goRenderer.isVisible) continue;

                            // if object is part of static batch, check collider bounds (if existing)
                            if (goRenderer.isPartOfStaticBatch) {
                                if (ssrRenderer.hasStaticBounds) {
                                    // check artifically computed bounds
                                    if (!GeometryUtility.TestPlanesAABB(frustumPlanes, ssrRenderer.staticBounds)) continue;
                                } else if (ssrRenderer.collider != null) {
                                    // check if object is visible by current camera using collider bounds
                                    if (!GeometryUtility.TestPlanesAABB(frustumPlanes, ssrRenderer.collider.bounds)) continue;
                                }
                            } else {
                                // check if object is visible by current camera using renderer bounds
                                if (!GeometryUtility.TestPlanesAABB(frustumPlanes, goRenderer.bounds)) continue;
                            }

                            if (!ssrRenderer.isInitialized) {
                                ssrRenderer.Init(sMat);
                                ssrRenderer.UpdateMaterialProperties(go, settings);
                            }
#if UNITY_EDITOR
                        else if (!Application.isPlaying) {
                                ssrRenderer.UpdateMaterialProperties(go, settings);
                            }
#endif
                            if (ssrRenderer.exclude) continue;

                            if (firstSSR) {
                                firstSSR = false;

                                // init command buffer
                                cmd.Clear();

                                // prepare ssr target
                                cmd.GetTemporaryRT(ShaderParams.RayCast, sourceDesc, FilterMode.Point);
                                cmd.SetRenderTarget(ShaderParams.RayCast);
                                cmd.ClearRenderTarget(true, true, new Color(0, 0, 0, 0));
                            }
                            for (int s = 0; s < ssrRenderer.ssrMaterials.Length; s++) {
                                if (go.subMeshMask <= 0 || ((1 << s) & go.subMeshMask) != 0) {
                                    Material ssrMat = ssrRenderer.ssrMaterials[s];
                                    cmd.DrawRenderer(goRenderer, ssrMat, s, (int)Pass.SSRSurf);
                                }
                            }
                        }
                    }

                    if (firstSSR) return;
                }

                // Resolve reflections
                RenderTextureDescriptor copyDesc = sourceDesc;
                copyDesc.depthBufferBits = 0;

                cmd.GetTemporaryRT(ShaderParams.ReflectionsTex, copyDesc);
                cmd.Blit(source, ShaderParams.ReflectionsTex, sMat, (int)Pass.Resolve);
                RenderTargetIdentifier input = ShaderParams.ReflectionsTex;

                // Pyramid blur
                copyDesc.width /= settings.blurDownsampling;
                copyDesc.height /= settings.blurDownsampling;
                for (int k = 0; k < MIP_COUNT; k++) {
                    copyDesc.width = Mathf.Max(2, copyDesc.width / 2);
                    copyDesc.height = Mathf.Max(2, copyDesc.height / 2);
                    cmd.GetTemporaryRT(rtPyramid[k], copyDesc, FilterMode.Bilinear);
                    cmd.GetTemporaryRT(ShaderParams.BlurRT, copyDesc, FilterMode.Bilinear);
                    cmd.Blit(input, ShaderParams.BlurRT, sMat, (int)Pass.BlurHoriz);
                    cmd.Blit(ShaderParams.BlurRT, rtPyramid[k], sMat, (int)Pass.BlurVert);
                    cmd.ReleaseTemporaryRT(ShaderParams.BlurRT);
                    input = rtPyramid[k];
                }

                // Output
                int finalPass;
                if (settings.outputMode == OutputMode.Final) {
                    finalPass = (int)Pass.Combine;
                } else if (settings.outputMode == OutputMode.SideBySideComparison) {
                    finalPass = (int)Pass.CombineWithCompare;
                } else {
                    finalPass = (int)Pass.Debug;
                }
                cmd.Blit(ShaderParams.ReflectionsTex, source, sMat, finalPass);

                if (settings.stopNaN) {
                    RenderTextureDescriptor nanDesc = source.descriptor;
                    nanDesc.depthBufferBits = 0;
                    nanDesc.msaaSamples = 1;
                    cmd.GetTemporaryRT(ShaderParams.NaNBuffer, nanDesc);
                    cmd.Blit(source, ShaderParams.NaNBuffer, sMat, (int)Pass.CopyExact);
                    cmd.Blit(ShaderParams.NaNBuffer, source, sMat, (int)Pass.CopyExact);
                }

                // Clean up
                for (int k = 0; k < rtPyramid.Length; k++) {
                    cmd.ReleaseTemporaryRT(rtPyramid[k]);
                }
                cmd.ReleaseTemporaryRT(ShaderParams.RayCast);

                Graphics.ExecuteCommandBuffer(cmd);

            }

            public void Cleanup() {
                if (sMat != null) {
                    DestroyImmediate(sMat);
                }
            }
        }

        [Header("General Settings")]

        [Tooltip("Show reflections in SceneView window")]
        public bool showInSceneView = true;

        [Tooltip("Downsampling multiplier applied to the final blurred reflections")]
        [Range(1, 8)] public int downsampling = 1;

        [Tooltip("Bias applied to depth checking. Increase if reflections desappear at the distance when downsampling is used")]
        [Min(0)] public float depthBias = 0.01f;

        [Tooltip("Show final result / debug view or compare view")]
        public OutputMode outputMode = OutputMode.Final;

        [Tooltip("Position of the dividing line")]
        [Range(-0.01f, 1.01f)] public float separationPos = 0.5f;

        [Tooltip("HDR reflections")]
        public bool lowPrecision;

        [Tooltip("Prevents out of range colors when composing reflections in the destination buffer. This operation performs a ping-pong copy of the frame buffer which can be expensive. Use only if required.")]
        public bool stopNaN;

        [Tooltip("Max number of samples used during the raymarch loop")]
        [Range(4, 128)] public int sampleCount = 16;

        [HideInInspector]
        public float stepSize; // no longer used; kept for backward compatibility during upgrade

        [Tooltip("Maximum reflection distance")]
        public float maxRayLength;

        [Tooltip("Assumed thickness of geometry in the depth buffer before binary search")]
        public float thickness = 0.2f;

        [Tooltip("Number of refinements steps when a reflection hit is found")]
        [Range(0, 16)] public int binarySearchIterations = 6;

        [Tooltip("Increase accuracy of reflection hit after binary search by discarding points further than a reduced thickness.")]
        public bool refineThickness;

        [Tooltip("Assumed thickness of geometry in the depth buffer after binary search")]
        [Range(0.005f, 1f)]
        public float thicknessFine = 0.05f;

        [Tooltip("Jitter helps smoothing edges")]
        [Range(0, 1f)] public float jitter = 0.3f;

        [Tooltip("Animates jitter every frame")]
        public bool animatedJitter = true;

        [Header("Reflection Intensity")]

        [Tooltip("Reflection multiplier")]
        [Range(0, 2)]
        public float reflectionsMultiplier = 1f;

        [Tooltip("Reflection min intensity")]
        [Range(0, 1)]
        public float reflectionsMinIntensity;

        [Tooltip("Reflection max intensity")]
        [Range(0, 1)]
        public float reflectionsMaxIntensity = 1f;

        [Range(0, 1)]
        [Tooltip("Reduces reflection based on view angle")]
        public float fresnel = 0.75f;

        [Tooltip("Reflection decay with distance to reflective point")]
        public float decay = 2f;

        [Tooltip("Reduces intensity of specular reflections")]
        public bool specularControl;

        [Min(0), Tooltip("Power of the specular filter")]
        public float specularSoftenPower = 15f;

        [Tooltip("Controls the attenuation range of effect on screen borders")]
        [Range(0.5f, 2f)]
        public float vignetteSize = 1.1f;

        [Header("Reflection Sharpness")]

        [Min(0)]
        [Tooltip("Ray dispersion with distance")]
        public float fuzzyness;

        [Tooltip("Makes sharpen reflections near objects")]
        public float contactHardening;

        [Range(0, 4f)]
        [Tooltip("Produces sharper reflections based on distance")]
        public float minimumBlur = 0.25f;

        [Tooltip("Downsampling multiplier applied to the blur")]
        [Range(1, 8)] public int blurDownsampling = 1;

        [Tooltip("Custom directional blur strength")]
        public Vector2 blurStrength = Vector2.one;

        SSRPass renderPass;

        Camera cam;

        public bool isDeferredActive {
            get {
                if (cam != null) {
                    return cam.renderingPath != RenderingPath.Forward;
                }
                return false;
            }
        }

        void OnDisable() {
            if (renderPass != null) {
                renderPass.Cleanup();
            }
        }

        void OnEnable() {
            if (maxRayLength == 0) {
                maxRayLength = Mathf.Max(0.1f, stepSize * sampleCount);
            }
            if (renderPass == null) {
                renderPass = new SSRPass();
            }
            cam = GetComponent<Camera>();
            cam.depthTextureMode |= DepthTextureMode.Depth;
        }

        void OnValidate() {
            decay = Mathf.Max(1f, decay);
            if (maxRayLength == 0) {
                maxRayLength = stepSize * sampleCount;
            }
            maxRayLength = Mathf.Max(0.1f, maxRayLength);
            fuzzyness = Mathf.Max(0, fuzzyness);
            thickness = Mathf.Max(0.01f, thickness);
            thicknessFine = Mathf.Max(0.01f, thicknessFine);
            contactHardening = Mathf.Max(0, contactHardening);
            reflectionsMaxIntensity = Mathf.Max(reflectionsMinIntensity, reflectionsMaxIntensity);
            blurStrength.x = Mathf.Max(blurStrength.x, 0f);
            blurStrength.y = Mathf.Max(blurStrength.y, 0f);
        }

        void OnRenderImage(RenderTexture source, RenderTexture destination) {
            renderPass.Setup(this, isDeferredActive);
            renderPass.Execute(source, destination, cam);
        }

        public void ApplyRaytracingPreset(RaytracingPreset preset) {
            switch (preset) {
                case RaytracingPreset.Fast:
                    sampleCount = 16;
                    maxRayLength = 6;
                    binarySearchIterations = 4;
                    downsampling = 3;
                    thickness = 0.5f;
                    refineThickness = false;
                    jitter = 0.3f;
                    break;
                case RaytracingPreset.Medium:
                    sampleCount = 24;
                    maxRayLength = 12;
                    binarySearchIterations = 5;
                    downsampling = 2;
                    refineThickness = false;
                    break;
                case RaytracingPreset.High:
                    sampleCount = 48;
                    maxRayLength = 24;
                    binarySearchIterations = 6;
                    downsampling = 1;
                    refineThickness = false;
                    thicknessFine = 0.05f;
                    break;
                case RaytracingPreset.Superb:
                    sampleCount = 88;
                    maxRayLength = 48;
                    binarySearchIterations = 7;
                    downsampling = 1;
                    refineThickness = true;
                    thicknessFine = 0.02f;
                    break;
                case RaytracingPreset.Ultra:
                    sampleCount = 128;
                    maxRayLength = 64;
                    binarySearchIterations = 8;
                    downsampling = 1;
                    refineThickness = true;
                    thicknessFine = 0.02f;
                    break;
            }
        }

    }

}
