Преглед на файлове

[ts][pixi] clipping + alpha for pixi objects added to slots

Davide Tantillo преди 1 година
родител
ревизия
61e9be2dcc

+ 1 - 0
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/"

BIN
spine-ts/spine-pixi/example/assets/raptor-jaw-tooth.png


BIN
spine-ts/spine-pixi/example/assets/spine_logo.png


+ 58 - 41
spine-ts/spine-pixi/example/slot-objects.html

@@ -29,7 +29,7 @@
 
         // Create the spine display object
         const spineboy = spine.Spine.from("spineboyData", "spineboyAtlas", {
-          scale: 0.5,
+          scale: 0.25,
         });
 
         // Set the default mix time to use when transitioning
@@ -46,79 +46,96 @@
         // Add the display object to the stage.
         app.stage.addChild(spineboy);
 
-        const logo1 = PIXI.Sprite.from('assets/spine_logo.png');
-        const logo2 = PIXI.Sprite.from('assets/spine_logo.png');
-        const logo3 = PIXI.Sprite.from('assets/spine_logo.png');
-        const logo4 = PIXI.Sprite.from('assets/spine_logo.png');
-        const text = new PIXI.Text('Spine Text');
+        const tooth1 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
+        const tooth2 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
+        const text = new PIXI.Text('Text GUN');
 
-        // putting logo1 on top of the gun
-        spineboy.addSlotObject("gun", logo1);
+        const toothContainer = new PIXI.Container();
+        toothContainer.addChild(tooth1);
+        toothContainer.name = "tooth";
+        text.name = "text";
 
         // putting logo2 on top of the hand using slot directly and remove the attachment hand
         let frontFist;
         setTimeout(() => {
-          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);
       })();
     </script>
   </body>

+ 74 - 25
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<Slot, DisplayObject>();
+	private slotsObject = new Map<Slot, Container>();
 	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,11 +309,45 @@ export class Spine extends Container {
 	}
 
 	private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
+	private clippingSlotToPixiMasks: Record<string, Graphics> = {};
+	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<number> | 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++) {
@@ -317,15 +357,16 @@ export class Spine extends Container {
 			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 = <SpineTexture>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;
 
 /**