// Upgrade NOTE: commented out 'float4x4 _WorldToCamera', a built-in variable
// Upgrade NOTE: replaced '_WorldToCamera' with 'unity_WorldToCamera'

#ifndef __AMBIENT_OCCLUSION__
#define __AMBIENT_OCCLUSION__

#include "UnityCG.cginc"
#include "Common.cginc"

// --------
// Options for further customization
// --------

// By default, a 5-tap Gaussian with the linear sampling technique is used
// in the bilateral noise filter. It can be replaced with a 7-tap Gaussian
// with adaptive sampling by enabling the macro below. Although the
// differences are not noticeable in most cases, it may provide preferable
// results with some special usage (e.g. NPR without textureing).
// #define BLUR_HIGH_QUALITY

// By default, a fixed sampling pattern is used in the AO estimator. Although
// this gives preferable results in most cases, a completely random sampling
// pattern could give aesthetically better results. Disable the macro below
// to use such a random pattern instead of the fixed one.
#define FIX_SAMPLING_PATTERN

// The SampleNormal function normalizes samples from G-buffer because
// they're possibly unnormalized. We can eliminate this if it can be said
// that there is no wrong shader that outputs unnormalized normals.
// #define VALIDATE_NORMALS

// The constant below determines the contrast of occlusion. This allows
// users to control over/under occlusion. At the moment, this is not exposed
// to the editor because it�s rarely useful.
static const float kContrast = 0.6;

// The constant below controls the geometry-awareness of the bilateral
// filter. The higher value, the more sensitive it is.
static const float kGeometryCoeff = 0.8;

// The constants below are used in the AO estimator. Beta is mainly used
// for suppressing self-shadowing noise, and Epsilon is used to prevent
// calculation underflow. See the paper (Morgan 2011 http://goo.gl/2iz3P)
// for further details of these constants.
static const float kBeta = 0.002;

// --------

// System built-in variables
sampler2D _CameraGBufferTexture2;
sampler2D_float _CameraDepthTexture;
sampler2D _CameraDepthNormalsTexture;

float4 _CameraDepthTexture_ST;

// Sample count
#if !defined(SHADER_API_GLES)
int _SampleCount;
#else
// GLES2: In many cases, dynamic looping is not supported.
static const int _SampleCount = 3;
#endif

// Source texture properties
sampler2D _OcclusionTexture;
float4 _OcclusionTexture_TexelSize;

// Other parameters
half _Intensity;
float _Radius;
float _Downsample;
float3 _FogParams; // x: density, y: start, z: end

// Accessors for packed AO/normal buffer
fixed4 PackAONormal(fixed ao, fixed3 n)
{
    return fixed4(ao, n * 0.5 + 0.5);
}

fixed GetPackedAO(fixed4 p)
{
    return p.r;
}

fixed3 GetPackedNormal(fixed4 p)
{
    return p.gba * 2.0 - 1.0;
}

// Boundary check for depth sampler
// (returns a very large value if it lies out of bounds)
float CheckBounds(float2 uv, float d)
{
    float ob = any(uv < 0) + any(uv > 1);
#if defined(UNITY_REVERSED_Z)
    ob += (d <= 0.00001);
#else
    ob += (d >= 0.99999);
#endif
    return ob * 1e8;
}

// Depth/normal sampling functions
float SampleDepth(float2 uv)
{
#if defined(SOURCE_GBUFFER) || defined(SOURCE_DEPTH)
    float d = LinearizeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
#else
    float4 cdn = tex2D(_CameraDepthNormalsTexture, uv);
    float d = DecodeFloatRG(cdn.zw);
#endif
    return d * _ProjectionParams.z + CheckBounds(uv, d);
}

float3 SampleNormal(float2 uv)
{
#if defined(SOURCE_GBUFFER)
    float3 norm = tex2D(_CameraGBufferTexture2, uv).xyz;
    norm = norm * 2 - any(norm); // gets (0,0,0) when norm == 0
    norm = mul((float3x3)unity_WorldToCamera, norm);
#if defined(VALIDATE_NORMALS)
    norm = normalize(norm);
#endif
    return norm;
#else
    float4 cdn = tex2D(_CameraDepthNormalsTexture, uv);
    return DecodeViewNormalStereo(cdn) * float3(1.0, 1.0, -1.0);
#endif
}

float SampleDepthNormal(float2 uv, out float3 normal)
{
#if defined(SOURCE_GBUFFER) || defined(SOURCE_DEPTH)
    normal = SampleNormal(uv);
    return SampleDepth(uv);
#else
    float4 cdn = tex2D(_CameraDepthNormalsTexture, uv);
    normal = DecodeViewNormalStereo(cdn) * float3(1.0, 1.0, -1.0);
    float d = DecodeFloatRG(cdn.zw);
    return d * _ProjectionParams.z + CheckBounds(uv, d);
#endif
}

