Help with GPU Pro 5 Hi-Z Screen Space Reflections

Started by
77 comments, last by WFP 9 years, 3 months ago

You're right that it looks that way in the demo video, but I think it's actually a little bit misleading. I think what's happening is the video is actually showing the cone, then at the top of it superimposing a circle with the full blended color for the current pixel. I could be wrong, but that's what it looks like to me, especially around the 18 second mark, where the circle looks too big to for the cone in general.

I think the idea is that for anything but a perfectly smooth, mirrored surface, you'll never actually sample at the ray position. In fact, the below lines basically ensure that to be true for anything but a 0 cone angle.


// get the sample position in screen space
float2 samplePos = positionSS.xy + adjacentUnit * (adjacentLength - incircleSize);

I think this is done purposefully to give you stretched reflections (think how your specular highlights stretch at grazing angles). The wider the cone (rougher the surface), the more stretched and faded the reflection should become. Of course, properly blending/weighting this is the main issue we're still working through. I've got a few new ideas, as well as some I've already tried that didn't pan out, but any suggestions from anyone following along are welcome and encouraged!

Advertisement

OK, so a few updates from working on the cone tracing stuff a bit this weekend.

First, the pre-integration stuff didn't pan out as I had hoped. The changes proposed in the thread did give better results in certain situations, but it also introduced a lot of artifacts into my visibility buffer. I've since reverted back to the previous way that's closer to what's in the book and have sought out new means of weighting the sample contributions for the cone tracing pass.

I've tried several combinations of using the hi-z buffer, but at this point haven't gotten very far, but I have noticed some improved (aesthetically, anyway) results by using a distance attenuation function when weighting the sample. See below:


float4 coneSampleWeightedColor(float2 samplePos, float mipChannel, float3 rayStartVS)
{
	// placeholder - this is just to get something on screen
	float3 sampleColor = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos, mipChannel).rgb;
	float visibility = visibilityBuffer.SampleLevel(sampTrilinearClamp, samplePos, mipChannel).r;
	float4 hizMinVis = hiZBuffer.GatherRed(sampTrilinearClamp, samplePos, mipChannel);
	float4 hizMaxVis = hiZBuffer.GatherGreen(sampTrilinearClamp, samplePos, mipChannel);
	
	float minz = min(min(hizMinVis.r, hizMinVis.g), min(hizMinVis.b, hizMinVis.a));
	float maxz = max(max(hizMaxVis.r, hizMaxVis.g), max(hizMaxVis.b, hizMaxVis.a));
	float depth = hiZBuffer.SampleLevel(sampPointClamp, samplePos, 0.0f).r;

	float3 rayEndVS = viewSpacePositionFromDepth(samplePos, depth);
	float distanceTraveled = length(rayEndVS - rayStartVS);
	// distance squared caused the effect to fade much to fast for larger objects in the distance
	float attenuation = distanceTraveled == 0.0f ? 1.0f : saturate(1.0f / (distanceTraveled));

	return float4(sampleColor * visibility * attenuation, visibility * attenuation);
}

You'll notice a couple of new variables that are obtained but not used. Those are just things that constantly seem to come up while trying to figure out a weighting scheme for the hi-z buffer in all of this, so I'm just leaving them in for now to avoid re-typing them for the thousandth times. The distance attenuation is nothing too special. I attenuate the distance from the ray starting position in view space to the current sampling position in view space. I actually tried this with a distance-squared attenuation function, and it blended close objects nicely, but fades way too fast for distant objects, so I'm sticking with linear. This set up helps give results similar to what I had with the altered pre-integration pass, but with my visibility buffer still in tact.

If anyone has suggestions for using the hi-z buffer to add to the weighting scheme, they would be greatly appreciated. The effect is starting to come along more nicely now, but I think that is still the big missing piece.

Thanks,

WFP

Screenshot with old visibility set + new attenuation weighting: https://www.dropbox.com/s/uyc7g5p4ellshuu/screenshot_18.png?dl=0

EDIT: grammar

A quick update for anyone following along. I've found at least one way in addition to the ones mentioned above to better the overall results of the cone tracing pass. It's kind of an obvious one (and one I was hoping to avoid), but the results are undeniably better in its current states. By taking several samples around the circle's perimeter and inside the circle area, I was able to get rid of a lot of the ugly jaggedness that can show up from only taking the one sample at the calculated position. You can see an example of this in my last post when looking at the color transitions between blocks in the reflection. The implementation is straight forward enough - just offset by the circle radius in x and y directions and take samples, then halve the radius and take samples of the square created by offsetting again from the radius (see code below). I've tried a few variations, including moving the last four samples to the circle radius and giving the center sample more weight than the offsets, but in addition to saving myself from a sine and cosine operation per iteration, for me at least weighing the samples evenly seems to give the best results.

I'm going to stop putting it off now and actually hunker down on weighting with the course depth volume and the sphere, but I wanted to at least provide this small update so everyone can see that it's starting to look halfway decent :) .

Thanks,

WFP

Screenshot with higher sampling inside cone: https://www.dropbox.com/s/o0y6z5a1doa16vl/screenshot_19.png?dl=0

New weighting method in cone tracing pixel shader:


float4 coneSampleWeightedColor(float2 samplePos, float mipChannel, float3 rayStartVS, float radiusSS)
{
	// sample center and take additional sample points around the cone perimeter and inside to better blend the color result
	float3 sampleColor = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos, mipChannel).rgb;
	float3 sr = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(radiusSS, 0.0f), mipChannel).rgb;
	float3 sl = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(radiusSS, 0.0f), mipChannel).rgb;
	float3 sb = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(0.0f, radiusSS), mipChannel).rgb;
	float3 st = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(0.0f, radiusSS), mipChannel).rgb;
	float halfRadiusSS = radiusSS * 0.5f;
	float3 srh = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;
	float3 slh = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;
	float3 sbh = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;
	float3 sth = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;

	float3 blendedColor = (sr + sl + sb + st + srh + slh + sbh + sth + sampleColor) / 9.0f;

	float visibility = visibilityBuffer.SampleLevel(sampTrilinearClamp, samplePos, mipChannel).r;
	float4 hizMinVis = hiZBuffer.GatherRed(sampTrilinearClamp, samplePos, ceil(mipChannel));
	float4 hizMaxVis = hiZBuffer.GatherGreen(sampTrilinearClamp, samplePos, ceil(mipChannel));
	
	float minz = linearizeDepth(min(min(hizMinVis.r, hizMinVis.g), min(hizMinVis.b, hizMinVis.a)));
	float maxz = linearizeDepth(max(max(hizMaxVis.r, hizMaxVis.g), max(hizMaxVis.b, hizMaxVis.a)));
	float depth = hiZBuffer.SampleLevel(sampPointClamp, samplePos, 0.0f).r;

	float3 rayEndVS = viewSpacePositionFromDepth(samplePos, depth);
	float distanceTraveled = length(rayEndVS - rayStartVS);
	// distance squared caused the effect to fade much to fast for larger objects in the distance
	float attenuation = distanceTraveled == 0.0f ? 1.0f : saturate(1.0f / (distanceTraveled));

	float weight = 1.0f;
	depth = linearizeDepth(depth);

	///////// the below is just to force the debugger to keep the variables around for inspection
	if(depth > 5000.0f && (maxz + minz) > 500002.0f)
	{
		return 100000.0f;
	}
	///////// delete the above once weighting is figured out

	return float4(blendedColor * visibility * attenuation * weight, visibility * attenuation * weight);
}

That looks to be a good solution for reflection of the Physical Based Rendering for metallic material.

I bought the book thinking i would get access to the source for this, damn it. Great work going on in here, Suggestion - Mixing this technique as is having to be reverse engineered anyway why not think about crossing with idea's from Deep G Buffers for fast screen space GI and AO http://graphics.cs.williams.edu/papers/DeepGBuffer14/ . Morgans paper is brilliant, he's also provided base code for the system. Screen space cone traced reflections mixed with Deep G Buffers would be a superb step in the right direction for near to offline results (as we can get right now anyway).

Guys take a look. Cheers

I bought the book thinking i would get access to the source for this, damn it. Great work going on in here, Suggestion - Mixing this technique as is having to be reverse engineered anyway why not think about crossing with idea's from Deep G Buffers for fast screen space GI and AO http://graphics.cs.williams.edu/papers/DeepGBuffer14/ . Morgans paper is brilliant, he's also provided base code for the system. Screen space cone traced reflections mixed with Deep G Buffers would be a superb step in the right direction for near to offline results (as we can get right now anyway).

Guys take a look. Cheers

Eh, that's a lot of extra scene complexity dependent work for not a lot of benefit. SSR already only looks good if you've a specific art direction, are blending it with cubemaps/other pre-computed reflections, or only use it for specific and controlled materials like a rough, partially reflective flat floor or puddles. Otherwise you get extreme temporal instability and it just ends up looking weird.

Great work on sussing out the details of this though! I'd been eyeing the book just for this article, but it's good to know that it wasn't terribly complete and B. that there's now this thread with a very well documented journey of work and code to look at anyway!

Hey 3DLuver and Frenetic Pony,

I've actually read through that paper a few times and while it's a good read, very detailed, and does create improved results, I ultimately landed on not pursuing it for the time being, mostly due to the reasons Frenetic Pony mentioned.

There are so many new and promising-looking GI techniques emerging that I'm hesitant to spend too much more time on screen-space techniques like this that will be better served by proper GI techniques. Cyril Crassin's Voxel Cone Tracing (a great paper in its own right, and where the book's technique gets a lot of its inspiration from) is gaining a good amount of popularity, and if you saw the NVIDIA 900 series release announcement (http://www.geforce.com/whats-new/articles/maxwell-architecture-gtx-980-970) you'll notice that they're touting this method becoming pertinent to running in real-time. If I've been following correctly, it seems like UE4 had it in their engine for a while (Elemental demo), but ultimately decided to pull it out for the release version and go with their Enlighten approach instead. I'm guessing this was due to the cost of building and maintaining the octree on every frame - a cost that rises with more dynamic elements per scene. With the higher horsepower of the next iteration of GPUs combined with current and upcoming lower overhead graphics APIs, it seems like those costs are finally becoming manageable. I've actually come across a YouTube video showing this technique working in Unity, also, so it's definitely gaining a lot of ground. The Tomorrow Children's 3D texture adaptation of GI (http://fumufumu.q-games.com/archives/2014_09.php#000934) is also looking really good, and as with anything else, it's great to have a competitor to help drive further research and development.

Frenetic Pony has a good point in that this SSLR technique, and really any SSLR technique, is not meant to stand entirely on its own. There needs to be a fallback for missed ray hits and edge cases. For my engine, I go with the one global cube map + several local cubemaps + SSLR approach and blend them together for my final result. After some code cleanup today, I'm going to post again later to show the sample weighting I'm currently going with that seems to give nice results and blends nicely depending on surface roughness and what data can actually be obtained from the ray tracing buffer. I'll discuss some of the current issues and ideas to address them, also.

It's great to have more people interested in helping work out the nuances of this technique. Please feel encouraged to share any of your thoughts, questions, or suggestions in helping to improve what we've already worked out!

Thanks,

WFP

As mentioned in my previous post, here is the way I've settled on handling weighting and blending the cone tracing results. For anyone following along, you'll notice that I decided not to bother with how much the cone sphere intersected the hi-z buffer. All the various combinations I tried with that approach gave bad results with a lot of artifacts. If someone else gets the weighting working properly and wants to share, I'd like to revisit it, but for the time being I'm content with what I have below. You'll notice I've brought in another new parameter to the function. The gloss is simply what you'd store in a gloss map (for me it's actually [1.0f - roughness], but you get the point). I simply use this as a further weighting on the output color. I noticed previously that even when I had a roughness of 1 (very rough), I was still getting more reflection than I would have expected, even at non-grazing angles. This new parameter helps clean that up nicely and fades more smoothly into your backup environment maps the rougher the surface becomes. I have it commented in the code, but it's worth noting that I leave gloss out of the alpha calculation. The more terms you add to that value, the further down you drive it for a particular iteration, and I've found that I get better results by just using the visibility and attenuation values. If you're implementing this effect, I encourage you to sub values in and out and see which works best for your needs.

One of the issues I would like to address in the future is the color buffer convolution stage. While the fading and blending helps this to an extent, you can easily get colors bleeding to areas where they shouldn't be due to the plain Gaussian blur used in creating this buffer's resources. In my setup, I also need to more properly blend this effect with the local cubemaps (most likely what I will tackle next).

As would be expected, this effect tends to work better the more depth information you have. For example, I've been posting images of the effect running in an outdoor scene with a few blocks and other programmer art, but largely an empty space. There are a lot of ray misses and some large depth discrepancies, and that results in some hard edge artifacts even with rougher surfaces. In the sponza scene with more "going on", I've found this technique to perform better aesthetically. I will post images of the same scene I've been using below, and later on after some more testing and polish I will try to get some out showing it running in sponza.

A great suggestion that Bruzer100 gave me was to not let the effect run to far. In other words even in the ray tracing steps you want to stop and return a non-hit after the ray has traveled a certain distance. I've made this into a constant buffer variable so I can update it per scene, and is the approach I would suggest be taken. This helps clean up a good amount of artifacts, and lets your fallback environment maps take over completely at known distances.


float4 coneSampleWeightedColor(float2 samplePos, float mipChannel, float3 rayStartVS, float radiusSS, float gloss)
{
	// sample center and take additional sample points around the cone perimeter and inside to better blend the color result
	float3 sampleColor = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos, mipChannel).rgb;
	float3 sr = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(radiusSS, 0.0f), mipChannel).rgb;
	float3 sl = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(radiusSS, 0.0f), mipChannel).rgb;
	float3 sb = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(0.0f, radiusSS), mipChannel).rgb;
	float3 st = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(0.0f, radiusSS), mipChannel).rgb;
	float halfRadiusSS = radiusSS * 0.5f;
	float3 srh = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;
	float3 slh = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;
	float3 sbh = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos + float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;
	float3 sth = colorBuffer.SampleLevel(sampTrilinearClamp, samplePos - float2(halfRadiusSS, halfRadiusSS), mipChannel).rgb;

	float3 blendedColor = (sr + sl + sb + st + srh + slh + sbh + sth + sampleColor) / 9.0f;

	float visibility = visibilityBuffer.SampleLevel(sampTrilinearClamp, samplePos, mipChannel).r;

	float depth = hiZBuffer.SampleLevel(sampPointClamp, samplePos, 0.0f).r;
	float3 rayEndVS = viewSpacePositionFromDepth(samplePos, depth);
	float distanceTraveled = length(rayEndVS - rayStartVS);
	// distance squared caused the effect to fade too fast for larger objects in the distance
	float attenuation = distanceTraveled == 0.0f ? 1.0f : saturate(1.0f / distanceTraveled);

	// try mix and matching what goes into the alpha component - don't want to drive it down too much
	return float4(blendedColor * visibility * attenuation * gloss, visibility * attenuation); // gloss intentionally left out of alpha
}

