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

This commit is contained in:
Davide Tantillo 2024-06-06 18:14:18 +02:00
parent 4cdee3130b
commit 61e9be2dcc
5 changed files with 133 additions and 66 deletions

View File

@ -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-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.atlas "$ROOT/spine-ts/spine-pixi/example/assets/"
cp -f ../raptor/export/raptor.png "$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.json "$ROOT/spine-ts/spine-pixi/example/assets/"
cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-pixi/example/assets/" cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-pixi/example/assets/"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -29,7 +29,7 @@
// Create the spine display object // Create the spine display object
const spineboy = spine.Spine.from("spineboyData", "spineboyAtlas", { const spineboy = spine.Spine.from("spineboyData", "spineboyAtlas", {
scale: 0.5, scale: 0.25,
}); });
// Set the default mix time to use when transitioning // Set the default mix time to use when transitioning
@ -46,79 +46,96 @@
// Add the display object to the stage. // Add the display object to the stage.
app.stage.addChild(spineboy); app.stage.addChild(spineboy);
const logo1 = PIXI.Sprite.from('assets/spine_logo.png'); const tooth1 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
const logo2 = PIXI.Sprite.from('assets/spine_logo.png'); const tooth2 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png');
const logo3 = PIXI.Sprite.from('assets/spine_logo.png'); const text = new PIXI.Text('Text GUN');
const logo4 = PIXI.Sprite.from('assets/spine_logo.png');
const text = new PIXI.Text('Spine Text');
// putting logo1 on top of the gun const toothContainer = new PIXI.Container();
spineboy.addSlotObject("gun", logo1); toothContainer.addChild(tooth1);
toothContainer.name = "tooth";
text.name = "text";
// putting logo2 on top of the hand using slot directly and remove the attachment hand // putting logo2 on top of the hand using slot directly and remove the attachment hand
let frontFist; let frontFist;
setTimeout(() => { setTimeout(() => {
frontFist = spineboy.skeleton.findSlot("front-fist"); frontFist = spineboy.skeleton.findSlot("front-foot");
spineboy.addSlotObject(frontFist, logo2); tooth1.x = -10;
tooth1.y = -70;
spineboy.addSlotObject(frontFist, toothContainer);
frontFist.setAttachment(null); frontFist.setAttachment(null);
}, 2000) }, 1000);
// scaling the bone, will scale the pixi object too // scaling the bone, will scale the pixi object too
setTimeout(() => { setTimeout(() => {
frontFist.bone.scaleX = .5 frontFist.bone.scaleX = .5;
frontFist.bone.scaleY = .5 frontFist.bone.scaleY = .5;
}, 3000) }, 2000);
// adding a pixi text in a slot using slot index // adding a pixi text in a slot using slot index
let mouth; let mouth;
setTimeout(() => { setTimeout(() => {
mouth = spineboy.skeleton.findSlot("mouth"); spineboy.addSlotObject("gun", text);
spineboy.addSlotObject(mouth, text); }, 4000);
}, 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 // and move the given one to the slot
setTimeout(() => { setTimeout(() => {
spineboy.addSlotObject(mouth, logo1); spineboy.addSlotObject("gun", toothContainer);
}, 5000) }, 5000);
// adding multiple DisplayObjects to a slot using a Container to control their offset, size, ... // adding multiple DisplayObjects to a slot using a Container to control their offset, size, ...
setTimeout(() => { setTimeout(() => {
const container = new PIXI.Container(); toothContainer.addChild(tooth2);
container.addChild(logo3, logo4); tooth2.x = 30;
logo3.y = 20; tooth2.y = -70;
logo3.scale.set(.5); tooth2.angle = 30;
logo4.scale.set(.5); tooth2.tint = 0xFF5500;
logo4.tint = 0xFF5500; }, 6000);
spineboy.addSlotObject("gun", container);
}, 6000)
// removing the container won't automatically destroy the displayObject contained, so take care of them // removing the container won't automatically destroy the displayObject contained, so take care of them
setTimeout(() => { setTimeout(() => {
const container = new PIXI.Container();
spineboy.removeSlotObject("gun"); spineboy.removeSlotObject("gun");
logo3.destroy(); console.log(toothContainer.destroyed)
logo4.destroy(); console.log(tooth1.destroyed)
}, 7000) 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 // removing a specific slot object, that is not in that slot do nothing
setTimeout(() => { setTimeout(() => {
const container = new PIXI.Container(); spineboy.addSlotObject("gun", tooth2);
spineboy.removeSlotObject(frontFist, text); spineboy.removeSlotObject("gun", text);
text.destroy(); }, 8000);
}, 8000)
// removing a specific slot object // removing a specific slot object
setTimeout(() => { setTimeout(() => {
const container = new PIXI.Container(); spineboy.removeSlotObject("gun", tooth2);
spineboy.removeSlotObject(frontFist, logo2); tooth2.destroy();
logo2.destroy(); }, 9000);
}, 9000)
// resetting the slot with the original attachment // resetting the slot with the original attachment
setTimeout(() => { setTimeout(() => {
frontFist.setToSetupPose(); 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> </script>
</body> </body>

View File

@ -54,6 +54,7 @@ import type { IPointData } from "@pixi/core";
import { Ticker } from "@pixi/core"; import { Ticker } from "@pixi/core";
import type { IDestroyOptions, DisplayObject } from "@pixi/display"; import type { IDestroyOptions, DisplayObject } from "@pixi/display";
import { Container } from "@pixi/display"; import { Container } from "@pixi/display";
import { Graphics } from "@pixi/graphics";
/** /**
* Options to configure a {@link Spine} game object. * Options to configure a {@link Spine} game object.
@ -205,6 +206,13 @@ export class Spine extends Container {
this.debug = undefined; this.debug = undefined;
this.meshesCache.clear(); this.meshesCache.clear();
this.slotsObject.clear(); this.slotsObject.clear();
for (let maskKey in this.clippingSlotToPixiMasks) {
const mask = this.clippingSlotToPixiMasks[maskKey];
mask.destroy();
delete this.clippingSlotToPixiMasks[maskKey];
}
super.destroy(options); 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 { private getSlotFromRef (slotRef: number | string | Slot): Slot {
let slot: Slot | null; let slot: Slot | null;
if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef]; if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef];
@ -243,54 +251,52 @@ export class Spine extends Container {
return slot; return slot;
} }
/** /**
* Add a pixi DisplayObject as a child of the Spine object. * Add a pixi Container as a child of the Spine object.
* The DisplayObject will be rendered coherently with the draw order of the slot. * The Container 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 an attachment is active on the slot, the pixi Container will be rendered on top of it.
* If the DisplayObject is already attached to the given slot, nothing will happen. * If the Container 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 * If the Container is already attached to another slot, it will be removed from that slot
* before adding it to the given one. * 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. * 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 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 slot = this.getSlotFromRef(slotRef);
let oldPixiObject = this.slotsObject.get(slot); let oldPixiObject = this.slotsObject.get(slot);
if (oldPixiObject === pixiObject) return;
// search if the pixiObject was already in another slotObject // search if the pixiObject was already in another slotObject
if (!oldPixiObject) { for (const [otherSlot, oldPixiObjectAnotherSlot] of this.slotsObject) {
for (const [slot, oldPixiObjectAnotherSlot] of this.slotsObject) { if (otherSlot !== slot && oldPixiObjectAnotherSlot === pixiObject) {
if (oldPixiObjectAnotherSlot === pixiObject) { this.removeSlotObject(otherSlot, pixiObject);
this.removeSlotObject(slot, pixiObject); break;
break;
}
} }
} }
if (oldPixiObject === pixiObject) return;
if (oldPixiObject) this.removeChild(oldPixiObject); if (oldPixiObject) this.removeChild(oldPixiObject);
this.slotsObject.set(slot, pixiObject); this.slotsObject.set(slot, pixiObject);
this.addChild(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 * Otherwise return undefined
* @param pixiObject - The slot index, or the slot name, or the Slot to get the DisplayObject from. * @param pixiObject - The slot index, or the slot name, or the Slot to get the Container from.
* @returns a DisplayObject if any, undefined otherwise. * @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)); return this.slotsObject.get(this.getSlotFromRef(slotRef));
} }
/** /**
* Remove a slot object from the given slot. * 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 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 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 slot = this.getSlotFromRef(slotRef);
let slotObject = this.slotsObject.get(slot); let slotObject = this.slotsObject.get(slot);
if (!slotObject) return; if (!slotObject) return;
@ -303,11 +309,45 @@ export class Spine extends Container {
} }
private verticesCache: NumberArrayLike = Utils.newFloatArray(1024); 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 { private renderMeshes (): void {
this.resetMeshes(); this.resetMeshes();
let triangles: Array<number> | null = null; let triangles: Array<number> | null = null;
let uvs: NumberArrayLike | null = null; let uvs: NumberArrayLike | null = null;
let pixiMaskSource: PixiMaskSource | null = null;
const drawOrder = this.skeleton.drawOrder; const drawOrder = this.skeleton.drawOrder;
for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) { 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 pixiObject = this.slotsObject.get(slot);
let zIndex = i + slotObjectsCounter; let zIndex = i + slotObjectsCounter;
if (pixiObject) { if (pixiObject) {
pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.getWorldScaleX(), slot.bone.getWorldScaleX(), slot.bone.getWorldRotationX() * MathUtils.degRad); this.updatePixiObject(pixiObject, slot, zIndex + 1);
pixiObject.zIndex = zIndex + 1;
slotObjectsCounter++; slotObjectsCounter++;
this.updateAndSetPixiMask(pixiMaskSource, pixiObject);
} }
const useDarkColor = slot.darkColor != null; const useDarkColor = slot.darkColor != null;
const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE; const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
if (!slot.bone.active) { if (!slot.bone.active) {
Spine.clipper.clipEndWithSlot(slot); Spine.clipper.clipEndWithSlot(slot);
this.pixiMaskCleanup(slot);
continue; continue;
} }
const attachment = slot.getAttachment(); const attachment = slot.getAttachment();
@ -353,9 +394,11 @@ export class Spine extends Container {
texture = <SpineTexture>mesh.region?.texture; texture = <SpineTexture>mesh.region?.texture;
} else if (attachment instanceof ClippingAttachment) { } else if (attachment instanceof ClippingAttachment) {
Spine.clipper.clipStart(slot, attachment); Spine.clipper.clipStart(slot, attachment);
pixiMaskSource = { slot, computed: false };
continue; continue;
} else { } else {
Spine.clipper.clipEndWithSlot(slot); Spine.clipper.clipEndWithSlot(slot);
this.pixiMaskCleanup(slot);
continue; continue;
} }
if (texture != null) { if (texture != null) {
@ -423,6 +466,7 @@ export class Spine extends Container {
} }
Spine.clipper.clipEndWithSlot(slot); Spine.clipper.clipEndWithSlot(slot);
this.pixiMaskCleanup(slot);
} }
Spine.clipper.clipEnd(); 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; Skeleton.yDown = true;
/** /**