From 6f98fd1895c2ee5115291b9df720a077fea07b1e Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Wed, 5 Jun 2024 13:23:59 +0100 Subject: [PATCH 01/10] - correct depth sorting on spine slots - `attachToSlot` as opposed to `attachToBone` - use `Skeleton.yDown` to simplify the maths --- src/BatchableClippedSpineSlot.ts | 2 +- src/BatchableSpineSlot.ts | 2 +- src/Spine.ts | 70 ++++++++++++++++++-------------- src/SpinePipe.ts | 19 +++++++-- src/getSkeletonBounds.ts | 8 ---- 5 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/BatchableClippedSpineSlot.ts b/src/BatchableClippedSpineSlot.ts index 9cae5da..1d3b983 100644 --- a/src/BatchableClippedSpineSlot.ts +++ b/src/BatchableClippedSpineSlot.ts @@ -100,7 +100,7 @@ export class BatchableClippedSpineSlot implements BatchableObject // position float32View[index++] = clippedVertices[localIndex]; - float32View[index++] = clippedVertices[localIndex + 1] * -1; + float32View[index++] = clippedVertices[localIndex + 1]; // uv float32View[index++] = clippedVertices[localIndex + 6]; diff --git a/src/BatchableSpineSlot.ts b/src/BatchableSpineSlot.ts index 3ca8c9a..850b0f3 100644 --- a/src/BatchableSpineSlot.ts +++ b/src/BatchableSpineSlot.ts @@ -153,7 +153,7 @@ export class BatchableSpineSlot implements BatchableObject // index++; // float32View[index++] *= -1; const x = float32View[index]; - const y = -float32View[index + 1]; + const y = float32View[index + 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 3258213..064bc89 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -27,7 +27,7 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Assets, Bounds, Cache, Container, ContainerOptions, DEG_TO_RAD, DestroyOptions, PointData, Ticker, View } from 'pixi.js'; +import { Assets, Bounds, Cache, Container, ContainerOptions, DEG_TO_RAD, DestroyOptions, InstructionSet, PointData, Ticker, View } from 'pixi.js'; import { getSkeletonBounds } from './getSkeletonBounds'; import { ISpineDebugRenderer } from './SpineDebugRenderer'; import { @@ -40,6 +40,7 @@ import { SkeletonBounds, SkeletonData, SkeletonJson, + Slot, type TextureAtlas, TrackEntry, Vector2 @@ -53,6 +54,8 @@ export type SpineFromOptions = { const vectorAux = new Vector2(); +Skeleton.yDown = true; + export interface SpineOptions extends ContainerOptions { skeletonData: SkeletonData; @@ -86,7 +89,7 @@ export class Spine extends Container implements View public skeletonBounds: SkeletonBounds; private _debug?: ISpineDebugRenderer | undefined = undefined; - private readonly _mappings:{bone:Bone, container:Container}[] = []; + readonly _slotAttachments:{slot:Slot, container:Container}[] = []; public get debug(): ISpineDebugRenderer | undefined { @@ -192,7 +195,7 @@ export class Spine extends Container implements View else { bone.x = vectorAux.x; - bone.y = -vectorAux.y; + bone.y = vectorAux.y; } } @@ -218,7 +221,7 @@ export class Spine extends Container implements View } outPos.x = bone.worldX; - outPos.y = -bone.worldY; + outPos.y = bone.worldY; return outPos; } @@ -228,13 +231,15 @@ export class Spine extends Container implements View this.state.update(dt); this._boundsDirty = true; - // update the mappings.. - - this._mappings.forEach((mapping) => + for (let i = 0; i < this._slotAttachments.length; i++) { - const { bone, container } = mapping; + const slotAttachment = this._slotAttachments[i]; + + const { slot, container } = slotAttachment; + + const bone = slot.bone; - container.position.set(bone.worldX, -bone.worldY); + container.position.set(bone.worldX, bone.worldY); container.scale.x = bone.getWorldScaleX(); container.scale.y = bone.getWorldScaleY(); @@ -242,11 +247,12 @@ export class Spine extends Container implements View const rotationX = bone.getWorldRotationX() * DEG_TO_RAD; const rotationY = bone.getWorldRotationY() * DEG_TO_RAD; - container.rotation = -Math.atan2( + container.rotation = Math.atan2( Math.sin(rotationX) + Math.sin(rotationY), Math.cos(rotationX) + Math.cos(rotationY) ); - }); + } + this.onViewUpdate(); } @@ -279,26 +285,28 @@ export class Spine extends Container implements View * @param container - The container to attach to the bone * @param bone - The bone id or bone to attach to */ - attachToBone(container:Container, bone:string | Bone) + attachToSlot(container:Container, slot:string | Slot) { - this.detachFromBone(container, bone); + this.detachFromSlot(container, slot); - if (typeof bone === 'string') + container.includeInBuild = false; + + if (typeof slot === 'string') { - bone = this.skeleton.findBone(bone) as Bone; + slot = this.skeleton.findSlot(slot) as Slot; } - if (!bone) + if (!slot) { - throw new Error(`Bone ${bone} not found`); + throw new Error(`Slot ${slot} not found`); } // TODO only add once?? this.addChild(container); // TODO search for copies... - one container - to one bone! - this._mappings.push({ - bone, + this._slotAttachments.push({ + slot, container }); } @@ -307,29 +315,31 @@ export class Spine extends Container implements View * Removes a PixiJS container from the bone it is attached to. * * @param container - The container to detach from the bone - * @param bone - The bone id or bone to detach from + * @param slot - The bone id or bone to detach from */ - detachFromBone(container:Container, bone:string | Bone) + detachFromSlot(container:Container, slot:string | Slot) { - if (typeof bone === 'string') + container.includeInBuild = true; + + if (typeof slot === 'string') { - bone = this.skeleton.findBone(bone) as Bone; + slot = this.skeleton.findSlot(slot) as Slot; } - if (!bone) + if (!slot) { - throw new Error(`Bone ${bone} not found`); + throw new Error(`Bone ${slot} not found`); } this.removeChild(container); - for (let i = 0; i < this._mappings.length; i++) + for (let i = 0; i < this._slotAttachments.length; i++) { - const mapping = this._mappings[i]; + const mapping = this._slotAttachments[i]; - if (mapping.bone === bone && mapping.container === container) + if (mapping.slot === slot && mapping.container === container) { - this._mappings.splice(i, 1); + this._slotAttachments.splice(i, 1); break; } } @@ -396,7 +406,7 @@ export class Spine extends Container implements View this.debug = undefined; this.skeleton = null as any; this.state = null as any; - (this._mappings as any) = null; + (this._slotAttachments as any) = null; } /** Whether or not to round the x/y position of the sprite. */ diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts index f2443fc..5c944a8 100644 --- a/src/SpinePipe.ts +++ b/src/SpinePipe.ts @@ -27,7 +27,7 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { BigPool, extensions, ExtensionType, type Renderer, type RenderPipe, Texture } from 'pixi.js'; +import { BigPool, collectAllRenderables, extensions, ExtensionType, type Renderer, type RenderPipe, Texture } from 'pixi.js'; import { BatchableClippedSpineSlot } from './BatchableClippedSpineSlot'; import { BatchableSpineSlot } from './BatchableSpineSlot'; import { Spine } from './Spine'; @@ -40,6 +40,8 @@ const QUAD_VERTS = new Float32Array(8); const lightColor = new Color(); const darkColor = new Color(); +const clipper = new SkeletonClipping(); + // eslint-disable-next-line max-len export class SpinePipe implements RenderPipe { @@ -79,7 +81,7 @@ export class SpinePipe implements RenderPipe this._returnActiveBatches(); } - addRenderable(spine: Spine) + addRenderable(spine: Spine, instructionSet) { const batcher = this.renderer.renderPipes.batch; @@ -98,8 +100,6 @@ export class SpinePipe implements RenderPipe const activeBatchableSpineSlot = this.activeBatchableSpineSlots; - const clipper = new SkeletonClipping(); - for (let i = 0, n = drawOrder.length; i < n; i++) { const slot = drawOrder[i]; @@ -168,6 +168,17 @@ export class SpinePipe implements RenderPipe { clipper.clipEndWithSlot(slot); } + + const containerAttachment = spine._slotAttachments.find((mapping) => mapping.slot === slot); + + if (containerAttachment) + { + const container = containerAttachment.container; + + container.includeInBuild = true; + collectAllRenderables(container, instructionSet, this.renderer.renderPipes); + container.includeInBuild = false; + } } clipper.clipEnd(); diff --git a/src/getSkeletonBounds.ts b/src/getSkeletonBounds.ts index d8f9e64..40fe875 100644 --- a/src/getSkeletonBounds.ts +++ b/src/getSkeletonBounds.ts @@ -44,14 +44,6 @@ export function getSkeletonBounds(skeleton:Skeleton, out:Bounds) { out.clear(); - const rootBone = skeleton.getRootBone() as Bone; - - rootBone.x = 0; - rootBone.y = 0; - rootBone.scaleX = 1; - rootBone.scaleY = -1; - rootBone.rotation = 0; - skeleton.updateWorldTransform(); const drawOrder = skeleton.drawOrder; From 6cb00e86fdfadf305642d0a2c12655d6cfff15aa Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Wed, 5 Jun 2024 13:32:17 +0100 Subject: [PATCH 02/10] some linty bits --- src/Spine.ts | 13 ++++++++++++- src/SpinePipe.ts | 4 +++- src/getSkeletonBounds.ts | 1 - 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index 064bc89..498a6cb 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -27,7 +27,18 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Assets, Bounds, Cache, Container, ContainerOptions, DEG_TO_RAD, DestroyOptions, InstructionSet, PointData, Ticker, View } from 'pixi.js'; +import { + Assets, + Bounds, + Cache, + Container, + ContainerOptions, + DEG_TO_RAD, + DestroyOptions, + PointData, + Ticker, + View +} from 'pixi.js'; import { getSkeletonBounds } from './getSkeletonBounds'; import { ISpineDebugRenderer } from './SpineDebugRenderer'; import { diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts index 5c944a8..7d54cfa 100644 --- a/src/SpinePipe.ts +++ b/src/SpinePipe.ts @@ -100,6 +100,8 @@ export class SpinePipe implements RenderPipe const activeBatchableSpineSlot = this.activeBatchableSpineSlots; + const roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1; + for (let i = 0, n = drawOrder.length; i < n; i++) { const slot = drawOrder[i]; @@ -135,7 +137,7 @@ export class SpinePipe implements RenderPipe activeBatchableSpineSlot.push(batchableSpineSlot); batchableSpineSlot.texture = (attachment.region?.texture.texture) || Texture.WHITE; - batchableSpineSlot.roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1; + batchableSpineSlot.roundPixels = roundPixels; batchableSpineSlot.setClipper(clipper); batchableSpineSlot.renderable = spine; diff --git a/src/getSkeletonBounds.ts b/src/getSkeletonBounds.ts index 40fe875..fb34f21 100644 --- a/src/getSkeletonBounds.ts +++ b/src/getSkeletonBounds.ts @@ -28,7 +28,6 @@ *****************************************************************************/ import { - type Bone, ClippingAttachment, MeshAttachment, RegionAttachment, From 16b0cb920ee3238b9234998c432eb0d4d3a961a1 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Wed, 5 Jun 2024 14:06:09 +0100 Subject: [PATCH 03/10] only render attachments when visible --- src/Spine.ts | 25 +++++++++++++++---------- src/SpinePipe.ts | 12 ++++++++++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index 498a6cb..a96911c 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -248,20 +248,25 @@ export class Spine extends Container implements View const { slot, container } = slotAttachment; - const bone = slot.bone; + container.visible = this.skeleton.drawOrder.includes(slot); - container.position.set(bone.worldX, bone.worldY); + if (container.visible) + { + const bone = slot.bone; + + container.position.set(bone.worldX, bone.worldY); - container.scale.x = bone.getWorldScaleX(); - container.scale.y = bone.getWorldScaleY(); + container.scale.x = bone.getWorldScaleX(); + container.scale.y = bone.getWorldScaleY(); - const rotationX = bone.getWorldRotationX() * DEG_TO_RAD; - const rotationY = bone.getWorldRotationY() * DEG_TO_RAD; + const rotationX = bone.getWorldRotationX() * DEG_TO_RAD; + const rotationY = bone.getWorldRotationY() * DEG_TO_RAD; - container.rotation = Math.atan2( - Math.sin(rotationX) + Math.sin(rotationY), - Math.cos(rotationX) + Math.cos(rotationY) - ); + container.rotation = Math.atan2( + Math.sin(rotationX) + Math.sin(rotationY), + Math.cos(rotationX) + Math.cos(rotationY) + ); + } } this.onViewUpdate(); diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts index 7d54cfa..18f8047 100644 --- a/src/SpinePipe.ts +++ b/src/SpinePipe.ts @@ -27,7 +27,15 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { BigPool, collectAllRenderables, extensions, ExtensionType, type Renderer, type RenderPipe, Texture } from 'pixi.js'; +import { + BigPool, + collectAllRenderables, + extensions, ExtensionType, + InstructionSet, + type Renderer, + type RenderPipe, + Texture +} from 'pixi.js'; import { BatchableClippedSpineSlot } from './BatchableClippedSpineSlot'; import { BatchableSpineSlot } from './BatchableSpineSlot'; import { Spine } from './Spine'; @@ -81,7 +89,7 @@ export class SpinePipe implements RenderPipe this._returnActiveBatches(); } - addRenderable(spine: Spine, instructionSet) + addRenderable(spine: Spine, instructionSet:InstructionSet) { const batcher = this.renderer.renderPipes.batch; From 2985fd74df25e5714fdae4de590306e2e272fb98 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Wed, 5 Jun 2024 14:08:23 +0100 Subject: [PATCH 04/10] focs --- src/Spine.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index a96911c..7ab2e37 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -295,11 +295,11 @@ export class Spine extends Container implements View } /** - * Attaches a PixiJS container to a specified bone. This will map the world transform of the bone - * to the attached container. A container can only be attached to one bone at a time. + * Attaches a PixiJS container to a specified slot. This will map the world transform of the slots bone + * to the attached container. A container can only be attached to one slot at a time. * - * @param container - The container to attach to the bone - * @param bone - The bone id or bone to attach to + * @param container - The container to attach to the slot + * @param slot - The slot id or slot to attach to */ attachToSlot(container:Container, slot:string | Slot) { @@ -328,10 +328,10 @@ export class Spine extends Container implements View } /** - * Removes a PixiJS container from the bone it is attached to. + * Removes a PixiJS container from the slot it is attached to. * - * @param container - The container to detach from the bone - * @param slot - The bone id or bone to detach from + * @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) { From a75213c9584a4d0990e52dbb3a7352beb6f21637 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 6 Jun 2024 15:35:22 +0100 Subject: [PATCH 05/10] 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'); - } - } -} From 239fbbcb85c23d0b9186b890afb75f906e17ff5f Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 6 Jun 2024 15:35:59 +0100 Subject: [PATCH 06/10] fix rotations --- src/Spine.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index 3e47243..5069624 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -524,13 +524,7 @@ export class Spine extends Container implements View container.scale.x = bone.getWorldScaleX(); container.scale.y = bone.getWorldScaleY(); - const rotationX = bone.getWorldRotationX() * DEG_TO_RAD; - const rotationY = bone.getWorldRotationY() * DEG_TO_RAD; - - container.rotation = Math.atan2( - Math.sin(rotationX) + Math.sin(rotationY), - Math.cos(rotationX) + Math.cos(rotationY), - ); + container.rotation = bone.getWorldRotationX() * DEG_TO_RAD; } } } From c9f04a4bfb5f3d3344d7638af4fa8f7dc42b677a Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:41:16 +0100 Subject: [PATCH 07/10] rename var --- src/Spine.ts | 7 ++++--- src/SpinePipe.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index 5069624..c308371 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -385,7 +385,7 @@ export class Spine extends Container implements View { if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) { - const cacheData = this.getCachedData(slot, attachment); + const cacheData = this._getCachedData(slot, attachment); if (attachment instanceof RegionAttachment) { @@ -529,7 +529,8 @@ export class Spine extends Container implements View } } - getCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData + /** @internal */ + _getCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData { const key = `${slot.data.index}-${attachment.name}`; @@ -695,7 +696,7 @@ export class Spine extends Container implements View if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) { - const cacheData = this.getCachedData(slot, attachment); + const cacheData = this._getCachedData(slot, attachment); bounds.addVertexData(cacheData.vertices, 0, cacheData.vertices.length); } diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts index 9c8d432..f87a6bd 100644 --- a/src/SpinePipe.ts +++ b/src/SpinePipe.ts @@ -98,7 +98,7 @@ export class SpinePipe implements RenderPipe if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) { - const cacheData = spine.getCachedData(slot, attachment); + const cacheData = spine._getCachedData(slot, attachment); const batchableSpineSlot = gpuSpine.slotBatches[cacheData.id] ||= new BatchableSpineSlot(); if (!cacheData.clipped || (cacheData.clipped && cacheData.clippedData.vertices.length > 0)) @@ -146,7 +146,7 @@ export class SpinePipe implements RenderPipe if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) { - const batchableSpineSlot = gpuSpine.slotBatches[spine.getCachedData(slot, attachment).id]; + const batchableSpineSlot = gpuSpine.slotBatches[spine._getCachedData(slot, attachment).id]; batchableSpineSlot.batcher.updateElement(batchableSpineSlot); } From 730472131505c235ee28ed81b99762cf8ea9ad05 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 6 Jun 2024 15:54:52 +0100 Subject: [PATCH 08/10] update at start --- src/Spine.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Spine.ts b/src/Spine.ts index 5069624..32e0089 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -204,6 +204,8 @@ export class Spine extends Container implements View this.skeleton = new Skeleton(skeletonData); this.state = new AnimationState(new AnimationStateData(skeletonData)); this.autoUpdate = options?.autoUpdate ?? true; + + this._updateState(0); } public update(dt: number): void From 619a9c4247da51bf5435fbc507d95a119acd9bf2 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 6 Jun 2024 16:02:31 +0100 Subject: [PATCH 09/10] fix slot misalignment on first frame --- src/Spine.ts | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index daed5ac..2c35d0e 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -334,7 +334,7 @@ export class Spine extends Container implements View this.transformAttachments(); - this.updateSlotAttachments(); + this.updateSlotObjects(); } private validateAttachments() @@ -505,7 +505,7 @@ export class Spine extends Container implements View * ensure that attached containers map correctly to their slots * along with their position, rotation, scale, and visibility. */ - private updateSlotAttachments() + private updateSlotObjects() { for (const i in this._slotsObject) { @@ -513,21 +513,26 @@ export class Spine extends Container implements View if (!slotAttachment) continue; - const { slot, container } = slotAttachment; + this.updateSlotObject(slotAttachment); + } + } - container.visible = this.skeleton.drawOrder.includes(slot); + private updateSlotObject(slotAttachment: {slot:Slot, container:Container}) + { + const { slot, container } = slotAttachment; - if (container.visible) - { - const bone = slot.bone; + container.visible = this.skeleton.drawOrder.includes(slot); - container.position.set(bone.worldX, bone.worldY); + if (container.visible) + { + const bone = slot.bone; - container.scale.x = bone.getWorldScaleX(); - container.scale.y = bone.getWorldScaleY(); + container.position.set(bone.worldX, bone.worldY); - container.rotation = bone.getWorldRotationX() * DEG_TO_RAD; - } + container.scale.x = bone.getWorldScaleX(); + container.scale.y = bone.getWorldScaleY(); + + container.rotation = bone.getWorldRotationX() * DEG_TO_RAD; } } @@ -622,18 +627,12 @@ export class Spine extends Container implements View // TODO only add once?? this.addChild(container); - // TODO search for copies... - one container - to one bone! this._slotsObject[slot.data.name] = { container, slot }; - const renderGroup = this.renderGroup || this.parentRenderGroup; - - if (renderGroup) - { - renderGroup.structureDidChange = true; - } + this.updateSlotObject(this._slotsObject[slot.data.name]); } /** From 01aa907162ee38308301d69fbfce1f545df37a12 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 6 Jun 2024 16:41:03 +0100 Subject: [PATCH 10/10] allow for `removeSlotObject` to have a container passed to it --- src/Spine.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Spine.ts b/src/Spine.ts index 2c35d0e..0f459b9 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -639,22 +639,39 @@ export class Spine extends Container implements View * Removes a PixiJS container from the slot it is attached to. * * @param container - The container to detach from the slot - * @param slot - The slot id or slot to detach from + * @param slotOrContainer - The container, slot id or slot to detach from */ - removeSlotObject(slot: number | string | Slot) + removeSlotObject(slotOrContainer: number | string | Slot | Container) { - slot = this.getSlotFromRef(slot); + let containerToRemove: Container | undefined; - const container = this._slotsObject[slot.data.name]?.container; + if (slotOrContainer instanceof Container) + { + for (const i in this._slotsObject) + { + if (this._slotsObject[i]?.container === slotOrContainer) + { + this._slotsObject[i] = null; - if (container) + containerToRemove = slotOrContainer; + break; + } + } + } + else { - this.removeChild(container); + const slot = this.getSlotFromRef(slotOrContainer); - container.includeInBuild = true; + containerToRemove = this._slotsObject[slot.data.name]?.container; + this._slotsObject[slot.data.name] = null; } - this._slotsObject[slot.data.name] = null; + if (containerToRemove) + { + this.removeChild(containerToRemove); + + containerToRemove.includeInBuild = true; + } } /**