# Last hole artifact in screen space refection with Hierarchical Z

After lot of efforts I have an almost nice shader doing SSR with HiZ. The shader is provided below. Some parameters are tunable and you can speed up things but the difficulty in my scene (picture) are the spheres that limit the coarsiness of the ray marching. So the benefit of HiZ is not as high as expected for this scene but there’s some (130 FPS instead of 110-120, 960x540).

I'm using a HiZ loop working only with increasing Mip level and a second one is used to get finer result as at that point I did not understand fully how the cell boundary calculations work.

Unfortunately there is a last annoying problem I was not able to solve in this SSR HiZ shader. The problem is shown in green on the left picture and on right what I have without HiZ. It is annoying because I did not have this problem with my regular SSR shader whitout HiZ. I have tried a thickness test like in the McGuirre method but it is not working.

 e.g : if abs ( ray.z-zbuff)< thickness.


So if someone can help below is my shader. enjoy if you're interested.

  //parameter for finer or coarser primary while results
#define offset0 0.005
#define offsetR 2
//my initial resolution (Mip=0)
static const int2 Resolution = {960, 540};
//uv offset1 : critical to ensure correct uv sampling. modulate to see effect
static const float2 offset1 = {1, 1.015};
//parameter for finer or coarser secondary while results
#define offset2  1.15
//parameter for final dicotomic refinement
#define refinemax 3
//parameter for removing vertical artifact for objects not on ground
//based on the normal of the object
#define dot3falloff 0.00005
#define falloff 0.00003

{
float D = txDepth1.SampleLevel(samPoint, Input.Tex, 0).r;
if ( D == 1 ) return float4(0,0,0,0);
float4 ColorOut = txDiffuse1.SampleLevel(samPoint, Input.Tex, 0);
if ( ColorOut.a <0.9)  return ColorOut;
float3 PosV = float3(InvProj.x*(Input.Tex.x*2-1),InvProj.y*(1-Input.Tex.y*2), 1)/(InvProj.z*D+1);
if ( txDiffuse1.SampleLevel(samPoint, Input.Tex, 0).a == 0)  return ColorOut;

float3 VSDir = normalize(reflect(normalize(PosV.xyz),txNormal1.SampleLevel(samPoint, Input.Tex, 0).rgb*2-1));
if ( VSDir.z < 0 ) return ColorOut;

float4 SSEnd = mul(float4(PosV + VSDir*200, 1), Proj);
SSEnd/= SSEnd.w;
SSEnd.xy = SSEnd.xy*float2(0.5,-0.5)+float2(0.5,0.5);
float3 SSray = float3(Input.Tex, D);
float3 SSDir = normalize(SSEnd.xyz-SSray);

float dSS;
if ( abs(SSDir.x) < abs(SSDir.y) ) dSS = abs(SSDir.y); else dSS = abs(SSDir.x);
SSDir/=dSS;//"normalize" SSDir according to longest x or y
SSDir*=offset0;//modulate step size

int Mip = 0;//you can start at higher Mip level but I don't see improvements
float ZBufferVal=1;
float2 CurResol;
float3 curSSDir = SSDir*pow(2,Mip)*offsetR;//pow useless at Mip=0
//primary while : move ray with Mip-dependant increasing steps
//until ray.z > zbuffer
float rayZmin = 0;
float3 poslast = SSray;
int i=0;
float4 Color ;
while ((Mip<10)&&(i<20))//my resolution corresponds to 10 miplevel
{
i++;
rayZmin = SSray.z;
poslast = SSray;
float2 MipSize = (Resolution>>Mip);
//use Load like below to have better sampling than
//SampleLevel(samPoint,SSray.xy,Mip).r;
if ( ZBufferVal==0 ) ZBufferVal=1;    //avoid disappearing reflexions at bottomscreen
if (SSray.z > ZBufferVal)  break;
if ( Mip < 9 )
{
Mip++;
}
}
if (poslast.z>=1) return ColorOut;
//secondary while : move ray from last position (z<zbuffer) now only at level Mip = 0
//until ray.z > zbuffer
SSDir=normalize(SSDir)*0.02;
i=0;
SSray = poslast;
while (i<40)
{
i++;
ZBufferVal = txDepth1.SampleLevel(samPoint, SSray.xy, 0).r;
if (SSray.z > ZBufferVal)
{
float3 MinRay = poslast;
float3 MaxRay = SSray;
for(int j = 0; j < refinemax; j++)
{
poslast = (MinRay+MaxRay)*0.5f;
ZBufferVal = txDepth1.SampleLevel(samPoint, poslast.xy, 0).r;
if ( poslast.z > ZBufferVal) MaxRay = poslast; else MinRay = poslast;
}
float3 N = txNormal1.SampleLevel(samPoint, poslast.xy,0).rgb*2-1;
float Dot3 = ( N.x+N.y+N.z>2.99f)?dot3falloff:(dot(-VSDir, N)<0)?0:dot3falloff;
if (abs(poslast.z-ZBufferVal)<Dot3)
{
Color = float4(txDiffuse1.SampleLevel(samPoint, poslast.xy, 0).rgb, 1-abs(poslast.z-D)*500);
return lerp(Color,ColorOut,1-Color.a);
}
return ColorOut;
}
poslast = SSray;
SSray += SSDir;
}
return ColorOut;
}


