Post-effect bloom new feature: rgb threshold

As you may have noticed, I have been desperately sending messages to every bloom forum thread in the face of the earth in search for a solution to my problem.

As it turns out, I don’t actually need a bloom effect on a specific entity. Rather, is it possible to modify the bloom script to add an rgb threshold parameter? The idea is this…the entity I want to apply the bloom to is blue, and there won’t be anything else blue around. So if I can set a threshold for blue to apply the bloom, it would work. Not the best solution, but it will do, given that it seems that applying bloom to specific entities is an impossible task.

I’d like to add an rgb threshold parameter to the bloom script. How can I go about this? Even better, remove the normal threshold parameter and have only an rgba parameter as threshold. Thanks.

Hi @Marks,

That would require updating the effect shader code to target a specific color value. You are interested in the following line, that is the bloom extract fragment pass:

Right now the effect has a threshold value that limits it to spots of a certain brightness. You could update that to check a range of color values. There are several ways to do that, you can try searching online for shader examples.

This PlayCanvas chromakey shader does something similar to detect how close a pixel color is to green:

https://playcanvas.com/editor/scene/574164

Hope that helps.

1 Like

I’ve been messing around with modifying the bloom to only work on a specific threshold of the ‘blue’ and above:

You can potentially use this as a starting point: https://playcanvas.com/project/988195/overview/blue-bloom

3 Likes

Thank you, this solved my problem.

I’m leaving the script here for the community. There’s an rgba attribute as the parameter now, and a method to change the settings of the bloom at runtime. The color can be set to have the rgba OR’d or AND’d so the bloom can apply to multiple colors or only a specific color. If you choose to OR it only ORs a specific color if you set the threshold to be different than 0 (for example if you set the ‘r’ to 0 it will not OR the r, otherwise all pixels of the screen have an r that is bigger or equal to 0). For all my use cases this works just fine, even though ideally I’d like to apply this effect on specific models, not the camera.

// --------------- POST EFFECT DEFINITION --------------- //

var SAMPLE_COUNT = 15;

function computeGaussian(n, theta) {

    return ((1.0 / Math.sqrt(2 * Math.PI * theta)) * Math.exp(-(n * n) / (2 * theta * theta)));

}

function calculateBlurValues(sampleWeights, sampleOffsets, dx, dy, blurAmount) {

    // Look up how many samples our gaussian blur effect supports.

    // Create temporary arrays for computing our filter settings.

    // The first sample always has a zero offset.

    sampleWeights[0] = computeGaussian(0, blurAmount);

    sampleOffsets[0] = 0;

    sampleOffsets[1] = 0;

    // Maintain a sum of all the weighting values.

    var totalWeights = sampleWeights[0];

    // Add pairs of additional sample taps, positioned

    // along a line in both directions from the center.

    var i, len;

    for (i = 0, len = Math.floor(SAMPLE_COUNT / 2); i < len; i++) {

        // Store weights for the positive and negative taps.

        var weight = computeGaussian(i + 1, blurAmount);

        sampleWeights[i * 2] = weight;

        sampleWeights[i * 2 + 1] = weight;

        totalWeights += weight * 2;

        // To get the maximum amount of blurring from a limited number of

        // pixel shader samples, we take advantage of the bilinear filtering

        // hardware inside the texture fetch unit. If we position our texture

        // coordinates exactly halfway between two texels, the filtering unit

        // will average them for us, giving two samples for the price of one.

        // This allows us to step in units of two texels per sample, rather

        // than just one at a time. The 1.5 offset kicks things off by

        // positioning us nicely in between two texels.

        var sampleOffset = i * 2 + 1.5;

        // Store texture coordinate offsets for the positive and negative taps.

        sampleOffsets[i * 4] = dx * sampleOffset;

        sampleOffsets[i * 4 + 1] = dy * sampleOffset;

        sampleOffsets[i * 4 + 2] = -dx * sampleOffset;

        sampleOffsets[i * 4 + 3] = -dy * sampleOffset;

    }

    // Normalize the list of sample weightings, so they will always sum to one.

    for (i = 0, len = sampleWeights.length; i < len; i++) {

        sampleWeights[i] /= totalWeights;

    }

}

