import Base from './core/Base'; import util from './core/util'; import colorUtil from './core/color'; var parseColor = colorUtil.parseToFloat; var programKeyCache = {}; function getDefineCode(defines) { var defineKeys = Object.keys(defines); defineKeys.sort(); var defineStr = []; // Custom Defines for (var i = 0; i < defineKeys.length; i++) { var key = defineKeys[i]; var value = defines[key]; if (value === null) { defineStr.push(key); } else{ defineStr.push(key + ' ' + value.toString()); } } return defineStr.join('\n'); } function getProgramKey(vertexDefines, fragmentDefines, enabledTextures) { enabledTextures.sort(); var defineStr = []; for (var i = 0; i < enabledTextures.length; i++) { var symbol = enabledTextures[i]; defineStr.push(symbol); } var key = getDefineCode(vertexDefines) + '\n' + getDefineCode(fragmentDefines) + '\n' + defineStr.join('\n'); if (programKeyCache[key]) { return programKeyCache[key]; } var id = util.genGUID(); programKeyCache[key] = id; return id; } /** * Material defines the appearance of mesh surface, like `color`, `roughness`, `metalness`, etc. * It contains a {@link clay.Shader} and corresponding uniforms. * * Here is a basic example to create a standard material ```js var material = new clay.Material({ shader: new clay.Shader( clay.Shader.source('clay.vertex'), clay.Shader.source('clay.fragment') ) }); ``` * @constructor clay.Material * @extends clay.core.Base */ var Material = Base.extend(function () { return /** @lends clay.Material# */ { /** * @type {string} */ name: '', /** * @type {Object} */ // uniforms: null, /** * @type {clay.Shader} */ // shader: null, /** * @type {boolean} */ depthTest: true, /** * @type {boolean} */ depthMask: true, /** * @type {boolean} */ transparent: false, /** * Blend func is a callback function when the material * have custom blending * The gl context will be the only argument passed in tho the * blend function * Detail of blend function in WebGL: * http://www.khronos.org/registry/gles/specs/2.0/es_full_spec_2.0.25.pdf * * Example : * function(_gl) { * _gl.blendEquation(_gl.FUNC_ADD); * _gl.blendFunc(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA); * } */ blend: null, /** * If update texture status automatically. */ autoUpdateTextureStatus: true, uniforms: {}, vertexDefines: {}, fragmentDefines: {}, _textureStatus: {}, // shadowTransparentMap : null // PENDING enable the uniform that only used in shader. _enabledUniforms: null, }; }, function () { if (!this.name) { this.name = 'MATERIAL_' + this.__uid__; } if (this.shader) { // Keep status, mainly preset uniforms, vertexDefines and fragmentDefines this.attachShader(this.shader, true); } }, /** @lends clay.Material.prototype */ { precision: 'highp', /** * Set material uniform * @example * mat.setUniform('color', [1, 1, 1, 1]); * @param {string} symbol * @param {number|array|clay.Texture|ArrayBufferView} value */ setUniform: function (symbol, value) { if (value === undefined) { console.warn('Uniform value "' + symbol + '" is undefined'); } var uniform = this.uniforms[symbol]; if (uniform) { if (typeof value === 'string') { // Try to parse as a color. Invalid color string will return null. value = parseColor(value) || value; } uniform.value = value; if (this.autoUpdateTextureStatus && uniform.type === 't') { if (value) { this.enableTexture(symbol); } else { this.disableTexture(symbol); } } } }, /** * @param {Object} obj */ setUniforms: function(obj) { for (var key in obj) { var val = obj[key]; this.setUniform(key, val); } }, /** * @param {string} symbol * @return {boolean} */ isUniformEnabled: function (symbol) { return this._enabledUniforms.indexOf(symbol) >= 0; }, getEnabledUniforms: function () { return this._enabledUniforms; }, getTextureUniforms: function () { return this._textureUniforms; }, /** * Alias of setUniform and setUniforms * @param {object|string} symbol * @param {number|array|clay.Texture|ArrayBufferView} [value] */ set: function (symbol, value) { if (typeof(symbol) === 'object') { for (var key in symbol) { var val = symbol[key]; this.setUniform(key, val); } } else { this.setUniform(symbol, value); } }, /** * Get uniform value * @param {string} symbol * @return {number|array|clay.Texture|ArrayBufferView} */ get: function (symbol) { var uniform = this.uniforms[symbol]; if (uniform) { return uniform.value; } }, /** * Attach a shader instance * @param {clay.Shader} shader * @param {boolean} keepStatus If try to keep uniform and texture */ attachShader: function(shader, keepStatus) { var originalUniforms = this.uniforms; // Ignore if uniform can use in shader. this.uniforms = shader.createUniforms(); this.shader = shader; var uniforms = this.uniforms; this._enabledUniforms = Object.keys(uniforms); // Make sure uniforms are set in same order to avoid texture slot wrong this._enabledUniforms.sort(); this._textureUniforms = this._enabledUniforms.filter(function (uniformName) { var type = this.uniforms[uniformName].type; return type === 't' || type === 'tv'; }, this); var originalVertexDefines = this.vertexDefines; var originalFragmentDefines = this.fragmentDefines; this.vertexDefines = util.clone(shader.vertexDefines); this.fragmentDefines = util.clone(shader.fragmentDefines); if (keepStatus) { for (var symbol in originalUniforms) { if (uniforms[symbol]) { uniforms[symbol].value = originalUniforms[symbol].value; } } util.defaults(this.vertexDefines, originalVertexDefines); util.defaults(this.fragmentDefines, originalFragmentDefines); } var textureStatus = {}; for (var key in shader.textures) { textureStatus[key] = { shaderType: shader.textures[key].shaderType, type: shader.textures[key].type, enabled: (keepStatus && this._textureStatus[key]) ? this._textureStatus[key].enabled : false }; } this._textureStatus = textureStatus; this._programKey = ''; }, /** * Clone a new material and keep uniforms, shader will not be cloned * @return {clay.Material} */ clone: function () { var material = new this.constructor({ name: this.name, shader: this.shader }); for (var symbol in this.uniforms) { material.uniforms[symbol].value = this.uniforms[symbol].value; } material.depthTest = this.depthTest; material.depthMask = this.depthMask; material.transparent = this.transparent; material.blend = this.blend; material.vertexDefines = util.clone(this.vertexDefines); material.fragmentDefines = util.clone(this.fragmentDefines); material.enableTexture(this.getEnabledTextures()); material.precision = this.precision; return material; }, /** * Add a #define macro in shader code * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol * @param {number} [val] */ define: function (shaderType, symbol, val) { var vertexDefines = this.vertexDefines; var fragmentDefines = this.fragmentDefines; if (shaderType !== 'vertex' && shaderType !== 'fragment' && shaderType !== 'both' && arguments.length < 3 ) { // shaderType default to be 'both' val = symbol; symbol = shaderType; shaderType = 'both'; } val = val != null ? val : null; if (shaderType === 'vertex' || shaderType === 'both') { if (vertexDefines[symbol] !== val) { vertexDefines[symbol] = val; // Mark as dirty this._programKey = ''; } } if (shaderType === 'fragment' || shaderType === 'both') { if (fragmentDefines[symbol] !== val) { fragmentDefines[symbol] = val; if (shaderType !== 'both') { this._programKey = ''; } } } }, /** * Remove a #define macro in shader code * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol */ undefine: function (shaderType, symbol) { if (shaderType !== 'vertex' && shaderType !== 'fragment' && shaderType !== 'both' && arguments.length < 2 ) { // shaderType default to be 'both' symbol = shaderType; shaderType = 'both'; } if (shaderType === 'vertex' || shaderType === 'both') { if (this.isDefined('vertex', symbol)) { delete this.vertexDefines[symbol]; // Mark as dirty this._programKey = ''; } } if (shaderType === 'fragment' || shaderType === 'both') { if (this.isDefined('fragment', symbol)) { delete this.fragmentDefines[symbol]; if (shaderType !== 'both') { this._programKey = ''; } } } }, /** * If macro is defined in shader. * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol */ isDefined: function (shaderType, symbol) { // PENDING hasOwnProperty ? switch (shaderType) { case 'vertex': return this.vertexDefines[symbol] !== undefined; case 'fragment': return this.fragmentDefines[symbol] !== undefined; } }, /** * Get macro value defined in shader. * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol */ getDefine: function (shaderType, symbol) { switch(shaderType) { case 'vertex': return this.vertexDefines[symbol]; case 'fragment': return this.fragmentDefines[symbol]; } }, /** * Enable a texture, actually it will add a #define macro in the shader code * For example, if texture symbol is diffuseMap, it will add a line `#define DIFFUSEMAP_ENABLED` in the shader code * @param {string} symbol */ enableTexture: function (symbol) { if (Array.isArray(symbol)) { for (var i = 0; i < symbol.length; i++) { this.enableTexture(symbol[i]); } return; } var status = this._textureStatus[symbol]; if (status) { var isEnabled = status.enabled; if (!isEnabled) { status.enabled = true; this._programKey = ''; } } }, /** * Enable all textures used in the shader */ enableTexturesAll: function () { var textureStatus = this._textureStatus; for (var symbol in textureStatus) { textureStatus[symbol].enabled = true; } this._programKey = ''; }, /** * Disable a texture, it remove a #define macro in the shader * @param {string} symbol */ disableTexture: function (symbol) { if (Array.isArray(symbol)) { for (var i = 0; i < symbol.length; i++) { this.disableTexture(symbol[i]); } return; } var status = this._textureStatus[symbol]; if (status) { var isDisabled = ! status.enabled; if (!isDisabled) { status.enabled = false; this._programKey = ''; } } }, /** * Disable all textures used in the shader */ disableTexturesAll: function () { var textureStatus = this._textureStatus; for (var symbol in textureStatus) { textureStatus[symbol].enabled = false; } this._programKey = ''; }, /** * If texture of given type is enabled. * @param {string} symbol * @return {boolean} */ isTextureEnabled: function (symbol) { var textureStatus = this._textureStatus; return !!textureStatus[symbol] && textureStatus[symbol].enabled; }, /** * Get all enabled textures * @return {string[]} */ getEnabledTextures: function () { var enabledTextures = []; var textureStatus = this._textureStatus; for (var symbol in textureStatus) { if (textureStatus[symbol].enabled) { enabledTextures.push(symbol); } } return enabledTextures; }, /** * Mark defines are updated. */ dirtyDefines: function () { this._programKey = ''; }, getProgramKey: function () { if (!this._programKey) { this._programKey = getProgramKey( this.vertexDefines, this.fragmentDefines, this.getEnabledTextures() ); } return this._programKey; } }); export default Material;