Virtex Edge Design
  • Home
  • Portfolio
  • Blog
  • Discord
  • About
June 24, 2018 by virtex.edge
Dev Blog, Shader Series, Tutorials

Shader Series – Screen Space Reflections

Shader Series – Screen Space Reflections
June 24, 2018 by virtex.edge
Dev Blog, Shader Series, Tutorials

Screen Space Reflections are used in every engine these days. Games from The Witcher 3 to Fortnite all employ this effect, so it was natural that I wanted it in Vertices. While I came across a lot of different examples and tutorials online, none seemed to be complete or clear enough to easily show the method, so I’ve decided to share my implementation here in hopes to help others with this technique.

Initial Approach

The Initial approach to Reflections in Vertices was to use planar reflections. which essentially was to redraw the entire scene from the reflected position of the camera.

Initial View of Planar Reflections

While this provided a great result with very little artifacts, it was expensive as it required re-drawing the scene for each reflected surface. Enter SSR.

The Concept

Before we can create someting that immitates the real world, we need to breakdown how it behaves in the real world, which for reflections is quiet intuitive.

A reflection is simply the reflected light bouncing off of a surface into a camera or eye. The amount of reflection and direction is based on the roughness of the surface it’s self.

A picture of reflections in a rice field from Bali I took last year

The Algorithm

For Screen Space Reflections, we can reverse this process by performing a ray mach for each pixel which we decide is reflectable (using values from the specular map & reflection map along with a calculated fresnel value). From the Camera’s Position and Viewing Direction, we can trace where light would come from off of a reflected surfaces that the Camera sees. We can then do a ray march using the depth buffer until we intersect with a surface.

Water Reflections running in Vertices

The SSR method breakdown is as follows.

Get Initial Location

For each UV screen coordinate, we would need to find the 3D World Position (reconstructed from the Depth Map and the Camera’s InverseViewMatrix). We also need to get the Normal of what ever surface is at this position from the Normal Map.

//Get initial Depth
float InitDepth = GetDepth(texCoord);
// Now get the position
float3 reflPosition = GetWorldPosition(texCoord, InitDepth);
// Get the Normal Data
float3 normalData = tex2D(NormalSampler, texCoord).xyz;
//tranform normal back into [-1,1] range
float3 reflNormal = 2.0f * normalData - 1.0f;

View Direction

Since we have the Normal and 3D World Position of what ever surface is at this pixel, we need to now find the View Directions and Reflected Vector.

The View Direction is simply the pixels 3D World Space Position minus the Camera’s World Space Position and can be thought of as the vector that the reflected light moves along to the Camera.

The Reflected Vector can be thought of as the direction the light was moving before it hit the surface and was reflected into the camera. But as we’re reversing the process, this is simply the reflection of the View Direction vector.

// First, Get the View Direction
float3 vDir = normalize(reflPosition - CameraPos);
float3 reflectDir = normalize(reflect(vDir, normalize(reflNormal)));

The Loop and Trace

Now that we have the physical data such as reflection vectors, world positions and surface normals, we can then do a ray trace.

Our shader uses a TraceRay function which returns a RayMarchResult struct holding whether it hit anything and if so, what are the uv coordinates of that hit.

struct RayTraceOutput
{
bool Hit;
float2 UV;
};

Our Trace Ray function will then perform a Ray march, getting the depth for each position in the loop and seeing if it’s close enough to the depth in the depth buffer.
The meat and potatoes of the Trace Ray function is the following:

// The Current Position in 3D
float3 curPos = 0;
 
// The Current UV
float3 curUV = 0;
 
// The Current Length
float curLength = 1;

// Now loop
    for (int i = 0; i < loops; i++)
    {
        // Has it hit anything yet
        if (output.Hit == false)
        {
            // Update the Current Position of the Ray
           curPos = reflPosition + reflDir * curLength ;
            // Get the UV Coordinates of the current Ray
            curUV = GetUVFromPosition(curPos);
            // The Depth of the Current Pixel
            float curDepth = GetDepth(curUV .xy);
            for (int i = 0; i < SAMPLE_COUNT; i++)
            {
                if (abs(curUV .z - curDepth) < DepthCheckBias)
                {
                    // If it's hit something, then return the UV position
                    output.Hit = true;
                    output.UV = curUV .xy;
                    break;
                }
                curDepth = GetDepth(curUV .xy + (RAND_SAMPLES[i].xy * HalfPixel * 2));
            }

            // Get the New Position and Vector
            float3 newPos = GetWorldPosition(curUV.xy, curDepth );
            curLength = length(reflPosition - newPos);
        }
    }
    return output;
}

