Skip to content

Implement multi-bounce ambient occlusion approximation#115426

Open
KenzieMac130 wants to merge 10 commits intogodotengine:masterfrom
KenzieMac130:dev-multi-bounce-occlusion
Open

Implement multi-bounce ambient occlusion approximation#115426
KenzieMac130 wants to merge 10 commits intogodotengine:masterfrom
KenzieMac130:dev-multi-bounce-occlusion

Conversation

@KenzieMac130
Copy link
Copy Markdown

@KenzieMac130 KenzieMac130 commented Jan 27, 2026

This patch implements a multi-bounce ambient occlusion approximation described in this 2016 Siggraph presentation that has since become widely adopted as an industry standard bounce approximation across other engines.

This approximation is applied before integrating ambient occlusion into indirect lighting. The bounce approximation affects both real-time (single bounce SSAO) and baked ambient occlusion (most often single bounce baked textures) which only stores monochromatic occlusion data. Currently this monochromatic data is directly multiplied onto color lighting can lead to over-darkening and produce artistically unpleasant gradients of color.

By bringing back some albedo data weighted along the occlusion visibility the appearance of local global illumination can be created (bringing SSAO closer to SSIL) while also creating a more artistically pleasing ambient gradient on saturated surfaces. (where otherwise directly multiplying monochromatic information on color can create sickly-grey colors)

Disabled:
MBColorOff

Enabled:
MBColorOn

This was proposed in godotengine/godot-proposals#13928.

@Calinou
Copy link
Copy Markdown
Member

Calinou commented Feb 5, 2026

@passivestar Could you give this a try on some materials of choice if you have time? I wonder how it behaves on wood, marble, stone, grass, etc.

@KenzieMac130
Copy link
Copy Markdown
Author

KenzieMac130 commented Feb 5, 2026

@passivestar Could you give this a try on some materials of choice if you have time? I wonder how it behaves on wood, marble, stone, grass, etc.

It is most visible on materials in indirect light with deep ambient occlusion and bright/saturated materials. Stylized materials and characters should be the clearest. Most scenes the ambient occlusion just looks slightly less over-darkened. I can get some better screenshots, this was just my playground scene.

@KenzieMac130
Copy link
Copy Markdown
Author

KenzieMac130 commented Feb 6, 2026

Here is a comparison of different albedo values with baked ao maps.

Multi-bounce off
approxoff

Multi-bounce on
approxon

Path Tracing (Blender Cycles)
pathtraced

Notice how the brighter values are now no-longer over-darkened and saturated albedo values appear to have a slight "color bleed".

Copy link
Copy Markdown
Contributor

@Ansraer Ansraer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How exactly should this be handled when another technique (e.g. SDFGI, SSIL) already provides local albedo & lighting data?

Intuitively I would think this approximation should automatically disable itself, no?

@KenzieMac130
Copy link
Copy Markdown
Author

KenzieMac130 commented Feb 10, 2026

How exactly should this be handled when another technique (e.g. SDFGI, SSIL) already provides local albedo & lighting data?

Intuitively I would think this approximation should automatically disable itself, no?

Multi-bounce can exist fully alongside these techniques. It simply brings baked ambient occlusion maps and ssao closer to global illimunation by integrating curve fitting that approximates the appearance of GI onto single bounce ambient occlusion.

  • SDFGI operates at a coarser level than the baked lighting/ambient occlusion and still needs ambient occlusion for micro detail.
  • SSIL is also not influenced by this approximation in the way SSAO is. I have used SSIL alongside this approximation in my testing, making sure there are no adverse side-effects.
  • A properly weighted blending term that approximates local GI will always be needed so long as ambient occlusion is used. Using it in combination with proper GI techniques only makes occlusion more grounded into it.*

(*there will come a point where if the GI solution can capture every pore of occlusion that pre-baked AO/GI maps at any scale are unnecessary, but that is a distant future)

