using UnityEngine;
using UnityEngine.Rendering;
#if ENABLE_VR
using XRSettings = UnityEngine.XR.XRSettings;
#endif
#if UNITY_EDITOR
using UnityEditor;
#endif
using System;
using System.Collections.Generic;

namespace HorizonBasedAmbientOcclusion
{
    [ExecuteInEditMode, ImageEffectAllowedInSceneView, AddComponentMenu("Image Effects/HBAO")]
    [RequireComponent(typeof(Camera))]
    public class HBAO : MonoBehaviour
    {
        public enum Preset
        {
            FastestPerformance,
            FastPerformance,
            Normal,
            HighQuality,
            HighestQuality,
            Custom
        }

        public enum PipelineStage
        {
            BeforeImageEffectsOpaque,
            AfterLighting,
            BeforeReflections
        }

        public enum Quality
        {
            Lowest,
            Low,
            Medium,
            High,
            Highest
        }

        public enum Resolution
        {
            Full,
            Half
        }

        public enum NoiseType
        {
            Dither,
            InterleavedGradientNoise,
            SpatialDistribution
        }

        public enum Deinterleaving
        {
            Disabled,
            x4
        }

        public enum DebugMode
        {
            Disabled,
            AOOnly,
            ColorBleedingOnly,
            SplitWithoutAOAndWithAO,
            SplitWithAOAndAOOnly,
            SplitWithoutAOAndAOOnly,
            ViewNormals
        }

        public enum BlurType
        {
            None,
            Narrow,
            Medium,
            Wide,
            ExtraWide
        }

        public enum PerPixelNormals
        {
            GBuffer,
            Camera,
            Reconstruct
        }

        public enum VarianceClipping
        {
            Disabled,
            _4Tap,
            _8Tap
        }

        public Shader hbaoShader;

        [Serializable]
        public struct Presets
        {
            public Preset preset;

            [SerializeField]
            public static Presets defaults
            {
                get
                {
                    return new Presets
                    {
                        preset = Preset.Normal
                    };
                }
            }
        }

        [Serializable]
        public struct GeneralSettings
        {
            [Tooltip("The stage the AO is injected into the rendering pipeline.")]
            [Space(6)]
            public PipelineStage pipelineStage;

            [Tooltip("The quality of the AO.")]
            [Space(10)]
            public Quality quality;

            [Tooltip("The deinterleaving factor.")]
            public Deinterleaving deinterleaving;

            [Tooltip("The resolution at which the AO is calculated.")]
            public Resolution resolution;

            [Tooltip("The type of noise to use.")]
            [Space(10)]
            public NoiseType noiseType;

            [Tooltip("The debug mode actually displayed on screen.")]
            [Space(10)]
            public DebugMode debugMode;

            [SerializeField]
            public static GeneralSettings defaults
            {
                get
                {
                    return new GeneralSettings
                    {
                        pipelineStage = PipelineStage.BeforeImageEffectsOpaque,
                        quality = Quality.Medium,
                        deinterleaving = Deinterleaving.Disabled,
                        resolution = Resolution.Full,
                        noiseType = NoiseType.Dither,
                        debugMode = DebugMode.Disabled
                    };
                }
            }
        }

        [Serializable]
        public struct AOSettings
        {
            [Tooltip("AO radius: this is the distance outside which occluders are ignored.")]
            [Space(6), Range(0.25f, 5f)]
            public float radius;

            [Tooltip("Maximum radius in pixels: this prevents the radius to grow too much with close-up " +
                      "object and impact on performances.")]
            [Range(16, 256)]
            public float maxRadiusPixels;

            [Tooltip("For low-tessellated geometry, occlusion variations tend to appear at creases and " +
                     "ridges, which betray the underlying tessellation. To remove these artifacts, we use " +
                     "an angle bias parameter which restricts the hemisphere.")]
            [Range(0, 0.5f)]
            public float bias;

            [Tooltip("This value allows to scale up the ambient occlusion values.")]
            [Range(0, 4)]
            public float intensity;

            [Tooltip("Enable/disable MultiBounce approximation.")]
            public bool useMultiBounce;

            [Tooltip("MultiBounce approximation influence.")]
            [Range(0, 1)]
            public float multiBounceInfluence;

            [Tooltip("The amount of AO offscreen samples are contributing.")]
            [Range(0, 1)]
            public float offscreenSamplesContribution;

            [Tooltip("The max distance to display AO.")]
            [Space(10)]
            public float maxDistance;

            [Tooltip("The distance before max distance at which AO start to decrease.")]
            public float distanceFalloff;

            [Tooltip("The type of per pixel normals to use.")]
            [Space(10)]
            public PerPixelNormals perPixelNormals;

            [Tooltip("This setting allow you to set the base color if the AO, the alpha channel value is unused.")]
            [Space(10)]
            public Color baseColor;

            [SerializeField]
            public static AOSettings defaults
            {
                get
                {
                    return new AOSettings
                    {
                        radius = 0.8f,
                        maxRadiusPixels = 128f,
                        bias = 0.05f,
                        intensity = 1f,
                        useMultiBounce = false,
                        multiBounceInfluence = 1f,
                        offscreenSamplesContribution = 0f,
                        maxDistance = 150f,
                        distanceFalloff = 50f,
                        perPixelNormals = PerPixelNormals.GBuffer,
                        baseColor = Color.black
                    };
                }
            }
        }

        [Serializable]
        public struct TemporalFilterSettings
        {
            [Space(6)]
            public bool enabled;

            [Tooltip("The type of variance clipping to use.")]
            public VarianceClipping varianceClipping;

            [SerializeField]
            public static TemporalFilterSettings defaults
            {
                get
                {
                    return new TemporalFilterSettings
                    {
                        enabled = false,
                        varianceClipping = VarianceClipping._4Tap
                    };
                }
            }
        }

        [Serializable]
        public struct BlurSettings
        {
            [Tooltip("The type of blur to use.")]
            [Space(6)]
            public BlurType type;

            [Tooltip("This parameter controls the depth-dependent weight of the bilateral filter, to " +
                     "avoid bleeding across edges. A zero sharpness is a pure Gaussian blur. Increasing " +
                     "the blur sharpness removes bleeding by using lower weights for samples with large " +
                     "depth delta from the current pixel.")]
            [Space(10), Range(0, 16)]
            public float sharpness;

            [SerializeField]
            public static BlurSettings defaults
            {
                get
                {
                    return new BlurSettings
                    {
                        type = BlurType.Medium,
                        sharpness = 8f
                    };
                }
            }
        }

        [Serializable]
        public struct ColorBleedingSettings
        {
            [Space(6)]
            public bool enabled;

            [Tooltip("This value allows to control the saturation of the color bleeding.")]
            [Space(10), Range(0, 4)]
            public float saturation;

            [Tooltip("This value allows to scale the contribution of the color bleeding samples.")]
            [Range(0, 32)]
            public float albedoMultiplier;

            [Tooltip("Use masking on emissive pixels")]
            [Range(0, 1)]
            public float brightnessMask;

            [Tooltip("Brightness level where masking starts/ends")]
            [MinMaxSlider(0, 2)]
            public Vector2 brightnessMaskRange;

            [SerializeField]
            public static ColorBleedingSettings defaults
            {
                get
                {
                    return new ColorBleedingSettings
                    {
                        enabled = false,
                        saturation = 1f,
                        albedoMultiplier = 4f,
                        brightnessMask = 1f,
                        brightnessMaskRange = new Vector2(0.0f, 0.5f)
                    };
                }
            }
        }

        [AttributeUsage(AttributeTargets.Field)]
        public class SettingsGroup : Attribute { }

        [SerializeField, SettingsGroup]
        private Presets m_Presets = Presets.defaults;
        public Presets presets
        {
            get { return m_Presets; }
            set { m_Presets = value; }
        }

        [SerializeField, SettingsGroup]
        private GeneralSettings m_GeneralSettings = GeneralSettings.defaults;
        public GeneralSettings generalSettings
        {
            get { return m_GeneralSettings; }
            set { m_GeneralSettings = value; }
        }

        [SerializeField, SettingsGroup]
        private AOSettings m_AOSettings = AOSettings.defaults;
        public AOSettings aoSettings
        {
            get { return m_AOSettings; }
            set { m_AOSettings = value; }
        }

        [SerializeField, SettingsGroup]
        private TemporalFilterSettings m_TemporalFilterSettings = TemporalFilterSettings.defaults;
        public TemporalFilterSettings temporalFilterSettings
        {
            get { return m_TemporalFilterSettings; }
            set { m_TemporalFilterSettings = value; }
        }

        [SerializeField, SettingsGroup]
        private BlurSettings m_BlurSettings = BlurSettings.defaults;
        public BlurSettings blurSettings
        {
            get { return m_BlurSettings; }
            set { m_BlurSettings = value; }
        }

