UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

762 lines (670 loc) 27.9 kB
/** * Effect base class. * * |-- copyright and license --| * @module Shaku * @file shaku\src\gfx\effects\effect.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const Color = require('../../utils/color.js'); const { TextureFilterModes } = require('../texture_filter_modes'); const { TextureWrapMode, TextureWrapModes } = require('../texture_wrap_modes'); const Matrix = require('../../utils/matrix.js'); const Vector2 = require('../../utils/vector2.js'); const TextureAssetBase = require('../../assets/texture_asset_base.js'); const _logger = require('../../logger.js').getLogger('gfx-effect'); // currently applied effect let _currEffect = null; // will store all supported depth funcs let _depthFuncs = null; /** * Effect base class. * An effect = vertex shader + fragment shader + uniforms & attributes + setup code. */ class Effect { /** * Create the effect. */ constructor() { this.#_build(Effect._gfx._internal.gl); } /** * Build the effect. * Called from gfx manager. * @private * @param {WebGl} gl WebGL context. */ #_build(gl) { // create program let program = gl.createProgram(); // build vertex shader { let shader = compileShader(this.constructor, gl, this.vertexCode, gl.VERTEX_SHADER); gl.attachShader(program, shader); } // build fragment shader { let shader = compileShader(this.constructor, gl, this.fragmentCode, gl.FRAGMENT_SHADER); gl.attachShader(program, shader); } // link program gl.linkProgram(program) // check for errors if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { _logger.error("Error linking shader program:"); _logger.error(gl.getProgramInfoLog(program)); throw new Error("Failed to link shader program."); } // store program and gl this._gl = gl; this._program = program; // a set of dynamically-created setters to set uniform values this.uniforms = {}; // dictionary to bind uniform to built-in roles, like main texture or color this._uniformBinds = {}; // values waiting to set as soon as the effect turns active this._pendingUniformValues = {}; this._pendingAttributeValues = {}; // initialize uniform setters for (let uniform in this.uniformTypes) { // get uniform location let uniformLocation = this._gl.getUniformLocation(this._program, uniform); if (uniformLocation === -1) { _logger.error("Could not find uniform: " + uniform); throw new Error(`Uniform named '${uniform}' was not found in shader code!`); } // get gl setter method let uniformData = this.uniformTypes[uniform]; if (!UniformTypes._values.has(uniformData.type)) { _logger.error("Uniform has invalid type: " + uniformData.type); throw new Error(`Uniform '${uniform}' have illegal value type '${uniformData.type}'!`); } // build setter method for matrices if (uniformData.type === UniformTypes.Matrix) { (function(_this, name, location, method) { _this.uniforms[name] = (mat) => { if (_currEffect !== _this) { _this._pendingUniformValues[name] = [mat]; return; } _this._gl[method](location, false, mat); } })(this, uniform, uniformLocation, uniformData.type); } // build setter method for textures else if (uniformData.type === UniformTypes.Texture) { (function(_this, name, location, method) { _this.uniforms[name] = (texture, index) => { if (_currEffect !== _this) { _this._pendingUniformValues[name] = [texture, index]; return; } index = index || 0; const glTexture = texture._glTexture || texture; const textureCode = _this._gl['TEXTURE' + (index || 0)]; _this._gl.activeTexture(textureCode); _this._gl.bindTexture(_this._gl.TEXTURE_2D, glTexture); _this._gl.uniform1i(location, (index || 0)); if (texture.filter) { _setTextureFilter(_this._gl, texture.filter); } if (texture.wrapMode) { _setTextureWrapMode(_this._gl, texture.wrapMode); } } })(this, uniform, uniformLocation, uniformData.type); } // build setter method for colors else if (uniformData.type === UniformTypes.Color) { (function(_this, name, location, method) { _this.uniforms[name] = (v1, v2, v3, v4) => { if (_currEffect !== _this) { _this._pendingUniformValues[name] = [v1, v2, v3, v4]; return; } if (v1.isColor) { _this._gl[method](location, v1.floatArray); } else { _this._gl[method](location, v1, v2, v3, v4); } } })(this, uniform, uniformLocation, uniformData.type); } // build setter method for other types else { (function(_this, name, location, method) { _this.uniforms[name] = (v1, v2, v3, v4) => { if (_currEffect !== _this) { _this._pendingUniformValues[name] = [v1, v2, v3, v4]; return; } _this._gl[method](location, v1, v2, v3, v4); } })(this, uniform, uniformLocation, uniformData.type); } // set binding let bindTo = uniformData.bind; if (bindTo) { this._uniformBinds[bindTo] = uniform; } } // a set of dynamically-created setters to set attribute values this.attributes = {}; // dictionary to bind attribute to built-in roles, like vertices positions or uvs this._attributeBinds = {}; // get attribute locations for (let attr in this.attributeTypes) { // get attribute location let attributeLocation = this._gl.getAttribLocation(this._program, attr); if (attributeLocation === -1) { _logger.error("Could not find attribute: " + attr); throw new Error(`Attribute named '${attr}' was not found in shader code!`); } // get attribute data let attributeData = this.attributeTypes[attr]; // build setter method (function(_this, name, location, data) { _this.attributes[name] = (buffer) => { if (_currEffect !== _this) { _this._pendingAttributeValues[name] = [buffer]; return; } if (buffer) { _this._gl.bindBuffer(_this._gl.ARRAY_BUFFER, buffer); _this._gl.vertexAttribPointer(location, data.size, _this._gl[data.type] || _this._gl.FLOAT, data.normalize || false, data.stride || 0, data.offset || 0); _this._gl.enableVertexAttribArray(location); } else { _this._gl.disableVertexAttribArray(location); } } })(this, attr, attributeLocation, attributeData); // set binding let bindTo = attributeData.bind; if (bindTo) { this._attributeBinds[bindTo] = attr; } } // values we already set for this effect, so we won't set them again this._cachedValues = {}; } /** * Get a dictionary with all shaders uniforms. * Key = uniform name, as appears in shader code. * Value = { * type: UniformTypes to represent uniform type, * bind: Optional bind to one of the built-in uniforms. See Effect.UniformBinds for details. * } * @returns {*} Dictionary with uniforms descriptions. */ get uniformTypes() { throw new Error("Not Implemented!"); } /** * Get a dictionary with all shader attributes. * Key = attribute name, as appears in shader code. * Value = { * size: size of every value in this attribute. * type: attribute type. See Effect.AttributeTypes for details. * normalize: if true, will normalize values. * stride: optional stride. * offset: optional offset. * bind: Optional bind to one of the built-in attributes. See Effect.AttributeBinds for details. * } * @returns {*} Dictionary with attributes descriptions. */ get attributeTypes() { throw new Error("Not Implemented!"); } /** * Make this effect active. * @param {*} overrideFlags Optional flags to override in effect. * May include the following: enableDepthTest, enableFaceCulling, enableStencilTest, enableDithering. */ setAsActive(overrideFlags) { // use effect program this._gl.useProgram(this._program); // enable / disable some features overrideFlags = overrideFlags || {}; if ((overrideFlags.enableDepthTest !== undefined) ? overrideFlags.enableDepthTest : this.enableDepthTest) { this._gl.enable(this._gl.DEPTH_TEST); } else { this._gl.disable(this._gl.DEPTH_TEST); } if ((overrideFlags.enableFaceCulling !== undefined) ? overrideFlags.enableFaceCulling : this.enableFaceCulling) { this._gl.enable(this._gl.CULL_FACE); } else { this._gl.disable(this._gl.CULL_FACE); } if ((overrideFlags.enableStencilTest !== undefined) ? overrideFlags.enableStencilTest : this.enableStencilTest) { this._gl.enable(this._gl.STENCIL_TEST); } else { this._gl.disable(this._gl.STENCIL_TEST); } if ((overrideFlags.enableDithering !== undefined) ? overrideFlags.enableDithering : this.enableDithering) { this._gl.enable(this._gl.DITHER); } else { this._gl.disable(this._gl.DITHER); } // set polygon offset const polygonOffset = (overrideFlags.polygonOffset !== undefined) ? overrideFlags.polygonOffset : this.polygonOffset; if (polygonOffset) { this._gl.enable(this._gl.POLYGON_OFFSET_FILL); this._gl.polygonOffset(polygonOffset.factor || 0, polygonOffset.units || 0); } else { this._gl.disable(this._gl.POLYGON_OFFSET_FILL); this._gl.polygonOffset(0, 0); } // default depth func this._gl.depthFunc(overrideFlags.depthFunc || this.depthFunc); // set as active _currEffect = this; // set pending uniforms that were set while this effect was not active for (let key in this._pendingUniformValues) { this.uniforms[key](...this._pendingUniformValues[key]) } this._pendingUniformValues = {}; // set pending attributes that were set while this effect was not active for (let key in this._pendingAttributeValues) { this.attributes[key](...this._pendingAttributeValues[key]) } this._pendingAttributeValues = {}; // reset cached values this._cachedValues = {}; } /** * Get a uniform method from a bind key. * @param {UniformBinds} bindKey Uniform bind key. * @returns Uniform set method, or null if not set. */ getBoundUniform(bindKey) { let key = this._uniformBinds[bindKey]; if (key) { return this.uniforms[key] || null; } return null; } /** * Get this effect's vertex shader code, as string. * @returns {String} Vertex shader code. */ get vertexCode() { throw new Error("Not Implemented!"); } /** * Get this effect's fragment shader code, as string. * @returns {String} Fragment shader code. */ get fragmentCode() { throw new Error("Not Implemented!"); } /** * Should this effect enable depth test? */ get enableDepthTest() { return false; } /** * Should this effect enable face culling? */ get enableFaceCulling() { return false; } /** * Get depth func to use when rendering using this effect. * Use 'DepthFuncs' to get options. */ get depthFunc() { return Effect.DepthFuncs.LessEqual; } /** * Should this effect enable stencil test? */ get enableStencilTest() { return false; } /** * Should this effect enable dithering? */ get enableDithering() { return false; } /** * Get polygon offset factor, to apply on depth value before checking. * @returns {Boolean|*} Return false to disable polygon offset, or {factor, unit} to apply polygon offset. */ get polygonOffset() { return false; } /** * Get all supported depth funcs we can set. * @returns {*} Depth func options: {Never, Less, Equal, LessEqual, Greater, GreaterEqual, Always, NotEqual}. */ static get DepthFuncs() { if (!_depthFuncs) { const gl = Effect._gfx._internal.gl; _depthFuncs = { Never: gl.NEVER, Less: gl.LESS, Equal: gl.EQUAL, LessEqual: gl.LEQUAL, Greater: gl.GREATER, GreaterEqual: gl.GEQUAL, Always: gl.ALWAYS, NotEqual: gl.NOTEQUAL, }; Object.freeze(_depthFuncs); } return _depthFuncs; } /** * Set the main texture. * Note: this will only work for effects that utilize the 'MainTexture' uniform. * @param {TextureAssetBase} texture Texture to set. * @returns {Boolean} True if texture was changed, false if there was no need to change the texture. */ setTexture(texture) { // already using this texture? skip if (texture === this._cachedValues.texture) { return false; } // get texture uniform let uniform = this.getBoundUniform(Effect.UniformBinds.MainTexture); // set texture if (uniform) { // set texture value this._cachedValues.texture = texture; let glTexture = texture._glTexture || texture; this._gl.activeTexture(this._gl.TEXTURE0); this._gl.bindTexture(this._gl.TEXTURE_2D, glTexture); uniform(texture, 0); // set texture size let textWidth = this.getBoundUniform(Effect.UniformBinds.TextureWidth); if (textWidth) { textWidth(texture.width); } let textHeight = this.getBoundUniform(Effect.UniformBinds.TextureHeight); if (textHeight) { textHeight(texture.height); } // success return true; } // didn't set.. return false; } /** * Set the main tint color. * Note: this will only work for effects that utilize the 'Color' uniform. * @param {Color} color Color to set. */ setColor(color) { let uniform = this.getBoundUniform(Effect.UniformBinds.Color); if (uniform) { if (color.equals(this._cachedValues.color)) { return; } this._cachedValues.color = color.clone(); uniform(color.floatArray); } } /** * Set the projection matrix uniform. * Note: this will only work for effects that utilize the 'Projection' uniform. * @param {Matrix} matrix Matrix to set. */ setProjectionMatrix(matrix) { let uniform = this.getBoundUniform(Effect.UniformBinds.Projection); if (uniform) { if (matrix.equals(this._cachedValues.projection)) { return; } this._cachedValues.projection = matrix.clone(); uniform(matrix.values); } } /** * Set the world matrix uniform. * Note: this will only work for effects that utilize the 'World' uniform. * @param {Matrix} matrix Matrix to set. */ setWorldMatrix(matrix) { let uniform = this.getBoundUniform(Effect.UniformBinds.World); if (uniform) { uniform(matrix.values); } } /** * Set the view matrix uniform. * Note: this will only work for effects that utilize the 'View' uniform. * @param {Matrix} matrix Matrix to set. */ setViewMatrix(matrix) { let uniform = this.getBoundUniform(Effect.UniformBinds.View); if (uniform) { uniform(matrix.values); } } /** * Set outline params. * Note: this will only work for effects that utilize the 'OutlineWeight' and 'OutlineColor' uniforms. * @param {Number} weight Outline weight, range from 0.0 to 1.0. * @param {Color} color Outline color. */ setOutline(weight, color) { let weightUniform = this.getBoundUniform(Effect.UniformBinds.OutlineWeight); if (weightUniform) { weightUniform(weight); } let colorUniform = this.getBoundUniform(Effect.UniformBinds.OutlineColor); if (colorUniform) { colorUniform(color); } } /** * Set a factor to normalize UV values to be 0-1. * Note: this will only work for effects that utilize the 'UvNormalizationFactor' uniform. * @param {Vector2} factor Normalize UVs factor. */ setUvNormalizationFactor(factor) { uniform = this.getBoundUniform(Effect.UniformBinds.UvNormalizationFactor); if (uniform) { uniform(factor.x, factor.y); } } /** * Set the vertices position buffer. * Only works if there's an attribute type bound to 'Position'. * @param {WebGLBuffer} buffer Vertices position buffer. * @param {Boolean} forceSetBuffer If true, will always set buffer even if buffer is currently set. */ setPositionsAttribute(buffer, forceSetBuffer) { let attr = this._attributeBinds[Effect.AttributeBinds.Position]; if (attr) { if (!forceSetBuffer && buffer === this._cachedValues.positions) { return; } this._cachedValues.positions = buffer; this.attributes[attr](buffer); } } /** * Set the vertices texture coords buffer. * Only works if there's an attribute type bound to 'TextureCoords'. * @param {WebGLBuffer} buffer Vertices texture coords buffer. * @param {Boolean} forceSetBuffer If true, will always set buffer even if buffer is currently set. */ setTextureCoordsAttribute(buffer, forceSetBuffer) { let attr = this._attributeBinds[Effect.AttributeBinds.TextureCoords]; if (attr) { if (!forceSetBuffer && buffer === this._cachedValues.coords) { return; } this._cachedValues.coords = buffer; this.attributes[attr](buffer); } } /** * Return if this effect have colors attribute on vertices. * @returns {Boolean} True if got vertices color attribute. */ get hasVertexColor() { return Boolean(this._attributeBinds[Effect.AttributeBinds.Colors]); } /** * Set the vertices colors buffer. * Only works if there's an attribute type bound to 'Colors'. * @param {WebGLBuffer} buffer Vertices colors buffer. * @param {Boolean} forceSetBuffer If true, will always set buffer even if buffer is currently set. */ setColorsAttribute(buffer, forceSetBuffer) { let attr = this._attributeBinds[Effect.AttributeBinds.Colors]; if (attr) { if (!forceSetBuffer && buffer === this._cachedValues.colors) { return; } this._cachedValues.colors = buffer; this.attributes[attr](buffer); } } /** * Set the vertices normals buffer. * Only works if there's an attribute type bound to 'Normals'. * @param {WebGLBuffer} buffer Vertices normals buffer. * @param {Boolean} forceSetBuffer If true, will always set buffer even if buffer is currently set. */ setNormalsAttribute(buffer, forceSetBuffer) { let attr = this._attributeBinds[Effect.AttributeBinds.Normals]; if (attr) { if (!forceSetBuffer && buffer === this._cachedValues.normals) { return; } this._cachedValues.normals = buffer; this.attributes[attr](buffer); } } } /** * Build a shader. */ function compileShader(effectClass, gl, code, type) { let shader = gl.createShader(type); gl.shaderSource(shader, code); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { _logger.error(`Error compiling ${type === gl.VERTEX_SHADER ? "vertex" : "fragment"} shader for effect '${effectClass.name}':`); _logger.error(gl.getShaderInfoLog(shader)); throw new Error("Failed to compile a shader."); } return shader; } /** @typedef {String} UniformType */ /** * Uniform types enum. * @readonly * @enum {UniformType} */ const UniformTypes = { Texture: 'texture', Matrix: 'uniformMatrix4fv', Color: 'uniform4fv', Float: 'uniform1f', FloatArray: 'uniform1fv', Int: 'uniform1i', IntArray: 'uniform1iv', Float2: 'uniform2f', Float2Array: 'uniform2fv', Int2: 'uniform2i', Int2Array: 'uniform2iv', Float3: 'uniform3f', Float3Array: 'uniform3fv', Int3: 'uniform3i', Int3Array: 'uniform3iv', Float4: 'uniform4f', Float4Array: 'uniform4fv', Int4: 'uniform4i', Int4Array: 'uniform4iv', } Object.defineProperty(UniformTypes, '_values', { value: new Set(Object.values(UniformTypes)), writable: false, }); Object.freeze(UniformTypes); // attach uniform types to effect Effect.UniformTypes = UniformTypes; /** * Default uniform binds. * This is a set of commonly used uniforms and their names inside the shader code. * * Every bind here comes with a built-in method to set and is used internally by Shaku. * For example, if you want to include outline properties in your effect, you can use the 'OutlineWeight' and 'OutlineColor' binds (with matching name in the shader code). * When you use the built-in binds, Shaku will know how to set them itself when relevant, for example in text rendering Shaku will use the outline binds if they exist. * * If you don't use the built-in binds you can just call your uniforms however you like, but you'll need to set them all manually. * Shaku will not know how to set them. */ Effect.UniformBinds = { MainTexture: 'mainTexture', // bind uniform to be used as the main texture. Color: 'color', // bind uniform to be used as a main color. Projection: 'projection', // bind uniform to be used as the projection matrix. World: 'world', // bind uniform to be used as the world matrix. View: 'view', // bind uniform to be used as the view matrix. UvOffset: 'uvOffset', // bind uniform to be used as UV offset. UvScale: 'uvScale', // bind uniform to be used as UV scale. OutlineWeight: 'outlineWeight', // bind uniform to be used as outline weight. OutlineColor: 'outlineColor', // bind uniform to be used as outline color. UvNormalizationFactor: 'uvNormalizationFactor', // bind uniform to be used as factor to normalize uv values to be 0-1. TextureWidth: 'textureWidth', // bind uniform to be used as texture width in pixels. TextureHeight: 'textureHeight' // bind uniform to be used as texture height in pixels. }; Object.freeze(Effect.UniformBinds); /** * Define attribute types. */ Effect.AttributeTypes = { Byte: 'BYTE', Short: 'SHORT', UByte: 'UNSIGNED_BYTE', UShort: 'UNSIGNED_SHORT', Float: 'FLOAT', HalfFloat: 'HALF_FLOAT', }; Object.freeze(Effect.AttributeTypes); /** * Define built-in attribute binds to connect attribute names for specific use cases like position, uvs, colors, etc. * If an effect support one or more of these attributes, Shaku will know how to fill them automatically. */ Effect.AttributeBinds = { Position: 'position', // bind attribute to be used for vertices position array. TextureCoords: 'uv', // bind attribute to be used for texture coords array. Colors: 'color', // bind attribute to be used for vertices colors array. Normals: 'normal', // bind attribute to be used for vertices normals array. } Object.freeze(Effect.AttributeBinds); /** * Set texture mag and min filters. * @private * @param {TextureFilterModes} filter Texture filter to set. */ function _setTextureFilter(gl, filter) { if (!TextureFilterModes._values.has(filter)) { throw new Error("Invalid texture filter mode! Please pick a value from 'TextureFilterModes'."); } let glMode = gl[filter]; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, glMode); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, glMode); } /** * Set texture wrap mode on X and Y axis. * @private * @param {TextureWrapMode} wrapX Wrap mode on X axis. * @param {TextureWrapMode} wrapY Wrap mode on Y axis. */ function _setTextureWrapMode(gl, wrapX, wrapY) { if (wrapY === undefined) { wrapY = wrapX; } if (!TextureWrapModes._values.has(wrapX)) { throw new Error("Invalid texture wrap mode! Please pick a value from 'TextureWrapModes'."); } if (!TextureWrapModes._values.has(wrapY)) { throw new Error("Invalid texture wrap mode! Please pick a value from 'TextureWrapModes'."); } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl[wrapX]); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl[wrapY]); } // will be set by the gfx manager Effect._gfx = null; // export the effect class. module.exports = Effect;