From a75213c9584a4d0990e52dbb3a7352beb6f21637 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 6 Jun 2024 15:35:22 +0100 Subject: [PATCH] chore: optimisation and fix for clipping and blend modes (#11) Co-authored-by: Mat Groves Co-authored-by: Zyie <24736175+Zyie@users.noreply.github.com> --- examples/assets/spine_logo.png | Bin 0 -> 3495 bytes examples/events-example.html | 6 +- examples/index.html | 2 +- examples/manual-loading.html | 6 +- examples/mix-and-match-example.html | 6 +- examples/mouse-following.html | 9 +- examples/simple-input.html | 58 +++- examples/slot-objects.html | 125 ++++++++ src/BatchableClippedSpineSlot.ts | 123 -------- src/BatchableSpineSlot.ts | 91 +++--- src/Spine.ts | 451 ++++++++++++++++++++++++---- src/SpinePipe.ts | 165 ++++------ src/getSkeletonBounds.ts | 75 ----- 13 files changed, 675 insertions(+), 442 deletions(-) create mode 100644 examples/assets/spine_logo.png create mode 100644 examples/slot-objects.html delete mode 100644 src/BatchableClippedSpineSlot.ts delete mode 100644 src/getSkeletonBounds.ts diff --git a/examples/assets/spine_logo.png b/examples/assets/spine_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..40e65c5c91884ba3af3a5d70cb8eb265c6b3baeb GIT binary patch literal 3495 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 spine-pixi - + @@ -35,8 +35,8 @@ document.body.appendChild(app.view); // Pre-load the skeleton data and atlas. You can also load .json skeleton data. - PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel"); - PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas"); + PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" }); + PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" }); await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]); // Create the Spine display object diff --git a/examples/index.html b/examples/index.html index da69aff..f1b9866 100644 --- a/examples/index.html +++ b/examples/index.html @@ -2,7 +2,7 @@ spine-pixi - + diff --git a/examples/manual-loading.html b/examples/manual-loading.html index 1792aa3..7acbd87 100644 --- a/examples/manual-loading.html +++ b/examples/manual-loading.html @@ -2,7 +2,7 @@ spine-pixi - + @@ -24,8 +24,8 @@ document.body.appendChild(app.view); // Pre-load the skeleton data and atlas. You can also load .json skeleton data. - PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel"); - PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas"); + PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" }); + PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" }); await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]); // Manually load the data and create a Spine display object from it using diff --git a/examples/mix-and-match-example.html b/examples/mix-and-match-example.html index 30a636a..ac69f38 100644 --- a/examples/mix-and-match-example.html +++ b/examples/mix-and-match-example.html @@ -2,7 +2,7 @@ spine-pixi - + @@ -24,8 +24,8 @@ document.body.appendChild(app.view); // Pre-load the skeleton data and atlas. You can also load .json skeleton data. - PIXI.Assets.add("mixAndMatchData", "./assets/mix-and-match-pro.skel"); - PIXI.Assets.add("mixAndMatchAtlas", "./assets/mix-and-match-pma.atlas"); + PIXI.Assets.add({alias: "mixAndMatchData", src: "./assets/mix-and-match-pro.skel" }); + PIXI.Assets.add({alias: "mixAndMatchAtlas", src: "./assets/mix-and-match-pma.atlas" }); await PIXI.Assets.load(["mixAndMatchData", "mixAndMatchAtlas"]); // Create the Spine display object diff --git a/examples/mouse-following.html b/examples/mouse-following.html index 9038b26..b157ba7 100644 --- a/examples/mouse-following.html +++ b/examples/mouse-following.html @@ -2,7 +2,7 @@ Spine Pixi Example - + @@ -24,8 +24,8 @@ document.body.appendChild(app.view); // Pre-load the skeleton data and atlas. You can also load .json skeleton data. - PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel"); - PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas"); + PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" }); + PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" }); await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]); // Create the spine display object @@ -47,6 +47,7 @@ // Add the display object to the stage. app.stage.addChild(spineboy); + app.stage.hitArea = new PIXI.Rectangle(0, 0, app.view.width, app.view.height); // Make the stage interactive and register pointer events app.stage.eventMode = "dynamic"; @@ -57,7 +58,7 @@ setBonePosition(e); }); - app.stage.on("pointermove", (e) => { + app.stage.on("globalpointermove", (e) => { if (isDragging) setBonePosition(e); }); diff --git a/examples/simple-input.html b/examples/simple-input.html index 8220bf1..5c13edd 100644 --- a/examples/simple-input.html +++ b/examples/simple-input.html @@ -2,7 +2,7 @@ spine-pixi - + @@ -24,8 +24,8 @@ document.body.appendChild(app.view); // Pre-load the skeleton data and atlas. You can also load .json skeleton data. - PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel"); - PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas"); + PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" }); + PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" }); await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]); // Create the spine display object @@ -35,7 +35,7 @@ // Set the default animation and the // default mix for transitioning between animations. - spineboy.state.setAnimation(0, "run", true); + spineboy.state.setAnimation(0, "hoverboard", true); spineboy.state.data.defaultMix = 0.2; // Center the spine object on screen. @@ -43,20 +43,56 @@ spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2; // Make it so that you can interact with Spineboy. - // Also, handle the case that you click or tap on the screen. - // The callback function definition can be seen below. + // Handle the case that you click/tap the screen. spineboy.eventMode = 'static'; spineboy.on('pointerdown', onClick); - + // Add the display object to the stage. app.stage.addChild(spineboy); + + // Add variables for movement, speed. + let moveLeft = false; + let moveRight = false; + const speed = 5; + + // Handle the case that the keyboard keys specified below are pressed. + function onKeyDown(key) { + if (key.code === "ArrowLeft" || key.code === "KeyA") { + moveLeft = true; + spineboy.skeleton.scaleX = -1; + } else if (key.code === "ArrowRight" || key.code === "KeyD") { + moveRight = true; + spineboy.skeleton.scaleX = 1; + } + } + + // Handle when the keys are released, if they were pressed. + function onKeyUp(key) { + if (key.code === "ArrowLeft" || key.code === "KeyA") { + moveLeft = false; + } else if (key.code === "ArrowRight" || key.code === "KeyD") { + moveRight = false; + } + } - // This callback function handles what happens - // when you click or tap on the screen. + // Handle if you click/tap the screen. function onClick() { - spineboy.state.addAnimation(0, "jump", false, 0); - spineboy.state.addAnimation(0, "idle", true, 0); + spineboy.state.setAnimation(1, "shoot", false, 0); } + + // Add event listeners so that the window will correctly handle input. + window.addEventListener("keydown", onKeyDown); + window.addEventListener("keyup", onKeyUp); + + // Update the application to move Spineboy if input is detected. + app.ticker.add(() => { + if (moveLeft) { + spineboy.x -= speed; + } + if (moveRight) { + spineboy.x += speed; + } + }); })(); diff --git a/examples/slot-objects.html b/examples/slot-objects.html new file mode 100644 index 0000000..3f09381 --- /dev/null +++ b/examples/slot-objects.html @@ -0,0 +1,125 @@ + + + + spine-pixi + + + + + + + + + + diff --git a/src/BatchableClippedSpineSlot.ts b/src/BatchableClippedSpineSlot.ts deleted file mode 100644 index 1d3b983..0000000 --- a/src/BatchableClippedSpineSlot.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** **************************************************************************** - * Spine Runtimes License Agreement - * Last updated July 28, 2023. Replaces all prior versions. - * - * Copyright (c) 2013-2023, Esoteric Software LLC - * - * Integration of the Spine Runtimes into software or otherwise creating - * derivative works of the Spine Runtimes is permitted under the terms and - * conditions of Section 2 of the Spine Editor License Agreement: - * http://esotericsoftware.com/spine-editor-license - * - * Otherwise, it is permitted to integrate the Spine Runtimes into software or - * otherwise create derivative works of the Spine Runtimes (collectively, - * "Products"), provided that each user of the Products must obtain their own - * Spine Editor license and redistribution of the Products in any form must - * include this license and copyright notice. - * - * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, - * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE - * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - *****************************************************************************/ - -import { Spine } from './Spine'; - -import type { Batch, BatchableObject, Batcher, IndexBufferArray, Texture } from 'pixi.js'; -import type { SkeletonClipping, Slot } from '@esotericsoftware/spine-core'; - -export class BatchableClippedSpineSlot implements BatchableObject -{ - indexStart: number; - textureId: number; - texture: Texture; - location: number; - batcher: Batcher; - batch: Batch; - renderable: Spine; - - slot:Slot; - indexSize: number; - vertexSize: number; - clippedVertices: number[] = []; - clippedTriangles: number[] = []; - - roundPixels: 0 | 1; - - get blendMode() { return this.renderable.groupBlendMode; } - - reset() - { - this.renderable = null as any; - this.texture = null as any; - this.batcher = null as any; - this.batch = null as any; - } - - setClipper(clipper:SkeletonClipping) - { - // copy clipped verts and triangles - copyArray(clipper.clippedVertices, this.clippedVertices); - copyArray(clipper.clippedTriangles, this.clippedTriangles); - - this.vertexSize = (clipper.clippedVertices.length / 8); - this.indexSize = clipper.clippedTriangles.length; - } - - packIndex(indexBuffer: IndexBufferArray, index: number, indicesOffset: number) - { - const indices = this.clippedTriangles; - - for (let i = 0; i < indices.length; i++) - { - indexBuffer[index++] = indices[i] + indicesOffset; - } - } - - packAttributes( - float32View: Float32Array, - uint32View: Uint32Array, - index: number, - textureId: number - ) - { - const clippedVertices = this.clippedVertices; - const vertexSize = this.vertexSize; - - const abgr = this.renderable.groupColor; - - const textureIdAndRound = (textureId << 16) | (this.roundPixels & 0xFFFF); - - for (let i = 0; i < vertexSize; i++) - { - const localIndex = i * 8; - - // position - float32View[index++] = clippedVertices[localIndex]; - float32View[index++] = clippedVertices[localIndex + 1]; - - // uv - float32View[index++] = clippedVertices[localIndex + 6]; - float32View[index++] = clippedVertices[localIndex + 7]; - // color - uint32View[index++] = abgr; - - // texture id - float32View[index++] = textureIdAndRound; - } - } -} - -function copyArray(a:number[], b:number[]) -{ - for (let i = 0; i < a.length; i++) - { - b[i] = a[i]; - } -} diff --git a/src/BatchableSpineSlot.ts b/src/BatchableSpineSlot.ts index 850b0f3..25ff043 100644 --- a/src/BatchableSpineSlot.ts +++ b/src/BatchableSpineSlot.ts @@ -27,12 +27,9 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Spine } from './Spine'; -import { MeshAttachment, RegionAttachment, Slot } from '@esotericsoftware/spine-core'; +import { AttachmentCacheData, Spine } from './Spine'; -import type { Batch, BatchableObject, Batcher, IndexBufferArray, Texture } from 'pixi.js'; - -const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; +import type { Batch, BatchableObject, Batcher, BLEND_MODES, IndexBufferArray, Texture } from 'pixi.js'; export class BatchableSpineSlot implements BatchableObject { @@ -44,43 +41,55 @@ export class BatchableSpineSlot implements BatchableObject batch: Batch; renderable: Spine; - slot:Slot; + vertices: Float32Array; + indices: number[] | Uint16Array; + uvs: Float32Array; + indexSize: number; vertexSize: number; roundPixels: 0 | 1; - - get blendMode() { return this.renderable.groupBlendMode; } - - reset() - { - this.renderable = null as any; - this.texture = null as any; - this.batcher = null as any; - this.batch = null as any; - } - - setSlot(slot:Slot) + data: AttachmentCacheData; + blendMode: BLEND_MODES; + + setData( + renderable:Spine, + data:AttachmentCacheData, + texture:Texture, + blendMode:BLEND_MODES, + roundPixels: 0 | 1) { - this.slot = slot; - - const attachment = slot.getAttachment(); + this.renderable = renderable; + this.data = data; - if (attachment instanceof RegionAttachment) + if (data.clipped) { - this.vertexSize = 4; - this.indexSize = 6; + const clippedData = data.clippedData; + + this.indexSize = clippedData.indicesCount; + this.vertexSize = clippedData.vertexCount; + this.vertices = clippedData.vertices; + this.indices = clippedData.indices; + this.uvs = clippedData.uvs; } - else if (attachment instanceof MeshAttachment) + else { - this.vertexSize = attachment.worldVerticesLength / 2; - this.indexSize = attachment.triangles.length; + this.indexSize = data.indices.length; + this.vertexSize = data.vertices.length / 2; + this.vertices = data.vertices; + this.indices = data.indices; + this.uvs = data.uvs; } + + this.texture = texture; + this.roundPixels = roundPixels; + + this.blendMode = blendMode; } packIndex(indexBuffer: IndexBufferArray, index: number, indicesOffset: number) { - const indices = (this.slot.getAttachment() as MeshAttachment).triangles ?? QUAD_TRIANGLES; + const indices = this.indices; for (let i = 0; i < indices.length; i++) { @@ -95,25 +104,13 @@ export class BatchableSpineSlot implements BatchableObject textureId: number ) { - const slot = this.slot; - const attachment = slot.getAttachment() as MeshAttachment | RegionAttachment; + const { uvs, vertices, vertexSize } = this; - if (attachment instanceof MeshAttachment) - { - attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, float32View, index, 6); - } - else if (attachment instanceof RegionAttachment) - { - attachment.computeWorldVertices(slot, float32View, index, 6); - } - - const vertexSize = this.vertexSize; + const slotColor = this.data.color; - const parentColor:number = this.renderable.groupColor; // BGR + const parentColor:number = this.renderable.groupColor; const parentAlpha:number = this.renderable.groupAlpha; - const slotColor: {r: number, g:number, b: number, a: number} = slot.color; - let abgr:number; const mixedA = (slotColor.a * parentAlpha) * 255; @@ -135,8 +132,6 @@ export class BatchableSpineSlot implements BatchableObject abgr = ((mixedA) << 24) | ((slotColor.b * 255) << 16) | ((slotColor.g * 255) << 8) | (slotColor.r * 255); } - const uvs = attachment.uvs; - const matrix = this.renderable.groupTransform; const a = matrix.a; @@ -150,10 +145,8 @@ export class BatchableSpineSlot implements BatchableObject for (let i = 0; i < vertexSize; i++) { - // index++; - // float32View[index++] *= -1; - const x = float32View[index]; - const y = float32View[index + 1]; + const x = vertices[i * 2]; + const y = vertices[(i * 2) + 1]; float32View[index++] = (a * x) + (c * y) + tx; float32View[index++] = (b * x) + (d * y) + ty; diff --git a/src/Spine.ts b/src/Spine.ts index 7ab2e37..3e47243 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -37,24 +37,29 @@ import { DestroyOptions, PointData, Ticker, - View + View, } from 'pixi.js'; -import { getSkeletonBounds } from './getSkeletonBounds'; import { ISpineDebugRenderer } from './SpineDebugRenderer'; import { AnimationState, AnimationStateData, AtlasAttachmentLoader, + Attachment, Bone, + ClippingAttachment, + Color, + MeshAttachment, + RegionAttachment, Skeleton, SkeletonBinary, SkeletonBounds, + SkeletonClipping, SkeletonData, SkeletonJson, Slot, type TextureAtlas, TrackEntry, - Vector2 + Vector2, } from '@esotericsoftware/spine-core'; export type SpineFromOptions = { @@ -64,9 +69,13 @@ export type SpineFromOptions = { }; const vectorAux = new Vector2(); +const lightColor = new Color(); +const darkColor = new Color(); Skeleton.yDown = true; +const clipper = new SkeletonClipping(); + export interface SpineOptions extends ContainerOptions { skeletonData: SkeletonData; @@ -83,6 +92,23 @@ export interface SpineEvents start: [trackEntry: TrackEntry]; } +export interface AttachmentCacheData +{ + id: string; + clipped: boolean; + vertices: Float32Array; + uvs: Float32Array; + indices: number[]; + color: { r: number; g: number; b: number; a: number }; + clippedData?: { + vertices: Float32Array; + uvs: Float32Array; + indices: Uint16Array; + vertexCount: number; + indicesCount: number; + }; +} + export class Spine extends Container implements View { // Pixi properties @@ -92,7 +118,7 @@ export class Spine extends Container implements View public _didSpineUpdate = false; public _boundsDirty = true; public _roundPixels: 0 | 1; - private _bounds:Bounds = new Bounds(); + private _bounds: Bounds = new Bounds(); // Spine properties public skeleton: Skeleton; @@ -100,12 +126,32 @@ export class Spine extends Container implements View public skeletonBounds: SkeletonBounds; private _debug?: ISpineDebugRenderer | undefined = undefined; - readonly _slotAttachments:{slot:Slot, container:Container}[] = []; + readonly _slotsObject: Record = Object.create(null); + + private getSlotFromRef(slotRef: number | string | Slot): Slot + { + let slot: Slot | null; + + if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef]; + else if (typeof slotRef === 'string') slot = this.skeleton.findSlot(slotRef); + else slot = slotRef; + + if (!slot) throw new Error(`No slot found with the given slot reference: ${slotRef}`); + + return slot; + } + + public spineAttachmentsDirty: boolean; + private _lastAttachments: Attachment[]; + + private _stateChanged: boolean; + private attachmentCacheData: Record = {}; public get debug(): ISpineDebugRenderer | undefined { return this._debug; } + public set debug(value: ISpineDebugRenderer | undefined) { if (this._debug) @@ -118,12 +164,15 @@ export class Spine extends Container implements View } this._debug = value; } + private autoUpdateWarned = false; private _autoUpdate = true; + public get autoUpdate(): boolean { return this._autoUpdate; } + public set autoUpdate(value: boolean) { if (value) @@ -135,15 +184,16 @@ export class Spine extends Container implements View { Ticker.shared.remove(this.internalUpdate, this); } + this._autoUpdate = value; } - constructor(options:SpineOptions | SkeletonData) + constructor(options: SpineOptions | SkeletonData) { if (options instanceof SkeletonData) { options = { - skeletonData: options + skeletonData: options, }; } @@ -160,10 +210,13 @@ export class Spine extends Container implements View { if (this.autoUpdate && !this.autoUpdateWarned) { - // eslint-disable-next-line max-len - console.warn('You are calling update on a Spine instance that has autoUpdate set to true. This is probably not what you want.'); + console.warn( + // eslint-disable-next-line max-len + 'You are calling update on a Spine instance that has autoUpdate set to true. This is probably not what you want.', + ); this.autoUpdateWarned = true; } + this.internalUpdate(0, dt); } @@ -171,7 +224,7 @@ export class Spine extends Container implements View { // Because reasons, pixi uses deltaFrames at 60fps. // We ignore the default deltaFrames and use the deltaSeconds from pixi ticker. - this.updateState(deltaSeconds ?? Ticker.shared.deltaMS / 1000); + this._updateState(deltaSeconds ?? Ticker.shared.deltaMS / 1000); } get bounds() @@ -237,14 +290,226 @@ export class Spine extends Container implements View return outPos; } - updateState(dt:number) + /** + * Will update the state based on the specified time, this will not apply the state to the skeleton + * as this is differed until the `applyState` method is called. + * + * @param time the time at which to set the state + * @internal + */ + _updateState(time: number) { - this.state.update(dt); + this.state.update(time); + + this._stateChanged = true; + this._boundsDirty = true; - for (let i = 0; i < this._slotAttachments.length; i++) + this.onViewUpdate(); + } + + /** + * Applies the state to this spine instance. + * - updates the state to the skeleton + * - updates its world transform (spine world transform) + * - validates the attachments - to flag if the attachments have changed this state + * - transforms the attachments - to update the vertices of the attachments based on the new positions + * - update the slot attachments - to update the position, rotation, scale, and visibility of the attached containers + * @internal + */ + _applyState() + { + if (!this._stateChanged) return; + this._stateChanged = false; + + const { skeleton } = this; + + this.state.apply(skeleton); + + skeleton.updateWorldTransform(); + + this.validateAttachments(); + + this.transformAttachments(); + + this.updateSlotAttachments(); + } + + private validateAttachments() + { + const currentDrawOrder = this.skeleton.drawOrder; + + const lastAttachments = (this._lastAttachments ||= []); + + let index = 0; + + let spineAttachmentsDirty = false; + + for (let i = 0; i < currentDrawOrder.length; i++) + { + const slot = currentDrawOrder[i]; + const attachment = slot.getAttachment(); + + if (attachment) + { + if (attachment !== lastAttachments[index]) + { + spineAttachmentsDirty = true; + lastAttachments[index] = attachment; + } + + index++; + } + } + + if (index !== lastAttachments.length) + { + spineAttachmentsDirty = true; + lastAttachments.length = index; + } + + this.spineAttachmentsDirty = spineAttachmentsDirty; + } + + private transformAttachments() + { + const currentDrawOrder = this.skeleton.drawOrder; + + for (let i = 0; i < currentDrawOrder.length; i++) + { + const slot = currentDrawOrder[i]; + + const attachment = slot.getAttachment(); + + if (attachment) + { + if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) + { + const cacheData = this.getCachedData(slot, attachment); + + if (attachment instanceof RegionAttachment) + { + attachment.computeWorldVertices(slot, cacheData.vertices, 0, 2); + } + else + { + attachment.computeWorldVertices( + slot, + 0, + attachment.worldVerticesLength, + cacheData.vertices, + 0, + 2, + ); + } + + cacheData.clipped = false; + + if (clipper.isClipping()) + { + this.updateClippingData(cacheData); + } + } + else if (attachment instanceof ClippingAttachment) + { + clipper.clipStart(slot, attachment); + } + else + { + clipper.clipEndWithSlot(slot); + } + } + } + + clipper.clipEnd(); + } + + private updateClippingData(cacheData: AttachmentCacheData) + { + cacheData.clipped = true; + + clipper.clipTriangles( + cacheData.vertices, + cacheData.vertices.length, + cacheData.indices, + cacheData.indices.length, + cacheData.uvs, + lightColor, + darkColor, + false, + ); + + const { clippedVertices, clippedTriangles } = clipper; + + const verticesCount = clippedVertices.length / 8; + const indicesCount = clippedTriangles.length; + + if (!cacheData.clippedData) + { + cacheData.clippedData = { + vertices: new Float32Array(verticesCount * 2), + uvs: new Float32Array(verticesCount * 2), + vertexCount: verticesCount, + indices: new Uint16Array(indicesCount), + indicesCount, + }; + + this.spineAttachmentsDirty = true; + } + + const clippedData = cacheData.clippedData; + + const sizeChange = clippedData.vertexCount !== verticesCount || indicesCount !== clippedData.indicesCount; + + if (sizeChange) { - const slotAttachment = this._slotAttachments[i]; + this.spineAttachmentsDirty = true; + + if (clippedData.vertexCount < verticesCount) + { + // buffer reuse! + clippedData.vertices = new Float32Array(verticesCount * 2); + clippedData.uvs = new Float32Array(verticesCount * 2); + } + + if (clippedData.indices.length < indicesCount) + { + clippedData.indices = new Uint16Array(indicesCount); + } + } + + const { vertices, uvs, indices } = clippedData; + + for (let i = 0; i < verticesCount; i++) + { + vertices[i * 2] = clippedVertices[i * 8]; + vertices[(i * 2) + 1] = clippedVertices[(i * 8) + 1]; + + uvs[i * 2] = clippedVertices[(i * 8) + 6]; + uvs[(i * 2) + 1] = clippedVertices[(i * 8) + 7]; + } + + clippedData.vertexCount = verticesCount; + + for (let i = 0; i < indices.length; i++) + { + indices[i] = clippedTriangles[i]; + } + + clippedData.indicesCount = indicesCount; + } + + /** + * ensure that attached containers map correctly to their slots + * along with their position, rotation, scale, and visibility. + */ + private updateSlotAttachments() + { + for (const i in this._slotsObject) + { + const slotAttachment = this._slotsObject[i]; + + if (!slotAttachment) continue; const { slot, container } = slotAttachment; @@ -264,21 +529,60 @@ export class Spine extends Container implements View container.rotation = Math.atan2( Math.sin(rotationX) + Math.sin(rotationY), - Math.cos(rotationX) + Math.cos(rotationY) + Math.cos(rotationX) + Math.cos(rotationY), ); } } + } - this.onViewUpdate(); + getCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData + { + const key = `${slot.data.index}-${attachment.name}`; + + return this.attachmentCacheData[key] || this.initCachedData(slot, attachment); + } + + private initCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData + { + const key = `${slot.data.index}-${attachment.name}`; + + let vertices: Float32Array; + + if (attachment instanceof RegionAttachment) + { + vertices = new Float32Array(8); + + this.attachmentCacheData[key] = { + id: key, + vertices, + clipped: false, + indices: [0, 1, 2, 0, 2, 3], + uvs: attachment.uvs as Float32Array, + color: slot.color, + }; + } + else + { + vertices = new Float32Array(attachment.worldVerticesLength); + + this.attachmentCacheData[key] = { + id: key, + vertices, + clipped: false, + indices: attachment.triangles, + uvs: attachment.uvs as Float32Array, + color: slot.color, + }; + } + + return this.attachmentCacheData[key]; } onViewUpdate() { // increment from the 12th bit! this._didChangeId += 1 << 12; - this._didSpineUpdate = true; - this._didSpineUpdate = true; this._boundsDirty = true; if (this.didViewUpdate) return; @@ -299,32 +603,40 @@ export class Spine extends Container implements View * to the attached container. A container can only be attached to one slot at a time. * * @param container - The container to attach to the slot - * @param slot - The slot id or slot to attach to + * @param slotRef - The slot id or slot to attach to */ - attachToSlot(container:Container, slot:string | Slot) + addSlotObject(slot: number | string | Slot, container: Container) { - this.detachFromSlot(container, slot); - - container.includeInBuild = false; + slot = this.getSlotFromRef(slot); - if (typeof slot === 'string') + // need to check in on the container too... + for (const i in this._slotsObject) { - slot = this.skeleton.findSlot(slot) as Slot; + if (this._slotsObject[i]?.container === container) + { + this.removeSlotObject(this._slotsObject[i].slot); + } } - if (!slot) - { - throw new Error(`Slot ${slot} not found`); - } + this.removeSlotObject(slot); + + container.includeInBuild = false; // TODO only add once?? this.addChild(container); // TODO search for copies... - one container - to one bone! - this._slotAttachments.push({ - slot, - container - }); + this._slotsObject[slot.data.name] = { + container, + slot + }; + + const renderGroup = this.renderGroup || this.parentRenderGroup; + + if (renderGroup) + { + renderGroup.structureDidChange = true; + } } /** @@ -333,32 +645,33 @@ export class Spine extends Container implements View * @param container - The container to detach from the slot * @param slot - The slot id or slot to detach from */ - detachFromSlot(container:Container, slot:string | Slot) + removeSlotObject(slot: number | string | Slot) { - container.includeInBuild = true; + slot = this.getSlotFromRef(slot); - if (typeof slot === 'string') - { - slot = this.skeleton.findSlot(slot) as Slot; - } + const container = this._slotsObject[slot.data.name]?.container; - if (!slot) + if (container) { - throw new Error(`Bone ${slot} not found`); + this.removeChild(container); + + container.includeInBuild = true; } - this.removeChild(container); + this._slotsObject[slot.data.name] = null; + } - for (let i = 0; i < this._slotAttachments.length; i++) - { - const mapping = this._slotAttachments[i]; + /** + * Returns a container attached to a slot, or undefined if no container is attached. + * + * @param slotRef - The slot id or slot to get the attachment from + * @returns - The container attached to the slot + */ + getSlotObject(slot: number | string | Slot) + { + slot = this.getSlotFromRef(slot); - if (mapping.slot === slot && mapping.container === container) - { - this._slotAttachments.splice(i, 1); - break; - } - } + return this._slotsObject[slot.data.name].container; } updateBounds() @@ -373,10 +686,26 @@ export class Spine extends Container implements View if (skeletonBounds.minX === Infinity) { - this.state.apply(this.skeleton); + this._applyState(); + + const drawOrder = this.skeleton.drawOrder; + const bounds = this._bounds; + + bounds.clear(); + + for (let i = 0; i < drawOrder.length; i++) + { + const slot = drawOrder[i]; - // now region bounding attachments.. - getSkeletonBounds(this.skeleton, this._bounds); + const attachment = slot.getAttachment(); + + if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) + { + const cacheData = this.getCachedData(slot, attachment); + + bounds.addVertexData(cacheData.vertices, 0, cacheData.vertices.length); + } + } } else { @@ -417,12 +746,15 @@ export class Spine extends Container implements View public override destroy(options: DestroyOptions = false) { super.destroy(options); + Ticker.shared.remove(this.internalUpdate, this); this.state.clearListeners(); this.debug = undefined; this.skeleton = null as any; this.state = null as any; - (this._slotAttachments as any) = null; + (this._slotsObject as any) = null; + this._lastAttachments = null; + this.attachmentCacheData = null as any; } /** Whether or not to round the x/y position of the sprite. */ @@ -436,7 +768,7 @@ export class Spine extends Container implements View this._roundPixels = value ? 1 : 0; } - static from({ skeleton, atlas, scale = 1 }:SpineFromOptions) + static from({ skeleton, atlas, scale = 1 }: SpineFromOptions) { const cacheKey = `${skeleton}-${atlas}`; @@ -450,7 +782,10 @@ export class Spine extends Container implements View const atlasAsset = Assets.get(atlas); const attachmentLoader = new AtlasAttachmentLoader(atlasAsset); // eslint-disable-next-line max-len - const parser = skeletonAsset instanceof Uint8Array ? new SkeletonBinary(attachmentLoader) : new SkeletonJson(attachmentLoader); + const parser + = skeletonAsset instanceof Uint8Array + ? new SkeletonBinary(attachmentLoader) + : new SkeletonJson(attachmentLoader); // TODO scale? parser.scale = scale; @@ -459,7 +794,7 @@ export class Spine extends Container implements View Cache.set(cacheKey, skeletonData); return new Spine({ - skeletonData + skeletonData, }); } } diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts index 18f8047..9c8d432 100644 --- a/src/SpinePipe.ts +++ b/src/SpinePipe.ts @@ -28,7 +28,6 @@ *****************************************************************************/ import { - BigPool, collectAllRenderables, extensions, ExtensionType, InstructionSet, @@ -36,20 +35,19 @@ import { type RenderPipe, Texture } from 'pixi.js'; -import { BatchableClippedSpineSlot } from './BatchableClippedSpineSlot'; import { BatchableSpineSlot } from './BatchableSpineSlot'; import { Spine } from './Spine'; -import { ClippingAttachment, Color, MeshAttachment, RegionAttachment, SkeletonClipping } from '@esotericsoftware/spine-core'; - -import type { Bone } from '@esotericsoftware/spine-core'; - -const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; -const QUAD_VERTS = new Float32Array(8); -const lightColor = new Color(); -const darkColor = new Color(); +import { MeshAttachment, RegionAttachment, SkeletonClipping } from '@esotericsoftware/spine-core'; const clipper = new SkeletonClipping(); +const spineBlendModeMap = { + 0: 'normal', + 1: 'add', + 2: 'multiply', + 3: 'screen' +}; + // eslint-disable-next-line max-len export class SpinePipe implements RenderPipe { @@ -65,121 +63,59 @@ export class SpinePipe implements RenderPipe renderer: Renderer; - private readonly activeBatchableSpineSlots: (BatchableSpineSlot | BatchableClippedSpineSlot)[] = []; + private gpuSpineData:Record = {}; constructor(renderer: Renderer) { this.renderer = renderer; - - renderer.runners.prerender.add({ - prerender: () => - { - this.buildStart(); - } - }); } - validateRenderable(_renderable: Spine): boolean + validateRenderable(spine: Spine): boolean { - return true; - } + spine._applyState(); + // loop through and see if the mesh lengths have changed.. - buildStart() - { - this._returnActiveBatches(); + return spine.spineAttachmentsDirty; } addRenderable(spine: Spine, instructionSet:InstructionSet) { - const batcher = this.renderer.renderPipes.batch; + const gpuSpine = this.gpuSpineData[spine.uid] ||= { slotBatches: {} }; - const rootBone = spine.skeleton.getRootBone() as Bone; - - rootBone.x = 0; - rootBone.y = 0; - rootBone.scaleX = 1; - rootBone.scaleY = 1; - rootBone.rotation = 0; - - spine.state.apply(spine.skeleton); - spine.skeleton.updateWorldTransform(); + const batcher = this.renderer.renderPipes.batch; const drawOrder = spine.skeleton.drawOrder; - const activeBatchableSpineSlot = this.activeBatchableSpineSlots; - const roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1; + spine._applyState(); + for (let i = 0, n = drawOrder.length; i < n; i++) { const slot = drawOrder[i]; const attachment = slot.getAttachment(); + const blendMode = spineBlendModeMap[slot.data.blendMode]; if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) { - if (clipper?.isClipping()) - { - if (attachment instanceof RegionAttachment) - { - const temp = QUAD_VERTS; - - attachment.computeWorldVertices(slot, temp, 0, 2); - - // TODO this function could be optimised.. no need to write colors for us! - clipper.clipTriangles( - QUAD_VERTS, - QUAD_VERTS.length, - QUAD_TRIANGLES, - QUAD_TRIANGLES.length, - attachment.uvs, - lightColor, - darkColor, - false // useDarkColor - ); - - // unwind it! - if (clipper.clippedVertices.length > 0) - { - const batchableSpineSlot = BigPool.get(BatchableClippedSpineSlot); - - activeBatchableSpineSlot.push(batchableSpineSlot); - - batchableSpineSlot.texture = (attachment.region?.texture.texture) || Texture.WHITE; - batchableSpineSlot.roundPixels = roundPixels; - - batchableSpineSlot.setClipper(clipper); - batchableSpineSlot.renderable = spine; - - batcher.addToBatch(batchableSpineSlot); - } - } - } - else - { - const batchableSpineSlot = BigPool.get(BatchableSpineSlot); - - activeBatchableSpineSlot.push(batchableSpineSlot); + const cacheData = spine.getCachedData(slot, attachment); + const batchableSpineSlot = gpuSpine.slotBatches[cacheData.id] ||= new BatchableSpineSlot(); - batchableSpineSlot.renderable = spine; - - batchableSpineSlot.setSlot(slot); - - batchableSpineSlot.texture = (attachment.region?.texture.texture) || Texture.EMPTY; - batchableSpineSlot.roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1; + if (!cacheData.clipped || (cacheData.clipped && cacheData.clippedData.vertices.length > 0)) + { + batchableSpineSlot.setData( + spine, + cacheData, + (attachment.region?.texture.texture) || Texture.EMPTY, + blendMode, + roundPixels + ); batcher.addToBatch(batchableSpineSlot); } } - else if (attachment instanceof ClippingAttachment) - { - clipper.clipStart(slot, attachment); - } - else - { - clipper.clipEndWithSlot(slot); - } - const containerAttachment = spine._slotAttachments.find((mapping) => mapping.slot === slot); + const containerAttachment = spine._slotsObject[slot.data.name]; if (containerAttachment) { @@ -194,35 +130,40 @@ export class SpinePipe implements RenderPipe clipper.clipEnd(); } - updateRenderable(_renderable: Spine) + updateRenderable(spine: Spine) { - // this does not happen.. yet! // we assume that spine will always change its verts size.. + const gpuSpine = this.gpuSpineData[spine.uid]; + + spine._applyState(); + + const drawOrder = spine.skeleton.drawOrder; + + for (let i = 0, n = drawOrder.length; i < n; i++) + { + const slot = drawOrder[i]; + const attachment = slot.getAttachment(); + + if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) + { + const batchableSpineSlot = gpuSpine.slotBatches[spine.getCachedData(slot, attachment).id]; + + batchableSpineSlot.batcher.updateElement(batchableSpineSlot); + } + } } - destroyRenderable(_renderable: Spine) + destroyRenderable(spine: Spine) { - this._returnActiveBatches(); + // TODO remove the renderable from the batcher + this.gpuSpineData[spine.uid] = null as any; } destroy() { - this._returnActiveBatches(); + this.gpuSpineData = null as any; this.renderer = null as any; } - - private _returnActiveBatches() - { - const activeBatchableSpineSlots = this.activeBatchableSpineSlots; - - for (let i = 0; i < activeBatchableSpineSlots.length; i++) - { - BigPool.return(activeBatchableSpineSlots[i]); - } - - // TODO this can be optimised - activeBatchableSpineSlots.length = 0; - } } extensions.add(SpinePipe); diff --git a/src/getSkeletonBounds.ts b/src/getSkeletonBounds.ts deleted file mode 100644 index fb34f21..0000000 --- a/src/getSkeletonBounds.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** **************************************************************************** - * Spine Runtimes License Agreement - * Last updated July 28, 2023. Replaces all prior versions. - * - * Copyright (c) 2013-2023, Esoteric Software LLC - * - * Integration of the Spine Runtimes into software or otherwise creating - * derivative works of the Spine Runtimes is permitted under the terms and - * conditions of Section 2 of the Spine Editor License Agreement: - * http://esotericsoftware.com/spine-editor-license - * - * Otherwise, it is permitted to integrate the Spine Runtimes into software or - * otherwise create derivative works of the Spine Runtimes (collectively, - * "Products"), provided that each user of the Products must obtain their own - * Spine Editor license and redistribution of the Products in any form must - * include this license and copyright notice. - * - * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, - * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE - * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - *****************************************************************************/ - -import { - ClippingAttachment, - MeshAttachment, - RegionAttachment, - type Skeleton -} from '@esotericsoftware/spine-core'; - -import type { Bounds } from 'pixi.js'; - -const QUAD_VERTS = new Float32Array(8); -const tempVerts:number[] = []; - -export function getSkeletonBounds(skeleton:Skeleton, out:Bounds) -{ - out.clear(); - - skeleton.updateWorldTransform(); - - const drawOrder = skeleton.drawOrder; - - for (let i = 0, n = drawOrder.length; i < n; i++) - { - const slot = drawOrder[i]; - const attachment = slot.getAttachment(); - - if (attachment instanceof RegionAttachment) - { - const temp = QUAD_VERTS; - - attachment.computeWorldVertices(slot, temp, 0, 2); - - // TODO this can be skipped if matrix is local?? - out.addVertexData(temp, 0, 8); - } - else if (attachment instanceof MeshAttachment) - { - attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, tempVerts, 0, 2); - - out.addVertexData(tempVerts as any as Float32Array, 0, attachment.worldVerticesLength); - } - else if (attachment instanceof ClippingAttachment) - { - console.warn('[Pixi Spine] ClippingAttachment bounds is not supported yet'); - } - } -}