        [SerializeField, SettingsGroup]
        private ColorBleedingSettings m_ColorBleedingSettings = ColorBleedingSettings.defaults;
        public ColorBleedingSettings colorBleedingSettings
        {
            get { return m_ColorBleedingSettings; }
            set { m_ColorBleedingSettings = value; }
        }

        public class MinMaxSliderAttribute : PropertyAttribute
        {
            public readonly float max;
            public readonly float min;

            public MinMaxSliderAttribute(float min, float max)
            {
                this.min = min;
                this.max = max;
            }
        }

        public Preset GetCurrentPreset()
        {
            return m_Presets.preset;
        }

        public void ApplyPreset(Preset preset)
        {
            if (preset == Preset.Custom)
            {
                m_Presets.preset = preset;
                return;
            }

            var debugMode = generalSettings.debugMode;

            m_GeneralSettings = GeneralSettings.defaults;
            m_AOSettings = AOSettings.defaults;
            m_ColorBleedingSettings = ColorBleedingSettings.defaults;
            m_BlurSettings = BlurSettings.defaults;

            SetDebugMode(debugMode);

            switch (preset)
            {
                case Preset.FastestPerformance:
                    SetQuality(Quality.Lowest);
                    SetAoRadius(0.5f);
                    SetAoMaxRadiusPixels(64.0f);
                    SetBlurType(BlurType.ExtraWide);
                    break;
                case Preset.FastPerformance:
                    SetQuality(Quality.Low);
                    SetAoRadius(0.5f);
                    SetAoMaxRadiusPixels(64.0f);
                    SetBlurType(BlurType.Wide);
                    break;
                case Preset.HighQuality:
                    SetQuality(Quality.High);
                    SetAoRadius(1.0f);
                    break;
                case Preset.HighestQuality:
                    SetQuality(Quality.Highest);
                    SetAoRadius(1.2f);
                    SetAoMaxRadiusPixels(256.0f);
                    SetBlurType(BlurType.Narrow);
                    break;
                case Preset.Normal:
                default:
                    break;
            }

            m_Presets.preset = preset;
        }

        public PipelineStage GetPipelineStage()
        {
            return m_GeneralSettings.pipelineStage;
        }

        public void SetPipelineStage(PipelineStage pipelineStage)
        {
            m_GeneralSettings.pipelineStage = pipelineStage;
        }

        public Quality GetQuality()
        {
            return m_GeneralSettings.quality;
        }

        public void SetQuality(Quality quality)
        {
            m_GeneralSettings.quality = quality;
        }

        public Deinterleaving GetDeinterleaving()
        {
            return m_GeneralSettings.deinterleaving;
        }

        public void SetDeinterleaving(Deinterleaving deinterleaving)
        {
            m_GeneralSettings.deinterleaving = deinterleaving;
        }

        public Resolution GetResolution()
        {
            return m_GeneralSettings.resolution;
        }

        public void SetResolution(Resolution resolution)
        {
            m_GeneralSettings.resolution = resolution;
        }

        public NoiseType GetNoiseType()
        {
            return m_GeneralSettings.noiseType;
        }

        public void SetNoiseType(NoiseType noiseType)
        {
            m_GeneralSettings.noiseType = noiseType;
        }

        public DebugMode GetDebugMode()
        {
            return m_GeneralSettings.debugMode;
        }

        public void SetDebugMode(DebugMode debugMode)
        {
            m_GeneralSettings.debugMode = debugMode;
        }

        public float GetAoRadius()
        {
            return m_AOSettings.radius;
        }

        public void SetAoRadius(float radius)
        {
            m_AOSettings.radius = Mathf.Clamp(radius, 0.25f, 5);
        }

        public float GetAoMaxRadiusPixels()
        {
            return m_AOSettings.maxRadiusPixels;
        }

        public void SetAoMaxRadiusPixels(float maxRadiusPixels)
        {
            m_AOSettings.maxRadiusPixels = Mathf.Clamp(maxRadiusPixels, 16, 256);
        }

        public float GetAoBias()
        {
            return m_AOSettings.bias;
        }

        public void SetAoBias(float bias)
        {
            m_AOSettings.bias = Mathf.Clamp(bias, 0, 0.5f);
        }

        public float GetAoOffscreenSamplesContribution()
        {
            return m_AOSettings.offscreenSamplesContribution;
        }

        public void SetAoOffscreenSamplesContribution(float contribution)
        {
            m_AOSettings.offscreenSamplesContribution = Mathf.Clamp01(contribution);
        }

        public float GetAoMaxDistance()
        {
            return m_AOSettings.maxDistance;
        }

        public void SetAoMaxDistance(float maxDistance)
        {
            m_AOSettings.maxDistance = maxDistance;
        }

        public float GetAoDistanceFalloff()
        {
            return m_AOSettings.distanceFalloff;
        }

        public void SetAoDistanceFalloff(float distanceFalloff)
        {
            m_AOSettings.distanceFalloff = distanceFalloff;
        }

        public PerPixelNormals GetAoPerPixelNormals()
        {
            return m_AOSettings.perPixelNormals;
        }

        public void SetAoPerPixelNormals(PerPixelNormals perPixelNormals)
        {
            m_AOSettings.perPixelNormals = perPixelNormals;
        }

        public Color GetAoColor()
        {
            return m_AOSettings.baseColor;
        }

        public void SetAoColor(Color color)
        {
            m_AOSettings.baseColor = color;
        }

        public float GetAoIntensity()
        {
            return m_AOSettings.intensity;
        }

        public void SetAoIntensity(float intensity)
        {
            m_AOSettings.intensity = Mathf.Clamp(intensity, 0, 4);
        }

        public bool UseMultiBounce()
        {
            return m_AOSettings.useMultiBounce;
        }

        public void EnableMultiBounce(bool enabled = true)
        {
            m_AOSettings.useMultiBounce = enabled;
        }

        public float GetAoMultiBounceInfluence()
        {
            return m_AOSettings.multiBounceInfluence;
        }

        public void SetAoMultiBounceInfluence(float multiBounceInfluence)
        {
            m_AOSettings.multiBounceInfluence = Mathf.Clamp01(multiBounceInfluence);
        }

        public bool IsTemporalFilterEnabled()
        {
            return m_TemporalFilterSettings.enabled;
        }

        public void EnableTemporalFilter(bool enabled = true)
        {
            m_TemporalFilterSettings.enabled = enabled;
        }

        public VarianceClipping GetTemporalFilterVarianceClipping()
        {
            return m_TemporalFilterSettings.varianceClipping;
        }

        public void SetTemporalFilterVarianceClipping(VarianceClipping varianceClipping)
        {
            m_TemporalFilterSettings.varianceClipping = varianceClipping;
        }

        public BlurType GetBlurType()
        {
            return m_BlurSettings.type;
        }

        public void SetBlurType(BlurType blurType)
        {
            m_BlurSettings.type = blurType;
        }

        public float GetBlurSharpness()
        {
            return m_BlurSettings.sharpness;
        }

        public void SetBlurSharpness(float sharpness)
        {
            m_BlurSettings.sharpness = Mathf.Clamp(sharpness, 0, 16);
        }

        public bool IsColorBleedingEnabled()
        {
            return m_ColorBleedingSettings.enabled;
        }

        public void EnableColorBleeding(bool enabled = true)
        {
            m_ColorBleedingSettings.enabled = enabled;
        }

        public float GetColorBleedingSaturation()
        {
            return m_ColorBleedingSettings.saturation;
        }

        public void SetColorBleedingSaturation(float saturation)
        {
            m_ColorBleedingSettings.saturation = Mathf.Clamp(saturation, 0, 4);
        }

        public float GetColorBleedingAlbedoMultiplier()
        {
            return m_ColorBleedingSettings.albedoMultiplier;
        }

        public void SetColorBleedingAlbedoMultiplier(float albedoMultiplier)
        {
            m_ColorBleedingSettings.albedoMultiplier = Mathf.Clamp(albedoMultiplier, 0, 32);
        }

        public float GetColorBleedingBrightnessMask()
        {
            return m_ColorBleedingSettings.brightnessMask;
        }

        public void SetColorBleedingBrightnessMask(float brightnessMask)
        {
            m_ColorBleedingSettings.brightnessMask = Mathf.Clamp01(brightnessMask);
        }

        public Vector2 GetColorBleedingBrightnessMaskRange()
        {
            return m_ColorBleedingSettings.brightnessMaskRange;
        }

        public void SetColorBleedingBrightnessMaskRange(Vector2 brightnessMaskRange)
        {
            brightnessMaskRange.x = Mathf.Clamp(brightnessMaskRange.x, 0, 2);
            brightnessMaskRange.y = Mathf.Clamp(brightnessMaskRange.y, 0, 2);
            brightnessMaskRange.x = Mathf.Min(brightnessMaskRange.x, brightnessMaskRange.y);
            m_ColorBleedingSettings.brightnessMaskRange = brightnessMaskRange;
        }

