Source: framework/components/camera/post-effect-pass.js

Object.assign(pc, function () {

    var _backbufferRt = [null, null]; // 2 RTs may be needed for ping-ponging
    var _constInput = null;
    var _constScreenSize;
    var _constScreenSizeValue = new pc.Vec4();
    var _constScreenSizeValueUniform = new Float32Array(4);
    var _postEffectChain = [];
    var _backbufferRtUsed = false;
    var _backbufferRt2Used = false;
    var _backbufferRtWrittenByPost = false;

    var _regexUniforms = /uniform[ \t\n\r]+\S+[ \t\n\r]+\S+[ \t\n\r]*\;/g;
    var _regexUniformStart = /\S+[ \t\n\r]*\;/;
    var _regexUniformEnd = /[ \t\n\r]*\;/;
    var _regexVariables = /(float|int|bool|vec2|vec3|vec4|struct)([ \t\n\r]+[^\;]+[ \t\n\r]*\,*)+\;/g;
    var _regexVariableSurroundings = /(float|int|bool|vec2|vec3|vec4|struct|\,|\;|\{|\})/g;
    var _regexIrrelevantVariables = /(uniform|varying|in|out)[ \t\n\r]+(float|int|bool|vec2|vec3|vec4|struct)([ \t\n\r]+[^\;]+[ \t\n\r]*\,*)+\;/g;
    var _regexIrrelevantVariableSurroundings = /(float|int|bool|vec2|vec3|vec4|struct|uniform|varying|in|out|\,|\;|\{|\})/g;
    var _regexVersion = /#version/g;
    var _regexFragColor = /out highp vec4 pc_fragColor;/g;
    var _regexFragColor2 = /#define gl_FragColor/g;
    var _regexFragColor3 = /gl_FragColor/g;
    var _regexColorBuffer = /uniform[ \t\n\r]+sampler2D[ \t\n\r]+uColorBuffer;/g;
    var _regexUv = /(varying|in)[ \t\n\r]+vec2[ \t\n\r]+vUv0;/g;
    var _regexColorBufferSample = /(texture2D|texture)[ \t\n\r]*\([ \t\n\r]*uColorBuffer/g;
    var _regexMain = /void[ \t\n\r]+main/g;

    var _createBackbufferRt = function (id, device, format) {
        var tex = new pc.Texture(device, {
            format: format,
            width: device.width,
            height: device.height
        });
        tex.name = 'posteffect-pass';
        tex.minFilter = pc.FILTER_NEAREST;
        tex.magFilter = pc.FILTER_NEAREST;
        tex.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
        tex.addressV = pc.ADDRESS_CLAMP_TO_EDGE;

        _backbufferRt[id]._colorBuffer = tex;
    };

    var _destroyBackbufferRt = function (id) {
        _backbufferRt[id].colorBuffer.destroy();
        _backbufferRt[id].destroy();
    };

    var _collectUniforms = function (code) {
        var strs = code.match(_regexUniforms) || []; // look ma I know regexp
        var start, end, uname;
        var uniforms = [];
        for (var i = 0; i < strs.length; i++) {
            start = strs[i].search(_regexUniformStart);
            end = strs[i].search(_regexUniformEnd);
            uname = strs[i].substr(start, end - start);
            if (uname !== "uColorBuffer") { // this one is OK to be shared
                uniforms.push(uname);
            }
        }
        return uniforms;
    };

    var _uniformsCollide = function (layers, chain, count, shader) {
        var uniforms = _collectUniforms(shader.definition.fshader);
        if (uniforms.length === 0) return false;

        var i, j, k, uniforms2;
        var uname;
        for (i = 0; i < count; i++) {
            for (j = 0; j < uniforms.length; j++) {
                uname = uniforms[j];
                uniforms2 = _collectUniforms(layers[chain[i]].shader.definition.fshader);
                for (k = 0; k < uniforms2.length; k++) {
                    if (uniforms2[k] === uname) {
                        return true;
                    }
                }
            }
        }

        return false;
    };

    // collect global vars and return collisions with what's already in the list
    var _collectGlobalTempVars = function (code, list) {
        // Get code without any scoped stuff
        var len = code.length;
        var chr;
        var scopeStart = 0;
        var scopeEnd = 0;
        var scopeDepth = 0;
        var codeStart = 0;
        var codeWithoutScopes = "";
        var i, j;
        for (i = 0; i < len; i++) {
            chr = code.charAt(i);
            if (chr === "{") {
                if (scopeDepth === 0) {
                    scopeStart = i;
                }
                scopeDepth++;
            } else if (chr === "}") {
                if (scopeDepth === 1) {
                    scopeEnd = i;
                    codeWithoutScopes += code.substr(codeStart, (scopeStart - codeStart) + 1);
                    codeStart = scopeEnd;
                }
                scopeDepth--;
            }
        }
        codeWithoutScopes += code.substr(codeStart, (code.length - codeStart) + 1);

        // Find all global variable declarations and detect collisions
        // ... won't work with re#defined types
        var collisions = null;
        var decls = codeWithoutScopes.match(_regexVariables) || [];
        var vars, varName;
        for (i = 0; i < decls.length; i++) {
            vars = decls[i].split(",");
            for (j = 0; j < vars.length; j++) {
                varName = vars[j].replace(_regexVariableSurroundings, "").trim();
                if (list.indexOf(varName) >= 0) {
                    if (!collisions) collisions = [];
                    collisions.push(varName);
                } else {
                    list.push(varName);
                }
            }
        }

        // Find all varying/uniform declarations (ideally should be possible to filter them out with first search...)
        // and remove from list
        var irrelevantDecls = codeWithoutScopes.match(_regexIrrelevantVariables) || [];
        var index;
        for (i = 0; i < irrelevantDecls.length; i++) {
            vars = irrelevantDecls[i].split(",");
            for (j = 0; j < vars.length; j++) {
                varName = vars[j].replace(_regexIrrelevantVariableSurroundings, "").trim();
                index = list.indexOf(varName);
                if (index >= 0) {
                    list.splice(index, 1);
                }
            }
        }

        return collisions;
    };

    /**
     * @private
     * @constructor
     * @name pc.PostEffectPass
     * @param {pc.Application} app The application.
     * @param {Object} options Optional options object.
     */
    function PostEffectPass(app, options) {
        this.app = app;
        this.srcRenderTarget = options.srcRenderTarget;
        this.hdr = options.hdr;
        this.blending = options.blending;
        this.shader = options.shader;
        this.setup = options.setup;

        var self = this;
        var device = app.graphicsDevice;

        this.layer = new pc.Layer({ // grab that and put to layer composition
            opaqueSortMode: pc.SORTMODE_NONE,
            transparentSortMode: pc.SORTMODE_NONE,
            passThrough: true,
            name: options.name,

            onPostRender: function () {
                if (self.srcRenderTarget) {
                    _constScreenSizeValue.x = self.srcRenderTarget.width;
                    _constScreenSizeValue.y = self.srcRenderTarget.height;
                    _constScreenSizeValue.z = 1.0 / self.srcRenderTarget.width;
                    _constScreenSizeValue.w = 1.0 / self.srcRenderTarget.height;
                } else {
                    _constScreenSizeValue.x = device.width;
                    _constScreenSizeValue.y = device.height;
                    _constScreenSizeValue.z = 1.0 / device.width;
                    _constScreenSizeValue.w = 1.0 / device.height;
                }
                _constScreenSizeValueUniform[0] = _constScreenSizeValue.x;
                _constScreenSizeValueUniform[1] = _constScreenSizeValue.y;
                _constScreenSizeValueUniform[2] = _constScreenSizeValue.z;
                _constScreenSizeValueUniform[3] = _constScreenSizeValue.w;
                _constScreenSize.setValue(_constScreenSizeValueUniform);

                if (this._postEffectCombined && this._postEffectCombined < 0) {
                    if (self.setup) self.setup(device, self, _constScreenSizeValue, null, this.renderTarget);
                    return;
                }

                var src;
                if (this._postEffectCombinedSrc) {
                    src = this._postEffectCombinedSrc;
                } else {
                    src = self.srcRenderTarget ? self.srcRenderTarget : _backbufferRt[this._backbufferRtId];
                }
                if (src._samples > 1) src.resolve(true, false);
                var tex = src._colorBuffer;
                tex.magFilter = (this._postEffectCombinedShader ? this._postEffectCombinedBilinear : this.postEffectBilinear) ? pc.FILTER_LINEAR : pc.FILTER_NEAREST;

                _constInput.setValue(tex);
                if (self.setup) self.setup(device, self, _constScreenSizeValue, src, this.renderTarget);

                var shader = this._postEffectCombinedShader ? this._postEffectCombinedShader : this.shader;
                if (shader) pc.drawQuadWithShader(device, this.renderTarget, shader, null, null, self.blending);

                if (self.srcRenderTarget) return; // don't do anything else if this effect was not reading backbuffer RT
                // remap RT back to actual backbuffer in all layers prior to this effect
                var layers = app.scene.layers.layerList;
                for (var i = 0; i < layers.length; i++) {
                    if (layers[i] === self.layer) break;
                    if (layers[i].renderTarget === _backbufferRt[0] || layers[i].renderTarget === _backbufferRt[1]) {
                        layers[i].renderTarget = null;
                    }
                }
            }
        });

        this.layer._generateCameraHash(); // post effect doesn't contain actual cameras, but we need to generate cam data
        this.layer.isPostEffect = true;
        this.layer.unmodifiedUvs = options.unmodifiedUvs;
        this.layer.postEffectBilinear = options.bilinear;
        this.layer.postEffect = this;
        this.layer.shader = options.shader;
        this.layer.renderTarget = options.destRenderTarget;

        if (!_constInput) {
            // system initialization
            _constInput = device.scope.resolve("uColorBuffer"); // default input texture uniform name
            _constScreenSize = device.scope.resolve("uScreenSize");
            var _backbufferMsaa = device.supportsMsaa ? 4 : 1; // if context is created with antialias: true, backbuffer RT will use 4 MSAA samples
            for (var i = 0; i < 2; i++) { // create backbuffer RT objects, but don't allocate any memory for them just yet
                _backbufferRt[i] = new pc.RenderTarget({
                    depth: true,
                    stencil: device.supportsStencil,
                    samples: _backbufferMsaa,
                    autoResolve: false
                });
                _backbufferRt[i].name = "backbuffer" + i;
            }
            app.on("prerender", function () { // before every app.render, if any effect reads from backbuffer, we must replace real backbuffer with our backbuffer RTs prior to effect

                var layers = app.scene.layers.layerList;
                var i, j;
                var offset = 0;
                var rtId = 0;
                _backbufferRtUsed = false;
                _backbufferRt2Used = false;
                _backbufferRtWrittenByPost = false;
                var backbufferRtFormat = pc.PIXELFORMAT_R8_G8_B8_A8;

                if (app.scene.layers._dirty) {
                    // only called if layer order changed
                    // detect chains of posteffects and combine if possible
                    // won't work with uniform collisions

                    // #ifdef DEBUG
                    console.log("Trying to combine shaders...");
                    // #endif
                    var iterator = 0;
                    var breakChain = false;
                    var collisions, k;
                    for (i = 0; i < layers.length; i++) {
                        breakChain = false;

                        if (layers[i].isPostEffect && (iterator === 0 || (layers[i].unmodifiedUvs && layers[i].shader && !_uniformsCollide(layers, _postEffectChain, iterator, layers[i].shader)))) {
                            _postEffectChain[iterator] = i; // add effect to chain
                            iterator++;
                            if (i === layers.length - 1) breakChain = true; // this is the last layer
                        } else {
                            if (iterator > 0) {
                                breakChain = true; // next layer is not effect
                            }
                        }

                        if (breakChain) {
                            if (iterator > 1) {
                                // Combine multiple shaders

                                var cachedName = "post_";
                                var layer;
                                for (j = 0; j < iterator; j++) {
                                    layer = layers[_postEffectChain[j]];
                                    cachedName += layer.name ? layer.name : layer.id;
                                    if (j < iterator - 1) cachedName += "_";
                                }
                                var shader = device.programLib._cache[cachedName];
                                if (!shader) {
                                    var subCode;
                                    var code = "vec4 shaderOutput;\n"; // this is will be used instead of gl_FragColor; reading from real gl_FragColor is buggy on some platforms
                                    var mainCode = "void main() {\n";
                                    var globalTempVars = [];

                                    for (j = 0; j < iterator; j++) {
                                        subCode = layers[_postEffectChain[j]].shader.definition.fshader + "\n";
                                        // For every shader's code:
                                        // - Replace #version, because createShaderFromCode will append a new one anyway;
                                        // - Replace pc_fragColor and #define gl_FragColor for the same reason;
                                        // - Replace any usage of gl_FragColor to shaderOutput;
                                        subCode = subCode.replace(_regexVersion, "//").replace(_regexFragColor, "//").replace(_regexFragColor2, "//").replace(_regexFragColor3, "shaderOutput");
                                        if (j > 0) {
                                            // For every shader's code > 0:
                                            // - Remove definition of uColorBuffer (should be defined in code 0 already);
                                            // - Remove definition of vUv0 (same reason);
                                            // - Replace reading from uColorBuffer with reading from shaderOutput.
                                            subCode = subCode.replace(_regexColorBuffer, "//").replace(_regexUv, "//").replace(_regexColorBufferSample, "shaderOutput;\/\/");
                                        }
                                        // Replace main() with mainX()
                                        subCode = subCode.replace(_regexMain, "void main" + j);

                                        // Check for global variable collisions
                                        collisions = _collectGlobalTempVars(subCode, globalTempVars);
                                        if (collisions) {
                                            for (k = 0; k < collisions.length; k++) {
                                                subCode = subCode.replace(new RegExp("\\b" + collisions[k] + "\\b", 'g'), collisions[k] + "NNNN" + j);
                                            }
                                        }

                                        code += subCode;
                                        mainCode += "main" + j + "();\n";
                                    }
                                    mainCode += "gl_FragColor = shaderOutput;\n}\n";
                                    shader = pc.shaderChunks.createShaderFromCode(device,
                                                                                  pc.shaderChunks.fullscreenQuadVS,
                                                                                  code + mainCode,
                                                                                  cachedName);
                                    // #ifdef DEBUG
                                    console.log("Combined " + cachedName);
                                    // #endif
                                }
                                for (j = 0; j < iterator; j++) {
                                    layers[_postEffectChain[j]]._postEffectCombined = (j === iterator - 1) ? 1 : -1;
                                }
                                layers[_postEffectChain[iterator - 1]]._postEffectCombinedShader = shader;
                                layers[_postEffectChain[iterator - 1]]._postEffectCombinedBilinear = layers[_postEffectChain[0]].postEffectBilinear;
                                layers[_postEffectChain[iterator - 1]]._postEffectCombinedSrc = layers[_postEffectChain[0]].postEffect.srcRenderTarget;
                            }
                            _postEffectChain[0] = i; // add effect to new chain
                            iterator = 1;
                        }
                    }
                }

                // getting from
                //     world -> backbuffer
                //     backbuffer -> post1 -> backbuffer
                //     backbuffer -> post2 -> backbuffer
                // to
                //     world -> rt0
                //     rt0 -> post1 -> rt1
                //     rt1 -> post2 -> backbuffer
                //
                // other case:
                //     world -> backbuffer
                //     backbuffer -> post -> someRt
                //     otherObjects -> backbuffer
                // ->
                //     world -> rt0
                //     rt0 -> post -> someRt
                //     otherObjects -> rt0
                //
                // if no posteffects writing backbuffer, rt0 -> backbuffer
                for (i = 0; i < layers.length; i++) {
                    if (layers[i].isPostEffect && ((!layers[i].postEffect.srcRenderTarget && !layers[i]._postEffectCombined) ||
                                                   (!layers[i].postEffect._postEffectCombinedSrc && layers[i]._postEffectCombined >= 0))) { // layer i is posteffect reading from backbuffer
                        for (j = i - 1; j >= offset; j--) {
                            if (!layers[j].renderTarget) { // layer j is prior to layer i and is rendering to backbuffer
                                layers[j].renderTarget = _backbufferRt[rtId]; // replace backbuffer with backbuffer RT
                            }
                        }
                        layers[i]._backbufferRtId = rtId; // set input hint for post effect
                        offset = i;
                        _backbufferRtUsed = true; // 1st backbuffer RT used
                        if (rtId === 1) _backbufferRt2Used = true; // 2nd backbuffer RT used
                        if (layers[i].postEffect.hdr) {
                            // backbuffer RT must be HDR
                            if (device.webgl2 && device.textureFloatRenderable) {
                                backbufferRtFormat = pc.PIXELFORMAT_111110F;
                            } else if (device.extTextureHalfFloatLinear && device.textureHalfFloatRenderable) {
                                backbufferRtFormat = pc.PIXELFORMAT_RGBA16F;
                            } else {
                                backbufferRtFormat = pc.PIXELFORMAT_R8_G8_B8_A8;
                            }
                        }
                        if (layers[i].postEffect.shader && !layers[i].renderTarget) rtId = 1 - rtId; // pingpong RT if posteffect both reads/writes backbuffer

                    } else if (!layers[i].isPostEffect && !layers[i].renderTarget && _backbufferRtUsed) { // layer is not post effect, rendering to backbuffer, and backbuffer was replaced before
                        layers[i].renderTarget = _backbufferRt[rtId]; // replace backbuffer with backbuffer RT
                    }

                    if (layers[i].isPostEffect && !layers[i].renderTarget) {
                        _backbufferRtWrittenByPost = true;
                    }
                }

                // create/resize backbuffer render target if needed

                if (_backbufferRtUsed) {
                    if (!_backbufferRt[0].colorBuffer) {
                        _createBackbufferRt(0, device, backbufferRtFormat);
                    } else if (_backbufferRt[0].width !== device.width || _backbufferRt[0].height !== device.height || _backbufferRt[0]._colorBuffer._format !== backbufferRtFormat) {
                        _destroyBackbufferRt(0);
                        _createBackbufferRt(0, device, backbufferRtFormat);
                    }
                }

                if (_backbufferRt2Used) {
                    if (!_backbufferRt[1].colorBuffer) {
                        _createBackbufferRt(1, device, backbufferRtFormat);
                    } else if (_backbufferRt[1].width !== device.width || _backbufferRt[1].height !== device.height || _backbufferRt[1]._colorBuffer._format !== backbufferRtFormat) {
                        _destroyBackbufferRt(1);
                        _createBackbufferRt(1, device, backbufferRtFormat);
                    }
                }

            }, this);

            app.on("postrender", function () { // after every app.render test if there were no effect writing to actual backbuffer, and if so, copy it from replaced backbuffer
                var device = app.graphicsDevice;
                if (_backbufferRtUsed && !_backbufferRtWrittenByPost) {
                    var layers = app.scene.layers.layerList;
                    var rt;
                    for (var i = layers.length - 1; i >= 0; i--) {
                        rt = layers[i].renderTarget;
                        if (rt === _backbufferRt[0] || rt === _backbufferRt[1]) {
                            break;
                        }
                    }
                    if (rt) {
                        if (rt._samples > 1) {
                            rt.resolve(true, false);
                        }
                        device.copyRenderTarget(rt, null, true, false);
                    }
                }
            }, this);
        }
    }

    PostEffectPass.prototype.addToComposition = function (order) {
        this.app.scene.layers.insertTransparent(this.layer, order);
    };

    return {
        PostEffectPass: PostEffectPass
    };
}());