UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

611 lines (560 loc) 23.3 kB
/** * @author Richard Davey <rich@phaser.io> * @copyright 2013-2026 Phaser Studio Inc. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var Camera = require('../../cameras/2d/BaseCamera'); var Vector2 = require('../../math/Vector2'); var ShaderQuad = require('../../renderer/webgl/renderNodes/ShaderQuad'); var DrawingContext = require('../../renderer/webgl/DrawingContext'); var Class = require('../../utils/Class'); var GetFastValue = require('../../utils/object/GetFastValue'); var Components = require('../components'); var GameObject = require('../GameObject'); var ShaderRender = require('./ShaderRender'); /** * @classdesc * A Shader Game Object. * * This Game Object allows you to easily add a quad with its own shader * into the display list, and manipulate it as you would any other Game Object, * including scaling, rotating, positioning and adding to Containers. * The Shader can be made interactive and used for input events. * It can also be used in filters to create visually stunning effects. * * It works by creating a custom RenderNode which runs a custom shader program * to draw a quad. The shader program can be loaded from the Shader Cache, * or provided in-line as strings. * * Please see the Phaser Examples GitHub repo for several examples * of loading and creating shaders dynamically. * * Due to the way in which they work, you cannot directly change the alpha * of a Shader. It should be handled via uniforms in the shader code itself. * * By default, a Shader has a uniform called `uProjectionMatrix` * which is set automatically. * You can control additional uniforms using the `setupUniforms` method * in the Shader configuration object, which runs every time the shader renders. * * Shaders are stand-alone renders: they finish any current render batch * and run once by themselves. As this costs a draw call, you should use them sparingly. * If you need to have a fully batched custom shader, then please look at using * a custom RenderNode instead. However, for background or special masking effects, * they are extremely effective. * * Note: be careful when using texture coordinates in shader code. * The built-in variable `gl_FragCoord` and the default uniform `outTexCoord` * both use WebGL coordinates, which are `0,0` in the bottom-left. * Additionally, `gl_FragCoord` says it's in "window relative" coordinates. * But this is actually relative to the framebuffer size. * * @example * // Loading a shader from the cache (good for simple shaders) * function preload () * { * this.load.glsl('fire', 'shaders/fire.glsl.js'); * } * * function create () * { * this.add.shader('fire', 400, 300, 512, 512); * } * * @example * // Using a configuration object (good for more control) * function create () * { * this.add.shader({ * fragmentKey: 'fire', // This will be overridden by fragmentSource * fragmentSource: '// your fragment shader source', * setupUniforms: (setUniform, drawingContext) => { * setUniform('time', this.game.loop.getDuration()); * } * }, 400, 300, 512, 512); * } * * @class Shader * @extends Phaser.GameObjects.GameObject * @memberof Phaser.GameObjects * @constructor * @since 3.17.0 * * @extends Phaser.GameObjects.Components.BlendMode * @extends Phaser.GameObjects.Components.ComputedSize * @extends Phaser.GameObjects.Components.Depth * @extends Phaser.GameObjects.Components.GetBounds * @extends Phaser.GameObjects.Components.Origin * @extends Phaser.GameObjects.Components.ScrollFactor * @extends Phaser.GameObjects.Components.Transform * @extends Phaser.GameObjects.Components.Visible * * @param {Phaser.Scene} scene - The Scene to which this Game Object belongs. * @param {string|Phaser.Types.GameObjects.Shader.ShaderQuadConfig} config - The configuration object this Shader will use. It can also be a key that corresponds to a shader in the shader cache, which will be used as `fragmentKey` in a new config object. * @param {number} [x=0] - The horizontal position of this Game Object in the world. * @param {number} [y=0] - The vertical position of this Game Object in the world. * @param {number} [width=128] - The width of the Game Object. * @param {number} [height=128] - The height of the Game Object. * @param {string[]|Phaser.Textures.Texture[]} [textures] - The textures that the shader uses, if any. If you intend to define the textures later, use `'__DEFAULT'` as a placeholder, to avoid initialization errors. */ var Shader = new Class({ Extends: GameObject, Mixins: [ Components.BlendMode, Components.ComputedSize, Components.Depth, Components.GetBounds, Components.Origin, Components.ScrollFactor, Components.Transform, Components.Visible, ShaderRender ], initialize: function Shader (scene, config, x, y, width, height, textures) { if (config === undefined) { config = {}; } if (typeof config === 'string') { config = { fragmentKey: config }; } if (x === undefined) { x = 0; } if (y === undefined) { y = 0; } if (width === undefined) { width = 128; } if (height === undefined) { height = 128; } GameObject.call(this, scene, 'Shader'); var renderer = scene.sys.renderer; /** * The textures that the shader uses. * These will be assigned to texture units 0 to N when the shader is * rendered, where N is `textures.length - 1`. * * @name Phaser.GameObjects.Shader#textures * @type {Phaser.Textures.Texture[]} * @since 4.0.0 */ this.textures = []; /** * The underlying RenderNode object that the shader uses to render with. * * @name Phaser.GameObjects.Shader#renderNode * @type {Phaser.Renderer.WebGL.RenderNodes.ShaderQuad} * @since 4.0.0 */ this.renderNode = new ShaderQuad(renderer.renderNodes, config); this.setupUniforms = GetFastValue(config, 'setupUniforms', function () {}); if (config.updateShaderConfig) { this.renderNode.updateShaderConfig = config.updateShaderConfig; } var initialUniforms = GetFastValue(config, 'initialUniforms', {}); Object.entries(initialUniforms).forEach(function (entry) { this.setUniform(entry[0], entry[1]); }, this); /** * The drawing context containing the framebuffer and texture that the shader is rendered to. * This is only set if the shader is rendering to a texture. * * @name Phaser.GameObjects.Shader#drawingContext * @type {?Phaser.Renderer.WebGL.DrawingContext} * @since 4.0.0 */ this.drawingContext = null; /** * A reference to the WebGLTextureWrapper this Shader is rendering to. * This property is only set if you have called `Shader.setRenderToTexture`. * * @name Phaser.GameObjects.Shader#glTexture * @type {?Phaser.Renderer.WebGL.Wrappers.WebGLTextureWrapper} * @since 3.19.0 */ this.glTexture = null; /** * A flag that indicates if this Shader has been set to render to a texture instead of the display list. * * This property is `true` if you have called `Shader.setRenderToTexture`, otherwise it's `false`. * * A Shader that is rendering to a texture _does not_ appear on the display list. * * @name Phaser.GameObjects.Shader#renderToTexture * @type {boolean} * @readonly * @since 3.19.0 */ this.renderToTexture = false; /** * A reference to the Phaser.Textures.Texture that has been stored in the Texture Manager for this Shader. * * This property is only set if you have called `Shader.setRenderToTexture` with a key, otherwise it is `null`. * * @name Phaser.GameObjects.Shader#texture * @type {Phaser.Textures.Texture} * @since 3.19.0 */ this.texture = null; /** * The top-left texture coordinate of the shader. * This is set to 0,1 by default. It uses WebGL texture coordinates. * * @name Phaser.GameObjects.Shader#textureCoordinateTopLeft * @type {Phaser.Math.Vector2} * @since 4.0.0 */ this.textureCoordinateTopLeft = new Vector2(0, 1); /** * The top-right texture coordinate of the shader. * This is set to 1,1 by default. It uses WebGL texture coordinates. * * @name Phaser.GameObjects.Shader#textureCoordinateTopRight * @type {Phaser.Math.Vector2} * @since 4.0.0 */ this.textureCoordinateTopRight = new Vector2(1, 1); /** * The bottom-left texture coordinate of the shader. * This is set to 0,0 by default. It uses WebGL texture coordinates. * * @name Phaser.GameObjects.Shader#textureCoordinateBottomLeft * @type {Phaser.Math.Vector2} * @since 4.0.0 */ this.textureCoordinateBottomLeft = new Vector2(0, 0); /** * The bottom-right texture coordinate of the shader. * This is set to 1,0 by default. It uses WebGL texture coordinates. * * @name Phaser.GameObjects.Shader#textureCoordinateBottomRight * @type {Phaser.Math.Vector2} * @since 4.0.0 */ this.textureCoordinateBottomRight = new Vector2(1, 0); this.setTextures(textures); this.setPosition(x, y); this.setSize(width, height); this.setOrigin(0.5, 0.5); }, /** * Returns the current value of a uniform from the render node. * This value is actually copied to all shaders that use it. * Modifications to non-primitive values such as arrays and objects * will modify the original. * * It's generally better to use the `setupUniforms` function in the * shader configuration object to set uniform values on changing uniforms. * This method is provided in the spirit of reading back the values. * * @method Phaser.GameObjects.Shader#getUniform * @since 4.0.0 * @param {string} name - The name of the uniform to get. * @return {any} The value of the uniform. */ getUniform: function (name) { return this.renderNode.programManager.uniforms[name]; }, /** * Set the value of a uniform in the shader. * This value is actually copied to all shaders that use it. * * It's generally better to use the `setupUniforms` function in the * shader configuration object to set uniform values on changing uniforms. * Use this method to set uniforms just once. * * @method Phaser.GameObjects.Shader#setUniform * @since 4.0.0 * @param {string} name - The name of the uniform to set. * @param {any} value - The value to set the uniform to. * @return {this} */ setUniform: function (name, value) { this.renderNode.programManager.setUniform(name, value); return this; }, /** * Set the textures that the shader uses. * * Some shaders don't use any textures. Some may use one or more. * The textures are assigned to texture units 0 to N when the shader is rendered, * where N is `textures.length - 1`. * You must set the uniforms in your shader to match these texture units. * * Calling this method will replace the existing textures array with the new one. * * @example * // In the shader source, use the `sampler2D` type. * sampler2D uMainSampler; * sampler2D uNormalSampler; * * // When creating the shader, set the textures. * var shader = this.add.shader({ * fragmentKey: 'myShader', * setupUniforms: (setUniform) => { * // In the `setupUniforms` function, set the texture to its array position. * setUniform('uMainSampler', 0); * setUniform('uNormalSampler', 1); * } * }, x, y, width, height, ['metal', 'normal']); * * @method Phaser.GameObjects.Shader#setTextures * @since 4.0.0 * @param {string[]|Phaser.Textures.Texture[]} [textures] - The textures that the shader uses. */ setTextures: function (textures) { if (textures === undefined) { textures = []; } this.textures.length = 0; for (var i = 0; i < textures.length; i++) { var texture = textures[i]; if (typeof texture === 'string') { texture = this.scene.textures.get(texture); } this.textures.push(texture); } return this; }, /** * Changes this Shader so instead of rendering to the display list * it renders to a WebGL Framebuffer and Texture instead. * This allows you to use the output of this shader as a texture. * * After calling this method the following properties are populated: * - `Shader.drawingContext` * - `Shader.glTexture` * * Additionally, you can provide a key to this method. * Doing so will create a Phaser Texture from this Shader, * store it in `Shader.texture`, * and save it into the Texture Manager, allowing you to then use it for * any texture-based Game Object, such as a Sprite or Image: * * ```javascript * var shader = this.add.shader('myShader', x, y, width, height); * * shader.setRenderToTexture('doodle'); * * this.add.image(400, 300, 'doodle'); * ``` * * Note that it stores an active reference to this Shader. That means as this shader updates, * so does the texture and any object using it to render with. Also, if you destroy this * shader, be sure to clear any objects that may have been using it as a texture too. * * By default it will create a single base texture. You can add frames to the texture * by using the `Texture.add` method. After doing this, you can then allow Game Objects * to use a specific frame from a Render Texture. * * If you want to update a texture only sporadically, don't use this method. * Instead, use a DynamicTexture: * * ```javascript * var shader = this.add.shader('myShader', x, y, width, height); * * var dynamic = this.textures.addDynamicTexture('myTexture', shader.width, shader.height); * * // To update the texture: * dynamic.clear().draw(shader).render(); * ``` * * @method Phaser.GameObjects.Shader#setRenderToTexture * @since 3.19.0 * * @param {string} [key] - The unique key to store the texture as within the global Texture Manager. * * @return {this} This Shader instance. */ setRenderToTexture: function (key) { if (this.renderToTexture) { return this; } var width = this.width; var height = this.height; var renderer = this.scene.sys.renderer; var scene = this.scene; var camera = new Camera(0, 0, width, height).setScene(scene.game.scene.systemScene, false); this.drawingContext = new DrawingContext(renderer, { width: width, height: height, camera: camera }); this.glTexture = this.drawingContext.texture; if (key) { this.texture = scene.sys.textures.addGLTexture(key, this.glTexture); } this.renderToTexture = true; // Render at least once, so our texture isn't blank on the first update this.renderWebGLStep(renderer, this, this.drawingContext); return this; }, /** * Render the shader immediately. * This is useful for a Shader that is not part of the display list, * but you want to use with `renderToTexture`. * * @method Phaser.GameObjects.Shader#renderImmediate * @since 4.0.0 * @return {this} This Shader instance. */ renderImmediate: function () { this.renderWebGLStep(this.scene.renderer, this, this.drawingContext); return this; }, /** * The function which sets uniforms for the shader. * This is called automatically during rendering. * It is set from the `config` object passed in the constructor. * You should use this function to set any uniform values you need for your shader to run. * * You can set this function directly after object creation, * but it's recommended to use the `config` object * to keep your logic encapsulated. * * The function is invoked with two arguments: `setUniform` and `drawingContext`. * `setUniform` is a function that takes two arguments: a string (the name of the uniform) and a value. * Ensure that the value matches the expected type in the shader. * You don't need to be too precise, as the system will convert * e.g. Array and Float32Array types as needed. * To set an array in a shader, append `[0]` to the uniform name. * `drawingContext` is a reference to the current drawing context, * which may be useful if you need to query the camera or similar. * * Note that `uProjectionMatrix` is set for you automatically. * * @method Phaser.GameObjects.Shader#setupUniforms * @since 4.0.0 * @param {function} setUniform - The function which sets uniforms. `(name: string, value: any) => void`. * @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - A reference to the current drawing context. */ setupUniforms: function (setUniform, drawingContext) { // NOOP }, /** * A NOOP method so you can pass a Shader to a Container. * Calling this method will do nothing. It is intentionally empty. * * @method Phaser.GameObjects.Shader#setAlpha * @private * @since 3.17.0 * @return {this} This Shader instance. */ setAlpha: function () { return this; }, /** * Set the texture coordinates of the shader. * These values are used to provide texture mapping to the shader, * and are commonly used to drive generative output. * * By default, the shader uses the whole texture, the range 0-1. * The coordinates are in WebGL texture space, which is 0,0 in the bottom-left. * This method allows you to specify a region of the texture to use, * or even go outside the 0-1 bounds. * This can be useful if you want to use a single frame from a texture, * repeat the shader's texture, use a larger numeric range, * or distort the shader. * * Note that a quad is made of two triangles, divided by the diagonal * from the top-left to the bottom-right. This means that some permutations * of coordinates may affect just one or the other triangle. * This can cause abrupt warping along the diagonal. * Keep an eye on your output. Rectangles and parallelograms are safe bets. * So are rotation and scaling transforms. Moving a single point is risky. * * Call this method with no arguments to reset the shader to use the whole texture. * * @method Phaser.GameObjects.Shader#setTextureCoordinates * @since 4.0.0 * @param {number} [topLeftX=0] - The top-left x coordinate of the texture. * @param {number} [topLeftY=1] - The top-left y coordinate of the texture. * @param {number} [topRightX=1] - The top-right x coordinate of the texture. * @param {number} [topRightY=1] - The top-right y coordinate of the texture. * @param {number} [bottomLeftX=0] - The bottom-left x coordinate of the texture. * @param {number} [bottomLeftY=0] - The bottom-left y coordinate of the texture. * @param {number} [bottomRightX=1] - The bottom-right x coordinate of the texture. * @param {number} [bottomRightY=0] - The bottom-right y coordinate of the texture. * @return {this} This Shader instance */ setTextureCoordinates: function ( topLeftX, topLeftY, topRightX, topRightY, bottomLeftX, bottomLeftY, bottomRightX, bottomRightY ) { if (topLeftX === undefined) { topLeftX = 0; } if (topLeftY === undefined) { topLeftY = 1; } if (topRightX === undefined) { topRightX = 1; } if (topRightY === undefined) { topRightY = 1; } if (bottomLeftX === undefined) { bottomLeftX = 0; } if (bottomLeftY === undefined) { bottomLeftY = 0; } if (bottomRightX === undefined) { bottomRightX = 1; } if (bottomRightY === undefined) { bottomRightY = 0; } this.textureCoordinateTopLeft.set(topLeftX, topLeftY); this.textureCoordinateTopRight.set(topRightX, topRightY); this.textureCoordinateBottomLeft.set(bottomLeftX, bottomLeftY); this.textureCoordinateBottomRight.set(bottomRightX, bottomRightY); return this; }, /** * Set the texture coordinates of the shader from a frame. * This is a convenience method that sets the texture coordinates * to match a frame from a texture. * * @method Phaser.GameObjects.Shader#setTextureCoordinatesFromFrame * @since 4.0.0 * @param {Phaser.Textures.Frame|string} frame - The frame to set the texture coordinates from. If a string is given, it will be used to look up the frame in the texture. * @param {Phaser.Textures.Texture|string} [texture] - The texture that the frame is from. This is only used if `frame` is a string. If a string is given, it will be used to look up the texture in the Texture Manager. If not given, the first member of `Shader.textures` is used. If `Shader.textures` is empty, an error will occur. */ setTextureCoordinatesFromFrame: function (frame, texture) { if (typeof frame === 'string') { if (!texture) { texture = this.textures[0]; } else if (typeof texture === 'string') { texture = this.scene.textures.get(texture); } frame = texture.get(frame); } var u0 = frame.u0; var v0 = frame.v0; var u1 = frame.u1; var v1 = frame.v1; this.setTextureCoordinates(u0, v0, u1, v0, u0, v1, u1, v1); }, /** * Internal destroy handler, called as part of the destroy process. * * @method Phaser.GameObjects.Shader#preDestroy * @protected * @since 3.17.0 */ preDestroy: function () { this.renderNode = null; this.textures.length = 0; if (this.drawingContext) { this.drawingContext.destroy(); if (this.texture) { this.texture.destroy(); } this.drawingContext = null; this.glTexture = null; this.texture = null; } } }); module.exports = Shader;