After some refinements I have found the solution (see picture). The vertical holes were due to the line "return ColorOut" just after the refinemax loop. This line must be removed to let the while loop continue to find an accurate position. In turn this slows down the shader. I add in this answer a corrected shader. This new shader is more complete and you can set or not HiZ. It is also intended for showing how I managed the deferred multilayers used to fill the last holes due to overlapping objects. How these layers are created is explained in one of my previous questions. After the Hiz search a second loop is used to fill these holes as the HiZ algorithm I used is no table to do it. At the end the speed is still not there but it is possibly because I want to filter off the black background as much as possible.

The corrected shader is below. The HiZ loop comes from (here . It was easy to implement and give fine results) .

   Texture2D txDiffuse[3] : register(t0);//0 main scene; 1 object occluded once 2 object occluded >1
Texture2D txNormal[3] : register(t3);//normals for the 3 layers
Texture2D txDepth1 : register(t6);//main depth (zbuf) of layer 0. Has mip levels
Texture2D txDepth23 : register(t7);//rg contains depth fo layers 1 and 2. No mip levels
Texture2D txDepth4 : register(t8);//not used
SamplerState samPoint : register(s2);

{
matrix Proj;
float4 InvProj;
float4 Resol_HiZ_Lay;
//xy is the resolution of Mip=0; z =0/1 to (de)activate HiZ; w =0/1 to (de)activate multilayers
}

{
float3 Pos : POSITION;
float2 Tex : TEXCOORD0;
};

{
float4 Pos : SV_POSITION;
float2 Tex : TEXCOORD0;
};

{
Output.Pos = float4(Input.Pos.xyz, 1);
Output.Tex = Input.Tex;
return Output;
};
#define refinemax 4
#define dot3falloff 0.00005
#define Zthickness 0.00005

void GetZLayer(float3 SSray, inout float3 ZBufferVal, inout float hit)
{
//for additional layer no Hierarchical Z is made so sample Mip 0
ZBufferVal *= float3(1,ZBufferVal.g<1.000005,ZBufferVal.b<1.000005);
float3 Delta = abs(SSray.zzz-ZBufferVal);
hit=1;
if ( Delta.b  <= Delta.r ) {Delta.r = Delta.b;ZBufferVal.r = ZBufferVal.b;hit=3;}
if ( Delta.g  <= Delta.r ) {ZBufferVal.r = ZBufferVal.g;hit=2;}
}

float4 GetColorSingle(float3 SSray, float Z, float D, float3 VSDir)
{
float3 Normal = txNormal[0].SampleLevel(samPoint, SSray.xy,0).rgb;
float3 Color = txDiffuse[0].SampleLevel(samPoint, SSray.xy, 0).rgb;
Normal=Normal*2-1;
float Dot3 = ( Normal.x+Normal.y+Normal.z>2.99f)?dot3falloff:(dot(-VSDir, Normal)<0)?0:dot3falloff;
if (abs(SSray.z-Z)<Dot3) return float4(Color,1-abs(SSray.z-D)*500);
return float4(0,0,0,0);
}

