StandardMaterial.js

import Material from './Material';

import Shader from './Shader';
import standardEssl from './shader/source/standard.glsl.js';
import util from './core/util';

// Import standard shader
Shader['import'](standardEssl);

var TEXTURE_PROPERTIES = ['diffuseMap', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'environmentMap', 'brdfLookup', 'ssaoMap', 'aoMap'];
var SIMPLE_PROPERTIES = ['color', 'emission', 'emissionIntensity', 'alpha', 'roughness', 'metalness', 'uvRepeat', 'uvOffset', 'aoIntensity', 'alphaCutoff', 'normalScale'];
var PROPERTIES_CHANGE_SHADER = ['linear', 'encodeRGBM', 'decodeRGBM', 'doubleSided', 'alphaTest', 'roughnessChannel', 'metalnessChannel', 'environmentMapPrefiltered'];

var NUM_DEFINE_MAP = {
    'roughnessChannel': 'ROUGHNESS_CHANNEL',
    'metalnessChannel': 'METALNESS_CHANNEL'
};
var BOOL_DEFINE_MAP = {
    'linear': 'SRGB_DECODE',
    'encodeRGBM': 'RGBM_ENCODE',
    'decodeRGBM': 'RGBM_DECODE',
    'doubleSided': 'DOUBLE_SIDED',
    'alphaTest': 'ALPHA_TEST',
    'environmentMapPrefiltered': 'ENVIRONMENTMAP_PREFILTER'
};


var standardShader;
/**
 * Standard material without custom shader.
 * @constructor clay.StandardMaterial
 * @extends clay.Base
 * @example
 * var mat = new clay.StandardMaterial({
 *     color: [1, 1, 1],
 *     diffuseMap: diffuseTexture
 * });
 * mat.roughness = 1;
 */
var StandardMaterial = Material.extend(function () {
    if (!standardShader) {
        standardShader = new Shader(Shader.source('clay.standardMR.vertex'), Shader.source('clay.standardMR.fragment'));
    }
    return /** @lends clay.StandardMaterial# */ {
        shader: standardShader
    };
}, function (option) {
    // PENDING
    util.extend(this, option);
    // Extend after shader is created.
    util.defaults(this, /** @lends clay.StandardMaterial# */  {
        /**
         * @type {Array.<number>}
         * @default [1, 1, 1]
         */
        color: [1, 1, 1],

        /**
         * @type {Array.<number>}
         * @default [0, 0, 0]
         */
        emission: [0, 0, 0],

        /**
         * @type {number}
         * @default 0
         */
        emissionIntensity: 0,

        /**
         * @type {number}
         * @default 0.5
         */
        roughness: 0.5,

        /**
         * @type {number}
         * @default 0
         */
        metalness: 0,

        /**
         * @type {number}
         * @default 1
         */
        alpha: 1,

        /**
         * @type {boolean}
         */
        alphaTest: false,

        /**
         * Cutoff threshold for alpha test
         * @type {number}
         */
        alphaCutoff: 0.9,

        /**
         * Scalar multiplier applied to each normal vector of normal texture.
         *
         * @type {number}
         *
         * XXX This value is considered only if a normal texture is specified.
         */
        normalScale: 1.0,

        /**
         * @type {boolean}
         */
        // TODO Must disable culling.
        doubleSided: false,

        /**
         * @type {clay.Texture2D}
         */
        diffuseMap: null,

        /**
         * @type {clay.Texture2D}
         */
        normalMap: null,

        /**
         * @type {clay.Texture2D}
         */
        roughnessMap: null,

        /**
         * @type {clay.Texture2D}
         */
        metalnessMap: null,
        /**
         * @type {clay.Texture2D}
         */
        emissiveMap: null,

        /**
         * @type {clay.TextureCube}
         */
        environmentMap: null,

        /**
         * @type {clay.BoundingBox}
         */
        environmentBox: null,
        /**
         * BRDF Lookup is generated by clay.util.cubemap.integrateBrdf
         * @type {clay.Texture2D}
         */
        brdfLookup: null,

        /**
         * @type {clay.Texture2D}
         */
        ssaoMap: null,

        /**
         * @type {clay.Texture2D}
         */
        aoMap: null,

        /**
         * @type {Array.<number>}
         * @default [1, 1]
         */
        uvRepeat: [1, 1],

        /**
         * @type {Array.<number>}
         * @default [0, 0]
         */
        uvOffset: [0, 0],

        /**
         * @type {number}
         * @default 1
         */
        aoIntensity: 1,

        /**
         * @type {boolean}
         */
        environmentMapPrefiltered: false,

        /**
         * @type {boolean}
         */
        linear: false,

        /**
         * @type {boolean}
         */
        encodeRGBM: false,

        /**
         * @type {boolean}
         */
        decodeRGBM: false,

        /**
         * @type {Number}
         */
        roughnessChannel: 0,
        /**
         * @type {Number}
         */
        metalnessChannel: 1
    });
}, {
    clone: function () {
        var material = new StandardMaterial({
            name: this.name
        });
        TEXTURE_PROPERTIES.forEach(function (propName) {
            if (this[propName]) {
                material[propName] = this[propName];
            }
        }, this);
        SIMPLE_PROPERTIES.concat(PROPERTIES_CHANGE_SHADER).forEach(function (propName) {
            material[propName] = this[propName];
        }, this);
        return material;
    }
});

SIMPLE_PROPERTIES.forEach(function (propName) {
    Object.defineProperty(StandardMaterial.prototype, propName, {
        get: function () {
            return this.get(propName);
        },
        set: function (value) {
            this.setUniform(propName, value);
        }
    });
});

TEXTURE_PROPERTIES.forEach(function (propName) {
    Object.defineProperty(StandardMaterial.prototype, propName, {
        get: function () {
            return this.get(propName);
        },
        set: function (value) {
            this.setUniform(propName, value);
        }
    });
});

PROPERTIES_CHANGE_SHADER.forEach(function (propName) {
    var privateKey = '_' + propName;
    Object.defineProperty(StandardMaterial.prototype, propName, {
        get: function () {
            return this[privateKey];
        },
        set: function (value) {
            this[privateKey] = value;
            if (propName in NUM_DEFINE_MAP) {
                var defineName = NUM_DEFINE_MAP[propName];
                this.define('fragment', defineName, value);
            }
            else {
                var defineName = BOOL_DEFINE_MAP[propName];
                value ? this.define('fragment', defineName) : this.undefine('fragment', defineName);
            }
        }
    });
});

Object.defineProperty(StandardMaterial.prototype, 'environmentBox', {
    get: function () {
        var envBox = this._environmentBox;
        if (envBox) {
            envBox.min.setArray(this.get('environmentBoxMin'));
            envBox.max.setArray(this.get('environmentBoxMax'));
        }
        return envBox;
    },

    set: function (value) {
        this._environmentBox = value;

        var uniforms = this.uniforms = this.uniforms || {};
        uniforms['environmentBoxMin'] = uniforms['environmentBoxMin'] || {
            value: null
        };
        uniforms['environmentBoxMax'] = uniforms['environmentBoxMax'] || {
            value: null
        };

        // TODO Can't detect operation like box.min = new Vector()
        if (value) {
            this.setUniform('environmentBoxMin', value.min.array);
            this.setUniform('environmentBoxMax', value.max.array);
        }

        if (value) {
            this.define('fragment', 'PARALLAX_CORRECTED');
        }
        else {
            this.undefine('fragment', 'PARALLAX_CORRECTED');
        }
    }
});

export default StandardMaterial;