UNPKG

phaser

Version:

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

445 lines (416 loc) 20.9 kB
/** * @author Benjamin D. Richards <benjamindrichards@gmail.com> * @copyright 2013-2026 Phaser Studio Inc. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var Class = require('../../utils/Class'); var StencilModifier = require('../components/StencilModifier'); var Container = require('../container/Container'); var Layer = require('../layer/Layer'); /** * @classdesc * A Stencil Game Object. * * A Stencil is a special type of Game Object used to place stencils over the canvas. * You can use it to efficiently control where subsequent objects are rendered. * It is WebGL-only. * Study the documentation carefully to understand how it works. * * A Stencil is an extended Container Game Object. * It contains a list of child Game Objects to render to the stencil buffer. * Think of these as opaque sheets of card held up over the canvas, * preventing anything from being drawn through them. * * The stencil buffer is provided by WebGL. * It is available if the game render config set `stencil` to `true`. * It is an 8-bit attachment to framebuffers, like an extra alpha channel. * But if the stencil channel is not 0 at a pixel, WebGL will skip rendering that pixel. * There are no degrees of transparency, only on or off. * * When you draw objects with alpha to a Stencil, * a special `alphaStrategy` is used. Compatible shaders switch from rendering * alpha, to discarding fragments based on their alpha value. * By default, this uses dithering to preserve alpha gradients. * You can change `stencilAlphaStrategy` to a threshold value to instead * discard without dithering. * If `stencilAlphaStrategy` is `'keep'`, * or the child's shader does not support alpha strategies, * transparent pixels will be drawn as opaque to the stencil buffer! * This is rarely what you want. * Fragment shaders must `discard` fragments for them to be transparent to the stencil buffer. * * By default, most Phaser shaders support alpha strategies. * Notable exceptions include: * * - PointLight (additive lighting) * - Shader game objects, and extended classes like Noise * * To apply an alpha strategy without a compatible shader, * force stencil composition by setting `stencilCompositeCheck` to `true`. * This will composite the stencil contents to a framebuffer, * which is rendered using a compatible shader. * * Stencils are drawn as order-independent layers. * You can add or remove layers in sequence using `addLayer` and `removeLayer`. * Each layer adds or subtracts 1 from the stencil buffer. * Only when the stencil is 0 at a pixel will anything be drawn there. * (This 0-test is a rule set by the renderer's base DrawingContext.) * Note that overlapping geometry within the same Stencil is additive, * and can adjust the layer by more than 1 in aggregate. * The results can be surprising, so try to avoid overlaps. * * You can invert the stencil by setting `stencilInvert` to `true`. * This will use an extra draw call to invert the stencil: * it adds a layer everywhere that the children would not draw. * It is more efficient to render a shape that covers the whole area you wish to stencil, * but if that's not possible, you can use this. * Inversion makes it possible to render to parts of the screen not touched * by child geometry. * It works by filling the camera, then drawing the child stencil in reverse. * * You can remove the stencil by using {@link Phaser.GameObjects.StencilReference}. * This object copies a target Stencil, and re-renders it * with different stencil options, elsewhere in the display list. * This is an efficient way to re-use stencil geometry. * * You can also clear the stencil by setting `stencilLayerMode` to `clear`. * It replaces all stencil buffer values with the `stencilClearValue`. * This should normally set them back to 0 so everything renders again. * This destroys all layer information. * It does not use the child list. * Be careful not to mess up your scene this way. * * Set `stencilLayerMode` to `clearRegion` to fill a region * of the stencil buffer defined by the children, with the `stencilClearValue`. * This can be used as a selective eraser, or to set a region to a specific value. * * You cannot invert the stencil if the `stencilLayerMode` is `clear` or `clearRegion`. * * Sequential stencil layers combine and persist, * because they are drawn to the stencil buffer and stay there until the next frame. * Do not add too many layers, though. There are only 8 bits in the stencil buffer, * so it only safely supports 255 layers. * If you go over this limit, the buffer wraps back to 0. * You can still add and remove layers in this case, * and they will continue to be accurately tracked, * but layer 256 (and subsequent multiples of 256) will be effectively 0 and allow drawing. * The same applies if you remove layers below 0: it wraps back to 255 * and prevents drawing. * * Deactivate `stencilValueWrap` to prevent the stencil buffer from wrapping. * This is useful when defining stencils with subtraction, * and you don't want to underflow from 0 to 255. * For example, you can use one stencil in `clearRegion` to define a value, * then use another stencil in `subtractLayer` to erase parts of that region. * But be careful when using stencils for different purposes: * if you mix stencil data, you will get unexpected results. * * Nested stencils are a separate concern. * If you add a Stencil as the child of another Stencil, * the parent Stencil will composite its contents to a framebuffer, * including child stencils. * This effectively traps the child stencil in the framebuffer, * and only the final composite from the framebuffer needs to be considered. * It is used as the source for the stencil, subject to alpha strategy. * This requires extra draw calls to composite, * and framebuffers have poor anti-aliasing quality, * so you should avoid nesting stencils unless you know what you are doing. * * To determine whether the stencil needs to composite to a framebuffer, * it runs a check before rendering (`'auto'` mode). * If you know the answer already, * or if you have a custom game object that Phaser doesn't understand, * you can set `stencilCompositeCheck` to `true` or `false` * to skip the auto check. * If you set it to `false`, it will never composite, * and any child stencils may render in unexpected ways. * (Generally, they will appear backwards from what you expect: * child stencils will not affect the parent stencil, but things drawn later.) * * Best practice: use few stencils and don't nest them. * * Stencil is best used for efficient, sharp-edged, reused masks. * You can draw a stencil once, and it will affect everything that is drawn later. * Its rendering cost is minimal: it is just the draw cost of its children. * This can be as low as 1 call. * If there are nested stencils, it will take more calls for the framebuffer. * * If you need better quality alpha handling, consider using a Mask filter instead. * Filters have a higher rendering cost, and apply to just 1 object at a time, * but they have the best quality. * (And you can apply them to Containers, to cheat the object limitation.) * * @class Stencil * @extends Phaser.GameObjects.Container * @extends Phaser.GameObjects.Components.StencilModifier * @memberof Phaser.GameObjects * @constructor * @since 4.2.0 * * @param {Phaser.Scene} scene - The Scene to which this Game Object belongs. * @param {number} x - The horizontal position of this Game Object in the world. * @param {number} y - The vertical position of this Game Object in the world. * @param {Phaser.GameObjects.GameObject[]} [children] - An optional array of Game Objects to add to the Stencil. * @param {Phaser.Types.GameObjects.Stencil.StencilOptions} [options] - The options for the Stencil. */ var Stencil = new Class({ Extends: Container, Mixins: [ StencilModifier ], initialize: function Stencil (scene, x, y, children, options) { Container.call(this, scene, x, y, children); if (options) { if (options.stencilAlphaStrategy !== undefined) { this.stencilAlphaStrategy = options.stencilAlphaStrategy; } else { this.stencilAlphaStrategy = scene.renderer.config.stencilAlphaStrategy; } if (options.stencilClearValue !== undefined) { this.stencilClearValue = options.stencilClearValue; } if (options.stencilCompositeCheck !== undefined) { this.stencilCompositeCheck = options.stencilCompositeCheck; } if (options.stencilInvert !== undefined) { this.stencilInvert = options.stencilInvert; } if (options.stencilLayerMode !== undefined) { this.stencilLayerMode = options.stencilLayerMode; } if (options.stencilValueWrap !== undefined) { this.stencilValueWrap = options.stencilValueWrap; } } else { this.stencilAlphaStrategy = scene.renderer.config.stencilAlphaStrategy; } // Add the stencil render step as the first render step. this.addRenderStep(this.stencilRenderStep, 0); }, /** * The stencil render step. * This is an internal function, which is automatically assigned; * you should not call it directly. * * This runs before other render steps, * so it can set up the drawing context to render properly. * It delegates to the appropriate render step function based on the `stencilLayerMode`. * * @method Phaser.GameObjects.Stencil#stencilRenderStep * @webglOnly * @since 4.2.0 * @param {Phaser.Renderer.WebGL.WebGLRenderer} renderer - A reference to the current active WebGL renderer. * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object being rendered in this call. * @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context. * @param {Phaser.GameObjects.Components.TransformMatrix} [parentMatrix] - This transform matrix is defined if the game object is nested * @param {number} [renderStep=0] - The index of this function in the Game Object's list of render processes. Used to support multiple rendering functions. * @param {Phaser.GameObjects.GameObject[]} [displayList] - The display list which is currently being rendered. * @param {number} [displayListIndex] - The index of the Game Object within the display list. */ stencilRenderStep: function (renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex) { switch (gameObject.stencilLayerMode) { case 'clear': { gameObject.stencilRenderStepClear(renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex); break; } case 'clearRegion': { gameObject.stencilRenderStepClearRegion(renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex); break; } case 'addLayer': case 'subtractLayer': default: { gameObject.stencilRenderStepLayers(renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex); break; } } }, /** * The render step used when the `stencilLayerMode` is `addLayer` or `subtractLayer`. * You should not call this directly. * * @method Phaser.GameObjects.Stencil#stencilRenderStepLayers * @webglOnly * @since 4.2.0 * @param {Phaser.Renderer.WebGL.WebGLRenderer} renderer - A reference to the current active WebGL renderer. * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object being rendered in this call. * @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context. * @param {Phaser.GameObjects.Components.TransformMatrix} [parentMatrix] - This transform matrix is defined if the game object is nested * @param {number} [renderStep=0] - The index of this function in the Game Object's list of render processes. Used to support multiple rendering functions. * @param {Phaser.GameObjects.GameObject[]} [displayList] - The display list which is currently being rendered. * @param {number} [displayListIndex] - The index of the Game Object within the display list. */ stencilRenderStepLayers: function (renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex) { var gl = renderer.gl; // If the tree of child game objects has any stencil children, // activate forced composite mode on `gameObject`. var filtersForceComposite = gameObject.filtersForceComposite; if ( gameObject.stencilCompositeCheck === true || (gameObject.stencilCompositeCheck === 'auto' && gameObject.hasStencilChildren(gameObject, drawingContext.camera)) ) { gameObject.enableFilters().setFiltersForceComposite(true); } // Set up the drawing context to render to the stencil buffer. var currentContext = drawingContext.getClone(); currentContext.setAlphaStrategy(gameObject.stencilAlphaStrategy); currentContext.setColorWritemask(false, false, false, false); var opIncr = gameObject.stencilValueWrap ? gl.INCR_WRAP : gl.INCR; var opDecr = gameObject.stencilValueWrap ? gl.DECR_WRAP : gl.DECR; var op = opIncr; if (gameObject.stencilLayerMode === 'subtractLayer') { op = opDecr; } currentContext.setStencil(true, gl.ALWAYS, 0, 0xFF, op, op, op, 0, 0xFF); currentContext.use(); // Invert the stencil area if needed. if (gameObject.stencilInvert) { renderer.renderNodes.getNode('FillCamera').run(currentContext, 0xff000000, drawingContext.useCanvas); currentContext = currentContext.getClone(); currentContext.use(); // Invert the stencil operation. op = op === opIncr ? opDecr : opIncr; currentContext.setStencil(true, gl.ALWAYS, 0, 0xFF, op, op, op, 0, 0xFF); } // Render the children. gameObject.renderWebGLStep( renderer, gameObject, currentContext, parentMatrix, renderStep + 1, displayList, displayListIndex ); currentContext.release(); gameObject.setFiltersForceComposite(filtersForceComposite); }, /** * The render step used when the `stencilLayerMode` is `clear`. * You should not call this directly. * * @method Phaser.GameObjects.Stencil#stencilRenderStepClear * @webglOnly * @since 4.2.0 * @param {Phaser.Renderer.WebGL.WebGLRenderer} renderer - A reference to the current active WebGL renderer. * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object being rendered in this call. * @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context. * @param {Phaser.GameObjects.Components.TransformMatrix} [parentMatrix] - This transform matrix is defined if the game object is nested * @param {number} [renderStep=0] - The index of this function in the Game Object's list of render processes. Used to support multiple rendering functions. * @param {Phaser.GameObjects.GameObject[]} [displayList] - The display list which is currently being rendered. * @param {number} [displayListIndex] - The index of the Game Object within the display list. */ stencilRenderStepClear: function (renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex) { var gl = renderer.gl; var currentContext = drawingContext.getClone(); currentContext.state.stencil.clear = gameObject.stencilClearValue; currentContext.state.stencil.writeMask = 0xFF; currentContext.use(); currentContext.clear(gl.STENCIL_BUFFER_BIT); }, /** * The render step used when the `stencilLayerMode` is `clearRegion`. * You should not call this directly. * * @method Phaser.GameObjects.Stencil#stencilRenderStepClearRegion * @webglOnly * @since 4.2.0 * @param {Phaser.Renderer.WebGL.WebGLRenderer} renderer - A reference to the current active WebGL renderer. * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object being rendered in this call. * @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context. * @param {Phaser.GameObjects.Components.TransformMatrix} [parentMatrix] - This transform matrix is defined if the game object is nested * @param {number} [renderStep=0] - The index of this function in the Game Object's list of render processes. Used to support multiple rendering functions. * @param {Phaser.GameObjects.GameObject[]} [displayList] - The display list which is currently being rendered. * @param {number} [displayListIndex] - The index of the Game Object within the display list. */ stencilRenderStepClearRegion: function (renderer, gameObject, drawingContext, parentMatrix, renderStep, displayList, displayListIndex) { var gl = renderer.gl; // If the tree of child game objects has any stencil children, // activate forced composite mode on `gameObject`. var filtersForceComposite = gameObject.filtersForceComposite; if ( gameObject.stencilCompositeCheck === true || (gameObject.stencilCompositeCheck === 'auto' && gameObject.hasStencilChildren(gameObject, drawingContext.camera)) ) { gameObject.enableFilters().setFiltersForceComposite(true); } // Set up the drawing context to render to the stencil buffer. var currentContext = drawingContext.getClone(); currentContext.setAlphaStrategy(gameObject.stencilAlphaStrategy); currentContext.setColorWritemask(false, false, false, false); var clearValue = gameObject.stencilClearValue; var op = gl.REPLACE; currentContext.setStencil(true, gl.ALWAYS, clearValue, 0xFF, op, op, op, clearValue, 0xFF); currentContext.use(); // Render the children. gameObject.renderWebGLStep( renderer, gameObject, currentContext, parentMatrix, renderStep + 1, displayList, displayListIndex ); currentContext.release(); gameObject.setFiltersForceComposite(filtersForceComposite); }, /** * Checks if the game object or any of its children has a stencil. * This is used internally to determine if the stencil should composite its contents to a framebuffer. * * This is a depth-first, succeed-fast search. * * @method Phaser.GameObjects.Stencil#hasStencilChildren * @since 4.2.0 * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object to check. * @param {Phaser.Cameras.Scene2D.Camera} camera - The camera to check. * @returns {boolean} Whether the game object or any of its children has a stencil. */ hasStencilChildren: function (gameObject, camera) { if (gameObject instanceof Container || gameObject instanceof Layer) { for (var i = 0; i < gameObject.list.length; i++) { var child = gameObject.list[i]; if ( child && child.willRender(camera) && ( child.isStencilModifier || this.hasStencilChildren(child, camera) ) ) { return true; } } } return false; } }); module.exports = Stencil;