Properly utilizing GI techniques together would look like this:

  • SDFGI/Voxel GI/Reflection Probes/Lightmaps: Large Scale GI (eg. the wall of the building casting bounce light onto the sidewalk)
  • SSIL/SSAO+multibounce: Meduim Scale GI (eg. the corner of the building meeting the sidewalk and small assets)
  • Baked AO+multibounce: Fine Scale GI (the texture's pebbles on the sidewalk and recessed brick mortar in the shade from sky/bounce light)
  • Microshadows/Cavity maps: blocking out where no light should enter (the cracks in the materials)

@passivestar
Copy link
Copy Markdown
Contributor

@passivestar Could you give this a try on some materials of choice if you have time? I wonder how it behaves on wood, marble, stone, grass, etc.

I did some quick testing and it may not be easy to see the albedo color contribution in a realistic scene with realistic materials besides the overall AO intensity. I would need to set up a stylized scene with clean and bright albedo to make the color contribution obvious

That said, in principle I support adding this because even disregarding the color contribution, I did notice that godot SSAO looks a bit dark out of the box and has somewhat of a sharp blobby look to it, so I usually try to make it softer with power and intensity settings. Having AO that better approximates local GI out of the box should make it easier for people to get better visuals without having to mess with settings too much

@KenzieMac130
Copy link
Copy Markdown
Author

KenzieMac130 commented Feb 18, 2026

I wanted to specify since this has been a problem recently: No generative AI was used in the development of this PR.

@KenzieMac130 KenzieMac130 requested a review from Ansraer February 18, 2026 16:25
@maityyy
Copy link
Copy Markdown

maityyy commented Feb 20, 2026

Am I correct in understanding that PR takes ambient light regardless of AO implementation? We have GTAO PR #113304, for which multi-bounce approximation was originally developed. Sorry for the off-topic question, but I think it's worth clarifying, since GTAO has proven itself and is probably implemented everywhere

@KenzieMac130
Copy link
Copy Markdown
Author

Am I correct in understanding that PR takes ambient light regardless of AO implementation? We have GTAO PR #113304, for which multi-bounce approximation was originally developed. Sorry for the off-topic question, but I think it's worth clarifying, since GTAO has proven itself and is probably implemented everywhere

Correct. This implements the multi-bounce integration from the GTAO paper which is agnostic of AO source. Any source of single bounce ambient occlusion (which most realtime implementations and offline bakers produce) will benefit.

I have also seen a misconception that the profile curve is based on human skin but that is not the case. The curve fitting was based on monochromatic albedo values and is scaled against the surfaces albedo.

@KenzieMac130
Copy link
Copy Markdown
Author

KenzieMac130 commented Mar 4, 2026

Decided to include a new stylized grass comparison screenshot from my test bed.
Enabled:
Screenshot_20260303_231158
Disabled:
Screenshot_20260303_231214

@BastiaanOlij
Copy link
Copy Markdown
Contributor

Rendering meeting: Kenzie gave a presentation on the how and why. All made good sense.
There was a discussion on whether this should be enabled by default, where my feedback was it should be enabled by default for new projects only, and remain disabled for existing projects.

Copy link
Copy Markdown
Contributor

@BastiaanOlij BastiaanOlij left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My nitpick is just that, probably not worth changing but I feel enabling syntax is better than disabling syntax (even though we use both in equal measures already).

Other than that, code looks good, and from the explanation of what this does, what's behind it, and that other engines have implemented this as well, it all makes sense to me that we adopt this.

<member name="rendering/lights_and_shadows/directional_shadow/soft_shadow_filter_quality.mobile" type="int" setter="" getter="" default="0">
Lower-end override for [member rendering/lights_and_shadows/directional_shadow/soft_shadow_filter_quality] on mobile devices, due to performance concerns or driver support.
</member>
<member name="rendering/lights_and_shadows/multi_bounce_occlusion/enabled" type="bool" setter="" getter="" default="true">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as discussed in the meeting, probably better to default this to false, but enable it in for new projects.
Existing projects become opt-in so people don't suddenly get an unexpected visual change.
New projects become opt-out, most devs will likely just accept the changed look.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure of how to make this option false for old projects but true for new ones. Would appreciate any pointers to where/how this has been handled in the past.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Calinou I believe you are of the opinion that it should be enabled going forward regardless of legacy project status?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Calinou I believe you are of the opinion that it should be enabled going forward regardless of legacy project status?

Yes, same as microshadows: #112449 (comment)

Comment thread doc/classes/BaseMaterial3D.xml Outdated
#endif // USE_LIGHTMAP_CAPTURE
#endif // !DISABLE_LIGHTMAP

#ifndef MULTI_BOUNCE_OCCLUSION_DISABLED
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pure nitpick, but my gut says:

Suggested change
#ifndef MULTI_BOUNCE_OCCLUSION_DISABLED
#ifdef MULTI_BOUNCE_OCCLUSION_ENABLED

Makes more sense?

Copy link
Copy Markdown
Author

@KenzieMac130 KenzieMac130 Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This was done to match #ifndef SPECULAR_OCCLUSION_DISABLED below it which also feels a bit off. Just a byproduct of some "When in Rome" programming.

Copy link
Copy Markdown
Author

@KenzieMac130 KenzieMac130 Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this front I would need to push this inversion of code a bit further up the pipeline and do some more significant logic changes in order to make this change. Currently in godot's shader code there is already a mixed usage of negatives, positives, and double negatives which feels somewhat inconsistent and needs a future unification task. So if there are no strong objections I might just leave it as is for now.

@clayjohn clayjohn requested review from clayjohn and removed request for Ansraer March 26, 2026 06:37
youfch added a commit to youfch/godot that referenced this pull request Mar 26, 2026
@KenzieMac130
Copy link
Copy Markdown
Author

My nitpick is just that, probably not worth changing but I feel enabling syntax is better than disabling syntax (even though we use both in equal measures already).

Agreed, negatives for features are a bit iffy. This was just something I adopted to stay consistent with specular occlusion (which is right next to this in material controls and looked out of place if I inverted it)

Copy link
Copy Markdown
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally, it works as expected. Code looks good to me.

Testing project: test_ssao_multibounce.zip

For benchmarking: test_ssao_multibounce.zip

Preview

The default SSAO intensity is 2.0. By increasing it to a value around 3.0, you can get a more similar result to multi-bounce disabled on brighter materials, but this will make it stronger than before on darker materials.

No multi-bounce With multi-bounce  With multi-bounce + intensity 3.0
Image Image Image
No multi-bounce With multi-bounce  With multi-bounce + intensity 3.0
Image Image Image
No multi-bounce With multi-bounce  With multi-bounce + intensity 3.0
Image Image Image

Benchmark

Using the second testing project linked above, run with a release export template (production=yes lto=full). All the viewport's pixels are covered by opaque geometry and other rendering features are turned down to the minimum, so that we can measure and isolate material shading cost as much as possible.

Differences might be subtle, but all scenarios are GPU-limited, and I've made sure the difference is reproducible across multiple runs.

PC specifications
  • CPU: AMD Ryzen 9 9950X3D
  • GPU: NVIDIA GeForce RTX 5090
  • RAM: 64 GB (2×32 GB DDR5-6000 CL30)
  • SSD: Solidigm P44 Pro 2 TB
  • OS: Linux (Fedora 43)

3840×2160 fullscreen

Before After
2186 FPS (0.46 mspf) 2166 FPS (0.46 mspf)

7680×4320 fullscreen

Before After
769 FPS (1.30 mspf) 760 FPS (1.32 mspf)

Copy link
Copy Markdown
Member

@blueskythlikesclouds blueskythlikesclouds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me! Bastiaan's feedback about only enabling by default for new projects should be addressed, though.

Co-authored-by: Hugo Locurcio <hugo.locurcio@hugo.pro>
@KenzieMac130 KenzieMac130 requested a review from a team as a code owner April 20, 2026 18:32
@KenzieMac130
Copy link
Copy Markdown
Author

Looks great to me! Bastiaan's feedback about only enabling by default for new projects should be addressed, though.

(There is currently a bit of ongoing disagreement on this issue that I am waiting for a conclusion on before implementing it)

@KenzieMac130
Copy link
Copy Markdown
Author

KenzieMac130 commented Apr 20, 2026

Tested locally, it works as expected. Code looks good to me.
...

Thank you for the benchmarks and feedback.

On the subject of bright materials an abledo value approaching 0.9 is basically snow, using 1.0 would be creating a lab made super white surface. So the bounce lighting drowing out darkness closely mimicks ground truth. There is a common instinct among users to use 1.0 as just white plastic or concrete but a better PBR value for that kind of surface is actually in the 0.6-0.8 range and balancing the lighting or adjusting exposure.

But for non-PBR aware or NPR games adjusting the AO intensity is an acceptable compromise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants