Source: ProcessingNodes/processingnode.js

//Matthew Shotton, R&D User Experience,© BBC 2015
import GraphNode from "../graphnode";
import {
    compileShader,
    createShaderProgram,
    createElementTexture,
    updateTexture
} from "../utils.js";
import { RenderException } from "../exceptions.js";

const TYPE = "ProcessingNode";

class ProcessingNode extends GraphNode {
    /**
     * Initialise an instance of a ProcessingNode.
     *
     * This class is not used directly, but is extended to create CompositingNodes, TransitionNodes, and EffectNodes.
     */
    constructor(gl, renderGraph, definition, inputNames, limitConnections) {
        super(gl, renderGraph, inputNames, limitConnections);
        this._vertexShader = compileShader(gl, definition.vertexShader, gl.VERTEX_SHADER);
        this._fragmentShader = compileShader(gl, definition.fragmentShader, gl.FRAGMENT_SHADER);
        this._definition = definition;
        this._properties = {}; //definition.properties;
        //copy definition properties
        for (let propertyName in definition.properties) {
            let propertyValue = definition.properties[propertyName].value;
            //if an array then shallow copy it
            if (Object.prototype.toString.call(propertyValue) === "[object Array]") {
                propertyValue = definition.properties[propertyName].value.slice();
            }
            let propertyType = definition.properties[propertyName].type;
            this._properties[propertyName] = {
                type: propertyType,
                value: propertyValue
            };
        }

        this._shaderInputsTextureUnitMapping = [];
        this._maxTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
        this._boundTextureUnits = 0;
        this._texture = createElementTexture(gl);
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.RGBA,
            gl.canvas.width,
            gl.canvas.height,
            0,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            null
        );
        //compile the shader
        this._program = createShaderProgram(gl, this._vertexShader, this._fragmentShader);

