   // Distance to center of shadow coverage circle
   float CoverageDistCenter = 0.5 * (CoverageDistUpper + CoverageDistLower);
   
-  // Shadow coverage center and radius
+  // Shadow coverage center and radius (in NDC)
   vec2 dir = (dist == 0.0) ? vec2(1.0, 0.0) : normalize(texCoordNDC.xy);
-  vec2 center = CoverageDistCenter * dir * 0.5 + 0.5; // [-1,1] NDC -> [0,1] texcoords
-  float radius = (CoverageDistUpper - CoverageDistCenter)*0.5; // NDC -> texcoords radius compression
-  return vec3(center, radius);
-}
-
-vec2 SearchBlockers(sampler2D depthTex, vec4 texCoord, vec3 coverageCircle, vec2 zNearFarInv, float planeSign, const int numSearchSamples)
-{
-  float phi = random(coverageCircle.xy)*TWOPI;
+  vec2 center = CoverageDistCenter * dir;
+  float radius = CoverageDistUpper - CoverageDistCenter;
+  
+  // If part of coverage circle is outside the texture, then only search samples inside the texture
+  // and attenuate using percentage of coverage circle inside texture
+  // This uses the circle-circle intersection area algorithm at:
+  // https://diego.assencio.com/?index=8d6ca3d82151bad815f78addf9b5c1c6
+  float r1 = 1.0;           // Radius of texture, assumes circular light
+  float r2 = radius;        // Radius of coverage circle
+  float d = length(center); // Distance from texture center (at origin) to coverage circle center
+  
+  float A1 = PI;
+  float A2 = PI*r2*r2;
+  
+  float Aintersect = 0.0;
+  if(d <= abs(r1 - r2)) Aintersect = min(A1, A2);
+  else if(d < r1 + r2)
+  {
+    float r1_sq = 1.0;
+    float r2_sq = r2*r2;
+    float d1 = (r1_sq - r2_sq + d*d)/(2.0*d);
+    float d2 = d - d1;
+    Aintersect =  r1_sq*acos(d1/r1) - d1*sqrt(r1_sq - d1*d1);
+    Aintersect += r2_sq*acos(d2/r2) - d2*sqrt(r2_sq - d2*d2);
+  }
+  
+  // Convert center and radius from NDC [-1,1] -> texcoords [0, 1]
+  center = center * 0.5 + 0.5;
+  radius = radius * 0.5;
+  
+  return vec4(center, radius, Aintersect/A2);
+}
+
+vec2 SearchBlockers(sampler2D depthTex, vec4 texCoord, vec4 coverageCircle, vec2 zNearFarInv, float planeSign, const int numSearchSamples)
+{
+  //phi = random(texCoord.xy)*TWOPI;
   vec3 sampleTexCoord = texCoord.xyz;
-  float clearDepth = 0.5 - planeSign*0.5; // Depth value if there is no blocker (0 for umbra, 1 for penumbra)
+  float clearDepth = (planeSign == 1.0) ? 0.0 : 1.0; // Depth value if there is no blocker (0 for umbra, 1 for penumbra)
   vec2 blockerData = vec2(0.0, 0.0); // (average blocker depth, number of blockers)
   float depth;
   
+  // Get coverage circle in NDC [-1, 1] coordinates
+  vec2 centerNDC = 2.0*coverageCircle.xy - 1.0;
+  float radiusNDC = 2.0*coverageCircle.z;
+  
+  // Compute min/max distances to intersection area between coverage circle and texture (assuming circular light source)
+  float rplus = length(centerNDC) + radiusNDC;
+  float rminus = rplus - 2.0*radiusNDC;
+  rplus = min(rplus, 1.0);
+  rminus = max(rminus, -1.0);
+  float dCenter = 0.5*(rplus + rminus);
+  float rAverage = abs(rplus - dCenter);
+  vec2 centerAverage = dCenter * normalize(centerNDC);
+  centerAverage = centerAverage * 0.5 + 0.5;
+  rAverage = rAverage*0.5;
+
   // Sample texture
   for (int i = 0; i < numSearchSamples; ++i)
   {
     // Get sample coordinates
-    vec2 offset = VogelDiskSample(i, numSearchSamples, phi) * coverageCircle.z;
-    sampleTexCoord.xy = coverageCircle.xy + offset;
+    vec2 currsample = VogelDiskSample(i, numSearchSamples, phi);
+    sampleTexCoord.xy = coverageCircle.xy + currsample*coverageCircle.z;
+    
+    if(length(sampleTexCoord.xy - vec2(0.5, 0.5)) > 0.5)
+    {
+      sampleTexCoord.xy = centerAverage + currsample*rAverage;
+    }
     
     // Lookup sample depth and compare it to fragment depth
     depth = texture2D(depthTex, sampleTexCoord.xy).r;

  //uVisibility = pVisibility;
  //pVisibility = uVisibility;
  
  float minVisibility = osgShadow_ambientBias.x;
  float shadowedVisibility = 0.5 * (uVisibility + pVisibility);
  if(shadowedVisibility == 1.0) shadowedVisibility -= 0.5*fwidth(shadowedVisibility);

  float uBlockerDistGuess = Depth2Distance(texture2D(osgShadow_umbraDepthTexture, uTexCoord.xy).r, osgShadow_umbraZNearFarInv);
  uBlockerDistGuess = max(uBlockerDistGuess, uTexCoord.w);
  uBlockerDistGuess = min(uBlockerDistGuess, 1.0/osgShadow_umbraZNearFarInv.y);
  vec4 uCoverageCircle = ShadowCoverageCircle(uTexCoordNDC, uTexCoord.w, uBlockerDistGuess, osgShadow_lightDistance + osgShadow_umbraDistance, 1.0);
  vec2 uBlockers = SearchBlockers(osgShadow_umbraDepthTexture, uTexCoord, uCoverageCircle, osgShadow_umbraZNearFarInv, 1.0, numBlockerSamples);
  
  // Penumbra blocker search (using penumbra near plane and adjusting for z increasing towards light)
  float pBlockerDistGuess = Depth2Distance(texture2D(osgShadow_penumbraDepthTexture, pTexCoord.xy).r, osgShadow_penumbraZNearFarInv);
  pBlockerDistGuess = min(pBlockerDistGuess, pTexCoord.w);
  pBlockerDistGuess = max(pBlockerDistGuess, 1.0/osgShadow_penumbraZNearFarInv.x);
  vec4 pCoverageCircle = ShadowCoverageCircle(pTexCoordNDC, -pTexCoord.w, -pBlockerDistGuess, osgShadow_lightDistance - osgShadow_penumbraDistance, -1.0);
  vec2 pBlockers = SearchBlockers(osgShadow_penumbraDepthTexture, pTexCoord, pCoverageCircle, osgShadow_penumbraZNearFarInv, -1.0, numBlockerSamples);
  
  // Adjust umbra blocker distance based on info from penumbra blocker search
