From 4012f12685dc1456e404fda46c21074118781b45 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 --- spine-ts/spine-core/src/SkeletonClipping.ts | 2 +- spine-ts/spine-pixi/example/assets/temp.atlas | 24 +++ spine-ts/spine-pixi/example/assets/temp.png | Bin 0 -> 7516 bytes spine-ts/spine-pixi/example/assets/temp.skel | Bin 0 -> 1870 bytes spine-ts/spine-pixi/example/slot-objects.html | 171 ++++++++++-------- spine-ts/spine-pixi/src/Spine.ts | 89 +++++++-- spine-ts/spine-pixi/src/SpineDebugRenderer.ts | 10 +- 7 files changed, 200 insertions(+), 96 deletions(-) create mode 100644 spine-ts/spine-pixi/example/assets/temp.atlas create mode 100644 spine-ts/spine-pixi/example/assets/temp.png create mode 100644 spine-ts/spine-pixi/example/assets/temp.skel diff --git a/spine-ts/spine-core/src/SkeletonClipping.ts b/spine-ts/spine-core/src/SkeletonClipping.ts index a6d88f841..4e1ae107d 100644 --- a/spine-ts/spine-core/src/SkeletonClipping.ts +++ b/spine-ts/spine-core/src/SkeletonClipping.ts @@ -41,7 +41,7 @@ export class SkeletonClipping { private scratch = new Array(); private clipAttachment: ClippingAttachment | null = null; - private clippingPolygons: Array> | null = null; + public clippingPolygons: Array> | null = null; clipStart (slot: Slot, clip: ClippingAttachment): number { if (this.clipAttachment) return 0; diff --git a/spine-ts/spine-pixi/example/assets/temp.atlas b/spine-ts/spine-pixi/example/assets/temp.atlas new file mode 100644 index 000000000..5c6300f4d --- /dev/null +++ b/spine-ts/spine-pixi/example/assets/temp.atlas @@ -0,0 +1,24 @@ +temp.png +size:154,78 +filter:Linear,Linear +pma:true +1/L1 +bounds:108,23,53,44 +offsets:1,1,55,46 +rotate:90 +Battery/border +bounds:74,10,66,16 +offsets:1,1,68,18 +rotate:90 +Battery/fill +bounds:92,16,60,14 +offsets:2,2,68,18 +rotate:90 +Dimmer/bollardDriveway1 +bounds:127,6,15,15 +offsets:1,1,17,17 +Dimmer/bollardNorth1 +bounds:108,4,17,17 +Fan/dance +bounds:2,2,70,74 +offsets:1,1,72,77 diff --git a/spine-ts/spine-pixi/example/assets/temp.png b/spine-ts/spine-pixi/example/assets/temp.png new file mode 100644 index 0000000000000000000000000000000000000000..b7530d3c894cf4ac0ee1b7eb10a40a040b4abd85 GIT binary patch literal 7516 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 { - frontFist = spineboy.skeleton.findSlot("front-fist"); - spineboy.addSlotObject(frontFist, logo2); - frontFist.setAttachment(null); - }, 2000) - - // scaling the bone, will scale the pixi object too - setTimeout(() => { - frontFist.bone.scaleX = .5 - frontFist.bone.scaleY = .5 - }, 3000) - - // adding a pixi text in a slot using slot index - let mouth; - setTimeout(() => { - mouth = spineboy.skeleton.findSlot("mouth"); - spineboy.addSlotObject(mouth, text); - }, 4000) - - // adding one display 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) - - // 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) - - // 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) - - // 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) - - // removing a specific slot object - setTimeout(() => { - const container = new PIXI.Container(); - spineboy.removeSlotObject(frontFist, logo2); - logo2.destroy(); - }, 9000) - - // resetting the slot with the original attachment - setTimeout(() => { - frontFist.setToSetupPose(); - }, 10000) + const gunSlot = spineboy.skeleton.findSlot("char2"); + // const gunSlot = spineboy.skeleton.findSlot("gun"); + spineboy.addSlotObject(gunSlot, logo1); + // spineboy.addSlotObject("head", logo2); + spineboy.addSlotObject("fill", logo2); + + spineboy.skeleton.color.a = 1; + gunSlot.color.a = 0.5; + + + + // setTimeout(() => { + // spineboy.destroy(); + // }, 2000) + + // const pixiContainer = new PIXI.Container(); + // pixiContainer.addChild(logo2); + // pixiContainer.addChild(text); + // logo2.x = - 150 + // text.x = - 150 + // text.scale.set(3); + // spineboy.addSlotObject("dance2", pixiContainer); + + // // 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.setAttachment(null); + // }, 2000) + + // // scaling the bone, will scale the pixi object too + // setTimeout(() => { + // frontFist.bone.scaleX = .5 + // frontFist.bone.scaleY = .5 + // }, 3000) + + // // adding a pixi text in a slot using slot index + // let mouth; + // setTimeout(() => { + // mouth = spineboy.skeleton.findSlot("mouth"); + // spineboy.addSlotObject(mouth, text); + // }, 4000) + + // // adding one display 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) + + // // 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) + + // // 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) + + // // 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) + + // // removing a specific slot object + // setTimeout(() => { + // const container = new PIXI.Container(); + // spineboy.removeSlotObject(frontFist, logo2); + // logo2.destroy(); + // }, 9000) + + // // resetting the slot with the original attachment + // setTimeout(() => { + // frontFist.setToSetupPose(); + // }, 10000) })(); diff --git a/spine-ts/spine-pixi/src/Spine.ts b/spine-ts/spine-pixi/src/Spine.ts index 4f383db9d..b491d814b 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,18 +251,18 @@ 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); @@ -275,22 +283,22 @@ export class Spine extends Container { 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 +311,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 +396,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 +468,7 @@ export class Spine extends Container { } Spine.clipper.clipEndWithSlot(slot); + this.pixiMaskCleanup(slot); } Spine.clipper.clipEnd(); } @@ -542,6 +588,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; /** diff --git a/spine-ts/spine-pixi/src/SpineDebugRenderer.ts b/spine-ts/spine-pixi/src/SpineDebugRenderer.ts index 135cd37e4..085d3a901 100644 --- a/spine-ts/spine-pixi/src/SpineDebugRenderer.ts +++ b/spine-ts/spine-pixi/src/SpineDebugRenderer.ts @@ -143,11 +143,11 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { }, }; - debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.bones); - debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.skeletonXY); - debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.regionAttachmentsShape); - debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshTrianglesLine); - debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshHullLine); + // debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.bones); + // debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.skeletonXY); + // debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.regionAttachmentsShape); + // debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshTrianglesLine); + // debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshHullLine); debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.clippingPolygon); debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesRect); debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesCircle);