From 42f93d5743d979fb4599dd3844f2fcef13031f85 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Thu, 6 Jun 2024 18:14:18 +0200 Subject: [PATCH] [ts][pixi] WIP: clipping + alpha for pixi objects added to slots --- examples/export/runtimes.sh | 1 + .../example/assets/raptor-jaw-tooth.png | Bin 0 -> 8235 bytes .../spine-pixi/example/assets/spine_logo.png | Bin 3495 -> 0 bytes spine-ts/spine-pixi/example/slot-objects.html | 99 ++++++++++------- spine-ts/spine-pixi/src/Spine.ts | 103 +++++++++++++----- 5 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png delete mode 100644 spine-ts/spine-pixi/example/assets/spine_logo.png diff --git a/examples/export/runtimes.sh b/examples/export/runtimes.sh index aa6f7e351e..ba42a27f9d 100755 --- a/examples/export/runtimes.sh +++ b/examples/export/runtimes.sh @@ -489,6 +489,7 @@ rm "$ROOT/spine-ts/spine-pixi/example/assets/"* cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../raptor/export/raptor.atlas "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../raptor/export/raptor.png "$ROOT/spine-ts/spine-pixi/example/assets/" +cp -f ../raptor/images/raptor-jaw-tooth.png "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../spineboy/export/spineboy-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-pixi/example/assets/" diff --git a/spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png b/spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png new file mode 100644 index 0000000000000000000000000000000000000000..59b3b69c7147777232bbc8cb2ed3981753adf76f GIT binary patch literal 8235 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 { - frontFist = spineboy.skeleton.findSlot("front-fist"); - spineboy.addSlotObject(frontFist, logo2); + frontFist = spineboy.skeleton.findSlot("front-foot"); + tooth1.x = -10; + tooth1.y = -70; + spineboy.addSlotObject(frontFist, toothContainer); frontFist.setAttachment(null); - }, 2000) + }, 1000); // scaling the bone, will scale the pixi object too setTimeout(() => { - frontFist.bone.scaleX = .5 - frontFist.bone.scaleY = .5 - }, 3000) + frontFist.bone.scaleX = .5; + frontFist.bone.scaleY = .5; + }, 2000); // adding a pixi text in a slot using slot index let mouth; setTimeout(() => { - mouth = spineboy.skeleton.findSlot("mouth"); - spineboy.addSlotObject(mouth, text); - }, 4000) + spineboy.addSlotObject("gun", text); + }, 4000); - // adding one display object to an already "occupied" slot will remove the old one, + // adding one pixi object to an already "occupied" slot will remove the old one, // and move the given one to the slot setTimeout(() => { - spineboy.addSlotObject(mouth, logo1); - }, 5000) + spineboy.addSlotObject("gun", toothContainer); + }, 5000); // adding multiple DisplayObjects to a slot using a Container to control their offset, size, ... setTimeout(() => { - const container = new PIXI.Container(); - container.addChild(logo3, logo4); - logo3.y = 20; - logo3.scale.set(.5); - logo4.scale.set(.5); - logo4.tint = 0xFF5500; - spineboy.addSlotObject("gun", container); - }, 6000) + toothContainer.addChild(tooth2); + tooth2.x = 30; + tooth2.y = -70; + tooth2.angle = 30; + tooth2.tint = 0xFF5500; + }, 6000); // removing the container won't automatically destroy the displayObject contained, so take care of them setTimeout(() => { - const container = new PIXI.Container(); spineboy.removeSlotObject("gun"); - logo3.destroy(); - logo4.destroy(); - }, 7000) + console.log(toothContainer.destroyed) + console.log(tooth1.destroyed) + console.log(tooth2.destroyed) + toothContainer.destroy(); + tooth1.destroy(); + console.log(toothContainer.destroyed) + console.log(tooth1.destroyed) + console.log(tooth2.destroyed) + }, 7000); // removing a specific slot object, that is not in that slot do nothing setTimeout(() => { - const container = new PIXI.Container(); - spineboy.removeSlotObject(frontFist, text); - text.destroy(); - }, 8000) + spineboy.addSlotObject("gun", tooth2); + spineboy.removeSlotObject("gun", text); + }, 8000); // removing a specific slot object setTimeout(() => { - const container = new PIXI.Container(); - spineboy.removeSlotObject(frontFist, logo2); - logo2.destroy(); - }, 9000) + spineboy.removeSlotObject("gun", tooth2); + tooth2.destroy(); + }, 9000); // resetting the slot with the original attachment setTimeout(() => { frontFist.setToSetupPose(); - }, 10000) + frontFist.bone.setToSetupPose(); + }, 10000); + + // showing an animation with clipping -> Pixi masks will be created + // for clipping attachments having slot objects + setTimeout(() => { + spineboy.state.setAnimation(0, "portal", true) + const tooth3 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png'); + tooth3.scale.set(2); + tooth3.x = -60; + tooth3.y = 120; + tooth3.angle = 180; + const foot1 = new PIXI.Container(); + foot1.addChild(tooth3); + spineboy.addSlotObject("rear-foot", foot1); + }, 11000); })(); diff --git a/spine-ts/spine-pixi/src/Spine.ts b/spine-ts/spine-pixi/src/Spine.ts index 4f383db9d2..edae4f41d4 100644 --- a/spine-ts/spine-pixi/src/Spine.ts +++ b/spine-ts/spine-pixi/src/Spine.ts @@ -54,6 +54,7 @@ import type { IPointData } from "@pixi/core"; import { Ticker } from "@pixi/core"; import type { IDestroyOptions, DisplayObject } from "@pixi/display"; import { Container } from "@pixi/display"; +import { Graphics } from "@pixi/graphics"; /** * Options to configure a {@link Spine} game object. @@ -205,6 +206,13 @@ export class Spine extends Container { this.debug = undefined; this.meshesCache.clear(); this.slotsObject.clear(); + + for (let maskKey in this.clippingSlotToPixiMasks) { + const mask = this.clippingSlotToPixiMasks[maskKey]; + mask.destroy(); + delete this.clippingSlotToPixiMasks[maskKey]; + } + super.destroy(options); } @@ -231,7 +239,7 @@ export class Spine extends Container { } } - private slotsObject = new Map(); + private slotsObject = new Map(); private getSlotFromRef (slotRef: number | string | Slot): Slot { let slot: Slot | null; if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef]; @@ -243,54 +251,52 @@ export class Spine extends Container { return slot; } /** - * Add a pixi DisplayObject as a child of the Spine object. - * The DisplayObject will be rendered coherently with the draw order of the slot. - * If an attachment is active on the slot, the pixi DisplayObject will be rendered on top of it. - * If the DisplayObject is already attached to the given slot, nothing will happen. - * If the DisplayObject is already attached to another slot, it will be removed from that slot + * Add a pixi Container as a child of the Spine object. + * The Container will be rendered coherently with the draw order of the slot. + * If an attachment is active on the slot, the pixi Container will be rendered on top of it. + * If the Container is already attached to the given slot, nothing will happen. + * If the Container is already attached to another slot, it will be removed from that slot * before adding it to the given one. - * If another DisplayObject is already attached to this slot, the old one will be removed from this + * If another Container is already attached to this slot, the old one will be removed from this * slot before adding it to the current one. * @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be added to. - * @param pixiObject - The pixi DisplayObject to add. + * @param pixiObject - The pixi Container to add. */ - addSlotObject (slotRef: number | string | Slot, pixiObject: DisplayObject): void { + addSlotObject (slotRef: number | string | Slot, pixiObject: Container): void { let slot = this.getSlotFromRef(slotRef); let oldPixiObject = this.slotsObject.get(slot); + if (oldPixiObject === pixiObject) return; // search if the pixiObject was already in another slotObject - if (!oldPixiObject) { - for (const [slot, oldPixiObjectAnotherSlot] of this.slotsObject) { - if (oldPixiObjectAnotherSlot === pixiObject) { - this.removeSlotObject(slot, pixiObject); - break; - } + for (const [otherSlot, oldPixiObjectAnotherSlot] of this.slotsObject) { + if (otherSlot !== slot && oldPixiObjectAnotherSlot === pixiObject) { + this.removeSlotObject(otherSlot, pixiObject); + break; } } - - if (oldPixiObject === pixiObject) return; + if (oldPixiObject) this.removeChild(oldPixiObject); this.slotsObject.set(slot, pixiObject); this.addChild(pixiObject); } /** - * Return the DisplayObject connected to the given slot, if any. + * Return the Container connected to the given slot, if any. * Otherwise return undefined - * @param pixiObject - The slot index, or the slot name, or the Slot to get the DisplayObject from. - * @returns a DisplayObject if any, undefined otherwise. + * @param pixiObject - The slot index, or the slot name, or the Slot to get the Container from. + * @returns a Container if any, undefined otherwise. */ - getSlotObject (slotRef: number | string | Slot): DisplayObject | undefined { + getSlotObject (slotRef: number | string | Slot): Container | undefined { return this.slotsObject.get(this.getSlotFromRef(slotRef)); } /** * Remove a slot object from the given slot. * If `pixiObject` is passed and attached to the given slot, remove it from the slot. - * If `pixiObject` is not passed and the given slot has an attached DisplayObject, remove it from the slot. + * If `pixiObject` is not passed and the given slot has an attached Container, remove it from the slot. * @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be remove from. - * @param pixiObject - Optional, The pixi DisplayObject to remove. + * @param pixiObject - Optional, The pixi Container to remove. */ - removeSlotObject (slotRef: number | string | Slot, pixiObject?: DisplayObject): void { + removeSlotObject (slotRef: number | string | Slot, pixiObject?: Container): void { let slot = this.getSlotFromRef(slotRef); let slotObject = this.slotsObject.get(slot); if (!slotObject) return; @@ -303,29 +309,64 @@ export class Spine extends Container { } private verticesCache: NumberArrayLike = Utils.newFloatArray(1024); + private clippingSlotToPixiMasks: Record = {}; + private pixiMaskCleanup (slot: Slot) { + let mask = this.clippingSlotToPixiMasks[slot.data.name]; + if (mask) { + delete this.clippingSlotToPixiMasks[slot.data.name]; + mask.destroy(); + } + } + private updatePixiObject(pixiObject: Container, slot: Slot, zIndex: number) { + pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad); + pixiObject.zIndex = zIndex + 1; + pixiObject.alpha = this.skeleton.color.a * slot.color.a; + } + private updateAndSetPixiMask(pixiMaskSource: PixiMaskSource | null, pixiObject: Container) { + if (Spine.clipper.isClipping() && pixiMaskSource) { + let mask = this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] as Graphics; + if (!mask) { + mask = new Graphics(); + this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] = mask; + this.addChild(mask); + } + if (!pixiMaskSource.computed) { + pixiMaskSource.computed = true; + const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment; + const world = Array.from(clippingAttachment.vertices); + clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, clippingAttachment.worldVerticesLength, world, 0, 2); + mask.clear().lineStyle(0).beginFill(0x000000).drawPolygon(world); + } + pixiObject.mask = mask; + } else if (pixiObject.mask) { + pixiObject.mask = null; + } + } private renderMeshes (): void { this.resetMeshes(); let triangles: Array | null = null; let uvs: NumberArrayLike | null = null; + let pixiMaskSource: PixiMaskSource | null = null; const drawOrder = this.skeleton.drawOrder; for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) { const slot = drawOrder[i]; - + // render pixi object on the current slot on top of the slot attachment let pixiObject = this.slotsObject.get(slot); let zIndex = i + slotObjectsCounter; if (pixiObject) { - pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad); - pixiObject.zIndex = zIndex + 1; + this.updatePixiObject(pixiObject, slot, zIndex + 1); slotObjectsCounter++; + this.updateAndSetPixiMask(pixiMaskSource, pixiObject); } const useDarkColor = slot.darkColor != null; const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE; if (!slot.bone.active) { Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); continue; } const attachment = slot.getAttachment(); @@ -353,9 +394,11 @@ export class Spine extends Container { texture = mesh.region?.texture; } else if (attachment instanceof ClippingAttachment) { Spine.clipper.clipStart(slot, attachment); + pixiMaskSource = { slot, computed: false }; continue; } else { Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); continue; } if (texture != null) { @@ -423,6 +466,7 @@ export class Spine extends Container { } Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); } Spine.clipper.clipEnd(); } @@ -542,6 +586,11 @@ export class Spine extends Container { } } +type PixiMaskSource = { + slot: Slot, + computed: boolean, // prevent to reculaculate vertices for a mask clipping multiple pixi objects +} + Skeleton.yDown = true; /**