I am trying to implement SSR (screen space reflections). I managed to get a basic effect, but there are some artifacts as you can see from the images and the video below. First of all I would like to solve these visual problems and then I would like to try to improve the performance of the algorithm if possible.
SSR artifacts: https://youtu.be/LFBAJHa_mFM
My SSR implementation is based on this tutorial.
Below you can find the code of the pixel shader that generates the reflection map (or reflected UV map) and here you can find the full implementation. As you can see it is practical identical to the code of the tutorial (screen-space-reflection.frag) except for few changes:
- I don't store the 3D position of each pixel in an off-screen texture, but I reconstruct the 3D position from the view space depth in the function
GetFullViewPosition
. - When I transform the fragment NDC into texture coordinates, I multiply by
float2(+0.5f, -0.5f)
instead offloat2(+0.5f, +0.5f)
. - I added a check that stops ray tracing if the current position is out of screen space.
SSRReflectionsMapComputePS.hlsl
cbuffer ConstantBuffer : register(b0)
{
float4 gFrustumFarCorner[4];
float4x4 gProj;
};
struct VertexOut
{
float4 PositionH : SV_POSITION;
float3 ToFarPlane : TEXCOORD0;
float2 TexCoord : TEXCOORD1;
};
Texture2D gNormalDepthMap : register(t0);
SamplerState gNormalDepthSamplerState : register(s2);
float3 GetFullViewPosition(float2 uv, float z)
{
// bilinear interpolation
float4 p0 = lerp(gFrustumFarCorner[0], gFrustumFarCorner[3], uv.x);
float4 p1 = lerp(gFrustumFarCorner[1], gFrustumFarCorner[2], uv.x);
float3 ToFarPlane = lerp(p0.xyz, p1.xyz, 1 - uv.y);
// reconstruct full view space position (x,y,z)
// find t such that p = t*pin.tofarplane
// p.z = t*pin.tofarplane.z ==> t = p.z / pin.tofarplane.z
return (z / ToFarPlane.z) * ToFarPlane;
}
float3 GetFullViewPosition(float2 uv)
{
float4 NormalDepth = gNormalDepthMap.SampleLevel(gNormalDepthSamplerState, uv, 0);
return GetFullViewPosition(uv, NormalDepth.w);
}
// https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html
// https://github.com/lettier/3d-game-shaders-for-beginners/blob/master/demonstration/shaders/fragment/screen-space-reflection.frag
float4 main(VertexOut pin) : sv_target
{
// view space normal and depth (z-coord) of this pixel
float4 normaldepth = gNormalDepthMap.SampleLevel(gNormalDepthSamplerState, pin.TexCoord, 0);
float3 n = normaldepth.xyz;
float pz = normaldepth.w;
//float3 p = (pz / pin.tofarplane.z) * pin.tofarplane;
float3 p = GetFullViewPosition(pin.TexCoord, pz);
float maxdistance = 100;
float resolution = 1;
int steps = 10;
float thickness = 0.5f;
float2 texsize;
gNormalDepthMap.GetDimensions(texsize.x, texsize.y);
float2 texcoord = pin.PositionH.xy / texsize;
float3 positionfrom = p;
float3 positionfromunit = normalize(positionfrom);
float3 normal = normalize(n);
float3 pivot = normalize(reflect(positionfromunit, normal));
float3 positionto = positionfrom;
float4 uv = 0;
float3 viewstart = positionfrom + pivot * 0;
float3 viewend = positionfrom + pivot * maxdistance;
float4 fragstart = float4(viewstart, 1);
fragstart = mul(gProj, fragstart);
fragstart.xy /= fragstart.w;
fragstart.xy = fragstart.xy * float2(+0.5f, -0.5f) + 0.5f;
fragstart.xy *= texsize;
float4 fragend = float4(viewend, 1);
fragend = mul(gProj, fragend);
fragend.xy /= fragend.w;
fragend.xy = fragend.xy * float2(+0.5f, -0.5f) + 0.5f;
fragend.xy *= texsize;
float2 frag = fragstart.xy;
uv.xy = frag / texsize;
float deltax = fragend.x - fragstart.x;
float deltay = fragend.y - fragstart.y;
float usex = abs(deltax) >= abs(deltay) ? 1 : 0;
float delta = lerp(abs(deltay), abs(deltax), usex) * clamp(resolution, 0, 1);
float2 increment = float2(deltax, deltay) / max(delta, 0.001f);
float search0 = 0;
float search1 = 0;
int hit0 = 0;
int hit1 = 0;
float viewdistance = viewstart.z;
float depth = thickness;
for (int i = 0; i < int(delta); ++i)
{
frag += increment;
uv.xy = frag / texsize;
// do not sample outside the screen space
if (any(uv.xy < float2(0, 0)) || any(uv.xy > float2(1, 1))) break;
positionto = GetFullViewPosition(uv.xy);
search1 = lerp((frag.y - fragstart.y) / deltay, (frag.x - fragstart.x) / deltax, usex);
search1 = clamp(search1, 0, 1);
viewdistance = (viewstart.z * viewend.z) / lerp(viewend.z, viewstart.z, search1);
depth = viewdistance - positionto.z;
if (depth > 0 && depth < thickness)
{
hit0 = 1;
break;
}
else
{
search0 = search1;
}
}
search1 = search0 + ((search1 - search0) / 2);
steps *= hit0;
for (int i = 0; i < steps; ++i)
{
frag = lerp(fragstart.xy, fragend.xy, search1);
uv.xy = frag / texsize;
// do not sample outside the screen space
if (any(uv.xy < float2(0, 0)) || any(uv.xy > float2(1, 1))) break;
positionto = GetFullViewPosition(uv.xy);
viewdistance = (viewstart.z * viewend.z) / lerp(viewend.z, viewstart.z, search1);
depth = viewdistance - positionto.z;
if (depth > 0 && depth < thickness)
{
hit1 = 1;
search1 = search0 + ((search1 - search0) / 2);
}
else
{
float temp = search1;
search1 = search1 + ((search1 - search0) / 2);
search0 = temp;
}
}
float visibility = hit1 *
(1 - max(dot(-positionfromunit, pivot), 0)) *
(1 - clamp(depth / thickness, 0, 1)) *
(1 - clamp(length(positionto - positionfrom) / maxdistance, 0, 1)) *
(uv.x < 0 || uv.x > 1 ? 0 : 1) *
(uv.y < 0 || uv.y > 1 ? 0 : 1);
return float4(uv.xy, 0, saturate(visibility));
}