/**

 * @class

 * @name BloomEffect

 * @classdesc Implements the BloomEffect post processing effect.

 * @description Creates new instance of the post effect.

 * @augments PostEffect

 * @param {GraphicsDevice} graphicsDevice - The graphics device of the application.

 * @property {number} bloomThreshold Only pixels brighter then this threshold will be processed. Ranges from 0 to 1.

 * @property {number} blurAmount Controls the amount of blurring.

 * @property {number} bloomIntensity The intensity of the effect.

 */

function BloomEffect(graphicsDevice) {

    pc.PostEffect.call(this, graphicsDevice);

    // Shaders

    var attributes = {

        aPosition: pc.SEMANTIC_POSITION

    };

    var passThroughVert = [

        "attribute vec2 aPosition;",

        "",

        "varying vec2 vUv0;",

        "",

        "void main(void)",

        "{",

        "    gl_Position = vec4(aPosition, 0.0, 1.0);",

        "    vUv0 = (aPosition + 1.0) * 0.5;",

        "}"

    ].join("\n");

    // Pixel shader extracts the brighter areas of an image.

    // This is the first step in applying a bloom postprocess.

    var bloomExtractFrag = [

        "precision " + graphicsDevice.precision + " float;",

        "",

        "varying vec2 vUv0;",

        "",

        "uniform sampler2D uBaseTexture;",

        "uniform float uAlphaThreshold;",

        "uniform float uRedThreshold;",

        "uniform float uGreenThreshold;",

        "uniform float uBlueThreshold;",

        "uniform float uOrColors;",

        "",

        "void main(void)",

        "{",

                // Look up the original image color.

        "    vec4 color = texture2D(uBaseTexture, vUv0);",

        "",

                // Adjust it to keep only values brighter than the specified threshold.

        "    float rDistance = uOrColors == 1.0 && uRedThreshold == 0.0 ? -1.0 : ((color.r - color.g) + (color.r - color.b)) * 0.5;",

        "    float gDistance = uOrColors == 1.0 && uGreenThreshold == 0.0 ? -1.0 : ((color.g - color.r) + (color.g - color.b)) * 0.5;",

        "    float bDistance = uOrColors == 1.0 && uBlueThreshold == 0.0 ? -1.0: ((color.b - color.r) + (color.b - color.g)) * 0.5;",

        "    float fThreshold = uOrColors == 1.0 ? rDistance >= uRedThreshold || gDistance >= uGreenThreshold || bDistance >= uBlueThreshold ? uAlphaThreshold : 1.0 : rDistance >= uRedThreshold && gDistance >= uGreenThreshold && bDistance >= uBlueThreshold ? uAlphaThreshold : 1.0;",

        "    gl_FragColor = clamp((color - fThreshold) / (1.0 - fThreshold), 0.0, 1.0);",

        "}"

    ].join("\n");

    // Pixel shader applies a one dimensional gaussian blur filter.

    // This is used twice by the bloom postprocess, first to

    // blur horizontally, and then again to blur vertically.

    var gaussianBlurFrag = [

        "precision " + graphicsDevice.precision + " float;",

        "",

        "#define SAMPLE_COUNT " + SAMPLE_COUNT,

        "",

        "varying vec2 vUv0;",

        "",

        "uniform sampler2D uBloomTexture;",

        "uniform vec2 uBlurOffsets[SAMPLE_COUNT];",

        "uniform float uBlurWeights[SAMPLE_COUNT];",

        "",

        "void main(void)",

        "{",

        "    vec4 color = vec4(0.0);",

                // Combine a number of weighted image filter taps.

        "    for (int i = 0; i < SAMPLE_COUNT; i++)",

        "    {",

        "        color += texture2D(uBloomTexture, vUv0 + uBlurOffsets[i]) * uBlurWeights[i];",

        "    }",

        "",

        "    gl_FragColor = color;",

        "}"

    ].join("\n");

    // Pixel shader combines the bloom image with the original

    // scene, using tweakable intensity levels.

    // This is the final step in applying a bloom postprocess.

    var bloomCombineFrag = [

        "precision " + graphicsDevice.precision + " float;",

        "",

        "varying vec2 vUv0;",

        "",

        "uniform float uBloomEffectIntensity;",

        "uniform sampler2D uBaseTexture;",

        "uniform sampler2D uBloomTexture;",

        "",

        "void main(void)",

        "{",

                // Look up the bloom and original base image colors.

        "    vec4 bloom = texture2D(uBloomTexture, vUv0) * uBloomEffectIntensity;",

        "    vec4 base = texture2D(uBaseTexture, vUv0);",

        "",

                // Darken down the base image in areas where there is a lot of bloom,

                // to prevent things looking excessively burned-out.

        "    base *= (1.0 - clamp(bloom, 0.0, 1.0));",

        "",

                // Combine the two images.

        "    gl_FragColor = base + bloom;",

        //"    gl_FragColor = bloom;",

        "}"

    ].join("\n");

    this.extractShader = new pc.Shader(graphicsDevice, {

        attributes: attributes,

        vshader: passThroughVert,

        fshader: bloomExtractFrag

    });

    this.blurShader = new pc.Shader(graphicsDevice, {

        attributes: attributes,

        vshader: passThroughVert,

        fshader: gaussianBlurFrag

    });

    this.combineShader = new pc.Shader(graphicsDevice, {

        attributes: attributes,

        vshader: passThroughVert,

        fshader: bloomCombineFrag

    });

    this.targets = [];

    // Effect defaults

    this.alphaThreshold = 0.25;

    this.blurAmount = 4;

    this.bloomIntensity = 1.25;

    // Uniforms

    this.sampleWeights = new Float32Array(SAMPLE_COUNT);

    this.sampleOffsets = new Float32Array(SAMPLE_COUNT * 2);

}

