-
-
Notifications
You must be signed in to change notification settings - Fork 36.1k
WebGLRenderer: Improve HDR handling for additive blending #32433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📦 Bundle sizeFull ESM build, minified and gzipped.
🌳 Bundle size after tree-shakingMinimal build including a renderer, camera, empty scene, and dependencies.
|
| gl_FragColor = vec4( outgoingLight, diffuseColor.a ); | ||
| #ifdef BLENDING_ADDITIVE | ||
| gl_FragColor = vec4( outgoingLight * diffuseColor.a, 1.0 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this approach just used for additive blending and not for all use cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WestLangley Does this change interfere when premultipliedAlpha is set to true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is something wrong with the model. Additive blending seems to have been added to the example after-the-fact. We have techniques to render glass correctly using normal blending.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this approach just used for additive blending and not for all use cases?
Does this change interfere whenpremultipliedAlphais set totrue?
Here you go:
Additive Blending & Pre-multiplication
Three approaches, same blend result, different tone mapping:
| Approach | Shader Output | Blend Function | Blend Result | What gets tone-mapped |
|---|---|---|---|---|
| Default | (rgb, 0.4) |
src * srcAlpha + dst |
rgb * 0.4 + dst |
toneMap(rgb) then * 0.4 in blend |
premultipliedAlpha: true |
(rgb, 0.4) |
src * 1 + dst |
rgb + dst |
toneMap(rgb) — alpha ignored! |
BLENDING_ADDITIVE shader |
(rgb * 0.4, 1.0) |
src * srcAlpha + dst |
rgb * 0.4 + dst |
toneMap(rgb * 0.4) |
Why only additive blending?
For normal alpha blending (src * a + dst * (1-a)), pre-multiplying in the shader would square the alpha — breaking the math. Additive blending only uses alpha as a source multiplier, so moving the multiplication into the shader works.
The core problem:
WebGL blending happens after the fragment shader. So tone mapping (in shader) always happens before blending. There's no way to blend in HDR space without post-processing.
The BLENDING_ADDITIVE change moved the * alpha before tone mapping, which changes the visual result because toneMap(x) * a ≠ toneMap(x * a).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... the words sound plausible, but I'm having a difficult time anchoring them to fundamentals I understand. Does this make sense to others, and/or do we have a reference beyond the LLM? Notably, if I put "premultiply alpha before tonemapping" or "why not premultiply alpha before tonemapping" into one, it is very happy to respond with well-written words arguing that the opposite choices are correct, preferred, and conventional.
The change is similar enough to tone mapping in a later pass, so that's encouraging, but turning it on globally for additive blending, and without reference to material.premultipliedAlpha, is confusing me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... the words sound plausible, but I'm having a difficult time anchoring them to fundamentals I understand. Does this make sense to others, and/or do we have a reference beyond the LLM? Notably, if I put "premultiply alpha before tonemapping" or "why not premultiply alpha before tonemapping" into one, it is very happy to respond with well-written words arguing that the opposite choices are correct, preferred, and conventional.
This is what I'm trying to solve:
// Rendering to LDR framebuffer (RGBA8) with HDR values
outgoingLight = 5.0, alpha = 0.1
Desired contribution: 5.0 * 0.1 = 0.5
Without BLENDING_ADDITIVE:
- Shader outputs: (5.0, 0.1)
- Clamp RGB to [0,1]: (1.0, 0.1) ← HDR value lost
- Blend (src * srcAlpha + dst): 1.0 * 0.1 + dst = 0.1 + dst ← wrong
With BLENDING_ADDITIVE:
- Shader outputs: (0.5, 1.0) ← RGB = 5.0 * 0.1, Alpha = 1.0
- Clamp RGB to [0,1]: (0.5, 1.0) ← already in range
- Blend (src * srcAlpha + dst): 0.5 * 1.0 + dst = 0.5 + dst ← correct
BLENDING_ADDITIVE premultiplies the light contribution and material.premultiplyAlpha is about the asset itself:
Input colors → Lighting calculations → outgoingLight → [BLENDING_ADDITIVE here] → Blend
↑ ↑
premultipliedAlpha premultipliedAlpha
affects this affects this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't correct. "Additive blending" is additive color and alpha - the alpha cannot just be set to 1.0 because the opacity has already been applied to the rgb channels. This change will break any additive blending on a transparent background. From the "webgl materials blending" demo with "alpha:true" setting passed to WebGLRenderer and a clear background color:
| dev | this pr |
|---|---|
![]() |
![]() |
The AI response doesn't make sense to me, either. The only reason you'd only apply this to additive blending is because it would be more obviously wrong in all other blending conditions. If you want to premultiply the alpha before applying tone mapping then we should use premultiplied alpha. And move the tone mapping stage after the "premultiplied alpha" operation (it's currently before). This change is really just adding in premultiplied alpha only for additive blending (with incorrect blend states set) even when the setting is false.
Fundamentally this is an issue of mapping a color to [0, 1] and then multiplying alpha into the color before blending. This will happen with non-tonemapped materials if premultiplied alpha is set to false, as well, since the graphics API will clamp the color before blending. You can see the effect in this fiddle with 25% opacity transparent spheres:
left: toneMapped=false, premultipliedAlpha=false, middle: toneMapped=false, premultipliedAlpha=true, right: toneMapped=true, premultipliedAlpha=true
You'll see that the "tone mapped" sphere has the same dull highlight as the non-premultiplied alpha sphere because it tonemaps the color to [0, 1] before applying alpha, as the graphics API does when blending.
Regarding whether it's "correct" to perform tonemapping before or after applying alpha - I don't have any strong opinions. But I believe it's the only way to avoid this problem in a forward rendering shader (unless our tone mapping functions will map to a range outside of [0, 1] which is a different discussion). Really you want to tone map the color after it's been blended to a final color on the screen (eg a postprocessing effect) so either way I think we're dealing with a compromise.
If this is something we want to improve I think we should look at rearranging the order of these shader fragments, though I know these have been arranged in this way for reason:
#include <tonemapping_fragment>
#include <colorspace_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>edit
it is very happy to respond with well-written words arguing that the opposite choices are correct, preferred, and conventional.
I'm generally seeing this kind of thing, as well, with other topics 😅 it can sound very convincing if a topic is unfamiliar and this premultiplied alpha topic is fairly complicated, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just did another PR that moves premultiplied_alpha before tonemapping: #32449
Seems like the right solution indeed.
Although it's definitely not great that the developer needs to know and set premultiplyAlpha: true if they want a opacity: 0.5 material to not get clamped IBL.
Edit:
If this is something we want to improve I think we should look at rearranging the order of these shader fragments
Oh good. Was just working on just that before I saw your post.



Description
For transparent materials with additive blending, opacity now scales HDR values before tonemapping rather than after. This preserves highlight proportions better.
tonemap(HDR) * opacity→ bright highlights get compressed then scaled downtonemap(HDR * opacity)→ scaling happens in linear HDR space, like reducing exposureChanges
opaque_fragment.glsl.js: AddBLENDING_ADDITIVEpath that premultiplies outgoingLight by opacityWebGLPrograms.js: AddblendingAdditiveparameterWebGLProgram.js: AddBLENDING_ADDITIVEdefine