New screenshots:

https://www.dropbox.com/s/pze4pi5k9kb8d5f/screenshot_20.png?dl=0

https://www.dropbox.com/s/dfxravl5ghbkhia/screenshot_21.png?dl=0

https://www.dropbox.com/s/bbkiuzsycj6d8l3/screenshot_22.png?dl=0

https://www.dropbox.com/s/kciy1pxvfap1pgn/screenshot_23.png?dl=0

https://www.dropbox.com/s/2xg9amomoj25kyn/screenshot_24.png?dl=0

https://www.dropbox.com/s/2n1tisaqlt028k5/screenshot_25.png?dl=0

https://www.dropbox.com/s/ty4xdomw2ppkzbl/screenshot_26.png?dl=0

I am still very confused about how to weight each spheres during the cone tracing path. It certainly is the last piece of the puzzle this thread hasn't solved yet. I'm not clear on how well the method is supposed to handle glossy reflections at depth discontinuities. I was never able to get glossy reflections at the surface edges. WPF, have you had any more luck with cases like this?

ct1.jpg

Hi jgrenier,

Good question, and the answer is a little disappointing when it comes right down to it. There is certainly a reason you're not seeing smooth fading on the edges, and your intuition is on the right track. Here are two screenshots:

https://www.dropbox.com/s/p03vg5ab6fpye3t/screenshot_27.png?dl=0

https://www.dropbox.com/s/8bn8zxtitie4x3h/screenshot_28.png?dl=0

Notice that in the first, the inside of the reflection fades and blends appropriately, but there are hard cutoffs at the edges. Then notice in the second how the inside still blends nicely, but the edges now blend, too. The only difference in setup between those two images is that in the second, the column is pushed back against that wall (in fact, it's intersecting it a little). The unseen difference in the two is what the ray tracing step was able to pick up and use. In the first image, there are a lot of discrepancies between the where the column's reflection extends to and the spaces beside it, whether they be outright misses or just large enough differences in hit location that the resulting pixels don't blend well - I mentioned the latter in an earlier post as being one of the weaknesses of using a plain Gaussian blur and not taking scene information into account during the blur. In the second image, the wall provides better information to the ray tracing step and gives the cone tracing step the extra data it needs in order to properly blend the edges. Having the rays that hit the column be in close proximity to the rays on its side helps even out discrepancies and the result is what you'd expect.

Take a look at the blocks again (below). You'll notice they are nicely blurred in the middle, but have the same ugly hard edges. Then look at the second image below and it should become pretty clear why this is the case - there's simply no data for those pixels to use, so they have no data to sample from.

https://www.dropbox.com/s/jdqd6ed2urzoktg/screenshot_29.PNG?dl=0

https://www.dropbox.com/s/iz5qp08itk04nq7/screenshot_30.PNG?dl=0

So a good next step would be figuring out how to fix this and there are a few options I'm toying with a little. The most obvious (on paper - not necessarily implementation-wise) is to improve the ray traced buffer. I've tried out a few options, but so far most of them have introduced an unacceptable amount of new artifacts. I will test out a few new ideas I've had and if anything good comes of it I'll be sure to post an update.

If you look at the video with the green bars the author posted (http://www.yasinuludag.com/HiZScreenSpaceConeTracing/v2.mp4), you'll notice towards the end it becomes clear that those green bars are up against (or recessed into) the wall. My guess is that he was having this same issue and wanted to show a case where the effect excelled (plenty of depth information and ray hits).

The other funny thing I noticed about the video posted showing Samus (http://www.yasinuludag.com/HiZScreenSpaceConeTracing/v1.mp4) was that the video was running in 1024x1024 - reminds me a bit of an old issue we had to hammer through. ;)

Let me know if you have any more questions or if there's anything else I can help with!

Thanks,

WFP

This topic is closed to new replies.

Advertisement