BloomEffect.prototype = Object.create(pc.PostEffect.prototype);

BloomEffect.prototype.constructor = BloomEffect;

BloomEffect.prototype._destroy = function () {

    if (this.targets) {

        var i;

        for (i = 0; i < this.targets.length; i++) {

            this.targets[i].destroyTextureBuffers();

            this.targets[i].destroy();

        }

    }

    this.targets.length = 0;

};

BloomEffect.prototype._resize = function (target) {

    var width = target.colorBuffer.width;

    var height = target.colorBuffer.height;

    if (width === this.width && height === this.height)

        return;

    this.width = width;

    this.height = height;

    this._destroy();

    // Render targets

    var i;

    for (i = 0; i < 2; i++) {

        var colorBuffer = new pc.Texture(this.device, {

            name: "Bloom Texture" + i,

            format: pc.PIXELFORMAT_R8_G8_B8_A8,

            width: width >> 1,

            height: height >> 1,

            mipmaps: false

        });

        colorBuffer.minFilter = pc.FILTER_LINEAR;

        colorBuffer.magFilter = pc.FILTER_LINEAR;

        colorBuffer.addressU = pc.ADDRESS_CLAMP_TO_EDGE;

        colorBuffer.addressV = pc.ADDRESS_CLAMP_TO_EDGE;

        colorBuffer.name = 'pe-bloom';

        var bloomTarget = new pc.RenderTarget({

            name: "Bloom Render Target " + i,

            colorBuffer: colorBuffer,

            depth: false

        });

        this.targets.push(bloomTarget);

    }

};

