Newer
Older
pixi.js / src / core / renderers / webgl / shaders / Shader.js
@Chad Engler Chad Engler on 7 Jan 2015 13 KB update colorAttribute to be aColor
var utils = require('../../../utils');

/**
 * @class
 * @namespace PIXI
 * @param [fragmentSrc] {string} The source of the fragment shader.
 * @param [vertexSrc] {string} The source of the vertex shader.
 */
function Shader(gl, fragmentSrc, vertexSrc, customUniforms, customAttributes) {
    /**
     * @member {number}
     * @readonly
     */
    this.uuid = utils.uuid();

    /**
     * @member {WebGLContext}
     * @readonly
     */
    this.gl = gl;

    /**
     * The WebGL program.
     * @member {WebGLProgram}
     * @readonly
     */
    this.program = null;

    this.uniforms = {
        uSampler:           { type: 'sampler2D', value: 0 },
        projectionVector:   { type: '2f', value: { x: 0, y: 0 } },
        offsetVector:       { type: '2f', value: { x: 0, y: 0 } },
        dimensions:         { type: '4f', value: new Float32Array(4) }
    };

    for (var u in customUniforms) {
        this.uniforms[u] = customUniforms[u];
    }

    this.attributes = {
        aVertexPosition:    0,
        aTextureCoord:      0,
        aColor:             0
    };

    for (var a in customAttributes) {
        this.attributes[a] = customAttributes[a];
    }

    this.textureCount = 0;

    /**
     * The vertex shader.
     * @member {Array}
     */
    this.vertexSrc = vertexSrc || [
        'attribute vec2 aVertexPosition;',
        'attribute vec2 aTextureCoord;',
        'attribute vec4 aColor;',

        'uniform vec2 projectionVector;',
        'uniform vec2 offsetVector;',

        'varying vec2 vTextureCoord;',
        'varying vec4 vColor;',

        'const vec2 center = vec2(-1.0, 1.0);',

        'void main(void) {',
        '   gl_Position = vec4( ((aVertexPosition + offsetVector) / projectionVector) + center , 0.0, 1.0);',
        '   vTextureCoord = aTextureCoord;',
        '   vColor = vec4(aColor.rgb * aColor.a, aColor.a);',
        '}'
    ].join('\n');

    /**
     * The fragment shader.
     * @member {Array}
     */
    this.fragmentSrc = fragmentSrc || [
        'precision lowp float;',

        'varying vec2 vTextureCoord;',
        'varying vec4 vColor;',

        'uniform sampler2D uSampler;',

        'void main(void) {',
        '   gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor ;',
        '}'
    ].join('\n');

    this.init();
}

Shader.prototype.constructor = Shader;
module.exports = Shader;

Shader.prototype.init = function () {
    this.compile();

    this.gl.useProgram(this.program);

    this.cacheUniformLocations(this.builtInUniforms.concat(Object.keys(this.uniforms)));
    this.cacheAttributeLocations(this.builtInAttributes.concat(Object.keys(this.attributes)));
};

Shader.prototype.cacheUniformLocations = function (keys) {
    for (var i = 0; i < keys.length; ++i) {
        this.uniforms[keys[i]]._location = this.gl.getUniformLocation(this.program, keys[i]);
    }
};

Shader.prototype.cacheAttributeLocations = function (keys) {
    for (var i = 0; i < keys.length; ++i) {
        this.attributes[keys[i]] = this.gl.getAttribLocation(this.program, keys[i]);
    }

    // TODO: Check if this is needed anymore...
    // Begin worst hack eva //

    // WHY??? ONLY on my chrome pixel the line above returns -1 when using filters?
    // maybe its something to do with the current state of the gl context.
    // I'm convinced this is a bug in the chrome browser as there is NO reason why this should be returning -1 especially as it only manifests on my chrome pixel
    // If theres any webGL people that know why could happen please help :)
    if (this._shader.attributes.aColor === -1) {
        this._shader.attributes.aColor = 2;
    }

    // End worst hack eva //
};

Shader.prototype.compile = function () {
    var gl = this.gl;

    var glVertShader = this._glCompile(gl.VERTEX_SHADER, this.vertexSrc);
    var glFragShader = this._glCompile(gl.FRAGMENT_SHADER, this.fragmentSrc);

    var program = gl.createProgram();

    gl.attachShader(program, glVertShader);
    gl.attachShader(program, glFragShader);
    gl.linkProgram(program);

    // if linking fails, then log and cleanup
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        window.console.error('Pixi.js Error: Could not initialize shader.');
        window.console.error('gl.VALIDATE_STATUS', gl.getProgramParameter(program, gl.VALIDATE_STATUS));
        window.console.error('gl.getError()', gl.getError());

        gl.deleteProgram(program);
        program = null;
    }

    // if there is a program info log, log it
    if (gl.getProgramInfoLog(program) !== '') {
        window.console.warn('Pixi.js Warning: gl.getProgramInfoLog()', gl.getProgramInfoLog(program));
    }

    // clean up some shaders
    gl.deleteShader(glVertShader);
    gl.deleteShader(glFragShader);

    return (this.program = program);
};

Shader.prototype.syncUniforms = function () {
    var gl = this.gl;

    this.textureCount = 1;

    for (var key in this.uniforms) {
        var uniform = this.uniforms[key],
            location = uniform._location,
            value = uniform.value,
            i, il;

        switch (uniform.type) {
            case 'i':
            case '1i':
                gl.uniform1i(location, value);
                break;

            case 'f':
            case '1f':
                gl.uniform1f(location, value);
                break;

            case '2f':
                gl.uniform2f(location, value[0], value[1]);
                break;

            case '3f':
                gl.uniform3f(location, value[0], value[1], value[2]);
                break;

            case '4f':
                gl.uniform4f(location, value[0], value[1], value[2], value[3]);
                break;

            // a 2D Point object
            case 'v2':
                gl.uniform2f(location, value.x, value.y);
                break;

            // a 3D Point object
            case 'v3':
                gl.uniform3f(location, value.x, value.y, value.z);
                break;

            // a 4D Point object
            case 'v4':
                gl.uniform4f(location, value.x, value.y, value.z, value.w);
                break;

            case '1iv':
                gl.uniform1iv(location, value);
                break;

            case '3iv':
                gl.uniform3iv(location, value);
                break;

            case '1fv':
                gl.uniform1fv(location, value);
                break;

            case '2fv':
                gl.uniform2fv(location, value);
                break;

            case '3fv':
                gl.uniform3fv(location, value);
                break;

            case '4fv':
                gl.uniform4fv(location, value);
                break;

            case 'm2':
            case 'mat2':
            case 'Matrix2fv':
                gl.uniformMatrix2fv(location, uniform.transpose, value);
                break;

            case 'm3':
            case 'mat3':
            case 'Matrix3fv':
                gl.uniformMatrix3fv(location, uniform.transpose, value);
                break;

            case 'm4':
            case 'mat4':
            case 'Matrix4fv':
                gl.uniformMatrix4fv(location, uniform.transpose, value);
                break;

            // a Color Value
            case 'c':
                if (typeof value === 'number') {
                    value = utils.hex2rgb(value);
                }

                gl.uniform3f(location, value[0], value[1], value[2]);
                break;

            // flat array of integers (JS or typed array)
            case 'iv1':
                gl.uniform1iv(location, value);
                break;

            // flat array of integers with 3 x N size (JS or typed array)
            case 'iv':
                gl.uniform3iv(location, value);
                break;

            // flat array of floats (JS or typed array)
            case 'fv1':
                gl.uniform1fv(location, value);
                break;

            // flat array of floats with 3 x N size (JS or typed array)
            case 'fv':
                gl.uniform3fv(location, value);
                break;

            // array of 2D Point objects
            case 'v2v':
                if (!uniform._array) {
                    uniform._array = new Float32Array(2 * value.length);
                }

                for (i = 0, il = value.length; i < il; ++i) {
                    uniform._array[i * 2]       = value[i].x;
                    uniform._array[i * 2 + 1]   = value[i].y;
                }

                gl.uniform2fv(location, uniform._array);
                break;

            // array of 3D Point objects
            case 'v3v':
                if (!uniform._array) {
                    uniform._array = new Float32Array(3 * value.length);
                }

                for (i = 0, il = value.length; i < il; ++i) {
                    uniform._array[i * 3]       = value[i].x;
                    uniform._array[i * 3 + 1]   = value[i].y;
                    uniform._array[i * 3 + 2]   = value[i].z;

                }

                gl.uniform3fv(location, uniform._array);
                break;

            // array of 4D Point objects
            case 'v4v':
                if (!uniform._array) {
                    uniform._array = new Float32Array(4 * value.length);
                }

                for (i = 0, il = value.length; i < il; ++i) {
                    uniform._array[i * 4]       = value[i].x;
                    uniform._array[i * 4 + 1]   = value[i].y;
                    uniform._array[i * 4 + 2]   = value[i].z;
                    uniform._array[i * 4 + 3]   = value[i].w;

                }

                gl.uniform4fv(location, uniform._array);
                break;

            case 't':
            case 'sampler2D':
                if (!uniform.value || !uniform.value.baseTexture || !uniform.value.baseTexture.hasLoaded) {
                    break;
                }

                // activate this texture
                gl.activeTexture(gl['TEXTURE' + this.textureCount]);

                // bind the texture
                gl.bindTexture(gl.TEXTURE_2D, uniform.value.baseTexture._glTextures[gl.id]);

                // set uniform to texture index
                gl.uniform1i(uniform._location, this.textureCount);

                // increment next texture id
                this.textureCount++;

                // initialize the texture if we haven't yet
                if (!uniform._init) {
                    this.initSampler2D(uniform);

                    uniform._init = true;
                }
                break;

            default:
                window.console.warn('Pixi.js Shader Warning: Unknown uniform type: ' + uniform.type);
        }
    }
};