// Normal vector comparer (for geometry-aware weighting)
half CompareNormal(half3 d1, half3 d2)
{
    return smoothstep(kGeometryCoeff, 1.0, dot(d1, d2));
}

// Common vertex shader
struct VaryingsMultitex
{
    float4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;    // Original UV
    half2 uv01 : TEXCOORD1;  // Alternative UV (supports v-flip case)
    half2 uvSPR : TEXCOORD2; // Single pass stereo rendering UV
};

VaryingsMultitex VertMultitex(AttributesDefault v)
{
    half2 uvAlt = v.texcoord.xy;

#if UNITY_UV_STARTS_AT_TOP
    if (_MainTex_TexelSize.y < 0.0) uvAlt.y = 1.0 - uvAlt.y;
#endif

    VaryingsMultitex o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = v.texcoord.xy;
    o.uv01 = uvAlt;
    o.uvSPR = UnityStereoTransformScreenSpaceTex(uvAlt);

    return o;
}

// Trigonometric function utility
float2 CosSin(float theta)
{
    float sn, cs;
    sincos(theta, sn, cs);
    return float2(cs, sn);
}

// Pseudo random number generator with 2D coordinates
float UVRandom(float u, float v)
{
    float f = dot(float2(12.9898, 78.233), float2(u, v));
    return frac(43758.5453 * sin(f));
}

// Check if the camera is perspective.
// (returns 1.0 when orthographic)
float CheckPerspective(float x)
{
    return lerp(x, 1.0, unity_OrthoParams.w);
}

// Reconstruct view-space position from UV and depth.
// p11_22 = (unity_CameraProjection._11, unity_CameraProjection._22)
// p13_31 = (unity_CameraProjection._13, unity_CameraProjection._23)
float3 ReconstructViewPos(float2 uv, float depth, float2 p11_22, float2 p13_31)
{
    return float3((uv * 2.0 - 1.0 - p13_31) / p11_22 * CheckPerspective(depth), depth);
}

// Sample point picker
float3 PickSamplePoint(float2 uv, float index)
{
    // Uniformaly distributed points on a unit sphere http://goo.gl/X2F1Ho
#if defined(FIX_SAMPLING_PATTERN)
    float gn = GradientNoise(uv * _Downsample);
    // FIXME: This was added to avoid a NVIDIA driver issue.
    //                                   vvvvvvvvvvvv
    float u = frac(UVRandom(0.0, index + uv.x * 1e-10) + gn) * 2.0 - 1.0;
    float theta = (UVRandom(1.0, index + uv.x * 1e-10) + gn) * UNITY_PI_2;
#else
    float u = UVRandom(uv.x + _Time.x, uv.y + index) * 2.0 - 1.0;
    float theta = UVRandom(-uv.x - _Time.x, uv.y + index) * UNITY_PI_2;
#endif
    float3 v = float3(CosSin(theta) * sqrt(1.0 - u * u), u);
    // Make them distributed between [0, _Radius]
    float l = sqrt((index + 1.0) / _SampleCount) * _Radius;
    return v * l;
}

// Fog handling in forward
half ComputeFog(float z)
{
    half fog = 0.0;
#if FOG_LINEAR
    fog = (_FogParams.z - z) / (_FogParams.z - _FogParams.y);
#elif FOG_EXP
    fog = exp2(-_FogParams.x * z);
#else // FOG_EXP2
    fog = _FogParams.x * z;
    fog = exp2(-fog * fog);
#endif
    return saturate(fog);
}

float ComputeDistance(float depth)
{
    float dist = depth * _ProjectionParams.z;
    dist -= _ProjectionParams.y;
    return dist;
}

