text: [PLATFORMS]
  ALL
  
  [PERMUTATIONS]
  
  CAMERA_MODE = CAMERA_MODE_PERSPECTIVE
  
  [RENDERSTATE]
  
  DepthTest = false
  DepthWrite = false
  CullMode = CullMode_None
  
  [VERTEXSHADER]
  
  #include <Shaders/Pipeline/FullscreenTriangleVertexShader.h>
  
  [PIXELSHADER]
  
  struct PS_IN
  {
    float4 Position : SV_Position;
    float2 TexCoord0 : TEXCOORD0;
  };
  
  #include <Shaders/Common/Common.h>
  #include <Shaders/Pipeline/Utils.h>
  #include "SSAOConstants.h"
  
  Texture2D DepthTexture;
  Texture2D LowResDepthTexture;
  SamplerState DepthSampler;
  
  Texture2D NoiseTexture;
  
  #define NUM_AO_SAMPLES 6
  #define SAMPLE_STEPS 1
  
  float SampleDepth(float2 texCoords)
  {
    return DepthTexture.SampleLevel(DepthSampler, texCoords, 0).r;
  }
  
  float SampleLowResDepth(float2 texCoords, float mipLevel)
  {
  #if 0
    return LinearizeZBufferDepth(DepthTexture.SampleLevel(DepthSampler, texCoords, 0).r);
  #else
    texCoords = texCoords * TexCoordsScale;
    return LowResDepthTexture.SampleLevel(DepthSampler, texCoords, mipLevel).r;
  #endif
  }
  
  float CalculateMipLevel(int sampleID, float scale)
  {
    float sampleDist = (sampleID + 0.5f) / NUM_AO_SAMPLES;
  
    return log2(MipLevelScale * scale * sampleDist);
  }
  
  float3 ReconstructScreenSpacePos(float2 normalizedCoords, float linearDepth)
  {
    return float3(normalizedCoords * linearDepth, linearDepth);
  }
  
  float ReconstructNormalZ(float2 normal)
  {
    return sqrt(1 - dot(normal, normal));
  }
  
  float3 ReconstructNormal(float2 texCoords, float centerDepth)
  {
    float2 texelSize = ViewportSize.zw;
  
    float leftDepth = SampleDepth(texCoords - float2(texelSize.x, 0));
    float topDepth = SampleDepth(texCoords - float2(0, texelSize.y));
    float rightDepth = SampleDepth(texCoords + float2(texelSize.x, 0));
    float bottomDepth = SampleDepth(texCoords + float2(0, texelSize.y));
    float2 normalizedCoords = texCoords * 2.0f - 1.0f;
    return ReconstructViewSpaceNormal(normalizedCoords, centerDepth, leftDepth, rightDepth, topDepth, bottomDepth);
  }
  
  float2 OcclusionSample(float2 normalizedCoords, float2 offset, float3 invFovCorrection, float3 centerPos, float3 viewSpaceNormal, float mipLevel)
  {
    float2 normalizedCoordA = normalizedCoords + offset;
    float2 normalizedCoordB = normalizedCoords - offset;
  
    float linearDepthA = SampleLowResDepth(normalizedCoordA * 0.5 + 0.5, mipLevel);
    float linearDepthB = SampleLowResDepth(normalizedCoordB * 0.5 + 0.5, mipLevel);
  
    float3 posA = ReconstructScreenSpacePos(normalizedCoordA, linearDepthA);
    float3 posB = ReconstructScreenSpacePos(normalizedCoordB, linearDepthB);
  
    float3 deltaA = (posA - centerPos) * invFovCorrection;
    float3 deltaB = (posB - centerPos) * invFovCorrection;
  
    float invNormAngleA = saturate(dot(deltaA, viewSpaceNormal) / (dot(deltaA, deltaA)));
    float invNormAngleB = saturate(dot(deltaB, viewSpaceNormal) / (dot(deltaB, deltaB)));
  
    return float2(invNormAngleA, invNormAngleB);
  }
  
  float4 main(PS_IN Input) : SV_Target
  {
    const float2 offsets[NUM_AO_SAMPLES]=
    {
      float2(0.000, 0.200),
      float2(0.325, 0.101),
      float2(0.272, -0.396),
      float2(-0.385, -0.488),
      float2(-0.711, 0.274),
      float2(0.060, 0.900)
    };
  
    float centerDepth = SampleDepth(Input.TexCoord0);
    float linearCenterDepth = LinearizeZBufferDepth(centerDepth);
  
    float fadeOutFactor = saturate(linearCenterDepth * FadeOutParams.x + FadeOutParams.y);
  
    [branch]
    if (fadeOutFactor == 0.0f)
    {
      return float4(1.0f, linearCenterDepth, 0, 1);
    }
  
    float2 normalizedCoords = Input.TexCoord0 * 2.0f - 1.0f;
    float3 centerPos = ReconstructScreenSpacePos(normalizedCoords, linearCenterDepth);
  
    float3 viewSpaceNormal = ReconstructNormal(Input.TexCoord0, centerDepth);
  
    float2 invTanHalfFov = float2(GetCameraToScreenMatrix()._11, GetCameraToScreenMatrix()._22);
    float3 fovCorrection = float3(invTanHalfFov, 1);
    float3 invFovCorrection = 1.0f / fovCorrection;
  
    // bias
    centerPos += PositionBias * linearCenterDepth * (viewSpaceNormal * fovCorrection);
    normalizedCoords = centerPos.xy / centerPos.z;
  
    float2 fovFixXY = fovCorrection.xy / centerPos.z;
    float aoRadius = WorldRadius;
    aoRadius = min(aoRadius * fovFixXY, MaxScreenSpaceRadius);
  
    //float noise = InterleavedGradientNoise(Input.Position.xy);
    //float2 randomAngle; sincos(noise * 2.0f * PI, randomAngle.x, randomAngle.y);
  
    float2 noiseCoords = Input.TexCoord0 * (ViewportSize.xy / 4.0f);
    float2 randomAngle = NoiseTexture.SampleLevel(PointSampler, noiseCoords, 0).rg * 2.0f - 1.0f;
  
    randomAngle *= aoRadius;;
    float2x2 randomRotation = {randomAngle.x, -randomAngle.y, randomAngle.y, randomAngle.x};
  
    float occlusion = 0.0f;
    float totalWeight = 0.0001f;
  
    float3 scaledViewSpaceNormal = viewSpaceNormal * 1.0f;
  
    [unroll]
    for (int i = 0; i < NUM_AO_SAMPLES; ++i)
    {
      float2 offset = mul(randomRotation, offsets[i]);
  
      float2 aoSample = 0;
  
      [unroll]
      for (uint step = 0; step < SAMPLE_STEPS; ++step)
      {
        float scale = (step + 1) / (float)SAMPLE_STEPS;
        float mipLevel = CalculateMipLevel(i, aoRadius * scale);
  
        float2 stepSample = OcclusionSample(normalizedCoords, offset * scale, invFovCorrection, centerPos, scaledViewSpaceNormal, mipLevel);
  
        // combine samples
        aoSample = max(aoSample.xy, stepSample.xy);
      }
  
      occlusion += Square(1 - aoSample.x);
      occlusion += Square(1 - aoSample.y);
      totalWeight += 2.0f;
    }
  
    occlusion /= (NUM_AO_SAMPLES * 2);
  
    occlusion = 1 - (1 - pow(abs(occlusion), Contrast)) * Intensity * fadeOutFactor;
  
    #if 1
      float2 thisPixel = float2(occlusion, linearCenterDepth);
  
      float2 deltaX = ddx_fine(thisPixel);
      float2 deltaY = ddy_fine(thisPixel);
  
      int2 m = (uint2)(Input.Position.xy) % 2;
  
      float occlusionX = occlusion - deltaX.x * (m.x * 2 - 1);
      float occlusionY = occlusion - deltaY.x * (m.y * 2 - 1);
  
      float2 weight;
  
      const float depthContrast = 2;
      float invDepth = 1.0f / linearCenterDepth;
      weight.x = saturate(1 - abs(deltaX.y) * depthContrast);
      weight.y = saturate(1 - abs(deltaY.y) * depthContrast);
  
      totalWeight = 1.0f / (1.0f + weight.x + weight.y);
      weight *= totalWeight;
  
      occlusion = totalWeight * occlusion + weight.x * occlusionX + weight.y * occlusionY;
    #endif
  
    return float4(occlusion, linearCenterDepth, 0, 1);
  }
  
  
  