Skip to content

Conversation

@mrdoob
Copy link
Owner

@mrdoob mrdoob commented Dec 2, 2025

Related issue: #32433

Description

  • Moved premultiplied alpha logic into opaque_fragment.glsl.js, pre-multiplying RGB by alpha before tone mapping
  • Removed premultiplied_alpha_fragment.glsl.js (was causing double-multiplication)

Motivation

Pre-multiplying in the shader preserves HDR information when rendering to LDR framebuffers:

Light Clamp Blend Result
Before (hardware) 5.0 1.0 × 0.1 0.1
After (shader) 5.0 × 0.1 0.5 0.5

@mrdoob mrdoob added this to the r182 milestone Dec 2, 2025
@github-actions
Copy link

github-actions bot commented Dec 2, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 350.43
83.06
350.1
83.03
-334 B
-33 B
WebGPU 616.03
171
616.03
171
+0 B
+0 B
WebGPU Nodes 614.64
170.74
614.64
170.74
+0 B
+0 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 482.47
117.86
482.14
117.83
-334 B
-34 B
WebGPU 687.42
186.76
687.42
186.76
+0 B
+0 B
WebGPU Nodes 637.26
173.94
637.26
173.94
+0 B
+0 B

@mrdoob
Copy link
Owner Author

mrdoob commented Dec 2, 2025

@gkjohnson

Interestingly, the only example that gets affected by this is the ldraw one:

Before After
Screenshot 2025-12-02 at 08 50 21 Screenshot 2025-12-02 at 08 50 26

@mrdoob
Copy link
Owner Author

mrdoob commented Dec 2, 2025

Oh no, this triggers some weird behaviour in SwiftShader...

Screen.Recording.2025-12-02.at.09.05.00.mov

@gkjohnson
Copy link
Collaborator

gkjohnson commented Dec 2, 2025

This ordering is right to fix the original issue but it does change the color space in which alpha is applied which can have an impact on the final blend color. This is a quick demo of a sphere with sRGB color 0x660000, 0.25 opacity, transparent=true, on a white background. The left is "premultipliedAlpha=true" and the right is "premultipliedAlpha=false". You'll see that the premultiplied alpha sphere is a richer red since the alpha is applied in linear-sRGB color space rather than sRGB, which is what the graphics API will do:

Before

image

After

image

Here's the same demo with a <div> colored the same way. You can see that it also performs blending in sRGB color space which does not match the new premultiplied alpha coloring, though on it's own I think the color actually looks bit nicer with the change and may be more "correct" by some interpretations. Though it's wrong if you're trying to match CSS colors:

image

For other context there have been some other forum posts in the past about this same phenomenon when using effect composer since it uses linear color targets (see here or here). Not exactly sure what the best thing to do here is but I'll let other's chime in. Applying tone mapping as a post process would avoid all of this but I know there are performance implications.

Interestingly, the only example that gets affected by this is the ldraw one:

I'll take a look at what might be going on here later.

Oh no, this triggers some weird behaviour in SwiftShader...

😬 This I can't help with, unfortunately 😅 But that's super odd

edit: It actually looks like there are a number of references to "premultiplied_alpha_fragment" still in the examples folder that would cause a number of examples to break, including the "ldraw" and "fat lines" ones

@gkjohnson
Copy link
Collaborator

gkjohnson commented Dec 2, 2025

Interestingly, the only example that gets affected by this is the ldraw one:

This is probably because the demo has tonemapping enabled and LDrawLoader implicitly sets "premultipliedAlpha" to true. Perhaps that line should be removed to align with other loaders - but all the same here's some testing to get a sense for what's happening:

dev this pr
toneMapping=NONE premultipliedAlpha=true image image
toneMapping=NONE premultipliedAlpha=false image image
toneMapping=ACES premultipliedAlpha=true image image
toneMapping=ACES premultipliedAlpha=false image image

All of the opacity values are set to ~0.5. From the change in the first row it seems like this is primarily due the blended color value having alpha applied a new color space. And since premultipliedAlpha was already enabled it's now taking effect since tone mapping is also enabled in the demo.

The colors picked for LDraw palette were likely determined based on the blending results in sRGB color space rather than linear - and definitely not originally selected for use with PBR materials or transparency.

@mrdoob
Copy link
Owner Author

mrdoob commented Dec 2, 2025

edit: It actually looks like there are a number of references to "premultiplied_alpha_fragment" still in the examples folder that would cause a number of examples to break, including the "ldraw" and "fat lines" ones

Oh, now I can reproduce. I wonder why I couldn't reproduce before... Fixing...

@gkjohnson
Copy link
Collaborator

I wonder why I couldn't reproduce before...

Possibly testing before deleting the premultiplied_alpha_fragment file?


Also one option that occurs to me in order to avoid the differences would be the following when premultiplied alpha is true:

  • convert color from linear-srgb to target color space
  • multiply color by alpha
  • convert color to target color space to linear-srgb
  • continue to apply tone mapping etc
  • apply final color space conversion

Not the most elegant feeling and requires two additional color space conversion operations (possibly not so terrible in the grand scheme of some of these shaders) but it would address the color consistency issue while still allowing for premultiplied alpha tone mapping.

@mrdoob
Copy link
Owner Author

mrdoob commented Dec 2, 2025

Was curious to see what the changes would be if we added an internal MSAA RT to WebGLRenderer: #32461

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants