Fixing Reflections in realism-effects SSGI: Ground is brighter when looking up
This post walks through our debugging process, what we learned about screen-space global illumination (SSGI), and how we resolved the issue by modifying shader behavior.
Understanding realism-effects and SSGI
The realism-effects is a powerful and modular set of post-processing effects for three.js, authored by 0beqz. It includes implementations for:
- Screen-Space Global Illumination (SSGI)
- Ray-Traced Screen-Space Reflections (SSR)
- Temporal accumulation and filtering
- Screen-space indirect lighting and ambient occlusion
SSGI is a technique that approximates indirect diffuse lighting by tracing rays in screen space. When a ray hits a surface, the shader samples lighting information from other screen pixels. If a ray misses—that is, it exits the current view—the shader typically falls back to an environment map to estimate indirect lighting.
This fallback is useful but has limitations: it's only an approximation, and more importantly, it can introduce lighting that doesn't actually exist in the scene.
The Problem: Unnatural Ground Brightness
When tilting the camera up slightly—enough that screen-space rays traced from the ground begin to miss—we observed the entire ground surface becoming significantly brighter. This behavior broke the illusion of realistic lighting and caused the materials to look flat and reflective beyond what was physically plausible. As it can be seen from the screenshots below; less the roughness more the reflection is inaccurate.
We confirmed that our material setup was not the source of the issue by testing it with the Blender. Our PBR settings (metalness, roughness, albedo, normals) were correct and consistent with other assets in the scene. The issue pointed back to the rendering pipeline—specifically the SSGI shader.
Exploring and Debugging the Shader
To pinpoint the issue, we began to observe the shader. After fiddling with the code, we have found the following code fragment, that was responsible from approximating out-of-screen rays. It turned out that it was benefiting from environment map to get the approximate colors to determine off screen reflections.
if (isMissedRay && !allowMissedRays)
return getEnvColor(l, worldPos, roughness, isDiffuseSample, isEnvSample);
Firs thing we did was to remove environment map. This also fixed our issue of not achieving pitch black in non-lighted scenes. Before that we always had a dim light in scenes, even without ambient light.
if (isMissedRay && !allowMissedRays)
// Return magenta color to emphasize real on-screen reflections
vec3(1.0, 0.0, 1.0);
This made the problem clear: when rays missed and the environment map was sampled, the shader was injecting light based on a global cube map. Since our cube map had a bright, gray ceiling, the shader "guessed" that this light should be reflected onto the ground—creating a misleadingly bright result. Then we decided not to return any color at all. That only allowed on screen parts to reflect on surfaces, which was satisfying enough.
if (isMissedRay && !allowMissedRays)
// Return black, since no reflections are needed
vec3(0.0);This way, the SSGI rendering of the kitchen was good enough to be considered realistic.
Root Cause: Environment Map Leakage
The issue was ultimately related to how missed rays are handled in screen-space lighting. By default, realism-effects falls back to sampling an environment map when rays exit the screen. This is standard practice, but it assumes that the environment map is a valid representation of the scene.
In our case, it wasn’t. We used a single environment map for the entire scene, but our architecture consisted of multiple rooms with different lighting conditions. Using one global map meant that surfaces in darker areas were reflecting an environment that didn’t apply to them—specifically the bright ceiling of our HDRI skybox.
This was a design oversight on our part. For realism, we should have prepared environment maps per room or dynamically generated localized probes—but we hadn’t done so yet.
The Fix: Force Missed Rays to Return Black
As a quick and controlled fix, we modified the shader to treat missed rays as black instead of sampling the environment map. This suppresses light injection from outside the screen, avoiding color leaks.
This simple change ensured that if the shader couldn’t confirm a light source via ray intersection, it wouldn’t assume one. The result is that only real, screen-visible lighting contributes to the indirect illumination calculation. The scene now behaves much more consistently across view angles.
Results and Visual Improvement
After implementing this change, ground materials no longer reflect false lighting from non-visible environment regions. Reflections feel more grounded, indirect bounce light is subtle and believable, and materials retain their depth and texture detail.
We also took this opportunity to integrate TypeScript support and refactor parts of the realism-effects internals for better type safety and composability within our engine.
Further Improvements
While overriding missed rays with black solved our immediate problem, we acknowledge this is a simplification. Future improvements could include:
- Generating local environment maps or reflection probes per room or zone
- Switching to real-time reflection probes or voxel-based GI for more accurate fallback sampling
- Conditionally adjusting fallback behavior based on scene context (e.g., indoor vs. outdoor)
However, these approaches come with complexity, performance trade-offs, and memory cost. For now, our current solution provides a clean, artifact-free rendering result without compromising frame rate or stability.
Conclusion
This issue was subtle and visually dependent on camera movement, but it reflected a broader lesson: fallback logic in rendering pipelines must be tightly aligned with scene context. Our use of a global environment map introduced misleading light, and while that was a mistake in content preparation, the shader made the assumption worse by trusting it implicitly.
By taking ownership of the shader's behavior and enforcing a conservative response to ray misses, we restored control over our visual fidelity. The result is a more predictable, visually consistent experience that doesn't overpromise light where none should exist.
We’ll continue evolving our rendering stack with more robust environment strategies, but for now, this fix allows us to move forward with confidence.