        //create and setup the framebuffer
        this._framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer);
        gl.framebufferTexture2D(
            gl.FRAMEBUFFER,
            gl.COLOR_ATTACHMENT0,
            gl.TEXTURE_2D,
            this._texture,
            0
        );
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        //create properties on this object for the passed properties
        for (let propertyName in this._properties) {
            Object.defineProperty(this, propertyName, {
                get: function() {
                    return this._properties[propertyName].value;
                },
                set: function(passedValue) {
                    this._properties[propertyName].value = passedValue;
                }
            });
        }

        //create texutres for any texture properties
        for (let propertyName in this._properties) {
            let propertyValue = this._properties[propertyName].value;
            if (propertyValue instanceof Image) {
                this._properties[propertyName].texture = createElementTexture(gl);
                this._properties[propertyName].textureUnit = gl.TEXTURE0 + this._boundTextureUnits;
                this._properties[propertyName].textureUnitIndex = this._boundTextureUnits;
                this._boundTextureUnits += 1;
                if (this._boundTextureUnits > this._maxTextureUnits) {
                    throw new RenderException(
                        "Trying to bind more than available textures units to shader"
                    );
                }
            }
        }

        // calculate texture units for input textures
        for (let inputName of definition.inputs) {
            this._shaderInputsTextureUnitMapping.push({
                name: inputName,
                textureUnit: gl.TEXTURE0 + this._boundTextureUnits,
                textureUnitIndex: this._boundTextureUnits,
                location: gl.getUniformLocation(this._program, inputName)
            });
            this._boundTextureUnits += 1;
            if (this._boundTextureUnits > this._maxTextureUnits) {
                throw new RenderException(
                    "Trying to bind more than available textures units to shader"
                );
            }
        }

        //find the locations of the properties in the compiled shader
        for (let propertyName in this._properties) {
            if (this._properties[propertyName].type === "uniform") {
                this._properties[propertyName].location = this._gl.getUniformLocation(
                    this._program,
                    propertyName
                );
            }
        }
        this._currentTimeLocation = this._gl.getUniformLocation(this._program, "currentTime");
        this._currentTime = 0;

        //Other setup
        let positionLocation = gl.getAttribLocation(this._program, "a_position");
        let buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
        gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
            gl.STATIC_DRAW
        );
        let texCoordLocation = gl.getAttribLocation(this._program, "a_texCoord");
        gl.enableVertexAttribArray(texCoordLocation);
        gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
        this._displayName = TYPE;
    }

    /**
     * Sets the passed processing node property to the passed value.
     * @param {string} name - The name of the processing node parameter to modify.
     * @param {Object} value - The value to set it to.
     *
     * @example
     * var ctx = new VideoContext();
     * var monoNode = ctx.effect(VideoContext.DEFINITIONS.MONOCHROME);
     * monoNode.setProperty("inputMix", [1.0,0.0,0.0]); //Just use red channel
     */
    setProperty(name, value) {
        this._properties[name].value = value;
    }

    /**
     * Sets the passed processing node property to the passed value.
     * @param {string} name - The name of the processing node parameter to get.
     *
     * @example
     * var ctx = new VideoContext();
     * var monoNode = ctx.effect(VideoContext.DEFINITIONS.MONOCHROME);
     * console.log(monoNode.getProperty("inputMix")); //Will output [0.4,0.6,0.2], the default value from the effect definition.
     *
     */
    getProperty(name) {
        return this._properties[name].value;
    }

    /**
     * Destroy and clean-up the node.
     */
    destroy() {
        super.destroy();
        //destrpy texutres for any texture properties
        for (let propertyName in this._properties) {
            let propertyValue = this._properties[propertyName].value;
            if (propertyValue instanceof Image) {
                this._gl.deleteTexture(this._properties[propertyName].texture);
                this._texture = undefined;
            }
        }
        //Destroy main
        this._gl.deleteTexture(this._texture);
        this._texture = undefined;
        //Detach shaders
        this._gl.detachShader(this._program, this._vertexShader);
        this._gl.detachShader(this._program, this._fragmentShader);
        //Delete shaders
        this._gl.deleteShader(this._vertexShader);
        this._gl.deleteShader(this._fragmentShader);
        //Delete program
        this._gl.deleteProgram(this._program);
        //Delete Framebuffer
        this._gl.deleteFramebuffer(this._framebuffer);
    }

    _update(currentTime) {
        this._currentTime = currentTime;
    }

    _seek(currentTime) {
        this._currentTime = currentTime;
    }

    _render() {
        this._rendered = true;
        let gl = this._gl;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

        gl.useProgram(this._program);

        //upload the default uniforms
        gl.uniform1f(this._currentTimeLocation, parseFloat(this._currentTime));

        for (let propertyName in this._properties) {
            let propertyValue = this._properties[propertyName].value;
            let propertyType = this._properties[propertyName].type;
            let propertyLocation = this._properties[propertyName].location;
            if (propertyType !== "uniform") continue;

            if (typeof propertyValue === "number") {
                gl.uniform1f(propertyLocation, propertyValue);
            } else if (Object.prototype.toString.call(propertyValue) === "[object Array]") {
                if (propertyValue.length === 1) {
                    gl.uniform1fv(propertyLocation, propertyValue);
                } else if (propertyValue.length === 2) {
                    gl.uniform2fv(propertyLocation, propertyValue);
                } else if (propertyValue.length === 3) {
                    gl.uniform3fv(propertyLocation, propertyValue);
                } else if (propertyValue.length === 4) {
                    gl.uniform4fv(propertyLocation, propertyValue);
                } else {
                    console.debug(
                        "Shader parameter",
                        propertyName,
                        "is too long an array:",
                        propertyValue
                    );
                }
            } else if (propertyValue instanceof Image) {
                let texture = this._properties[propertyName].texture;
                let textureUnit = this._properties[propertyName].textureUnit;
                let textureUnitIndex = this._properties[propertyName].textureUnit;
                updateTexture(gl, texture, propertyValue);

                gl.activeTexture(textureUnit);
                gl.uniform1i(propertyLocation, textureUnitIndex);
                gl.bindTexture(gl.TEXTURE_2D, texture);
            } else {
                //TODO - add tests for textures
                /*gl.activeTexture(gl.TEXTURE0 + textureOffset);
                gl.uniform1i(parameterLoctation, textureOffset);
                gl.bindTexture(gl.TEXTURE_2D, textures[textureOffset-1]);*/
            }
        }
    }
}

export { TYPE as PROCESSINGTYPE };

export default ProcessingNode;