Object.assign(BloomEffect.prototype, {

    render: function (inputTarget, outputTarget, rect) {

        this._resize(inputTarget);

        var device = this.device;

        var scope = device.scope;

        // Pass 1: draw the scene into rendertarget 1, using a

        // shader that extracts only the brightest parts of the image.

        scope.resolve("uAlphaThreshold").setValue(this.alphaThreshold);

        scope.resolve("uRedThreshold").setValue(this.redThreshold);

        scope.resolve("uGreenThreshold").setValue(this.greenThreshold);

        scope.resolve("uBlueThreshold").setValue(this.blueThreshold);

        scope.resolve("uOrColors").setValue(this.orColors ? 1.0 : 0.0);

        scope.resolve("uBaseTexture").setValue(inputTarget.colorBuffer);

        pc.drawFullscreenQuad(device, this.targets[0], this.vertexBuffer, this.extractShader);

        // Pass 2: draw from rendertarget 1 into rendertarget 2,

        // using a shader to apply a horizontal gaussian blur filter.

        calculateBlurValues(this.sampleWeights, this.sampleOffsets, 1.0 / this.targets[1].width, 0, this.blurAmount);

        scope.resolve("uBlurWeights[0]").setValue(this.sampleWeights);

        scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsets);

        scope.resolve("uBloomTexture").setValue(this.targets[0].colorBuffer);

        pc.drawFullscreenQuad(device, this.targets[1], this.vertexBuffer, this.blurShader);

        // Pass 3: draw from rendertarget 2 back into rendertarget 1,

        // using a shader to apply a vertical gaussian blur filter.

        calculateBlurValues(this.sampleWeights, this.sampleOffsets, 0, 1.0 / this.targets[0].height, this.blurAmount);

        scope.resolve("uBlurWeights[0]").setValue(this.sampleWeights);

        scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsets);

        scope.resolve("uBloomTexture").setValue(this.targets[1].colorBuffer);

        pc.drawFullscreenQuad(device, this.targets[0], this.vertexBuffer, this.blurShader);

        // Pass 4: draw both rendertarget 1 and the original scene

        // image back into the main backbuffer, using a shader that

        // combines them to produce the final bloomed result.

        scope.resolve("uBloomEffectIntensity").setValue(this.bloomIntensity);

        scope.resolve("uBloomTexture").setValue(this.targets[0].colorBuffer);

        scope.resolve("uBaseTexture").setValue(inputTarget.colorBuffer);

        pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.combineShader, rect);

    }

});

// ----------------- SCRIPT DEFINITION ------------------ //

var Bloom = pc.createScript('bloom');

Bloom.attributes.add('bloomIntensity', {

    type: 'number',

    default: 1,

    min: 0,

    title: 'Intensity'

});

Bloom.attributes.add('bloomThreshold', {

    type: 'rgba',

    default: [0, 0, 0, 0.25],

    title: 'Bloom Threshold'

});

Bloom.attributes.add('orColors', {

    type: 'boolean',

    default: true,

    title: 'Or Colors',

});

Bloom.attributes.add('blurAmount', {

    type: 'number',

    default: 4,

    min: 1,

    'title': 'Blur amount'

});

Bloom.prototype.initialize = function () {

    this.effect = new BloomEffect(this.app.graphicsDevice);

    this.effect.alphaThreshold = this.bloomThreshold.a;

    this.effect.redThreshold = this.bloomThreshold.r;

    this.effect.greenThreshold = this.bloomThreshold.g;

    this.effect.blueThreshold = this.bloomThreshold.b;

    this.effect.blurAmount = this.blurAmount;

    this.effect.bloomIntensity = this.bloomIntensity;

    this.effect.orColors = this.orColors;

   

    var queue = this.entity.camera.postEffects;

    queue.addEffect(this.effect);

    this.on('attr', function (name, value) {

        this.effect[name] = value;

    }, this);

    this.on('state', function (enabled) {

        if (enabled) {

            queue.addEffect(this.effect);

        } else {

            queue.removeEffect(this.effect);

        }

    });

    this.on('destroy', function () {

        queue.removeEffect(this.effect);

        this._destroy();

    });

};

Bloom.prototype.updateBloom = function (bloomThreshold, bloomIntensity, blurAmount, orColors) {

    bloomThreshold = bloomThreshold || this.bloomThreshold;

    bloomIntensity = bloomIntensity || this.bloomIntensity;

    blurAmount = blurAmount || this.blurAmount;

    orColors = orColors || this.orColors;

    this.effect.alphaThreshold = bloomThreshold.a;

    this.effect.redThreshold = bloomThreshold.r;

    this.effect.greenThreshold = bloomThreshold.g;

    this.effect.blueThreshold = bloomThreshold.b;

    this.effect.blurAmount = blurAmount;

    this.effect.bloomIntensity = bloomIntensity;

    this.effect.orColors = orColors;

};
2 Likes

Hello Marks
I used your code, but I didn’t see the correct effect. I successfully ran the sample before yaustar. Did I do something wrong?

If you really wanted to be fancy, presumably you could use multiple render targets to write a “bloom” buffer where only certain materials draw to it (e.g. white = bloom, black = none) . You would then compare this image buffer when doing extraction and exclude any pixels not in the “bloom” buffer. Mobile gpus might hate it though

1 Like