So let’s break this function down.

March In Step

The first part we’ll look at is the outer loop. This is what steps along the ray march check if it’s hit anything yet.

// Update the Current Position of the Ray
curPos = reflPosition + reflDir * curLength ;
// Get the UV Coordinates of the current Ray
curUV = GetUVFromPosition(curPos);
// The Depth of the Current Pixel
float curDepth = GetDepth(curUV .xy);

What we have here is finding the world position along this step in the ray march. We then get the screen space position of this world position. Note that in the ‘z’ value this returns the depth of this world space position, which is likely different than the curDepth value from the depth in the depth buffer at this screen space position.

It’s this different in depth which is important to us.

Check all the things

The next part then enters a loop checking if the differences between the world position depth and the depth buffer value is below a certain DepthBiasBuffer value. If not then though, it performs a loop checking the surrounding pixels if they are

for (int i = 0; i < SAMPLE_COUNT; i++)
{
    if (abs(curUV .z - curDepth) < DepthCheckBias)
    {
        // If it's hit something, then return the UV position
        output.Hit = true;
        output.UV = curUV.xy;
        break;
    }
    // If it hasn't hit anything, check the surrounding pixels
    curDepth = GetDepth(curUV .xy + (RAND_SAMPLES[i].xy * HalfPixel * 2));
}

So if we’ve hit something, then we can return the the output struct, but if not, then we can use a RAND_SAMPLES array we have to check surrounding pixels if there’s any collisions to help cut down on artifacts and missed positive hits.

Creating the UV Map

Once we’ve done the Ray Tracing, we can now output the result as the UV’s and the reflection amount to the SSR UV Map. We use a UV map as it allows us to perform a more precise blurring due to surface roughness later on in a separate pass.

float4 PixelShaderFunction(float2 texCoord : TEXCOORD0) : COLOR0
{
	// Get the Amount of reflection. Only calculate reflection on 
	// surfaces with reflection data.
	float amount = tex2D(NormalSampler, texCoord - HalfPixel * 2).a;

	if (amount > 0)
	{
		RayTraceOutput ray = TraceRay(texCoord - HalfPixel * 2);
	
		if (ray.Hit == true)
		{
                        // Fade at edges
			if (ray.UV.y < EdgeCutOff * 2)
				amount *= (ray.UV.y / EdgeCutOff / 2);

			return float4(ray.UV.xy, 0, amount);
		}
	}

	// If it didn't hit anything, then just simply return nothing
	return 0;
}

The result here is sent to a UV map which is used by a later Post Processor. Note that we fade at the edges to deal with artifacts at these edges.

Debug View of the UV Map

Painting the Picture

Now that we have the UV coordinates of the Reflection positions, we can take those and apply the reflection to the scene.

The same scene from the UV map with the Reflections extracted using the UV coordinates and  overlayed on the scene.

Next Steps

Basic View of the SSR’d scene.

Surface Roughness

We can use the Specular Power as a reference to the materials ‘roughness’ using that to apply a surface blur. The blur amount would be factored by the distance between the current screen coordinates and the reflected screen coordinates, i.e. how far away is the reflected point on the screen. We can then perform a circular blur with the size of that distance.

A basic surface roughness blur

Filling Missing Data

SSR works well with items which are in contact with the reflected surface, but it is only able to reflect surfaces which are visible in the screen. A long time artifact of SSR is that surfaces which are not visible will not be reflected, leaving a gap in the reflection. A good work around for this is using a Cube map which will at least add some level of reflection, but the artifact is still visible.

An example of the SSR artifact along with a cube map reflection over top.

Binary Test

The method I’ve shown is a quick and dirty way to get a good reflection in a small area, but this method starts to break down when there are item’s not on the surface. For a more versatile implementation for larger areas, we can add a binary test during the depth test to get a more accurate reflection value. It does add to the expense of this effect though and I’ll be posting an later on covering this.

Final Thoughts

SSR being used in Metric Racer, coming out soon.

SSR is a great effect which adds a lot to a scene, but it can also be temperamental with artifacts and is not a be all and end all solution. That said, when used in the right environment and geometric set up, it can be a versatile addition to your engine.

As always, thanks for reading and if you like what you see, leave a comment, give our blog a follow and give Virtex a follow on Twitter and Instagram.

https://www.youtube.com/watch?v=msO6b_lFkxc

Android C# Coding Design Editor Extentions Game Design Game Editor Game Engines Games Gaming Indie Games MonoGame Programming Racing sharing Tools videogames Visual Studio Web Xamarin XNA

Previous articleMesh Generation in Metric RacerNext article The Chaotic Workshop is Coming to Steam!

4 comments

nachocpol says:
June 25, 2018 at 6:25 am

Thanks a lot for the post! Could you explain why we need to perform the inner loop over SAMPLE_COUNT?

Reply
R.T. says:
June 26, 2018 at 4:24 pm

Hi,

Depending on scene geometry and viewing angles, sometimes bits of the scene are missed and then you get tiny holes in your reflection result. If you check surrounding pixels then you increase the chance of getting a positive hit.

Again, this could be removed by performing a binary search, I’ll have a blog post soon detailing that.

Reply
Nacho says:
June 25, 2018 at 10:24 am

Nice post! I’m wondering why do we need to perform the extra loop over SAMPLE_COUNT?

Reply
R.T. says:
June 26, 2018 at 4:26 pm

Reblogged this on rtroe and commented:

My 3rd entry in the weekly Shader Series is on Screen Space Reflections. It overviews the meat and potatoes of the shader along with a healthy amount of code samples.

Let me know what you think of the tutorial formats, I’d love some feedback, and give Virtex a follow to keep up with our shader series and upcoming releases.

Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

About The Blog

Virtex Edge Design is a a software design company which focusesĀ  Game Development and design of 3D Development Tools.

Recent Posts

INDIE RACERS FESTIVAL 2022!August 26, 2022
V0.9.1 – Steam Racing Fest, New Regions, Big Visual and Racing Improvements!!May 24, 2022
Metric Racer – Version 0.9 ReleasedMarch 8, 2022

Categories

  • Apps
  • Chrome Extensions
  • Design
  • Dev Blog
  • Games
  • Metric Racer
  • Preview
  • Shader Series
  • Space Esc8bit
  • The Chaotic Workshop
  • Tutorials
  • Uncategorized
  • Vertices Engine

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org

Tags

Android blender C# Chrome Coding content generation Design Editor Extensions Extentions Game Design Game Editor Game Engines Games Gaming Graphics HLSL Indie Game Dev Indie Games indieracers indie racers iOS Metric Racer MonoGame New Release Pew Pew Pew Programming Racing racing games sharing solo SoloDev Solo Indie Dev Steam Steam Workshop Tools Tutorials Updates videogames Visual Studio Web Wipeout Xamarin XNA Youtube

Virtex Edge Design

Virtex Edge Design is an Interactive software design company which focuses on Game Development, immersive experiences and design of 3D Development Tools.

Contact

Victoria, British Columbia
contact@virtexededesign.comvirtexededesign.com
Mon. - Fri. 8AM - 6PM

Recent Posts

INDIE RACERS FESTIVAL 2022!August 26, 2022
V0.9.1 – Steam Racing Fest, New Regions, Big Visual and Racing Improvements!!May 24, 2022
Metric Racer – Version 0.9 ReleasedMarch 8, 2022
Rife Wordpress Theme. Proudly Built By Apollo13

About

Virtex Edge Design is an Interactive software design company which focuses on Game Development, immersive experiences and design of 3D Development Tools.

Recent Posts

INDIE RACERS FESTIVAL 2022!August 26, 2022
V0.9.1 – Steam Racing Fest, New Regions, Big Visual and Racing Improvements!!May 24, 2022
Metric Racer – Version 0.9 ReleasedMarch 8, 2022

Categories

  • Apps
  • Chrome Extensions
  • Design
  • Dev Blog
  • Games
  • Metric Racer
  • Preview
  • Shader Series
  • Space Esc8bit
  • The Chaotic Workshop
  • Tutorials
  • Uncategorized
  • Vertices Engine

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org