//
// Distance-based AO estimator based on Morgan 2011 http://goo.gl/2iz3P
//
half4 FragAO(VaryingsMultitex i) : SV_Target
{
    float2 uv = i.uv;

    // Parameters used in coordinate conversion
    float3x3 proj = (float3x3)unity_CameraProjection;
    float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
    float2 p13_31 = float2(unity_CameraProjection._13, unity_CameraProjection._23);

    // View space normal and depth
    float3 norm_o;
    float depth_o = SampleDepthNormal(UnityStereoScreenSpaceUVAdjust(uv, _CameraDepthTexture_ST), norm_o);

#if defined(SOURCE_DEPTHNORMALS)
    // Offset the depth value to avoid precision error.
    // (depth in the DepthNormals mode has only 16-bit precision)
    depth_o -= _ProjectionParams.z / 65536;
#endif

    // Reconstruct the view-space position.
    float3 vpos_o = ReconstructViewPos(i.uv01, depth_o, p11_22, p13_31);

    float ao = 0.0;

    for (int s = 0; s < _SampleCount; s++)
    {
        // Sample point
#if defined(SHADER_API_D3D11)
        // This 'floor(1.0001 * s)' operation is needed to avoid a NVidia
        // shader issue. This issue is only observed on DX11.
        float3 v_s1 = PickSamplePoint(uv, floor(1.0001 * s));
#else
        float3 v_s1 = PickSamplePoint(uv, s);
#endif
        v_s1 = faceforward(v_s1, -norm_o, v_s1);
        float3 vpos_s1 = vpos_o + v_s1;

        // Reproject the sample point
        float3 spos_s1 = mul(proj, vpos_s1);
        float2 uv_s1_01 = (spos_s1.xy / CheckPerspective(vpos_s1.z) + 1.0) * 0.5;

        // Depth at the sample point
        float depth_s1 = SampleDepth(UnityStereoScreenSpaceUVAdjust(uv_s1_01, _CameraDepthTexture_ST));

        // Relative position of the sample point
        float3 vpos_s2 = ReconstructViewPos(uv_s1_01, depth_s1, p11_22, p13_31);
        float3 v_s2 = vpos_s2 - vpos_o;

        // Estimate the obscurance value
        float a1 = max(dot(v_s2, norm_o) - kBeta * depth_o, 0.0);
        float a2 = dot(v_s2, v_s2) + EPSILON;
        ao += a1 / a2;
    }

    ao *= _Radius; // intensity normalization

    // Apply other parameters.
    ao = pow(ao * _Intensity / _SampleCount, kContrast);

    // Apply fog when enabled (forward-only)
#if !FOG_OFF
    float d = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
    d = ComputeDistance(d);
    ao *= ComputeFog(d);
#endif

    return PackAONormal(ao, norm_o);
}

// Geometry-aware separable bilateral filter
half4 FragBlur(VaryingsMultitex i) : SV_Target
{
#if defined(BLUR_HORIZONTAL)
    // Horizontal pass: Always use 2 texels interval to match to
    // the dither pattern.
    float2 delta = float2(_MainTex_TexelSize.x * 2.0, 0.0);
#else
    // Vertical pass: Apply _Downsample to match to the dither
    // pattern in the original occlusion buffer.
    float2 delta = float2(0.0, _MainTex_TexelSize.y / _Downsample * 2.0);
#endif

#if defined(BLUR_HIGH_QUALITY)

    // High quality 7-tap Gaussian with adaptive sampling

    fixed4 p0  = tex2D(_MainTex, i.uvSPR);
    fixed4 p1a = tex2D(_MainTex, i.uvSPR - delta);
    fixed4 p1b = tex2D(_MainTex, i.uvSPR + delta);
    fixed4 p2a = tex2D(_MainTex, i.uvSPR - delta * 2.0);
    fixed4 p2b = tex2D(_MainTex, i.uvSPR + delta * 2.0);
    fixed4 p3a = tex2D(_MainTex, i.uvSPR - delta * 3.2307692308);
    fixed4 p3b = tex2D(_MainTex, i.uvSPR + delta * 3.2307692308);

#if defined(BLUR_SAMPLE_CENTER_NORMAL)
    fixed3 n0 = SampleNormal(i.uvSPR);
#else
    fixed3 n0 = GetPackedNormal(p0);
#endif

    half w0  = 0.37004405286;
    half w1a = CompareNormal(n0, GetPackedNormal(p1a)) * 0.31718061674;
    half w1b = CompareNormal(n0, GetPackedNormal(p1b)) * 0.31718061674;
    half w2a = CompareNormal(n0, GetPackedNormal(p2a)) * 0.19823788546;
    half w2b = CompareNormal(n0, GetPackedNormal(p2b)) * 0.19823788546;
    half w3a = CompareNormal(n0, GetPackedNormal(p3a)) * 0.11453744493;
    half w3b = CompareNormal(n0, GetPackedNormal(p3b)) * 0.11453744493;

    half s;
    s  = GetPackedAO(p0)  * w0;
    s += GetPackedAO(p1a) * w1a;
    s += GetPackedAO(p1b) * w1b;
    s += GetPackedAO(p2a) * w2a;
    s += GetPackedAO(p2b) * w2b;
    s += GetPackedAO(p3a) * w3a;
    s += GetPackedAO(p3b) * w3b;

    s /= w0 + w1a + w1b + w2a + w2b + w3a + w3b;

#else

    // Fater 5-tap Gaussian with linear sampling
    fixed4 p0  = tex2D(_MainTex, i.uvSPR);
    fixed4 p1a = tex2D(_MainTex, i.uvSPR - delta * 1.3846153846);
    fixed4 p1b = tex2D(_MainTex, i.uvSPR + delta * 1.3846153846);
    fixed4 p2a = tex2D(_MainTex, i.uvSPR - delta * 3.2307692308);
    fixed4 p2b = tex2D(_MainTex, i.uvSPR + delta * 3.2307692308);

#if defined(BLUR_SAMPLE_CENTER_NORMAL)
    fixed3 n0 = SampleNormal(i.uvSPR);
#else
    fixed3 n0 = GetPackedNormal(p0);
#endif

    half w0  = 0.2270270270;
    half w1a = CompareNormal(n0, GetPackedNormal(p1a)) * 0.3162162162;
    half w1b = CompareNormal(n0, GetPackedNormal(p1b)) * 0.3162162162;
    half w2a = CompareNormal(n0, GetPackedNormal(p2a)) * 0.0702702703;
    half w2b = CompareNormal(n0, GetPackedNormal(p2b)) * 0.0702702703;

    half s;
    s  = GetPackedAO(p0)  * w0;
    s += GetPackedAO(p1a) * w1a;
    s += GetPackedAO(p1b) * w1b;
    s += GetPackedAO(p2a) * w2a;
    s += GetPackedAO(p2b) * w2b;

    s /= w0 + w1a + w1b + w2a + w2b;

#endif

    return PackAONormal(s, n0);
}

