Setup Guide & Best Practices
This is an in-depth guide that covers setting up SSGI as well as different tips and tricks and best practices. It is assumed that you've already went through the quick start manual and imported the asset into your project without issues. If you encountered any errors by this point, it makes sense to first check the Known Issues section and make a bug report (if you think it's a bug indeed) before continuing with this guide. It's also assumed that you haven't altered any settings of the asset yet, so if you did - please, make sure to revert them to their default state. For instance, this guide assumes that the fallback is OFF until a certain point, so if you enabled it, you results may differ from what is described in the following steps.
It's also strongly recommended to disable any 3rd party assets / custom passes / renderer features before proceeding to ensure that nothing interferes with SSGI rendering.
Preparing a scene
Picking a scene
The best scene for testing would be the one where indirect lighting has visible impact. Semi-enclosed spaces (like a Sponza Palace) work well, while outdoor scenes are usually bad candidates. Ideally, you would want a scene that you have previously seen with some kind of GI, because then you would know where to look and to which details attention should be paid. Otherwise, consider making a quick (low quality is enough here) APV bake to get a rough reference - it would also come in handy for setting up the Fallback later.
Configuring scene graphics
Make sure to disable the following settings:
- Any kind of AA, especially TAA
- Any kind of Dynamic Resolution, Upscalers, Render Scale.
- Ambient Occlusion
- Motion Blur
- Bloom
These effects make it harder to properly evaluate SSGI settings, either masking certain issues, or, on the contrary, causing them.
Removing reflections
Diffuse and specular (reflections) lighting are separate systems in Unity and use different sources of data. HTrace SSGI provides only diffuse lighting and for the purposes of this guide we want reflections disabled, so they wouldn't get in a way of setting up and previewing our GI.
In HDRP: add Indirect Lighting Controller override to a volume in your scene and set both Reflections Multipliers to 0.
In URP: manually disable all reflection probes in your scene (if you have any) and set Indirect Multiplier in the Environment tab to 0.
Debugging scene content
The first thing you should do in a fresh scene is to go through the main Debug Buffers in HTrace SSGI. Most attention should be paid to Depth and Diffuse debug views. If you don't see some objects in the Depth view - they will not be able to either cast GI or receive it. Objects in the Diffuse buffer should have correct diffuse color. The most popular reasons for objects being absent in these views is because they are rendered on transparent queue (which is okay, we don't want GI for these) or because they use a custom shader that is either incorrectly configured or has some passes missing (e.g. depth prepass).
Configuring resolution
HTrace SSGI operates on screen pixels. It also has a denoiser, that not only is executed on each pixel, but also taps into neighboring pixels, especially during spatial passes. Which means the total number of pixels in you frame matters. The following screenshots show SSGI with exact same settings but rendered at different resolutions.
As you can see, the lower the resolution is, the harder it is for the effect to produce clean output. Therefore, it's very important to conduct all testing in the appropriate resolution. That is, the resolution in which you expect your audience to experience your application or the resolution in which you test your builds and preview your project. For this example, let's imagine that it is 1920 x 1080. In this case
- You shouldn't do your testing in the Scene View, because its resolution can't be directly controlled and instead it depends on your window layout, so it's very unlikely that this window covers 1920 x 1080 pixels off your monitor (if the monitor itself is FullHD then it definitely doesn't). On top of that, Unity doesn't write per-object motion vectors in scene view. So the results you will get in this window will be worse than in build.
- Testing in game view with fixed resolution is also not ideal, unless, again it doesn't cover the 1920 x 1080 area of your monitor. This can be easily checked, though, by lookin at Render Scale slider at the top of your screen. If it's more than 1 - then you're effectively super sampling, by rendering more pixels than is actually displayed and combining them into the final output which can make an image look better than it actually will be in your build.
This leaves us with 2 valid options:
- Set Game view resolution to 1920 x 1080 and then maximize it. If the Render Scale it close to 1 in this mode - you've hit the right spot
- Set Game view resolution to Free and then maximize it. Open the stats window on the right. If the resolution you see there is close to 1920 x 1080 - you're in good spot as well.
In a nutshell, you're looking for both the Render Scale close to 1 and resolution close to 1920 x 1080. This combination will ensure that you render and see (!) as many pixels as you would in your actual build. And remember that 1920 x 1080 is just an example, so use your actual target resolution instead if this number.
Direct lighting
What is direct lighting
In the case with SSGI direct lighting is each pixel directly illuminated by any light source, pixels belonging to the sky box and pixels belonging to emissive objects. It's important to keep in mind that screen-space algorithms don't know anything about your actual lights (e.g. their position in the scene) nor anything about the structure of your scene in general (e.g. meshes, prefabs, actual polygons, etc), they operate directly on the pixels of your frame and get data only from other pixels. Therefore, these directly lit pixels is the only source of lighting available for SSGI. Obviously, when you move your camera and look at different parts of your scene some content (objects, areas of direct lighting) leaves the frame and another one enters. So, for example, when you turn away from a directly lit wall - neither the wall itself, nor its lit part no longer exist for SSGI and can't produce light bounce. This is the main limitation of screen-space algorithms, and it should be always taken into account.
Removing all non-direct lighting
Before we move to indirect lighting produced by HTrace SSGI, we should take care of the native indirect lighting provided by Unity. This is handled very differently, depending on the pipeline.
- In HDRP, as soon as SSGI (either Unity's or HTrace SSGI) is enabled, all other types of indirect lighting is automatically disabled, and the pipeline expects all GI to now be deliver by SSGI or through SSGI. So, for example, if you had an APV baked - it's output on objects would be disabled, and instead it would be expected to come in the farm of a Fallback through SSGI (if configured so).
- In URP it's not possible to automatically disable native indirect lighting. So if we were to naively inject our SSGI output into the URP frame we would just add it on top of the existing indirect lighting, not instead of it, so we would end up with double-GI which is obviously incorrect. Fortunately, GI in URP comes only in 2 forms: APV and Sky Ambient (not counting the lightmaps, because if you can afford lightmapping, you wouldn't be reading this text most likely). We can reproduce each type of GI on our side and then subtract it from the frame, which leaves us only with direct lighting that we want. So, if, for example you had an APV baked - we reproduce its output on our side and then subtract it from URP's Color Buffer (for opaque objects only), virtually removing its contribution to the frame. You can then re-add it through the Fallback option in our SSGI - the same way it's done in HDRP. However, this way, it would be properly combined with whatever we managed to gather from screen-space lighting, not just overlayed on top of it. This replacement of the native indirect lighting is activated by the "Ambient Override" checkbox which is enabled by default and is strongly recommended to be kept this way, unless you know what you are doing.
To preview the direct lighting that we now have, we can use the Direct Lighting debug option. If the native indirect lighting was removed sucessfully and if you disabled all reflections as was recommended earlier, you should see an output similar to the screenshot above, where all non-directly lit pixels are purely black.
Adjusting direct lighting
The most crucial thing to understand is that the brightness of indirect lighting is very dependent on the brightness of direct lighting. They are not decoupled as in the case with, say, ambient sky lighting which can be controlled separately and doesn't care what your direct lighting looks like. Actual, traced (be it in screen-space or otherwise) GI will always be dependent on the emission intensity of your light sources. Indirect lighting looses energy (brightness) with each bounce, depending on the angle of bounce (steeper angles reduce energy more) and on the albedo color of your materials (darker color absorb more energy) - this is called energy conservation and the lost energy is dissipated to heat in real life. Obviously, we are not simulating actual optical and physical laws here, but we also can't ignore them completely and make the GI look as bright as we want without accounting for the source (direct lighting), otherwise this would look more like a gaussian blur, rather than GI - a simple averaging of all color in a given area.
So, the best way to affect the intensity of GI is through adjusting direct lighting. Lighting intensities are handled very differently between the two pipelines: HDRP uses real-world intensities, so you can use values and measurements from real-life as references, as well as official HDRP guides, cheetsheets and documentation to set up your direct lighting. URP, on the other hand, uses arbitrary intensity values, which are usually much lower than in HDRP, but the idea remains similar: if GI is not bright enough - try increasing the intensity of your lights. Both pipelines also offer exposure controls to manage the final look of your frame - don't neglect them, as the right combination of light intensity and exposure is key to a balanced frame with good looking global illumination.
Clamping direct lighting brightness
Now count all directly lit (non-black) pixels.
Okay, jokes aside, the number of directly lit pixels matters too. As mentioned earlier, we can construct GI in screen space only as long as there's some direct lighting in it. But can we do it if there's only one (or even a handful) directly lit pixels ? No, not really. At least not in the way that would provide a stable and pleasant image. The thing is, each pixels doesn't check for all other pixels to find direct lighting. This would be insanely costly (much more so than RTX stuff), because even in FullHD frame there's over 2 millions pixels and each would have to check the other 2 millions of its neighbors. We also need to detect occluders along the way (more on that later), so in reality we shoot rays from each pixel in different random directions, and hope that these rays stumble across lit pixels. The chance of finding this single lit pixel this way, as you can imagine, is extremely low and even if we had found it, it would be a statistical outlier, rather than a reliable source of lighting. Such outliers produce what we call "fireflies" - isolated bright spots, which then are turned into boiling noise by the spatial denoiser and can significantly reduce the quality and stability of the image. This happens when the following 2 conditions are met:
- There's insufficient number of directly lit pixels
- These few directly lit pixels that we have are excessively bright
We can't do anything about the number of directly lit pixels - we must to work with what we have on screen, but we can affect their brightness, clamping it to a level that wouldn't result in visible fireflies. It's important to understand that aggressive clamping reduces the brightness of the final GI output and also makes it a bit "flat" looking because the lighting becomes more uniform - which is good for denoising, but may be not ideal for artistic purposes, so there's a tradeoff. Clamping can be done in two ways:
1) Manual. We set an arbitrary value for max allowed brighntess and hope that it works in most cases. This is a straigtforward way, and many systems use it as primary method. For example, Unity's SSGI has a hardcoded clamping value of 7. RTGI exposes this value in UI letting the end user to adjust it. However, the obvious problem with a screen-space algorithm arises from its nature - the number of directly lit pixels may (and most likely will !) vary from frame to frame and is not guaranteed to be contained within some predictable range. For instance, take a look at the following screenshots
The number of directly lit pixels between them differs in orders of magnitudes. So, if we set a clamping value of, say, 10 - it can be not enough for cases where the number of directly lit pixels dip into very low numbers, or, on the contrary, too conservative in the cases when there's plenty of direct lighting and there's no danger of fireflies and instability. And as mentioned earlier, the brightness of these pixels matters too, so if we change the intensity of a light source the pixels it illuminates may become dim enough to stop posing a firefly threat, but the manual clamping threshold can't adjust to it.
2) So we have another method: Automatic. We go over all our pixels (once and in parallel, which is very fast) and actually count directly lit ones and record their brightness. Then we average the result and after a few more math operations we have a clamping value that is actually based on the content of the current frame, instead of an arbitrary number. So now we can adjust the clamping threshold automatically and dynamically by running statistic evaluating of each frame. This way, if you have a handful of directly lit pixels, the threshold will go down, preventing them to spawn fireflies, but if we have enough - it will go up safely allowing GI to get as bright as it can. Furthermore, automatic mode can also be biased towards brighter results via the Max Deviation parameter, so you still have the final say when using it.
The second method, although being convenient, adds another layer of screen-space dependency. Not only we are bound by the visible content, but now we are also dynamically changing the brighntess of our GI based on it. Arguably, this is still better, because outputting dimmer but more stable GI can me more preferable than having bright GI but with boiling artifacts all over the place. But in any case, this is up to the end user to choose between the two modes.
Another important thing to remember is that due to different HDRP and URP direct light intensities (covered in the previous section), the Manual method should be adjusted accordingly. For example, the previously mentioned value of 7 used by Unity's SSGI makes sense only in the HDRP environment, whereas in URP this would be a very high threshold in most cases and values around 1-2 are expected instead, unless, of course, your lights actually have such high intensities.
You also may have noticed another checkbox called "Firefly Suppression". This also fights with fireflies, but in a different way. Instead of clamping the source of lighting (directly lit pixels) - it clamps the GI itself, after the rays were traced. It doesn't operate on the full frame statistics and evaluates only a small region around each pixel, adjusting it locally. This is useful as a secondary method to remove all residual fireflies and it works alongside the Brightness Clamp, complementing it.
Indirect Lighting Intensity
Now let's see what are other ways to control indirect lighting intensity and sum up everything that is responsible for it. Some parameters in further sections will also have an affect on the brightness (more as a side effect), however, they also impact performance and therefore are not purely artistic controls, so we shouldn't rely on them as a tools to control brightness.
As we previously discussed, raising the intensity of direct lighting is the best way to boost GI. However, there's another important parameter that can be changed - the diffuse component of our materials. The brighter it is - the less bounced energy it consumes, allowing GI to remain bright. It's not unusual to underestimate this aspect and make materials darker than they should be, especially when the project is being worked on without a proper GI solution. Unfortunately, we can't boost albedo just for SSGI alone, as it can be done for baked lighting via the Albedo Boost parameter, because SSGI gathers lighting from the final color buffer - the same one that you see on the screen. Because of this, editing all diffuse values across all materials can be unfeasible, so this option, while being the most physically-accurate is not the most practical.
We can also boost indirect lighting intensity directly:
- In URP this can be done in HTrace UI with the Intensity slider.
- In HDRP the native Indirect Diffuse Lighting Multiplier in the Indirect Lighting Controller override should be used.
This way of controlling indirect intensity is always a low-hanging fruit, but, unfortunatelly this is the most physically incorrect way of doing it and breaks light transport logic: energy is not reduced with each bounced, as it should be, but increased, as if additional lighting is drawn from thin air. This should be used with care, especially in HDRP, where increasing this multipleir too much can make lighting infinitely bright through the temporal loop, resulting in an extreme overexposure of the frame. GI boosted through these means can also loose its contrast against direct lighting, making the frame look more flat.
So, we have three main knobs to control the overall brightness of GI, each with its pros and cons:
- Direct Lighting Intensity & Material Diffuse Intensity - most physically accurate, but not always feasible in production, requires changes that not always alight with artistic visiton.
- Brightness Clamping - GI brighntess is increased at the cost of denoising quality: too high values lead to temporal instability and boiling noise
- Indirect Lighting Intensity - the easisest and most straightforward way to control GI brightness, but the least physically correct. If used aggressively, can erase the difference between direct and indirect lighting, producing a flat image, or even trigger an uncotrollable overexposure.
You can use each of them or a combination of any, but the main point is to understand the tradeoffs of each approach.
Tracing in Screen Space
How rays gather lighting
We established that to construct good indirect lighting, we need directly lit pixels (and only them) in our frame and preferably not too few. But can all directly lit pixels equally cast GI onto all other pixels? No. And to establish what lighting can affect each given pixel we have to trace rays from this pixel and see what they find. There are two conditions which a ray should meet to succeffuly gather ligting and bring it back to the pixel it originated from:
- It should find a hit within the frame
- The normal of hit surface should pint toward the normal of the surface from which the ray originated.
Finding a hit means that there's an intersection between a surface visible on screen along the direction of a ray. A ray can fail to find an intersection if it goes behind an object and never comes out again until it reaches the edge of the frame or when it reaches the edge of the frame without meeting any intersection along the way at all. Both cases count as a miss and a Fallback (if active) is used as a source of lighting instead.
The normal condition can be described with this illustration
If the pixels (the origin and then hit) are facing each other, then a light transfer is possible between them, if they face away from each other - it is not. However, since we are already restricted by screen space data in our case, we can relax this condition a bit, to allow more lighting to be gathered if we are rady to sacrificy plausibility of light transfer. This is controlled with the Backface lighting parameter.