float4 GetColorLayer(float3 SSray, float hit, float Z, float D, float3 VSDir)
{
float3 Normal  = float3(1,1,1);
float3 Color  = float3(1,1,1);
if ( hit==1)
{
Normal = txNormal[0].SampleLevel(samPoint, SSray.xy,0).rgb;
Color = txDiffuse[0].SampleLevel(samPoint, SSray.xy, 0).rgb;
}
if ( hit==2)
{
Normal = txNormal[1].SampleLevel(samPoint, SSray.xy,0).rgb;
Color = txDiffuse[1].SampleLevel(samPoint, SSray.xy, 0).rgb;
}
if ( hit==3)
{
Normal = txNormal[2].SampleLevel(samPoint, SSray.xy,0).rgb;
Color = txDiffuse[2].SampleLevel(samPoint, SSray.xy, 0).rgb;
}
Normal=Normal*2-1;
float Dot3 = ( Normal.x+Normal.y+Normal.z>2.99f)?dot3falloff:(dot(-VSDir, Normal)<0)?0:dot3falloff;
if (abs(SSray.z-Z)<Dot3) return float4(Color,1-abs(SSray.z-D)*500);
return float4(0,0,0,0);
}

//NOTE the blend state used is classic opaque (no blend, src one dest zero blend add)
//destination render target is cleared to 0 at start
{
float D = txDepth1.SampleLevel(samPoint, Input.Tex, 0).r;
float4 ColorOut = txDiffuse[0].SampleLevel(samPoint, Input.Tex, 0);
if ( D == 1 ) return ColorOut;
float3 PosV = float3(InvProj.x*(Input.Tex.x*2-1),InvProj.y*(1-Input.Tex.y*2), 1)/(InvProj.z*D+1);
if ( ColorOut.a <0.9)  return ColorOut;

float3 VSDir = normalize(reflect(normalize(PosV),txNormal[0].SampleLevel(samPoint, Input.Tex, 0).rgb*2-1));
if ( VSDir.z < 0 ) return ColorOut;

float4 SSEnd = mul(float4(PosV + VSDir*200, 1), Proj);
SSEnd/= SSEnd.w;
SSEnd.xy = SSEnd.xy*float2(0.5,-0.5)+float2(0.5,0.5);
float3 SSray = float3(Input.Tex, D);
float3 SSDir = normalize(SSEnd.xyz-SSray);
int Mip = 0;//you can start at higher Mip level but I don't see improvements
float3 ZBufferVal=float3(1,1,1);
float3 poslast = SSray;
int i=0;
int maxi=45;
float DirSize = 0.02;
float3 Delta= float3(0,0,0);
float hit = 1;
float4 Color ;
//  mipmax is used to see the maximum mip level reached. see below
//   int MipMax=0;
if ( Resol_HiZ_Lay.z )
{
float4 X_stepoffset = float4(1,1,Zthickness,Zthickness);
if (SSDir.x<0) X_stepoffset.x = -1.0;
if (SSDir.y<0) X_stepoffset.y = -1.0;
X_stepoffset.zw *= X_stepoffset.xy;
X_stepoffset.xy = saturate(X_stepoffset.xy);
float zDir = SSDir.z;//store dir.z for later use
SSDir/=SSDir.z;// the algorithm needs normalization to SSDir.z = 1
float2 CurResol;
float3 tmp_ray;
SSray+=SSDir*Zthickness;
while((Mip > -1)&&(i < 40))
{
i++;
CurResol = (int2(Resol_HiZ_Lay.xy)>>Mip);
float2 old_cell_id = floor(SSray.xy * CurResol);
if ( Resol_HiZ_Lay.w ) GetZLayer(SSray, ZBufferVal, hit);
if (ZBufferVal.r==0)ZBufferVal.r=1;
float min_minus_ray = ZBufferVal.r - SSray.z;
tmp_ray = SSray;
if ( min_minus_ray > 0)
{
tmp_ray = SSray + SSDir*min_minus_ray;
float2 new_cell_id = floor(tmp_ray.xy * CurResol);
if ((new_cell_id.x != old_cell_id.x) || (new_cell_id.y != old_cell_id.y))
{
//repositionne tmp_ray to match cell
float2 planes = (old_cell_id+ X_stepoffset.xy)/CurResol;
float2 solutions = (planes - SSray.xy)/SSDir.xy;
tmp_ray = SSray + SSDir * min(solutions.x, solutions.y);
tmp_ray.xy += (solutions.x < solutions.y) ? float2(X_stepoffset.z, 0) : float2(0, X_stepoffset.w);
//going higher than mip 7 generate hole artifacts in my scene on the vertical plane
//when at bottom screen.
//help need to solve it if possible
Mip = min(7, Mip + 2.0f);
//if you wan tto see the maxmip level reached use this line
//if ( Mip > MipMax ) MipMax = Mip;
}
}
Mip--;
SSray = tmp_ray;
if ((SSray.x<0 )||(SSray.x >1 )||(SSray.y<0)||(SSray.y>1)) return ColorOut;
}
SSDir*=zDir;//restores appropriate SSDir increment for refinement
if ( Resol_HiZ_Lay.w )
Color = GetColorLayer(SSray, hit, ZBufferVal.r, D, VSDir);
else
Color = GetColorSingle(SSray, ZBufferVal.r, D, VSDir);
if ( Color.a > 0 ) return lerp(Color,ColorOut,1-Color.a);
//use these lines instead of previous one to see the maxmip level reached
/*     float4 MipColor = float4(MipMax==0||MipMax==3||MipMax==4||MipMax==6||MipMax==7, MipMax==1||MipMax==3||MipMax==5||MipMax==6||MipMax==8,                        MipMax==2||MipMax==4||MipMax==5||MipMax==6||MipMax==9,1);
if ( Color.a > 0 ) return lerp(MipColor,ColorOut,1-Color.a);*/

//for secondary loop with HiZ parameters maxi can be lower and DirSize coarser
maxi=25;
DirSize = 0.05;
i=0;
}
//to skip next while showing vertical holes due to the HiZ algorithm
//turn on return ColorOut;
//Any better solution welcome as this makes the shader slower
//  return ColorOut;
SSDir*=DirSize;
while (i<maxi)
{
i++;
poslast = SSray;
SSray += SSDir;
if ((SSray.x<0 )||(SSray.x >1 )||(SSray.y<0)||(SSray.y>1)) break;
ZBufferVal.r = txDepth1.SampleLevel(samPoint, SSray.xy, 0).r;
if ( Resol_HiZ_Lay.w ) GetZLayer(SSray, ZBufferVal, hit);
if (SSray.z > ZBufferVal.r)
{
float3 MinRay = poslast;
float3 MaxRay = SSray;
for(int j = 0; j < refinemax; j++)
{
poslast = (MinRay+MaxRay)*0.5f;
if( hit ==1 )
ZBufferVal.r = txDepth1.SampleLevel(samPoint, poslast.xy, 0).r;
else
if (Resol_HiZ_Lay.w)
{
if( hit ==2 ) ZBufferVal.r = txDepth23.SampleLevel(samPoint, poslast.xy, 0).r;
else ZBufferVal.r = txDepth23.SampleLevel(samPoint, poslast.xy, 0).g;
}
if ( poslast.z > ZBufferVal.r) MaxRay = poslast; else MinRay = poslast;
}
if ( Resol_HiZ_Lay.w )
Color = GetColorLayer(poslast, hit, ZBufferVal.r, D, VSDir);
else
Color  = GetColorSingle(poslast, ZBufferVal.r, D, VSDir);
if ( Color.a >0 ) return lerp(Color,ColorOut,1-Color.a);

//to visualize where refinement apply use this line insteadof previous one
//if ( Color.a >0 ) return lerp(float4(1,1,1,1),ColorOut,1-Color.a);
}
}
return ColorOut;
}


• Don't forget to click the checkmark icon in the top left of the Answer to mark it as "accepted" if it worked for you. Commented Sep 19, 2022 at 11:17