// Gamma encoding (only needed in gamma lighting mode)
half EncodeAO(half x)
{
    half x_g = 1.0 - max(1.055 * pow(1.0 - x, 0.416666667) - 0.055, 0.0);
    // ColorSpaceLuminance.w == 0 (gamma) or 1 (linear)
    return lerp(x_g, x, unity_ColorSpaceLuminance.w);
}

// Geometry-aware bilateral filter (single pass/small kernel)
half BlurSmall(sampler2D tex, float2 uv, float2 delta)
{
    fixed4 p0 = tex2D(tex, uv);
    fixed4 p1 = tex2D(tex, uv + float2(-delta.x, -delta.y));
    fixed4 p2 = tex2D(tex, uv + float2(+delta.x, -delta.y));
    fixed4 p3 = tex2D(tex, uv + float2(-delta.x, +delta.y));
    fixed4 p4 = tex2D(tex, uv + float2(+delta.x, +delta.y));

    fixed3 n0 = GetPackedNormal(p0);

    half w0 = 1.0;
    half w1 = CompareNormal(n0, GetPackedNormal(p1));
    half w2 = CompareNormal(n0, GetPackedNormal(p2));
    half w3 = CompareNormal(n0, GetPackedNormal(p3));
    half w4 = CompareNormal(n0, GetPackedNormal(p4));

    half s;
    s  = GetPackedAO(p0) * w0;
    s += GetPackedAO(p1) * w1;
    s += GetPackedAO(p2) * w2;
    s += GetPackedAO(p3) * w3;
    s += GetPackedAO(p4) * w4;

    return s / (w0 + w1 + w2 + w3 + w4);
}

// Final composition shader
half4 FragComposition(VaryingsMultitex i) : SV_Target
{
    float2 delta = _MainTex_TexelSize.xy / _Downsample;
    half ao = BlurSmall(_OcclusionTexture, i.uvSPR, delta);
    half4 color = tex2D(_MainTex, i.uvSPR);

#if !defined(DEBUG_COMPOSITION)
    color.rgb *= 1.0 - EncodeAO(ao);
#else
    color.rgb = 1.0 - EncodeAO(ao);
#endif

    return color;
}

// Final composition shader (ambient-only mode)
VaryingsDefault VertCompositionGBuffer(AttributesDefault v)
{
    VaryingsDefault o;
    o.pos = v.vertex;
#if UNITY_UV_STARTS_AT_TOP
    o.uv = v.texcoord.xy * float2(1.0, -1.0) + float2(0.0, 1.0);
#else
    o.uv = v.texcoord.xy;
#endif
    o.uvSPR = UnityStereoTransformScreenSpaceTex(o.uv);
    return o;
}

#if !SHADER_API_GLES // excluding the MRT pass under GLES2

struct CompositionOutput
{
    half4 gbuffer0 : SV_Target0;
    half4 gbuffer3 : SV_Target1;
};

CompositionOutput FragCompositionGBuffer(VaryingsDefault i)
{
    // Workaround: _OcclusionTexture_Texelsize hasn't been set properly
    // for some reasons. Use _ScreenParams instead.
    float2 delta = (_ScreenParams.zw - 1.0) / _Downsample;
    half ao = BlurSmall(_OcclusionTexture, i.uvSPR, delta);

    CompositionOutput o;
    o.gbuffer0 = half4(0.0, 0.0, 0.0, ao);
    o.gbuffer3 = half4((half3)EncodeAO(ao), 0.0);
    return o;
}

#else

fixed4 FragCompositionGBuffer(VaryingsDefault i) : SV_Target0
{
    return 0.0;
}

#endif

#endif // __AMBIENT_OCCLUSION__
