UNPKG

phaser

Version:

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

1,427 lines (1,273 loc) 56.6 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 Components = require('../components'); var GameObject = require('../GameObject.js'); var SubmitterSpriteGPULayer = require('../../renderer/webgl/renderNodes/submitter/SubmitterSpriteGPULayer.js'); var Utils = require('../../renderer/webgl/Utils.js'); var EasingEncoding = require('./EasingEncoding.js'); var EasingNaming = require('./EasingNaming.js'); var SpriteGPULayerRender = require('./SpriteGPULayerRender.js'); var getTint = Utils.getTintAppendFloatAlpha; /** * @classdesc * A SpriteGPULayer GameObject. This is a WebGL only GameObject. * It is optimized for rendering very large numbers of quads * following simple tween animations. * It is suited to complex backgrounds with animation. * * A SpriteGPULayer is a composite object that contains a collection of * Member objects. It stores the rendering data for these * objects in a GPU buffer, and renders them in a single draw call. * Because it only updates the GPU buffer when necessary, * it is up to 100 times faster than rendering the objects individually. * Avoid changing the contents of the SpriteGPULayer frequently, as this * requires the whole buffer to be updated. * * The layer can generally perform well with a million small quads. * The exact performance will depend on the device and the size of the quads. * If the quads are large, the layer will be fill-rate limited. * Avoid drawing more than a few million pixels per frame. * * When populating the SpriteGPULayer, use `addMember` to add a new member * to the top of the layer. You should populate the layer all at once, * and leave it unchanged, rather than frequently adding and removing members, * because it is expensive to update the buffer. * * Rather than create a new `SpriteGPULayer.Member` object for each `addMember` call, * you can reuse the same object. This is more efficient, * because creating millions of objects has a major performance cost * and may cause garbage collection issues. * * Notes on modifying the SpriteGPULayer: * * The following operations are expensive. They require some or all of the * buffer to be updated: * * - `addData` * - `addMember` * - `editMember` * - `patchMember` * - `resize` * - `removeMembers` * * Members are added at the end of the buffer. Removed members are spliced out * of the buffer, causing the whole buffer to be updated. * The index of later members will change if you remove an earlier member. * If you need to maintain a structure, such as a grid of tiles, * it's best to "remove" a member by setting its scaleX, scaleY, and alpha to 0. * It is still rendered, but it does not fill any pixels. * * Changes to a small segment of the buffer are less expensive. * The buffer is split into several segments, and each segment can be updated * independently. Editing and patching members will only update the segments * that contain the members being edited. * Updating occurs at render time, so edits all happen at once. * This can reduce the amount of data that needs to be updated, * but it is still more expensive than not updating the buffer at all. * If you're updating a large number of segments, it may be more efficient * to call `setAllSegmentsNeedUpdate` and update the whole buffer at once * rather than make several segment updates in a row. * * The animations in the initial member data are used to compile the shader * and `frameDataTexture`. If you add new animations after the initial * compilation, the shader and texture will be rebuilt, which is expensive. * * Notes on textures: * * This layer gains much of its speed from inflexibility. It can only use one * texture, and that texture must be a single image. * It cannot use multi-atlas textures. * * Further, if the texture is not a power of two in size, * some texture seaming may occur if you line up sprites exactly. * This is because the GPU precision is limited by binary logic, * and texture coordinates will only be perfectly accurate for power of two textures. * This can be avoided by adding/extruding a pixel of padding around each frame * in the texture, or by using a power of two texture. * * Which should you use? * * - If you are using pixel art mode or round pixels, * you should aim to use a power of two texture. * - If you are using smooth mode, you can use a non-power of two texture, * but you should add padding around each frame to avoid seaming. * - If you are using a single image, or none of the frames in the texture * need to tile, it doesn't matter. * * @class SpriteGPULayer * @extends Phaser.GameObjects.GameObject * @memberof Phaser.GameObjects * @webglOnly * * @extends Phaser.GameObjects.Components.Alpha * @extends Phaser.GameObjects.Components.BlendMode * @extends Phaser.GameObjects.Components.Depth * @extends Phaser.GameObjects.Components.ElapseTimer * @extends Phaser.GameObjects.Components.Lighting * @extends Phaser.GameObjects.Components.Mask * @extends Phaser.GameObjects.Components.RenderNodes * @extends Phaser.GameObjects.Components.TextureCrop * @extends Phaser.GameObjects.Components.Visible * * @constructor * @since 4.0.0 * @param {Phaser.Scene} scene - The Scene to which this SpriteGPULayer belongs. * @param {Phaser.Textures.Texture} texture - The texture that will be used to render the SpriteGPULayer. This must be sourced from a single image; a multi atlas will not work. * @param {number} size - The maximum number of quads that this SpriteGPULayer will hold. This can be increased later if necessary. */ var SpriteGPULayer = new Class({ Extends: GameObject, Mixins: [ Components.Alpha, Components.BlendMode, Components.Depth, Components.ElapseTimer, Components.Lighting, Components.Mask, Components.RenderNodes, Components.TextureCrop, Components.Visible, SpriteGPULayerRender ], initialize: function SpriteGPULayer (scene, texture, size) { GameObject.call(this, scene, 'SpriteGPULayer'); /** * The number of quad members in the SpriteGPULayer. * * @name Phaser.GameObjects.SpriteGPULayer#memberCount * @type {number} * @since 4.0.0 */ this.memberCount = 0; /** * The maximum number of quad members that can be in the SpriteGPULayer. * This value is read-only. Change buffer size with `resize`. * * @name Phaser.GameObjects.SpriteGPULayer#size * @type {number} * @since 4.0.0 * @readonly */ this.size = Math.max(size, 0); /** * The number of segments in the buffer. * This helps to optimize buffer updates by dividing them into smaller segments. * This is a constant value and should not be altered. * If you do, all hell will break loose. * * Segments divide the buffer into sequential chunks. * Only updated segments will be uploaded to the GPU. * Each upload has a fixed cost, but reducing the total amount of data * can improve performance. * * Don't change this value to anything higher than 31. * Segment logic uses bitwise operations, which are limited to 32 bits, * so going that high will cause overflows and break everything. * * @name Phaser.GameObjects.SpriteGPULayer#_segments * @type {number} * @since 4.0.0 * @readonly * @private */ this._segments = 24; /** * The state of `bufferUpdateSegments` when it's full. * This is a constant value and should not be altered. * If you do, all hell will break loose. * * @name Phaser.GameObjects.SpriteGPULayer#MAX_BUFFER_UPDATE_SEGMENTS_FULL * @type {number} * @since 4.0.0 * @readonly * @default 0xffffff */ this.MAX_BUFFER_UPDATE_SEGMENTS_FULL = 0xffffff; /** * Which segments of the buffer require updates. * This is a bitfield with segments equal to `_segments`. * * @name Phaser.GameObjects.SpriteGPULayer#bufferUpdateSegments * @type {number} * @since 4.0.0 */ this.bufferUpdateSegments = 0; /** * The size of each segment of the buffer that requires updates. * * @name Phaser.GameObjects.SpriteGPULayer#bufferUpdateSegmentSize * @type {number} * @since 4.0.0 */ this.bufferUpdateSegmentSize = Math.ceil(this.size / this._segments); /** * The gravity used by member animations in 'Gravity' mode. * This is the acceleration in pixels per second squared. * The default is 1024 pixels per second squared. * * Any animation can be set to `ease: 'Gravity'` to use this value. * Instead of `amplitude`, the animation takes * `velocity` (a number of pixels) and * `gravityFactor` (-1 to 1) parameters. * Note that a `gravityFactor` of 0 is assumed to be a mistake, * and will be converted to 1. * * @name Phaser.GameObjects.SpriteGPULayer#gravity * @type {number} * @since 4.0.0 * @default 1024 */ this.gravity = 1024; /** * The animations enabled for the SpriteGPULayer. * This is a map of animation names from `this.EASE` to boolean values. * Adjust these values with `setAnimationEnabled`. * * @name Phaser.GameObjects.SpriteGPULayer#_animationsEnabled * @type {object} * @since 4.0.0 * @private */ this._animationsEnabled = {}; var animations = Object.keys(EasingEncoding); var animLen = animations.length; for (var i = 0; i < animLen; i++) { this._animationsEnabled[animations[i]] = false; } /** * Strings for valid easing functions that can be assigned to * the `ease` property of an SpriteGPULayerMemberAnimation. * This is the reverse mapping of `this.EASE_CODES`. * * @name Phaser.GameObjects.SpriteGPULayer#EASE * @type {object} * @since 4.0.0 * @readonly */ this.EASE = EasingEncoding; /** * Codes for valid easing functions that can be assigned to * the `ease` property of an SpriteGPULayerMemberAnimation. * This is the reverse mapping of `this.EASE`. * * @name Phaser.GameObjects.SpriteGPULayer#EASE_CODES * @type {object} * @since 4.0.0 * @readonly */ this.EASE_CODES = EasingNaming; this.setTexture(texture); this.initRenderNodes(new Phaser.Structs.Map()); /** * A texture containing the frame data for the SpriteGPULayer. * This is used by the vertex shader. * * The texture is composed of pixel strides, where each stride * is interpreted as 6 16-bit unsigned integers, * representing the x, y, width, height, and origin x and y of a frame. * The texture will be up to 4096 pixels wide and as tall as necessary. * * There are two sets of data in the texture: frames and animations. * Frames are taken from the `texture`. * Animations are defined by calling `setAnimations`, * and consist of runs of frames suited to shader animation. * Although the texture will be regenerated by `setAnimations`, * the frames are stored first, so their indices won't change. * * If you change the `texture` of this layer, you will need to * regenerate this by calling `generateFrameDataTexture`. * * @name Phaser.GameObjects.SpriteGPULayer#frameDataTexture * @type {Phaser.Renderer.WebGL.Wrappers.WebGLTextureWrapper} * @since 4.0.0 */ this.frameDataTexture = null; /** * A map of frame names to indices in the frame data texture. * This is used to convert frame names to indices for the vertex shader. * * @name Phaser.GameObjects.SpriteGPULayer#frameDataIndices * @type {object} * @since 4.0.0 */ this.frameDataIndices = {}; /** * A map of indices to frame names in the frame data texture. * This is used to convert frame indices back to names for debugging. * * @name Phaser.GameObjects.SpriteGPULayer#frameDataIndicesInv * @type {object} * @since 4.0.0 */ this.frameDataIndicesInv = {}; /** * An ordered list of animations in the frame data texture. * * @name Phaser.GameObjects.SpriteGPULayer#animationData * @type {object[]} * @since 4.0.0 */ this.animationData = []; /** * A map of animation names to animation parameters in * the frame data texture. * This is used to convert animation names to indices and durations * for the vertex shader. * * @name Phaser.GameObjects.SpriteGPULayer#animationDataNames * @type {object} * @since 4.0.0 */ this.animationDataNames = {}; /** * A map of frame indices to animation parameters in * the frame data texture. * These are the starting frame indices used by the vertex shader. * They can be used to map back to names in `animationDataIndices`. * * @name Phaser.GameObjects.SpriteGPULayer#animationDataIndices * @type {object} * @since 4.0.0 */ this.animationDataIndices = {}; this.generateFrameDataTexture(); /** * The SubmitterSpriteGPULayer RenderNode for this SpriteGPULayer. * * This handles rendering the SpriteGPULayer to the GPU. * It is created automatically when the SpriteGPULayer is initialized. * Most RenderNodes are singletons stored in the RenderNodeManager, * but because this one holds very specific data, * it is stored in the SpriteGPULayer itself. * * @name Phaser.GameObjects.SpriteGPULayer#submitterNode * @type {Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer} * @since 4.0.0 */ this.submitterNode = new SubmitterSpriteGPULayer(scene.renderer.renderNodes, {}, this); this.defaultRenderNodes['Submitter'] = this.submitterNode; this.renderNodeData[this.submitterNode.name] = {}; this.resize(this.size); /** * The next member buffer, used to store member data * before it is added to the GPU buffer. * * @name Phaser.GameObjects.SpriteGPULayer#nextMember * @type {ArrayBuffer} * @since 4.0.0 */ this.nextMember = new ArrayBuffer(this.getDataByteSize()); /** * A Float32Array view of the next member buffer. * * @name Phaser.GameObjects.SpriteGPULayer#nextMemberF32 * @type {Float32Array} * @since 4.0.0 */ this.nextMemberF32 = new Float32Array(this.nextMember); /** * A Uint32Array view of the next member buffer. * This is used to write 32-bit integer data to the buffer. * It is used for color data. * * @name Phaser.GameObjects.SpriteGPULayer#nextMemberU32 * @type {Uint32Array} * @since 4.0.0 */ this.nextMemberU32 = new Uint32Array(this.nextMember); }, /** * Called when this SpriteGPULayer is added to a Scene. * Registers it with the Scene's update list so it receives `preUpdate` calls each frame. * * @method Phaser.GameObjects.SpriteGPULayer#addedToScene * @since 4.0.0 */ addedToScene: function () { this.scene.sys.updateList.add(this); }, /** * Called when this SpriteGPULayer is removed from a Scene. * Deregisters it from the Scene's update list so it no longer receives `preUpdate` calls. * * @method Phaser.GameObjects.SpriteGPULayer#removedFromScene * @since 4.0.0 */ removedFromScene: function () { this.scene.sys.updateList.remove(this); }, /** * The update step for this SpriteGPULayer, called each frame by the Scene. * Advances the internal elapsed timer used for member animations. * * @method Phaser.GameObjects.SpriteGPULayer#preUpdate * @since 4.0.0 * @param {number} time - The current timestamp, as generated by the Request Animation Frame or SetTimeout. * @param {number} delta - The delta time, in milliseconds, elapsed since the last frame. */ preUpdate: function (time, delta) { this.updateTimer(time, delta); }, /** * Get the number of bytes used to define a member. * If you are directly editing the buffer, you will need this value * as a 'stride' to move through the buffer. * * @method Phaser.GameObjects.SpriteGPULayer#getDataByteSize * @since 4.0.0 * @return {number} The number of bytes used for each member. */ getDataByteSize: function () { return this.submitterNode.instanceBufferLayout.layout.stride; }, /** * Return a list of features to enable in the shader program. * This is used when the shader program is compiled. * * @method Phaser.GameObjects.SpriteGPULayer#getShaderFeatures * @since 4.0.0 * @return {string[]} An array of features to enable in the shader program. */ getShaderFeatures: function () { var features = []; // Add enabled animations. var animations = Object.keys(this._animationsEnabled); var animLen = animations.length; for (var i = 0; i < animLen; i++) { if (this._animationsEnabled[animations[i]]) { features.push(animations[i]); } } return features; }, /** * Set the animations available to the SpriteGPULayer. * This will call `generateFrameDataTexture` to regenerate * `frameDataTexture`. * * Each animation can be either an Animation object, or an object * containing a name, duration, and an array of frame names/numbers. * If an Animation is used, it will be converted to the object form, * discarding any custom individual frame durations * and using the animation's duration as default. * * This is not a Phaser Animation. It is intended to cycle automatically * on the GPU without supervision or interaction. It will not emit events, * allow you to pause the animation, set number of repeats, etc. * * @method Phaser.GameObjects.SpriteGPULayer#setAnimations * @since 4.0.0 * @param {Phaser.Animations.Animation[]|Phaser.Types.GameObjects.SpriteGPULayer.SetAnimation[]} animations - An array of animations to set. * @return {this} This SpriteGPULayer object. */ setAnimations: function (animations) { var animLen = animations.length; // Animation frames will start after the texture frames. var frameNames = this.texture.getFrameNames(true); var index = frameNames.length; for (var i = 0; i < animLen; i++) { var anim = animations[i]; var data = {}; if (anim.key) { // This is a Phaser.Animations.Animation class. data.name = anim.key; data.duration = anim.duration; data.frames = anim.frames; } else { data.name = anim.name; data.duration = anim.duration; data.frames = anim.frames.slice(); } // Add frame indexing data. data.index = index; data.frameCount = data.frames.length; index += data.frameCount; // Store animation. this.animationData.push(data); this.animationDataNames[data.name] = data; this.animationDataIndices[data.index] = data; } this.generateFrameDataTexture(); return this; }, /** * Generate `frameDataTexture` for the SpriteGPULayer. * This is used by the vertex shader to access frame data. * * @method Phaser.GameObjects.SpriteGPULayer#generateFrameDataTexture * @since 4.0.0 */ generateFrameDataTexture: function () { // Get the frame data. var texture = this.texture; var frames = texture.getFrameNames(true); var frameLen = frames.length; // Update the frame data indices. this.frameDataIndices = {}; this.frameDataIndicesInv = {}; for (var i = 0; i < frameLen; i++) { var frameName = frames[i]; var frame = texture.get(frameName); this.frameDataIndices[frameName] = i; this.frameDataIndicesInv[i] = frameName; } // Append frames from animations. var anims = this.animationData; var animsLen = anims.length; for (i = 0; i < animsLen; i++) { var anim = this.animationData[i]; var frameCount = anim.frameCount; for (var j = 0; j < frameCount; j++) { frames.push(anim.frames[j]); } } frameLen = frames.length; var valuesPerFrame = 3; var pixelCount = frameLen * valuesPerFrame; var width = Math.min(pixelCount, 4096); var height = Math.ceil(pixelCount / 4096); var dataSize = width * height * 4; var textureManager = texture.manager; // Generate a Uint8Array with the frame data. var data = new ArrayBuffer(dataSize); var u16 = new Uint16Array(data); var u8 = new Uint8Array(data); for (i = 0; i < frameLen; i++) { var animFrame = frames[i]; if (typeof animFrame === 'string') { frame = texture.get(frames[i]); } else if (animFrame && animFrame.key !== undefined) { // animFrame comes from a SetAnimation object. var animTexture = textureManager.get(animFrame.key); frame = animTexture.get(animFrame.frame); } else { // animFrame is an AnimationFrame object. frame = animFrame.frame; } var offset = i * valuesPerFrame * u16.BYTES_PER_ELEMENT; // Position u16[offset] = frame.cutX; u16[offset + 1] = frame.cutY; // Size u16[offset + 2] = frame.cutWidth; u16[offset + 3] = frame.cutHeight; // Pivot offset // Multiplied by the size to convert to pixels. // Offset by 32768 to effectively store as a 16-bit signed integer. var pivotX = 0.5; var pivotY = 0.5; if (frame.customPivot) { pivotX = frame.pivotX; pivotY = frame.pivotY; } u16[offset + 4] = Math.round((pivotX - 0.5) * frame.cutWidth) + 32768; u16[offset + 5] = Math.round((pivotY - 0.5) * frame.cutHeight) + 32768; } // Create or update a texture with the frame data. if (this.frameDataTexture) { this.frameDataTexture.destroy(); } this.frameDataTexture = this.scene.renderer.createUint8ArrayTexture(u8, width, height, false, false); }, /** * Resizes the SpriteGPULayer buffer to a new size. * Optionally, clears the buffer. * * This is an expensive operation, as it requires the whole buffer to be updated. * It can take many frames to complete. * * @method Phaser.GameObjects.SpriteGPULayer#resize * @since 4.0.0 * @param {number} count - The new number of members in the SpriteGPULayer. * @param {boolean} [clear=false] - Whether to clear the buffer. * @return {this} This SpriteGPULayer object. */ resize: function (count, clear) { var layout = this.submitterNode.instanceBufferLayout; var buffer = layout.buffer; var u8 = buffer.viewU8; var targetByteSize = count * layout.layout.stride; this.size = count; buffer.resize(targetByteSize); if (clear) { this.memberCount = 0; } else { // Copy data from the old buffer to the new buffer. var newBuffer = buffer.viewU8; newBuffer.set(u8.subarray(0, Math.min(newBuffer.byteLength, targetByteSize))); this.memberCount = Math.min(this.memberCount, count); } this.bufferUpdateSegmentSize = Math.ceil(this.size / this._segments); this.setAllSegmentsNeedUpdate(); return this; }, /** * Sets a segment of the buffer to require an update. * * @method Phaser.GameObjects.SpriteGPULayer#setSegmentNeedsUpdate * @since 4.0.0 * @param {number} index - The index at which an update occurred, which requires the segment to be updated. */ setSegmentNeedsUpdate: function (index) { if ( index < 0 || index >= this.size || this.bufferUpdateSegments === this.MAX_BUFFER_UPDATE_SEGMENTS_FULL ) { return; } var segment = Math.floor(index / this.bufferUpdateSegmentSize); this.bufferUpdateSegments |= (1 << segment); }, /** * Sets all segments of the buffer to require an update. * * @method Phaser.GameObjects.SpriteGPULayer#setAllSegmentsNeedUpdate * @since 4.0.0 */ setAllSegmentsNeedUpdate: function () { this.bufferUpdateSegments = this.MAX_BUFFER_UPDATE_SEGMENTS_FULL; }, /** * Clears all segments of the buffer that require an update. * * @method Phaser.GameObjects.SpriteGPULayer#clearAllSegmentsNeedUpdate * @since 4.0.0 */ clearAllSegmentsNeedUpdate: function () { this.bufferUpdateSegments = 0; }, /** * Adds data to the SpriteGPULayer buffer. * It is inserted at the end of the buffer. * * This is mostly used internally by the SpriteGPULayer. * It takes raw data as a buffer, which is very efficient, * but `addMember` is easier to use. * * Note that, if you add a member with an animation, * the animation must either already be enabled, * or you must enable it with `setAnimationEnabled`, * e.g. `layer.setAnimationEnabled('Linear', true)` or * `layer.setAnimationEnabled(layer.EASE_CODES[layer.EASE.Linear], true)`. * * This is a buffer modification, and is expensive. * * @method Phaser.GameObjects.SpriteGPULayer#addData * @since 4.0.0 * @param {Float32Array} member - The raw data to add to the buffer. * @return {this} This SpriteGPULayer object. */ addData: function (member) { if (this.memberCount >= this.size) { return this; } var layout = this.submitterNode.instanceBufferLayout; var f32 = layout.buffer.viewF32; var offset = this.memberCount * layout.layout.stride; f32.set(member, offset / f32.BYTES_PER_ELEMENT); this.setSegmentNeedsUpdate(this.memberCount); this.memberCount++; return this; }, /** * Adds a member to the SpriteGPULayer. * This is the easiest way to add a member to the SpriteGPULayer. * * This is a buffer modification, and is expensive. * * @method Phaser.GameObjects.SpriteGPULayer#addMember * @since 4.0.0 * @param {Partial<Phaser.Types.GameObjects.SpriteGPULayer.Member>} [member] - The member to add to the SpriteGPULayer. * @return {this} This SpriteGPULayer object. */ addMember: function (member) { if (this.memberCount >= this.size) { return this; } var f32 = this.nextMemberF32; var u32 = this.nextMemberU32; if (!member) { member = {}; } var frame = this.frame; if (member.frame !== undefined) { frame = member.frame.base ? member.frame.base : member.frame; } if (typeof frame === 'string') { frame = this.texture.get(frame); if (!frame) { return this; } } var offset = 0; this._setAnimatedValue(member.x, offset); offset += 4; this._setAnimatedValue(member.y, offset); offset += 4; this._setAnimatedValue(member.rotation, offset); offset += 4; this._setAnimatedValue(member.scaleX, offset, 1); offset += 4; this._setAnimatedValue(member.scaleY, offset, 1); offset += 4; this._setAnimatedValue(member.alpha, offset, 1); offset += 4; var animation = member.animation; if (animation) { // Use frame animation. var animData; if ( (typeof animation === 'string') || (typeof animation === 'number') ) { if (typeof animation === 'string') { animData = this.animationDataNames[animation]; } else { animData = this.animationDataIndices[animation]; } this._setAnimatedValue({ base: animData.index, amplitude: animData.frameCount, duration: animData.duration, ease: EasingEncoding.Linear, yoyo: false }, offset); } else { var base = animation.base; if (typeof base === 'string') { animData = this.animationDataNames[base]; } else if (typeof base === 'number') { animData = this.animationDataIndices[base]; } else { // Bad data; fall back to first animation. animData = this.animationData[0]; } this._setAnimatedValue({ base: animData.index, amplitude: (typeof animation.amplitude === 'number') ? animation.amplitude : animData.frameCount, duration: animation.duration || animData.duration, delay: animation.delay || 0, ease: animation.ease || EasingEncoding.Linear, yoyo: !!animation.yoyo }, offset); } } else { // Use single frame. var frameIndex = this.frameDataIndices[frame.name]; var memberFrame = member.frame; if (memberFrame && memberFrame.base !== undefined) { this._setAnimatedValue({ base: frameIndex, amplitude: memberFrame.amplitude, duration: memberFrame.duration, delay: memberFrame.delay, ease: memberFrame.ease, yoyo: memberFrame.yoyo }, offset); } else { this._setAnimatedValue(frameIndex, offset); } } offset += 4; this._setAnimatedValue(member.tintBlend, offset, 1); offset += 4; var tintBottomLeft = member.tintBottomLeft === undefined ? 0xffffff : member.tintBottomLeft; var tintTopLeft = member.tintTopLeft === undefined ? 0xffffff : member.tintTopLeft; var tintBottomRight = member.tintBottomRight === undefined ? 0xffffff : member.tintBottomRight; var tintTopRight = member.tintTopRight === undefined ? 0xffffff : member.tintTopRight; var alphaBottomLeft = member.alphaBottomLeft === undefined ? 1 : member.alphaBottomLeft; var alphaTopLeft = member.alphaTopLeft === undefined ? 1 : member.alphaTopLeft; var alphaBottomRight = member.alphaBottomRight === undefined ? 1 : member.alphaBottomRight; var alphaTopRight = member.alphaTopRight === undefined ? 1 : member.alphaTopRight; u32[offset++] = getTint( tintBottomLeft, alphaBottomLeft ); u32[offset++] = getTint( tintTopLeft, alphaTopLeft ); u32[offset++] = getTint( tintBottomRight, alphaBottomRight ); u32[offset++] = getTint( tintTopRight, alphaTopRight ); f32[offset++] = member.originX === undefined ? 0.5 : member.originX; f32[offset++] = member.originY === undefined ? 0.5 : member.originY; f32[offset++] = member.tintMode || 0; f32[offset++] = member.creationTime === undefined ? this.timeElapsed : member.creationTime; f32[offset++] = member.scrollFactorX === undefined ? 1 : member.scrollFactorX; f32[offset++] = member.scrollFactorY === undefined ? 1 : member.scrollFactorY; this.addData(this.nextMemberF32); return this; }, /** * Edits a member of the SpriteGPULayer. * This will update the member's data in the GPU buffer. * Only the buffer segment containing the target member will be marked for update, * making this less expensive than a full buffer update. * * @method Phaser.GameObjects.SpriteGPULayer#editMember * @since 4.0.0 * @param {number} index - The index of the member to edit. * @param {Partial<Phaser.Types.GameObjects.SpriteGPULayer.Member>} member - The new member data. * @return {this} This SpriteGPULayer object. */ editMember: function (index, member) { if (index < 0 || index >= this.memberCount) { return this; } var currentMemberCount = this.memberCount; this.memberCount = index; this.addMember(member); this.memberCount = currentMemberCount; return this; }, /** * Update a member of the SpriteGPULayer with raw data. * This will update the member's data in the GPU buffer. * Only the buffer segment containing the target member will be marked for update, * making this less expensive than a full buffer update. * * You can supply a mask to control which properties are updated. * This can be useful for updating only a subset of properties. * Try using `getMemberData` to copy an existing member's data, * then modify the data you want to change. * * The data must be passed in as an Uint32Array. * This will preserve data that other TypedArrays would not. * As it uses an underlying ArrayBuffer, you can work on the data * with any TypedArray view before submitting it. * * @method Phaser.GameObjects.SpriteGPULayer#patchMember * @since 4.0.0 * @param {number} index - The index of the member to patch. * @param {Uint32Array} member - The new member data. * @param {number[]} [mask] - The mask to apply to the member data. A value of 1 will update the member data, a value of 0 will keep the existing member data. */ patchMember: function (index, member, mask) { if (index < 0 || index >= this.memberCount) { return; } var layout = this.submitterNode.instanceBufferLayout; var buffer = layout.buffer; var stride = layout.layout.stride; var byteOffset = index * stride; var u32 = buffer.viewU32; var offset = byteOffset / 4; if (mask) { for (var i = 0; i < member.length; i++) { if (mask[i]) { u32[offset + i] = member[i]; } } } else { u32.set(member, offset); } this.setSegmentNeedsUpdate(index); }, /** * Returns a member of the SpriteGPULayer. * * This returns an object copied from the buffer. * Editing it will not change anything in the SpriteGPULayer. * The object will be functionally identical to the data used to * create the buffer, but some values may be different. * * - Properties that support animation, but have no amplitude or duration or have easing 'None' (0), will be presented as numbers. * - Animation easing values will be presented as numbers (the values * in `this.EASE`). * - Animation delay values will be normalized to the duration, * e.g. a delay of 150 with a duration of 100 will return 50. * - Some rounding may occur due to floating point precision. * * @method Phaser.GameObjects.SpriteGPULayer#getMember * @since 4.0.0 * @param {number} index - The index of the member to get. * @return {?Phaser.Types.GameObjects.SpriteGPULayer.Member} The member data, or null if the index is out of bounds. */ getMember: function (index) { if (index < 0 || index >= this.memberCount) { return null; } var layout = this.submitterNode.instanceBufferLayout; var buffer = layout.buffer; var stride = layout.layout.stride; var byteOffset = index * stride; var f32 = buffer.viewF32; var u32 = buffer.viewU32; var member = {}; var offset = byteOffset / f32.BYTES_PER_ELEMENT; member.x = this._getAnimatedValue(offset); offset += 4; member.y = this._getAnimatedValue(offset); offset += 4; member.rotation = this._getAnimatedValue(offset); offset += 4; member.scaleX = this._getAnimatedValue(offset); offset += 4; member.scaleY = this._getAnimatedValue(offset); offset += 4; member.alpha = this._getAnimatedValue(offset); offset += 4; // Determine frame or animation values. var frame = this._getAnimatedValue(offset); offset += 4; if (typeof frame !== 'number') { frame = frame.base; } // Get name from frame index. var frameName = this.frameDataIndicesInv[frame]; if (frameName === undefined) { // Get name from animation index. var animData = this.animationDataIndices[frame]; if (animData) { member.animation = animData.name; } } else { member.frame = frameName; } member.tintBlend = this._getAnimatedValue(offset); offset += 4; member.tintBottomLeft = u32[offset++]; member.tintTopLeft = u32[offset++]; member.tintBottomRight = u32[offset++]; member.tintTopRight = u32[offset++]; member.alphaBottomLeft = (member.tintBottomLeft >>> 24) / 255; member.alphaTopLeft = (member.tintTopLeft >>> 24) / 255; member.alphaBottomRight = (member.tintBottomRight >>> 24) / 255; member.alphaTopRight = (member.tintTopRight >>> 24) / 255; member.tintBottomLeft &= 0xffffff; member.tintTopLeft &= 0xffffff; member.tintBottomRight &= 0xffffff; member.tintTopRight &= 0xffffff; member.originX = f32[offset++]; member.originY = f32[offset++]; member.tintMode = f32[offset++]; member.creationTime = f32[offset++]; member.scrollFactorX = f32[offset++]; member.scrollFactorY = f32[offset++]; return member; }, /** * Returns the raw data of a member of the SpriteGPULayer. * This can be useful as the base of efficient editing operations, * including calls to `addData` and `patchMember`, * so no data has to be converted. * * This returns an Uint32Array copied from the buffer. * Editing it will not change anything in the SpriteGPULayer. * The array will be functionally identical to the data used to * create the buffer. * * By default, the data is copied into `this.nextMember`. * You can use the views `this.nextMemberF32` and `this.nextMemberU32` * to access the data in different formats. * If you provide an `out` parameter, the data will be copied to that array, * and you must construct your own views. * * The primary data view is a 42-element array of 32-bit floats. * Some values are grouped to form animations, of the form: * * - 0: base value * - 1: amplitude * - 2: duration (if negative, the animation will yoyo) * - 3: delay (the integer part is the easing, the decimal part is the delay divided by 2 * duration; if negative, the animation will not loop) * * The overall structure is thus: * * - 0-3: x (animation) * - 4-7: y (animation) * - 8-11: rotation (animation) * - 12-15: scaleX (animation) * - 16-19: scaleY (animation) * - 20-23: alpha (animation) * - 24-27: frame index (animation) * - 28-31: tintBlend (animation) * - 32-35: no data * - 36: originX * - 37: originY * - 38: tintMode * - 39: creationTime * - 40: scrollFactorX * - 41: scrollFactorY * * Elements 32-35 are only visible in the Uint32Array view. * They store 32-bit RGBA values for the four corners of the tint: * * - 32: bottom-left * - 33: top-left * - 34: bottom-right * - 35: top-right * * If the ease for an animation is 'Gravity', the amplitude is replaced * with a two-part value: the integer part is the `velocity`, * and the fractional part is the remapped `gravityFactor`. * To get the true `gravityFactor`, use `gravityFactor * 2 - 1` to map from [0,1] to [-1,1]. * An output `gravityFactor` of 0 actually means 1. * * @method Phaser.GameObjects.SpriteGPULayer#getMemberData * @since 4.0.0 * @param {number} index - The index of the member to get. * @param {Uint32Array} [out] - An optional array to copy the data to. If not provided, `this.nextMember` will be populated, and `nextMemberU32` will be returned. * @return {?Uint32Array} The member data, or null if the index is out of bounds. */ getMemberData: function (index, out) { if (index < 0 || index >= this.memberCount) { return null; } var layout = this.submitterNode.instanceBufferLayout; var buffer = layout.buffer; var stride = layout.layout.stride; var byteOffset = index * stride; if (!out) { out = this.nextMemberU32; } var viewU32 = buffer.viewU32; var bytesPerElement = viewU32.BYTES_PER_ELEMENT; out.set(viewU32.subarray(byteOffset / bytesPerElement, byteOffset / bytesPerElement + stride / bytesPerElement)); return out; }, /** * Removes a member or a number of members from the SpriteGPULayer. * This will update the GPU buffer. * This is an expensive operation, as it requires the whole buffer to be updated. * * The buffer is not resized. * * @method Phaser.GameObjects.SpriteGPULayer#removeMembers * @since 4.0.0 * @param {number} index - The index of the member to remove. * @param {number} [count=1] - The number of members to remove, default 1. * @return {this} This SpriteGPULayer object. */ removeMembers: function (index, count) { if (index < 0 || index >= this.memberCount) { return this; } if (count === undefined) { count = 1; } count = Math.min(count, this.memberCount - index); var layout = this.submitterNode.instanceBufferLayout; var stride = layout.layout.stride; var byteOffset = index * stride; var byteLength = count * stride; var u8 = layout.buffer.viewU8; u8.set(u8.subarray(byteOffset + byteLength), byteOffset); // Mark segments for update. for (var i = index; i < this.memberCount; i += this.bufferUpdateSegmentSize) { this.setSegmentNeedsUpdate(i); } // Update layer properties. this.memberCount -= count; return this; }, /** * Inserts members into the SpriteGPULayer. * This will update the GPU buffer. * This is an expensive operation, as it requires the whole buffer to be * updated after the insertion point. * * @method Phaser.GameObjects.SpriteGPULayer#insertMembers * @since 4.0.0 * @param {number} index - The index at which to insert members. * @param {Phaser.Types.GameObjects.SpriteGPULayer.Member|Phaser.Types.GameObjects.SpriteGPULayer.Member[]} members - The members to insert. * @return {this} This SpriteGPULayer object. */ insertMembers: function (index, members) { if (index < 0 || index > this.memberCount) { return this; } if (!Array.isArray(members)) { members = [ members ]; } var oldMemberCount = this.memberCount; var layout = this.submitterNode.instanceBufferLayout; var stride = layout.layout.stride; var byteOffset = index * stride; var byteLength = members.length * stride; // Move the data after the insertion point. layout.buffer.viewU8.copyWithin( // Target byteOffset + byteLength, // Source byteOffset, // End oldMemberCount * stride ); // Insert members. this.memberCount = index; for (var i = 0; i < members.length; i++) { this.addMember(members[i]); } this.memberCount = Math.min(this.size, oldMemberCount + members.length); // Mark segments for update. for (i = index; i < this.memberCount; i += this.bufferUpdateSegmentSize) { this.setSegmentNeedsUpdate(i); } return this; }, /** * Inserts raw data into the SpriteGPULayer. * This will update the GPU buffer. * This is an expensive operation, as it requires the whole buffer to be * updated after the insertion point. * * The data must be passed in as a Uint32Array. * This will preserve data that other TypedArrays would not. * As it uses an underlying ArrayBuffer, you can work on the data * with any TypedArray view before submitting it. * * The buffer can contain 1 or more members. * Ensure that the buffer is the correct size for the number of members. * See `getMemberData` for the structure of the data. * * Note that, if you add a member with an animation, * the animation must either already be enabled, * or you must enable it with `setAnimationEnabled`, * e.g. `layer.setAnimationEnabled('Linear', true)` or * `layer.setAnimationEnabled(layer.EASE_CODES[layer.EASE.Linear], true)`. * * @method Phaser.GameObjects.SpriteGPULayer#insertMembersData * @since 4.0.0 * @param {number} index - The index at which to insert members. * @param {Uint32Array} data - The members to insert. * @return {this} This SpriteGPULayer object. */ insertMembersData: function (index, data) { if (index < 0 || index > this.memberCount) { return this; } var byteLength = data.length * data.BYTES_PER_ELEMENT; var layout = this.submitterNode.instanceBufferLayout; var stride = layout.layout.stride; var byteOffset = index * stride; // Move the data after the insertion point. layout.buffer.viewU8.copyWithin( // Target byteOffset + byteLength, // Source byteOffset, // End this.memberCount * stride ); // Insert members. layout.buffer.viewU32.set(data, byteOffset / data.BYTES_PER_ELEMENT); this.memberCount = Math.min(this.size, this.memberCount + byteLength / stride); // Mark segments for update. for (var i = index; i < this.memberCount; i += this.bufferUpdateSegmentSize) { this.setSegmentNeedsUpdate(i); } return this; }, /** * Sets the values of an animation for a member of this SpriteGPULayer. * The values are set on `nextMember`, used to add data. * * @method Phaser.GameObjects.SpriteGPULayer#_setAnimatedValue * @since 4.0.0 * @private * @param {undefined|number|Phaser.Types.GameObjects.SpriteGPULayer.MemberAnimation} value - The value to set. * @param {number} index - The offset in `nextMember` to write to. * @param {number} [defaultValue=0] - A default value to use if `value` is undefined. */ _setAnimatedVal