/**
 * Initialises a Sampler2D uniform (which may only be available later on after initUniforms once the texture has loaded)
 *
 */
Shader.prototype.initSampler2D = function (uniform) {
    var gl = this.gl;

    //  Extended texture data
    if (uniform.textureData) {
        var data = uniform.textureData;

        // GLTexture = mag linear, min linear_mipmap_linear, wrap repeat + gl.generateMipmap(gl.TEXTURE_2D);
        // GLTextureLinear = mag/min linear, wrap clamp
        // GLTextureNearestRepeat = mag/min NEAREST, wrap repeat
        // GLTextureNearest = mag/min nearest, wrap clamp
        // AudioTexture = whatever + luminance + width 512, height 2, border 0
        // KeyTexture = whatever + luminance + width 256, height 2, border 0

        //  magFilter can be: gl.LINEAR, gl.LINEAR_MIPMAP_LINEAR or gl.NEAREST
        //  wrapS/T can be: gl.CLAMP_TO_EDGE or gl.REPEAT

        var magFilter = (data.magFilter) ? data.magFilter : gl.LINEAR;
        var minFilter = (data.minFilter) ? data.minFilter : gl.LINEAR;
        var wrapS = (data.wrapS) ? data.wrapS : gl.CLAMP_TO_EDGE;
        var wrapT = (data.wrapT) ? data.wrapT : gl.CLAMP_TO_EDGE;
        var format = (data.luminance) ? gl.LUMINANCE : gl.RGBA;

        if (data.repeat) {
            wrapS = gl.REPEAT;
            wrapT = gl.REPEAT;
        }

        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY);

        if (data.width) {
            var width = (data.width) ? data.width : 512;
            var height = (data.height) ? data.height : 2;
            var border = (data.border) ? data.border : 0;

            // void texImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, ArrayBufferView? pixels);
            gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, border, format, gl.UNSIGNED_BYTE, null);
        }
        else {
            //  void texImage2D(GLenum target, GLint level, GLenum internalformat, GLenum format, GLenum type, ImageData? pixels);
            gl.texImage2D(gl.TEXTURE_2D, 0, format, gl.RGBA, gl.UNSIGNED_BYTE, uniform.value.baseTexture.source);
        }

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
    }
};

/**
 * Destroys the shader.
 *
 */
Shader.prototype.destroy = function () {
    this.gl.deleteProgram(this.program);

    this.gl = null;
    this.uniforms = null;
    this.attributes = null;

    this.vertexSrc = null;
    this.fragmentSrc = null;
};

Shader.prototype._glCompile = function (type, src) {
    var shader = this.gl.createShader(type);

    this.gl.shaderSource(shader, src);
    this.gl.compileShader(shader);

    if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
        window.console.log(this.gl.getShaderInfoLog(shader));
        return null;
    }

    return shader;
};