Physically Based HDR Bloom

Hey guys, long time no see!

I’m excited to open a new chapter in my PlayCanvas-related body of work, so please welcome this fancy HDR Bloom script into your projects!

  • Highly customizable effect with no external dependencies
  • Support for all currently available tonemappers and both 1.0 and 2.2 gamma outputs
  • Almost constant performance cost, mostly bottlenecked by frame buffer copy
  • MIT license

Please find the script and sample project here:

Try it live:

For professional inquiries please find me at:

What can be better than a good night sleep? We all need one!

Thanks to guys on this awesome thread that inspired me to make it all happen!

Feel free to post screenshots of your enhanced projects in the comments!


That looks fantastic!
Do you render to some HDR format, or just encode to RGBM or similar?

1 Like

Thanks @mvaligursky!

Yep, its full HDR, format resolving function is inspired by app.graphicsDevice.getHdrFormat(…), with addition of pc.PIXELFORMAT_111110F as default. Framebuffer allocated by PostEffect constructor is still using built-in getHdrFormat(…) though, so those are either 16 or 32 bit under the hood.


This is quite inspirational! :heart_eyes:

Would you consider adding it here:

It could potentially replace the existing LDR bloom there (which is not a great implementation!).


Thank you @will !

Sure, would be happy to add it to official engine posteffects stack! :+1:

One important issue though is that its currently full HDR (using this.hdr = true in constructor which is used by PostEffect internally), so it will not work well with any LDR posteffects coming before it in posteffects queue (i.e. LDR depth of field or SSAO). More over, what it does internally - it turns off tonemapping at scene / material level (by making it flat pc.TONEMAP_NONE), and replaces it with tonemapping during final bloom combine stage (when HDR becomes LDR).

So yeah, we could push it in its current form for now, but eventually all other posteffects would need to be updated to HDR as well. May be at its current stage its worth having 2 posteffects folders - LDR and HDR. And slowly phasing out from in-material tonemapping to pure posteffect tonemapping, may be introducing posteffectqueue component coming with camera and including at least a simple tonemapping posteffect in it by default.


I’m so impressed with this. The effect itself looks great and your code is lovely and clean <3.

We are planning a major engine post-effect refactor in the not-too-distant-future precisely so we can correctly support HDR effects like this one.

Thanks so much for sharing!


Thank you @slimbuck , really appreciated! :cowboy_hat_face:

Very nice effect, I enjoy watching it :candle:

Would also love to see it using light sources with dynamic intensity, lets say some glowing stars or candle light(s) or both. :night_with_stars: :candle: :candle: :city_sunset:

I went a bit over the code and e.g.

    setupShaders (params)
        this.shader =
            downsample: this.createDownsampleShader (params),
            upsample: this.createUpsampleShader (params),
            combine: this.createCombineShader (params)

Why do you pass params to everything? You are in the same class and saved it in this.params already.

Another line:

tonemapper: pc.TONEMAP_NONE

I know why you access it, but this one doesn’t exist, even tho all these exist: Object.keys(pc).filter(key => key.includes("NONE"));


Maybe PC should have pc.TONEMAP_NONE?

Every once in a while it also seems like it renders a complete black frame, anyone knows what’s up with that?

Can’t see the black frame flicker when it’s off:

const bloom ="Bloom");
bloom.script["HDR Bloom"].enabled = false;

@kungfooman thanks for taking a look!

Good catch indeed, pc.TONEMAP_NONE doesn’t exist in docs and export, but it still gets suggested in PlayCanvas code editor (with value undefined) and shader chunk for it does exist (pc.shaderChunks.tonemappingNonePS). So this is a default pass through tonemapping function, which is important in our case since unlike pc.TONEMAP_LINEAR it doesn’t multiply color by exposure. Hence we reset global scene tonemapper to pass through, and then set specific tonemapper with exposure in bloom combine.

As for params - that’s just design decision, it doesn’t hold any special meaning. Just embracing old school paradigms from C age :sweat_smile: Passing param explicitly helps to trace where its used.

Re: black frame issue - what device is that? I’ve tested that script on multitude of browserstack devices and didn’t have such issues. Let’s see if I could reproduce that!

Yea, my C brain still thinks 'uSource' looks wrong, because ' is denoting a char type :sweat_smile:

Some methods like createDownsampleShader don’t use (params) at all, so the code could just be a bit shorter.

I’m on Linux Mint + Chrome browser… truth be told, I often read that Chrome on Linux “isn’t as good as it could be”, so if it works on other devices, I’m sure it’s another Chrome/Linux issue.

That’s true, but it’s mostly for symmetry - its supposed that shaders would be recompiled on certain params change, so may be in the future other shaders will also use params for parametric code. Starting uniform names with ‘u’ is pretty common in some GLSL communities :smiley:

Yeah unfortunately can’t say anything about Linux. Did you try it in Firefox? But if it generally works ok and happens just occasionally - that gets it even more difficult to guess!

Right, just tested a while on Linux/Firefox and could see the flicker multiple times… it only happens when I move the camera around (both in only orbit rotation + only orbit translation).

Nice work. I gave it a go (so I can compare it to my own attempt) and I’m seeing these artifacts…

Is this expected?

Hi @Adriaaaaan , could you please make a screenshot of current script params? Looks like filter radius is too high. Or may be you’re using 32 bit HDR format which can be buggy on some devices (basically unfilterable)