        private static class Pass
        {
            public const int AO = 0;
            public const int AO_Deinterleaved = 1;

            public const int Deinterleave_Depth = 2;
            public const int Deinterleave_Normals = 3;
            public const int Atlas_AO_Deinterleaved = 4;
            public const int Reinterleave_AO = 5;

            public const int Blur = 6;

            public const int Temporal_Filter = 7;

            public const int Copy = 8;

            public const int Composite = 9;
            public const int Composite_AfterLighting = 10;
            public const int Composite_BeforeReflections = 11;

            public const int Debug_ViewNormals = 12;
        }

        private static class ShaderProperties
        {
            public static int mainTex;
            public static int hbaoTex;
            public static int tempTex;
            public static int tempTex2;
            public static int noiseTex;
            public static int depthTex;
            public static int normalsTex;
            public static int[] depthSliceTex;
            public static int[] normalsSliceTex;
            public static int[] aoSliceTex;
            public static int[] deinterleaveOffset;
            public static int atlasOffset;
            public static int jitter;
            public static int uvTransform;
            public static int inputTexelSize;
            public static int aoTexelSize;
            public static int deinterleavedAOTexelSize;
            public static int reinterleavedAOTexelSize;
            public static int uvToView;
            public static int worldToCameraMatrix;
            public static int targetScale;
            public static int radius;
            public static int maxRadiusPixels;
            public static int negInvRadius2;
            public static int angleBias;
            public static int aoMultiplier;
            public static int intensity;
            public static int multiBounceInfluence;
            public static int offscreenSamplesContrib;
            public static int maxDistance;
            public static int distanceFalloff;
            public static int baseColor;
            public static int colorBleedSaturation;
            public static int albedoMultiplier;
            public static int colorBleedBrightnessMask;
            public static int colorBleedBrightnessMaskRange;
            public static int blurDeltaUV;
            public static int blurSharpness;
            public static int temporalParams;

            static ShaderProperties()
            {
                mainTex = Shader.PropertyToID("_MainTex");
                hbaoTex = Shader.PropertyToID("_HBAOTex");
                tempTex = Shader.PropertyToID("_TempTex");
                tempTex2 = Shader.PropertyToID("_TempTex2");
                noiseTex = Shader.PropertyToID("_NoiseTex");
                depthTex = Shader.PropertyToID("_DepthTex");
                normalsTex = Shader.PropertyToID("_NormalsTex");
                depthSliceTex = new int[4 * 4];
                normalsSliceTex = new int[4 * 4];
                aoSliceTex = new int[4 * 4];
                for (int i = 0; i < 4 * 4; i++)
                {
                    depthSliceTex[i] = Shader.PropertyToID("_DepthSliceTex" + i);
                    normalsSliceTex[i] = Shader.PropertyToID("_NormalsSliceTex" + i);
                    aoSliceTex[i] = Shader.PropertyToID("_AOSliceTex" + i);
                }
                deinterleaveOffset = new int[] {
                Shader.PropertyToID("_Deinterleave_Offset00"),
                Shader.PropertyToID("_Deinterleave_Offset10"),
                Shader.PropertyToID("_Deinterleave_Offset01"),
                Shader.PropertyToID("_Deinterleave_Offset11")
            };
                atlasOffset = Shader.PropertyToID("_AtlasOffset");
                jitter = Shader.PropertyToID("_Jitter");
                uvTransform = Shader.PropertyToID("_UVTransform");
                inputTexelSize = Shader.PropertyToID("_Input_TexelSize");
                aoTexelSize = Shader.PropertyToID("_AO_TexelSize");
                deinterleavedAOTexelSize = Shader.PropertyToID("_DeinterleavedAO_TexelSize");
                reinterleavedAOTexelSize = Shader.PropertyToID("_ReinterleavedAO_TexelSize");
                uvToView = Shader.PropertyToID("_UVToView");
                worldToCameraMatrix = Shader.PropertyToID("_WorldToCameraMatrix");
                targetScale = Shader.PropertyToID("_TargetScale");
                radius = Shader.PropertyToID("_Radius");
                maxRadiusPixels = Shader.PropertyToID("_MaxRadiusPixels");
                negInvRadius2 = Shader.PropertyToID("_NegInvRadius2");
                angleBias = Shader.PropertyToID("_AngleBias");
                aoMultiplier = Shader.PropertyToID("_AOmultiplier");
                intensity = Shader.PropertyToID("_Intensity");
                multiBounceInfluence = Shader.PropertyToID("_MultiBounceInfluence");
                offscreenSamplesContrib = Shader.PropertyToID("_OffscreenSamplesContrib");
                maxDistance = Shader.PropertyToID("_MaxDistance");
                distanceFalloff = Shader.PropertyToID("_DistanceFalloff");
                baseColor = Shader.PropertyToID("_BaseColor");
                colorBleedSaturation = Shader.PropertyToID("_ColorBleedSaturation");
                albedoMultiplier = Shader.PropertyToID("_AlbedoMultiplier");
                colorBleedBrightnessMask = Shader.PropertyToID("_ColorBleedBrightnessMask");
                colorBleedBrightnessMaskRange = Shader.PropertyToID("_ColorBleedBrightnessMaskRange");
                blurDeltaUV = Shader.PropertyToID("_BlurDeltaUV");
                blurSharpness = Shader.PropertyToID("_BlurSharpness");
                temporalParams = Shader.PropertyToID("_TemporalParams");
            }

            public static string GetOrthographicOrDeferredKeyword(bool orthographic, GeneralSettings settings)
            {
                // need to check that integrationStage is not BeforeImageEffectOpaque as Gbuffer0 is not available in this case
                return orthographic ? "ORTHOGRAPHIC_PROJECTION" : settings.pipelineStage != PipelineStage.BeforeImageEffectsOpaque ? "DEFERRED_SHADING" : "__";
            }

            public static string GetQualityKeyword(GeneralSettings settings)
            {
                switch (settings.quality)
                {
                    case Quality.Lowest:
                        return "QUALITY_LOWEST";
                    case Quality.Low:
                        return "QUALITY_LOW";
                    case Quality.Medium:
                        return "QUALITY_MEDIUM";
                    case Quality.High:
                        return "QUALITY_HIGH";
                    case Quality.Highest:
                        return "QUALITY_HIGHEST";
                    default:
                        return "QUALITY_MEDIUM";
                }
            }

            public static string GetNoiseKeyword(GeneralSettings settings)
            {
                switch (settings.noiseType)
                {
                    case NoiseType.InterleavedGradientNoise:
                        return "INTERLEAVED_GRADIENT_NOISE";
                    case NoiseType.Dither:
                    case NoiseType.SpatialDistribution:
                    default:
                        return "__";
                }
            }

            public static string GetDeinterleavingKeyword(GeneralSettings settings)
            {
                switch (settings.deinterleaving)
                {
                    case Deinterleaving.x4:
                        return "DEINTERLEAVED";
                    case Deinterleaving.Disabled:
                    default:
                        return "__";
                }
            }

            public static string GetDebugKeyword(GeneralSettings settings)
            {
                switch (settings.debugMode)
                {
                    case DebugMode.AOOnly:
                        return "DEBUG_AO";
                    case DebugMode.ColorBleedingOnly:
                        return "DEBUG_COLORBLEEDING";
                    case DebugMode.SplitWithoutAOAndWithAO:
                        return "DEBUG_NOAO_AO";
                    case DebugMode.SplitWithAOAndAOOnly:
                        return "DEBUG_AO_AOONLY";
                    case DebugMode.SplitWithoutAOAndAOOnly:
                        return "DEBUG_NOAO_AOONLY";
                    case DebugMode.Disabled:
                    default:
                        return "__";
                }
            }

            public static string GetMultibounceKeyword(AOSettings settings)
            {
                return settings.useMultiBounce ? "MULTIBOUNCE" : "__";
            }

            public static string GetOffscreenSamplesContributionKeyword(AOSettings settings)
            {
                return settings.offscreenSamplesContribution > 0 ? "OFFSCREEN_SAMPLES_CONTRIBUTION" : "__";
            }

            public static string GetPerPixelNormalsKeyword(AOSettings settings)
            {
                switch (settings.perPixelNormals)
                {
                    case PerPixelNormals.Camera:
                        return "NORMALS_CAMERA";
                    case PerPixelNormals.Reconstruct:
                        return "NORMALS_RECONSTRUCT";
                    case PerPixelNormals.GBuffer:
                    default:
                        return "__";
                }
            }

            public static string GetBlurRadiusKeyword(BlurSettings settings)
            {
                switch (settings.type)
                {
                    case BlurType.Narrow:
                        return "BLUR_RADIUS_2";
                    case BlurType.Medium:
                        return "BLUR_RADIUS_3";
                    case BlurType.Wide:
                        return "BLUR_RADIUS_4";
                    case BlurType.ExtraWide:
                        return "BLUR_RADIUS_5";
                    case BlurType.None:
                    default:
                        return "BLUR_RADIUS_3";
                }
            }

            public static string GetVarianceClippingKeyword(TemporalFilterSettings settings)
            {
                switch (settings.varianceClipping)
                {
                    case VarianceClipping._4Tap:
                        return "VARIANCE_CLIPPING_4TAP";
                    case VarianceClipping._8Tap:
                        return "VARIANCE_CLIPPING_8TAP";
                    case VarianceClipping.Disabled:
                    default:
                        return "__";
                }
            }

            public static string GetColorBleedingKeyword(ColorBleedingSettings settings)
            {
                return settings.enabled ? "COLOR_BLEEDING" : "__";
            }

            public static string GetLightingLogEncodedKeyword(bool hdr)
            {
                return hdr ? "__" : "LIGHTING_LOG_ENCODED";
            }
        }

        private static class MersenneTwister
        {
            // Mersenne-Twister random numbers in [0,1).
            public static float[] Numbers = new float[] {
            //0.463937f,0.340042f,0.223035f,0.468465f,0.322224f,0.979269f,0.031798f,0.973392f,0.778313f,0.456168f,0.258593f,0.330083f,0.387332f,0.380117f,0.179842f,0.910755f,
            //0.511623f,0.092933f,0.180794f,0.620153f,0.101348f,0.556342f,0.642479f,0.442008f,0.215115f,0.475218f,0.157357f,0.568868f,0.501241f,0.629229f,0.699218f,0.707733f
            0.556725f,0.005520f,0.708315f,0.583199f,0.236644f,0.992380f,0.981091f,0.119804f,0.510866f,0.560499f,0.961497f,0.557862f,0.539955f,0.332871f,0.417807f,0.920779f,
            0.730747f,0.076690f,0.008562f,0.660104f,0.428921f,0.511342f,0.587871f,0.906406f,0.437980f,0.620309f,0.062196f,0.119485f,0.235646f,0.795892f,0.044437f,0.617311f
        };
        }

        private static readonly Vector2[] s_jitter = new Vector2[4 * 4];
        private static readonly float[] s_temporalRotations = { 60.0f, 300.0f, 180.0f, 240.0f, 120.0f, 0.0f };
        private static readonly float[] s_temporalOffsets = { 0.0f, 0.5f, 0.25f, 0.75f };

        private Material material { get; set; }
        private Camera hbaoCamera { get; set; }
        private CommandBuffer cmdBuffer { get; set; }
        private int width { get; set; }
        private int height { get; set; }
        private bool stereoActive { get; set; }
        private int numberOfEyes { get; set; }
        private int xrActiveEye { get; set; }
        private XRSettings.StereoRenderingMode stereoRenderingMode { get; set; }
        private int screenWidth { get; set; }
        private int screenHeight { get; set; }
        private int aoWidth { get; set; }
        private int aoHeight { get; set; }
        private int reinterleavedAoWidth { get; set; }
        private int reinterleavedAoHeight { get; set; }
        private int deinterleavedAoWidth { get; set; }
        private int deinterleavedAoHeight { get; set; }
        private int frameCount { get; set; }
        private bool motionVectorsSupported { get; set; }
        private RenderTexture aoHistoryBuffer { get; set; }
        private RenderTexture colorBleedingHistoryBuffer { get; set; }
        private Texture2D noiseTex { get; set; }

        private Mesh fullscreenTriangle
        {
            get
            {
                if (m_FullscreenTriangle != null)
                    return m_FullscreenTriangle;

                m_FullscreenTriangle = new Mesh { name = "Fullscreen Triangle" };

                // Because we have to support older platforms (GLES2/3, DX9 etc) we can't do all of
                // this directly in the vertex shader using vertex ids :(
                m_FullscreenTriangle.SetVertices(new List<Vector3>
                {
                    new Vector3(-1f, -1f, 0f),
                    new Vector3(-1f,  3f, 0f),
                    new Vector3( 3f, -1f, 0f)
                });
                m_FullscreenTriangle.SetIndices(new[] { 0, 1, 2 }, MeshTopology.Triangles, 0, false);
                m_FullscreenTriangle.UploadMeshData(false);

                return m_FullscreenTriangle;
            }
        }

        private CameraEvent cameraEvent
        {
            get
            {
                // Forces BeforeImageEffectsOpaque as soon  as normal display mode isn't used,
                // the component still display its integration stage but we force it here to ensure
                // all debug modes display correctly.
                if (generalSettings.debugMode != DebugMode.Disabled)
                    return CameraEvent.BeforeImageEffectsOpaque;

                switch (generalSettings.pipelineStage)
                {
                    case PipelineStage.BeforeReflections:
                        return CameraEvent.BeforeReflections;
                    case PipelineStage.AfterLighting:
                        return CameraEvent.AfterLighting;
                    case PipelineStage.BeforeImageEffectsOpaque:
                    default:
                        return CameraEvent.BeforeImageEffectsOpaque;
                }
            }
        }

        private bool isCommandBufferDirty
        {
            get
            {
                if (m_IsCommandBufferDirty || m_PreviousPipelineStage != generalSettings.pipelineStage || m_PreviousResolution != generalSettings.resolution ||
                    m_PreviousDebugMode != generalSettings.debugMode || m_PreviousAllowHDR != hbaoCamera.allowHDR || m_PreviousWidth != width || m_PreviousHeight != height ||
                    m_PreviousDeinterleaving != generalSettings.deinterleaving || m_PreviousBlurAmount != blurSettings.type ||
                    m_PreviousColorBleedingEnabled != colorBleedingSettings.enabled || m_PreviousTemporalFilterEnabled != temporalFilterSettings.enabled ||
                    m_PreviousRenderingPath != hbaoCamera.actualRenderingPath)
                {
                    m_PreviousPipelineStage = generalSettings.pipelineStage;
                    m_PreviousResolution = generalSettings.resolution;
                    m_PreviousDebugMode = generalSettings.debugMode;
                    m_PreviousAllowHDR = hbaoCamera.allowHDR;
                    m_PreviousWidth = width;
                    m_PreviousHeight = height;
                    m_PreviousDeinterleaving = generalSettings.deinterleaving;
                    m_PreviousBlurAmount = blurSettings.type;
                    m_PreviousColorBleedingEnabled = colorBleedingSettings.enabled;
                    m_PreviousTemporalFilterEnabled = temporalFilterSettings.enabled;
                    m_PreviousRenderingPath = hbaoCamera.actualRenderingPath;

                    return true;
                }

                return false;
            }
            set
            {
                m_IsCommandBufferDirty = value;
            }
        }

        private static RenderTextureFormat defaultHDRRenderTextureFormat
        {
            get
            {
#if UNITY_ANDROID || UNITY_IPHONE || UNITY_TVOS || UNITY_SWITCH || UNITY_EDITOR
                RenderTextureFormat format = RenderTextureFormat.RGB111110Float;
#if UNITY_EDITOR
                var target = EditorUserBuildSettings.activeBuildTarget;
                if (target != BuildTarget.Android && target != BuildTarget.iOS && target != BuildTarget.tvOS && target != BuildTarget.Switch)
                    return RenderTextureFormat.DefaultHDR;
#endif // UNITY_EDITOR
                if (SystemInfo.SupportsRenderTextureFormat(format))
                    return format;
#endif // UNITY_ANDROID || UNITY_IPHONE || UNITY_TVOS || UNITY_SWITCH || UNITY_EDITOR
                return RenderTextureFormat.DefaultHDR;
            }
        }

        private RenderTextureFormat sourceFormat { get { return hbaoCamera.allowHDR ? defaultHDRRenderTextureFormat : RenderTextureFormat.Default; } }
        //private static RenderTextureFormat colorFormat { get { return SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGB64) ? RenderTextureFormat.ARGB64 : RenderTextureFormat.ARGB32; } }
        private static RenderTextureFormat colorFormat { get { return SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGBHalf) ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.Default; } }
        private static RenderTextureFormat depthFormat { get { return SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.RFloat) ? RenderTextureFormat.RFloat : RenderTextureFormat.RHalf; } }
        private static RenderTextureFormat normalsFormat { get { return SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGB2101010) ? RenderTextureFormat.ARGB2101010 : RenderTextureFormat.Default; } }
        private static bool isLinearColorSpace { get { return QualitySettings.activeColorSpace == ColorSpace.Linear; } }
        private bool renderingInSceneView { get { return hbaoCamera.cameraType == CameraType.SceneView; } }

        private RenderTextureDescriptor m_sourceDescriptor;
        private string[] m_ShaderKeywords;
        private bool m_IsCommandBufferDirty;
        private Mesh m_FullscreenTriangle;
        private PipelineStage? m_PreviousPipelineStage;
        private Resolution? m_PreviousResolution;
        private Deinterleaving? m_PreviousDeinterleaving;
        private DebugMode? m_PreviousDebugMode;
        private NoiseType? m_PreviousNoiseType;
        private BlurType? m_PreviousBlurAmount;
        private int m_PreviousWidth;
        private int m_PreviousHeight;
        private bool m_PreviousAllowHDR;
        private bool m_PreviousColorBleedingEnabled;
        private bool m_PreviousTemporalFilterEnabled;
        private RenderingPath m_PreviousRenderingPath;

        void OnEnable()
        {
            if (!SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.Depth))
            {
                Debug.LogWarning("HBAO shader is not supported on this platform.");
                this.enabled = false;
                return;
            }

            if (hbaoShader == null) hbaoShader = Shader.Find("Hidden/HBAO");
            if (hbaoShader == null)
            {
                Debug.LogError("HBAO shader was not found...");
                return;
            }

            if (!hbaoShader.isSupported)
            {
                Debug.LogWarning("HBAO shader is not supported on this platform.");
                this.enabled = false;
                return;
            }

            Initialize();
        }

        void OnDisable()
        {
            ClearCommandBuffer(cmdBuffer);

            ReleaseHistoryBuffers();

            if (material != null)
                DestroyImmediate(material);
            if (noiseTex != null)
                DestroyImmediate(noiseTex);
            if (fullscreenTriangle != null)
                DestroyImmediate(fullscreenTriangle);
        }

        void OnPreRender()
        {
            if (hbaoShader == null || hbaoCamera == null) return;

            FetchRenderParameters();
            CheckParameters();
            UpdateMaterialProperties();
            UpdateShaderKeywords();

            if (isCommandBufferDirty)
            {
                ClearCommandBuffer(cmdBuffer);
                BuildCommandBuffer(cmdBuffer, cameraEvent);
                hbaoCamera.AddCommandBuffer(cameraEvent, cmdBuffer);
                isCommandBufferDirty = false;
            }
        }

        void OnPostRender()
        {
            frameCount++;
        }

        void OnValidate()
        {
            if (hbaoShader == null || hbaoCamera == null) return;

            CheckParameters();
        }

        private void Initialize()
        {
            m_sourceDescriptor = new RenderTextureDescriptor(0, 0);

            hbaoCamera = GetComponent<Camera>();
            hbaoCamera.forceIntoRenderTexture = true;

            material = new Material(hbaoShader);
            material.hideFlags = HideFlags.HideAndDontSave;

            // For platforms not supporting motion vectors texture
            // https://docs.unity3d.com/ScriptReference/DepthTextureMode.MotionVectors.html
            motionVectorsSupported = SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.RGHalf);

            cmdBuffer = new CommandBuffer { name = "HBAO" };
            isCommandBufferDirty = true;
        }

        private void FetchRenderParameters()
        {
#if !UNITY_SWITCH && ENABLE_VR
            if (hbaoCamera.stereoEnabled)
            {
                var xrDesc = XRSettings.eyeTextureDesc;
                stereoRenderingMode = XRSettings.StereoRenderingMode.SinglePass;
                numberOfEyes = 1;

                if (XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.MultiPass)
                    stereoRenderingMode = XRSettings.StereoRenderingMode.MultiPass;

#if UNITY_STANDALONE || UNITY_EDITOR || UNITY_PS4
                if (xrDesc.dimension == TextureDimension.Tex2DArray)
                    stereoRenderingMode = XRSettings.StereoRenderingMode.SinglePassInstanced;
#endif

                if (stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePassInstanced)
                    numberOfEyes = 2;

                if (stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePass)
                {
                    numberOfEyes = 2;
                    //xrDesc.width /= 2;
                    xrDesc.vrUsage = VRTextureUsage.None;
                }

                width = xrDesc.width;
                height = xrDesc.height;
                m_sourceDescriptor = xrDesc;

                if (hbaoCamera.stereoActiveEye == Camera.MonoOrStereoscopicEye.Right)
                    xrActiveEye = (int)Camera.StereoscopicEye.Right;

                screenWidth = XRSettings.eyeTextureWidth;
                screenHeight = XRSettings.eyeTextureHeight;

                if (stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePass)
                    screenWidth /= 2;

                stereoActive = true;
            }
            else
#endif
            {
                width = hbaoCamera.pixelWidth;
                height = hbaoCamera.pixelHeight;
                m_sourceDescriptor.width = width;
                m_sourceDescriptor.height = height;
                screenWidth = width;
                screenHeight = height;
                stereoActive = false;
                numberOfEyes = 1;
            }

            var downsamplingFactor = generalSettings.resolution == Resolution.Full ? 1 : generalSettings.deinterleaving == Deinterleaving.Disabled ? 2 : 1;
            if (downsamplingFactor > 1)
            {
                aoWidth = (width + width % 2) / downsamplingFactor;
                aoHeight = (height + height % 2) / downsamplingFactor;
            }
            else
            {
                aoWidth = width;
                aoHeight = height;
            }

            reinterleavedAoWidth = width + (width % 4 == 0 ? 0 : 4 - (width % 4));
            reinterleavedAoHeight = height + (height % 4 == 0 ? 0 : 4 - (height % 4));
            deinterleavedAoWidth = reinterleavedAoWidth / 4;
            deinterleavedAoHeight = reinterleavedAoHeight / 4;
        }

        private void AllocateHistoryBuffers()
        {
            ReleaseHistoryBuffers();

            aoHistoryBuffer = GetScreenSpaceRT(widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
            if (colorBleedingSettings.enabled)
                colorBleedingHistoryBuffer = GetScreenSpaceRT(widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);

            // Clear history buffers to default
            var lastActive = RenderTexture.active;
            RenderTexture.active = aoHistoryBuffer;
            GL.Clear(false, true, Color.white);
            if (colorBleedingSettings.enabled)
            {
                RenderTexture.active = colorBleedingHistoryBuffer;
                GL.Clear(false, true, new Color(0, 0, 0, 1));
            }
            RenderTexture.active = lastActive;

            frameCount = 0;
        }

        private void ReleaseHistoryBuffers()
        {
            if (aoHistoryBuffer != null)
                aoHistoryBuffer.Release();

            if (colorBleedingHistoryBuffer != null)
                colorBleedingHistoryBuffer.Release();
        }

        private void ClearCommandBuffer(CommandBuffer cmd)
        {
            if (cmd != null)
            {
                if (hbaoCamera != null)
                {
                    hbaoCamera.RemoveCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, cmd);
                    hbaoCamera.RemoveCommandBuffer(CameraEvent.AfterLighting, cmd);
                    hbaoCamera.RemoveCommandBuffer(CameraEvent.BeforeReflections, cmd);
                }
                cmd.Clear();
            }
        }

        private void BuildCommandBuffer(CommandBuffer cmd, CameraEvent cameraEvent)
        {
            // AO
            if (generalSettings.deinterleaving == Deinterleaving.Disabled)
            {
                GetScreenSpaceTemporaryRT(cmd, ShaderProperties.hbaoTex, widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
                AO(cmd);
            }
            else
            {
                GetScreenSpaceTemporaryRT(cmd, ShaderProperties.hbaoTex, widthOverride: reinterleavedAoWidth, heightOverride: reinterleavedAoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
                DeinterleavedAO(cmd);
            }

            // Blur
            Blur(cmd);

            // Temporal Filter
            TemporalFilter(cmd);

            // Composite
            Composite(cmd, cameraEvent);

            ReleaseTemporaryRT(cmd, ShaderProperties.hbaoTex);

            //Debug.Log("CommandBuffer has been rebuilt");
        }

        private void AO(CommandBuffer cmd)
        {
            BlitFullscreenTriangleWithClear(cmd, BuiltinRenderTextureType.CameraTarget, ShaderProperties.hbaoTex, material, new Color(0, 0, 0, 1), Pass.AO);
        }

        private void DeinterleavedAO(CommandBuffer cmd)
        {
            // Deinterleave depth & normals (4x4)
            for (int i = 0; i < 4; i++)
            {
                var rtsDepth = new RenderTargetIdentifier[] {
                    ShaderProperties.depthSliceTex[(i << 2) + 0],
                    ShaderProperties.depthSliceTex[(i << 2) + 1],
                    ShaderProperties.depthSliceTex[(i << 2) + 2],
                    ShaderProperties.depthSliceTex[(i << 2) + 3]
                };
                var rtsNormals = new RenderTargetIdentifier[] {
                    ShaderProperties.normalsSliceTex[(i << 2) + 0],
                    ShaderProperties.normalsSliceTex[(i << 2) + 1],
                    ShaderProperties.normalsSliceTex[(i << 2) + 2],
                    ShaderProperties.normalsSliceTex[(i << 2) + 3]
                };

                int offsetX = (i & 1) << 1; int offsetY = (i >> 1) << 1;
                cmd.SetGlobalVector(ShaderProperties.deinterleaveOffset[0], new Vector2(offsetX + 0, offsetY + 0));
                cmd.SetGlobalVector(ShaderProperties.deinterleaveOffset[1], new Vector2(offsetX + 1, offsetY + 0));
                cmd.SetGlobalVector(ShaderProperties.deinterleaveOffset[2], new Vector2(offsetX + 0, offsetY + 1));
                cmd.SetGlobalVector(ShaderProperties.deinterleaveOffset[3], new Vector2(offsetX + 1, offsetY + 1));
                for (int j = 0; j < 4; j++)
                {
                    GetScreenSpaceTemporaryRT(cmd, ShaderProperties.depthSliceTex[j + 4 * i], widthOverride: deinterleavedAoWidth, heightOverride: deinterleavedAoHeight, colorFormat: depthFormat, readWrite: RenderTextureReadWrite.Linear, filter: FilterMode.Point);
                    GetScreenSpaceTemporaryRT(cmd, ShaderProperties.normalsSliceTex[j + 4 * i], widthOverride: deinterleavedAoWidth, heightOverride: deinterleavedAoHeight, colorFormat: normalsFormat, readWrite: RenderTextureReadWrite.Linear, filter: FilterMode.Point);
                }
                BlitFullscreenTriangle(cmd, BuiltinRenderTextureType.CameraTarget, rtsDepth, material, Pass.Deinterleave_Depth); // outputs 4 render textures
                BlitFullscreenTriangle(cmd, BuiltinRenderTextureType.CameraTarget, rtsNormals, material, Pass.Deinterleave_Normals); // outputs 4 render textures
            }

            // AO on each layer
            for (int i = 0; i < 4 * 4; i++)
            {
                cmd.SetGlobalTexture(ShaderProperties.depthTex, ShaderProperties.depthSliceTex[i]);
                cmd.SetGlobalTexture(ShaderProperties.normalsTex, ShaderProperties.normalsSliceTex[i]);
                cmd.SetGlobalVector(ShaderProperties.jitter, s_jitter[i]);
                GetScreenSpaceTemporaryRT(cmd, ShaderProperties.aoSliceTex[i], widthOverride: deinterleavedAoWidth, heightOverride: deinterleavedAoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear, filter: FilterMode.Point);
                BlitFullscreenTriangleWithClear(cmd, BuiltinRenderTextureType.CameraTarget, ShaderProperties.aoSliceTex[i], material, new Color(0, 0, 0, 1), Pass.AO_Deinterleaved); // ao
                ReleaseTemporaryRT(cmd, ShaderProperties.depthSliceTex[i]);
                ReleaseTemporaryRT(cmd, ShaderProperties.normalsSliceTex[i]);
            }

            // Atlas Deinterleaved AO, 4x4
            GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, widthOverride: reinterleavedAoWidth, heightOverride: reinterleavedAoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
            for (int i = 0; i < 4 * 4; i++)
            {
                cmd.SetGlobalVector(ShaderProperties.atlasOffset, new Vector2(((i & 1) + (((i & 7) >> 2) << 1)) * deinterleavedAoWidth, (((i & 3) >> 1) + ((i >> 3) << 1)) * deinterleavedAoHeight));
                BlitFullscreenTriangle(cmd, ShaderProperties.aoSliceTex[i], ShaderProperties.tempTex, material, Pass.Atlas_AO_Deinterleaved); // atlassing
                ReleaseTemporaryRT(cmd, ShaderProperties.aoSliceTex[i]);
            }

            // Reinterleave AO
            ApplyFlip(cmd);
            BlitFullscreenTriangle(cmd, ShaderProperties.tempTex, ShaderProperties.hbaoTex, material, Pass.Reinterleave_AO); // reinterleave
            ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
        }

        private void Blur(CommandBuffer cmd)
        {
            if (blurSettings.type != BlurType.None)
            {
                GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
                cmd.SetGlobalVector(ShaderProperties.blurDeltaUV, new Vector2(1f / width, 0));
                BlitFullscreenTriangle(cmd, ShaderProperties.hbaoTex, ShaderProperties.tempTex, material, Pass.Blur); // blur X
                cmd.SetGlobalVector(ShaderProperties.blurDeltaUV, new Vector2(0, 1f / height));
                BlitFullscreenTriangle(cmd, ShaderProperties.tempTex, ShaderProperties.hbaoTex, material, Pass.Blur); // blur Y
                ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
            }
        }

        private void TemporalFilter(CommandBuffer cmd)
        {
            if (temporalFilterSettings.enabled && !renderingInSceneView)
            {
                AllocateHistoryBuffers();

                if (colorBleedingSettings.enabled)
                {
                    // For Color Bleeding we have 2 history buffers to fill so there are 2 render targets.
                    // AO is still contained in Color Bleeding history buffer (alpha channel) so that we
                    // can use it as a render texture for the composite pass.
                    var rts = new RenderTargetIdentifier[] {
                    aoHistoryBuffer,
                    colorBleedingHistoryBuffer
                };
                    GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
                    GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex2, widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
                    BlitFullscreenTriangle(cmd, aoHistoryBuffer, ShaderProperties.tempTex2, material, Pass.Copy);
                    BlitFullscreenTriangle(cmd, colorBleedingHistoryBuffer, ShaderProperties.tempTex, material, Pass.Copy);
                    BlitFullscreenTriangle(cmd, ShaderProperties.tempTex2, rts, material, Pass.Temporal_Filter);
                    ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
                    ReleaseTemporaryRT(cmd, ShaderProperties.tempTex2);
                    cmd.SetGlobalTexture(ShaderProperties.hbaoTex, colorBleedingHistoryBuffer);
                }
                else
                {
                    // AO history buffer contains ao in aplha channel so we can just use history as
                    // a render texture for the composite pass.
                    GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, widthOverride: aoWidth, heightOverride: aoHeight, colorFormat: colorFormat, readWrite: RenderTextureReadWrite.Linear);
                    BlitFullscreenTriangle(cmd, aoHistoryBuffer, ShaderProperties.tempTex, material, Pass.Copy);
                    BlitFullscreenTriangle(cmd, ShaderProperties.tempTex, aoHistoryBuffer, material, Pass.Temporal_Filter);
                    ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
                    cmd.SetGlobalTexture(ShaderProperties.hbaoTex, aoHistoryBuffer);
                }
            }
        }

        private void Composite(CommandBuffer cmd, CameraEvent cameraEvent)
        {
            if (generalSettings.debugMode == DebugMode.Disabled)
            {
                if (cameraEvent == CameraEvent.BeforeReflections)
                    CompositeBeforeReflections(cmd);
                else if (cameraEvent == CameraEvent.AfterLighting)
                    CompositeAfterLighting(cmd);
                else // if (BeforeImageEffectsOpaque)
                    CompositeBeforeImageEffectsOpaque(cmd);
            }
            else // debug mode
                CompositeBeforeImageEffectsOpaque(cmd, generalSettings.debugMode == DebugMode.ViewNormals ? Pass.Debug_ViewNormals : Pass.Composite);
        }

        // Cases of BeforeReflections & AfterLighting
        // If using HDR there’s no separate rendertarget being created for Emission+lighting buffer (RT3);
        // instead the rendertarget that the Camera renders into (that is, the one that is passed to the image effects) is used as RT3.
        // If non-HDR GBuffer3 texture is available. HDR uses ARGBHalf, non-HDR uses ARGB2101010
        // Emission + lighting buffer(RT3) is logarithmically encoded to provide greater dynamic range than is usually possible with
        // an ARGB32 texture, when the Camera is not using HDR.
        // https://docs.unity3d.com/Manual/RenderTech-DeferredShading.html

        private void CompositeBeforeReflections(CommandBuffer cmd)
        {
            var hdr = hbaoCamera.allowHDR;
            var rts = new RenderTargetIdentifier[] {
            BuiltinRenderTextureType.GBuffer0, // Albedo, Occ
            hdr ? BuiltinRenderTextureType.CameraTarget : BuiltinRenderTextureType.GBuffer3 // Ambient
        };
            GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, colorFormat: RenderTextureFormat.ARGB32);
            GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex2, colorFormat: hdr ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB2101010);
            BlitFullscreenTriangle(cmd, rts[0], ShaderProperties.tempTex, material, Pass.Copy);
            BlitFullscreenTriangle(cmd, rts[1], ShaderProperties.tempTex2, material, Pass.Copy);
            BlitFullscreenTriangle(cmd, ShaderProperties.tempTex2, rts, material, Pass.Composite_BeforeReflections);
            ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
            ReleaseTemporaryRT(cmd, ShaderProperties.tempTex2);
        }

        private void CompositeAfterLighting(CommandBuffer cmd)
        {
            var hdr = hbaoCamera.allowHDR;
            var rt3 = hdr ? BuiltinRenderTextureType.CameraTarget : BuiltinRenderTextureType.GBuffer3;
            GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, colorFormat: hdr ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB2101010);
            BlitFullscreenTriangle(cmd, rt3, ShaderProperties.tempTex, material, Pass.Copy);
            BlitFullscreenTriangle(cmd, ShaderProperties.tempTex, rt3, material, Pass.Composite_AfterLighting);
            ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
        }

        private void CompositeBeforeImageEffectsOpaque(CommandBuffer cmd, int finalPassId = Pass.Composite)
        {
            // This pass can be used to display all debug mode as well
            GetScreenSpaceTemporaryRT(cmd, ShaderProperties.tempTex, colorFormat: sourceFormat);
            if (stereoActive && hbaoCamera.actualRenderingPath != RenderingPath.DeferredShading)
                cmd.Blit(BuiltinRenderTextureType.CameraTarget, ShaderProperties.tempTex);
            else
                BlitFullscreenTriangle(cmd, BuiltinRenderTextureType.CameraTarget, ShaderProperties.tempTex, material, Pass.Copy);
            ApplyFlip(cmd, SystemInfo.graphicsUVStartsAtTop);
            BlitFullscreenTriangle(cmd, ShaderProperties.tempTex, BuiltinRenderTextureType.CameraTarget, material, finalPassId);
            ReleaseTemporaryRT(cmd, ShaderProperties.tempTex);
        }

        private void UpdateMaterialProperties()
        {
            //float tanHalfFovY = Mathf.Tan(0.5f * hbaoCamera.fieldOfView * Mathf.Deg2Rad);
            //float invFocalLenX = 1.0f / (1.0f / tanHalfFovY * (screenHeight / (float)screenWidth));
            //float invFocalLenY = 1.0f / (1.0f / tanHalfFovY);
            float invFocalLenX = 1.0f / hbaoCamera.projectionMatrix.m00;
            float invFocalLenY = 1.0f / hbaoCamera.projectionMatrix.m11;
            float maxRadInPixels = Mathf.Max(16, aoSettings.maxRadiusPixels * Mathf.Sqrt((screenWidth * numberOfEyes * screenHeight) / (1080.0f * 1920.0f)));
            maxRadInPixels /= (generalSettings.deinterleaving == Deinterleaving.x4 ? 4 : 1);

            var targetScale = generalSettings.deinterleaving == Deinterleaving.x4 ?
                                  new Vector4(reinterleavedAoWidth / (float)width, reinterleavedAoHeight / (float)height, 1.0f / (reinterleavedAoWidth / (float)width), 1.0f / (reinterleavedAoHeight / (float)height)) :
                                  generalSettings.resolution == Resolution.Half /*&& aoSettings.perPixelNormals == PerPixelNormals.Reconstruct*/ ?
                                      new Vector4((width + 0.5f) / width, (height + 0.5f) / height, 1f, 1f) :
                                      Vector4.one;

            material.SetTexture(ShaderProperties.noiseTex, noiseTex);
            material.SetVector(ShaderProperties.inputTexelSize, new Vector4(1f / width, 1f / height, width, height));
            material.SetVector(ShaderProperties.aoTexelSize, new Vector4(1f / aoWidth, 1f / aoHeight, aoWidth, aoHeight));
            material.SetVector(ShaderProperties.deinterleavedAOTexelSize, new Vector4(1.0f / deinterleavedAoWidth, 1.0f / deinterleavedAoHeight, deinterleavedAoWidth, deinterleavedAoHeight));
            material.SetVector(ShaderProperties.reinterleavedAOTexelSize, new Vector4(1f / reinterleavedAoWidth, 1f / reinterleavedAoHeight, reinterleavedAoWidth, reinterleavedAoHeight));
            material.SetVector(ShaderProperties.targetScale, targetScale);
            material.SetVector(ShaderProperties.uvToView, new Vector4(2.0f * invFocalLenX, -2.0f * invFocalLenY, -1.0f * invFocalLenX, 1.0f * invFocalLenY));
            material.SetMatrix(ShaderProperties.worldToCameraMatrix, hbaoCamera.worldToCameraMatrix);
            //material.SetFloat(ShaderProperties.radius, aoSettings.radius * 0.5f * ((screenHeight / (generalSettings.deinterleaving == Deinterleaving.x4 ? 4 : 1)) / (tanHalfFovY * 2.0f)));
            material.SetFloat(ShaderProperties.radius, aoSettings.radius * 0.5f * ((screenHeight / (generalSettings.deinterleaving == Deinterleaving.x4 ? 4 : 1)) / (invFocalLenY * 2.0f)));
            material.SetFloat(ShaderProperties.maxRadiusPixels, maxRadInPixels);
            material.SetFloat(ShaderProperties.negInvRadius2, -1.0f / (aoSettings.radius * aoSettings.radius));
            material.SetFloat(ShaderProperties.angleBias, aoSettings.bias);
            material.SetFloat(ShaderProperties.aoMultiplier, 2.0f * (1.0f / (1.0f - aoSettings.bias)));
            material.SetFloat(ShaderProperties.intensity, isLinearColorSpace ? aoSettings.intensity : aoSettings.intensity * 0.454545454545455f);
            material.SetColor(ShaderProperties.baseColor, aoSettings.baseColor);
            material.SetFloat(ShaderProperties.multiBounceInfluence, aoSettings.multiBounceInfluence);
            material.SetFloat(ShaderProperties.offscreenSamplesContrib, aoSettings.offscreenSamplesContribution);
            material.SetFloat(ShaderProperties.maxDistance, aoSettings.maxDistance);
            material.SetFloat(ShaderProperties.distanceFalloff, aoSettings.distanceFalloff);
            material.SetFloat(ShaderProperties.blurSharpness, blurSettings.sharpness);
            material.SetFloat(ShaderProperties.colorBleedSaturation, colorBleedingSettings.saturation);
            material.SetFloat(ShaderProperties.albedoMultiplier, colorBleedingSettings.albedoMultiplier);
            material.SetFloat(ShaderProperties.colorBleedBrightnessMask, colorBleedingSettings.brightnessMask);
            material.SetVector(ShaderProperties.colorBleedBrightnessMaskRange, AdjustBrightnessMaskToGammaSpace(new Vector2(Mathf.Pow(colorBleedingSettings.brightnessMaskRange.x, 3), Mathf.Pow(colorBleedingSettings.brightnessMaskRange.y, 3))));
            material.SetVector(ShaderProperties.temporalParams, temporalFilterSettings.enabled && !renderingInSceneView ? new Vector2(s_temporalRotations[frameCount % 6] / 360.0f, s_temporalOffsets[frameCount % 4]) : Vector2.zero);
        }

        private void UpdateShaderKeywords()
        {
            if (m_ShaderKeywords == null || m_ShaderKeywords.Length != 12) m_ShaderKeywords = new string[12];

            m_ShaderKeywords[0] = ShaderProperties.GetOrthographicOrDeferredKeyword(hbaoCamera.orthographic, generalSettings);
            m_ShaderKeywords[1] = ShaderProperties.GetQualityKeyword(generalSettings);
            m_ShaderKeywords[2] = ShaderProperties.GetNoiseKeyword(generalSettings);
            m_ShaderKeywords[3] = ShaderProperties.GetDeinterleavingKeyword(generalSettings);
            m_ShaderKeywords[4] = ShaderProperties.GetDebugKeyword(generalSettings);
            m_ShaderKeywords[5] = ShaderProperties.GetMultibounceKeyword(aoSettings);
            m_ShaderKeywords[6] = ShaderProperties.GetOffscreenSamplesContributionKeyword(aoSettings);
            m_ShaderKeywords[7] = ShaderProperties.GetPerPixelNormalsKeyword(aoSettings);
            m_ShaderKeywords[8] = ShaderProperties.GetBlurRadiusKeyword(blurSettings);
            m_ShaderKeywords[9] = ShaderProperties.GetVarianceClippingKeyword(temporalFilterSettings);
            m_ShaderKeywords[10] = ShaderProperties.GetColorBleedingKeyword(colorBleedingSettings);
            m_ShaderKeywords[11] = ShaderProperties.GetLightingLogEncodedKeyword(hbaoCamera.allowHDR);

            material.shaderKeywords = m_ShaderKeywords;
        }

        private void CheckParameters()
        {
            // Camera textures
            hbaoCamera.depthTextureMode |= DepthTextureMode.Depth;
            if (aoSettings.perPixelNormals == PerPixelNormals.Camera)
                hbaoCamera.depthTextureMode |= DepthTextureMode.DepthNormals;
            if (temporalFilterSettings.enabled)
                hbaoCamera.depthTextureMode |= DepthTextureMode.MotionVectors;

            // Settings to force
            if (hbaoCamera.actualRenderingPath != RenderingPath.DeferredShading && aoSettings.perPixelNormals == PerPixelNormals.GBuffer)
                SetAoPerPixelNormals(PerPixelNormals.Camera);

            if (generalSettings.deinterleaving != Deinterleaving.Disabled && SystemInfo.supportedRenderTargetCount < 4)
                SetDeinterleaving(Deinterleaving.Disabled);

            if (generalSettings.pipelineStage != PipelineStage.BeforeImageEffectsOpaque && hbaoCamera.actualRenderingPath != RenderingPath.DeferredShading)
                SetPipelineStage(PipelineStage.BeforeImageEffectsOpaque);

            if (generalSettings.pipelineStage != PipelineStage.BeforeImageEffectsOpaque && aoSettings.perPixelNormals == PerPixelNormals.Camera)
                SetAoPerPixelNormals(PerPixelNormals.GBuffer);

            if (stereoActive && hbaoCamera.actualRenderingPath != RenderingPath.DeferredShading && aoSettings.perPixelNormals != PerPixelNormals.Reconstruct)
                SetAoPerPixelNormals(PerPixelNormals.Reconstruct);

            if (temporalFilterSettings.enabled && !motionVectorsSupported)
                EnableTemporalFilter(false);

            if (colorBleedingSettings.enabled && temporalFilterSettings.enabled && SystemInfo.supportedRenderTargetCount < 2)
                EnableTemporalFilter(false);

            // Noise texture
            if (noiseTex == null || m_PreviousNoiseType != generalSettings.noiseType)
            {
                if (noiseTex != null) DestroyImmediate(noiseTex);

                CreateNoiseTexture();

                m_PreviousNoiseType = generalSettings.noiseType;
            }
        }

        private RenderTextureDescriptor GetDefaultDescriptor(int depthBufferBits = 0, RenderTextureFormat colorFormat = RenderTextureFormat.Default, RenderTextureReadWrite readWrite = RenderTextureReadWrite.Default)
        {
            var modifiedDesc = new RenderTextureDescriptor(m_sourceDescriptor.width, m_sourceDescriptor.height,
                                                           m_sourceDescriptor.colorFormat, depthBufferBits);
            modifiedDesc.dimension = m_sourceDescriptor.dimension;
            modifiedDesc.volumeDepth = m_sourceDescriptor.volumeDepth;
            modifiedDesc.vrUsage = m_sourceDescriptor.vrUsage;
            modifiedDesc.msaaSamples = m_sourceDescriptor.msaaSamples;
            modifiedDesc.memoryless = m_sourceDescriptor.memoryless;

            modifiedDesc.useMipMap = m_sourceDescriptor.useMipMap;
            modifiedDesc.autoGenerateMips = m_sourceDescriptor.autoGenerateMips;
            modifiedDesc.enableRandomWrite = m_sourceDescriptor.enableRandomWrite;
            modifiedDesc.shadowSamplingMode = m_sourceDescriptor.shadowSamplingMode;

            if (hbaoCamera.allowDynamicResolution)
                modifiedDesc.useDynamicScale = true;

            if (colorFormat != RenderTextureFormat.Default)
                modifiedDesc.colorFormat = colorFormat;

            if (readWrite == RenderTextureReadWrite.sRGB)
                modifiedDesc.sRGB = true;
            else if (readWrite == RenderTextureReadWrite.Linear)
                modifiedDesc.sRGB = false;
            else if (readWrite == RenderTextureReadWrite.Default)
                modifiedDesc.sRGB = isLinearColorSpace;

            return modifiedDesc;
        }

        private RenderTexture GetScreenSpaceRT(int depthBufferBits = 0, RenderTextureFormat colorFormat = RenderTextureFormat.Default, RenderTextureReadWrite readWrite = RenderTextureReadWrite.Default,
                                               FilterMode filter = FilterMode.Bilinear, int widthOverride = 0, int heightOverride = 0)
        {
            var desc = GetDefaultDescriptor(depthBufferBits, colorFormat, readWrite);
            if (widthOverride > 0)
                desc.width = widthOverride;
            if (heightOverride > 0)
                desc.height = heightOverride;

            //intermediates in VR are unchanged
            if (stereoActive && desc.dimension == TextureDimension.Tex2DArray)
                desc.dimension = TextureDimension.Tex2D;

            var rt = new RenderTexture(desc);
            rt.filterMode = filter;
            return rt;
        }

        private void GetScreenSpaceTemporaryRT(CommandBuffer cmd, int nameID,
                                               int depthBufferBits = 0, RenderTextureFormat colorFormat = RenderTextureFormat.Default, RenderTextureReadWrite readWrite = RenderTextureReadWrite.Default,
                                               FilterMode filter = FilterMode.Bilinear, int widthOverride = 0, int heightOverride = 0)
        {
            var desc = GetDefaultDescriptor(depthBufferBits, colorFormat, readWrite);
            if (widthOverride > 0)
                desc.width = widthOverride;
            if (heightOverride > 0)
                desc.height = heightOverride;

            //intermediates in VR are unchanged
            if (stereoActive && desc.dimension == TextureDimension.Tex2DArray)
                desc.dimension = TextureDimension.Tex2D;

            cmd.GetTemporaryRT(nameID, desc, filter);
        }

        private void ReleaseTemporaryRT(CommandBuffer cmd, int nameID)
        {
            cmd.ReleaseTemporaryRT(nameID);
        }

        private void BlitFullscreenTriangle(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material, int pass = 0)
        {
            cmd.SetGlobalTexture(ShaderProperties.mainTex, source);
            cmd.SetRenderTarget(destination);
            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, material, 0, pass);
        }

        private void BlitFullscreenTriangle(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier[] destinations, Material material, int pass = 0)
        {
            cmd.SetGlobalTexture(ShaderProperties.mainTex, source);
            cmd.SetRenderTarget(destinations, destinations[0]);
            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, material, 0, pass);
        }

        private void BlitFullscreenTriangleWithClear(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material, Color clearColor, int pass = 0)
        {
            cmd.SetGlobalTexture(ShaderProperties.mainTex, source);
            cmd.SetRenderTarget(destination);
            cmd.ClearRenderTarget(false, true, clearColor);
            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, material, 0, pass);
        }

        private static void ApplyFlip(CommandBuffer cmd, bool flip = true)
        {
            if (flip)
                cmd.SetGlobalVector(ShaderProperties.uvTransform, new Vector4(1f, -1f, 0f, 1f));
            else
                cmd.SetGlobalVector(ShaderProperties.uvTransform, new Vector4(1f, 1f, 0f, 0f));
        }

        private static Vector2 AdjustBrightnessMaskToGammaSpace(Vector2 v)
        {
            return isLinearColorSpace ? v : ToGammaSpace(v);
        }

        private static float ToGammaSpace(float v)
        {
            return Mathf.Pow(v, 0.454545454545455f);
        }

        private static Vector2 ToGammaSpace(Vector2 v)
        {
            return new Vector2(ToGammaSpace(v.x), ToGammaSpace(v.y));
        }

        private void CreateNoiseTexture()
        {
            noiseTex = new Texture2D(4, 4, SystemInfo.SupportsTextureFormat(TextureFormat.RGHalf) ? TextureFormat.RGHalf : TextureFormat.RGB24, false, true);
            noiseTex.filterMode = FilterMode.Point;
            noiseTex.wrapMode = TextureWrapMode.Repeat;
            int z = 0;
            for (int x = 0; x < 4; ++x)
            {
                for (int y = 0; y < 4; ++y)
                {
                    float r1 = generalSettings.noiseType != NoiseType.Dither ? 0.25f * (0.0625f * ((x + y & 3) << 2) + (x & 3)) : MersenneTwister.Numbers[z++];
                    float r2 = generalSettings.noiseType != NoiseType.Dither ? 0.25f * ((y - x) & 3) : MersenneTwister.Numbers[z++];
                    Color color = new Color(r1, r2, 0);
                    noiseTex.SetPixel(x, y, color);
                }
            }
            noiseTex.Apply();

            for (int i = 0, j = 0; i < s_jitter.Length; ++i)
            {
                float r1 = MersenneTwister.Numbers[j++];
                float r2 = MersenneTwister.Numbers[j++];
                s_jitter[i] = new Vector2(r1, r2);
            }
        }
    }
}
