Skip to content

Enable MSAA with OIT - #25345

Open
cormacrelf wants to merge 4 commits into
bevyengine:mainfrom
cormacrelf:oit-msaa
Open

Enable MSAA with OIT#25345
cormacrelf wants to merge 4 commits into
bevyengine:mainfrom
cormacrelf:oit-msaa

Conversation

@cormacrelf

@cormacrelf cormacrelf commented Aug 10, 2026

Copy link
Copy Markdown

Objective

Enable MSAA in combination with Order Independent Transparency. Currently bevy panics if you enable both because OIT does not take MSAA into account.

Solution

Keep the single linked list per output pixel, but add a bitmask to each node. We scale up the resolver to the MSAA sample rate, and when resolving and walking the list, we skip nodes whose triangle didn't cover that sample point and keep walking. With depth prepass, we also mask off any fully occluded samples in oit_draw so the resolver never finds any nodes for those samples.

Testing

  • The OIT example runs with all combinations of settings, including new msaa setting

  • Tested with a backport to bevy 17 in a fairly involved application with a lot of transparency. Native linux and WebGPU.

  • Ultimately left it disabled because we have so much transparency it overflows too much. Hopefully the newer linked list version lets us use it when we upgrade.

  • Needs a bit of attention to the depth prepass probably, I don't understand the depth pass that well and in bevy 17 I couldn't enable depth prepass without lots of artifacts (it was disabled before). That may have been our custom shader behaving badly though.

  • Will need release notes and migration guide for the breaking API change to oit_draw. If you like it I'll write em. Something like this

 @fragment
 fn fragment(
     in: VertexOutput,
     ...
+    @if(MATERIAL_OIT_ENABLED)
+        @builtin(sample_mask) sample_mask: u32,
 ) -> FragmentOutput {
     // ...
 @if(MATERIAL_OIT_ENABLED) {
-    oit_draw(in.position, color);
+    oit_draw(in.position, color, sample_mask);
     discard;
 }
     ...
 }

Showcase

Naive approach gives you... image

And the fixed version with MSAA all the way through the OIT shaders:

oit_msaa4_demo

Remove the check_msaa system that panicked when an OIT camera had
MSAA enabled, and make the OIT resolve pass MSAA-compatible. This
is really basic support and has artifacts on triangle edges.
With MSAA, the fragment shader runs once per triangle covering any
sample of a pixel, so pixels on shared mesh edges get one OIT fragment
per adjacent triangle. The OIT buffers store fragments per
pixel, not per sample, so both invocations blended at full coverage,
double-blending the surface along every interior triangle edge and
drawing a visible wireframe pattern on smooth meshes. Or something like
that.

This fixes it, with an API change to oit_draw.
oit_draw now always takes the @Builtin(sample_mask) fragment input.

Each OitFragmentNode stores the sample_mask. Node size increases from 12
-> 16 bytes, +33% memory use on the nodes buffer. The resolve shader takes
@Builtin(sample_index), which forces it to run at sample rate. Each
sample walks the pixel's list compositing only fragments that cover it,
and hardware MSAA averages the results.

Adjacent triangles now composite exactly at any alpha with antialiasing.
As long as you aren't stacking too many transparent triangles to be
sorted correctly. I only tested this with a backport to bevy 17, and
overflowing the sort brings about some kind of mismatch in the order
between two samples which can appear as visible mesh edges again.
Nevertheless this works a lot of the time.

The cost is MSAA 4 -> ~4x resolve pass invocations, which are expensive.
Takes up more lanes. It's up to the user. We could make it zero cost
for Msaa::Off by compiling in a different OitFragmentNode type.
With a depth prepass provided, oit_draw tested prepass_depth at sample 0
and culled the whole fragment on failure. That's obviously no good under
MSAA. A fragment might be occluded at only some of the samples. We can describe
this with a reduced sample mask, so we test each sample for prepass
depth and clear the bits that are occluded.
@github-actions

Copy link
Copy Markdown
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨

color: u32,
depth_alpha: u32,
next: u32,
sample_mask: u32,

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.

sample_mask should be gated behind @if to avoid wasting memory when MSAA is off.

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 have actually done this, it's just a messy change and wasn't sure. I'll push it too

@kristoff3r kristoff3r added A-Rendering Drawing game state to the screen D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 10, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

3 participants