diff --git a/spine-ts/spine-pixi-v7/example/slot-objects.html b/spine-ts/spine-pixi-v7/example/slot-objects.html index d34bc74ee..46c6b91d9 100644 --- a/spine-ts/spine-pixi-v7/example/slot-objects.html +++ b/spine-ts/spine-pixi-v7/example/slot-objects.html @@ -127,7 +127,7 @@ // for clipping attachments having slot objects setTimeout(() => { spineboy.state.setAnimation(0, "portal", true) - const tooth3 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png'); + const tooth3 = PIXI.Sprite.from('/assets/raptor-jaw-tooth.png'); tooth3.scale.set(2); tooth3.x = -60; tooth3.y = 120; diff --git a/spine-ts/spine-pixi-v7/src/Spine.ts b/spine-ts/spine-pixi-v7/src/Spine.ts index 6c975430b..a39b451c0 100644 --- a/spine-ts/spine-pixi-v7/src/Spine.ts +++ b/spine-ts/spine-pixi-v7/src/Spine.ts @@ -36,6 +36,7 @@ import { Color, MeshAttachment, Physics, + Pool, RegionAttachment, Skeleton, SkeletonBinary, @@ -323,7 +324,6 @@ export class Spine extends Container { private lightColor = new Color(); private darkColor = new Color(); - private clippingVertAux = new Float32Array(6); private _boundsProvider?: SpineBoundsProvider; /** The bounds provider to use. If undefined the bounds will be dynamic, calculated when requested and based on the current frame. */ @@ -441,8 +441,8 @@ export class Spine extends Container { this.slotsObject.clear(); for (let maskKey in this.clippingSlotToPixiMasks) { - const mask = this.clippingSlotToPixiMasks[maskKey]; - mask.destroy(); + const maskObj = this.clippingSlotToPixiMasks[maskKey]; + maskObj.mask?.destroy(); delete this.clippingSlotToPixiMasks[maskKey]; } @@ -575,14 +575,7 @@ 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 clippingSlotToPixiMasks: Record = {}; private updateSlotObject (element: { container: Container, followAttachmentTimeline: boolean }, slot: Slot, zIndex: number) { const { container: slotObject, followAttachmentTimeline } = element @@ -615,30 +608,62 @@ export class Spine extends Container { } } - private updateAndSetPixiMask (pixiMaskSource: PixiMaskSource | null, pixiObject: Container) { - if (Spine.clipper.isClipping() && pixiMaskSource) { - let mask = this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] as Graphics; + private currentClippingSlot: SlotsToClipping | undefined; + private updateAndSetPixiMask (slot: Slot, last: boolean) { + // assign/create the currentClippingSlot + const attachment = slot.attachment; + if (attachment && attachment instanceof ClippingAttachment) { + const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array() }); + clip.maskComputed = false; + this.currentClippingSlot = clip; + return; + } + + // assign the currentClippingSlot mask to the slot object + let currentClippingSlot = this.currentClippingSlot; + const slotObject = this.slotsObject.get(slot); + if (currentClippingSlot && slotObject) { + // create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot + let mask = currentClippingSlot.mask; if (!mask) { - mask = new Graphics(); - this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] = mask; + mask = maskPool.obtain(); + currentClippingSlot.mask = mask; this.addChild(mask); } - if (!pixiMaskSource.computed) { - pixiMaskSource.computed = true; - const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment; + + // compute the pixi mask polygon, if the clipped slot is the first one clipped by this currentClippingSlot + if (!currentClippingSlot.maskComputed) { + let slotClipping = currentClippingSlot.slot; + let clippingAttachment = slotClipping.attachment as ClippingAttachment; + currentClippingSlot.maskComputed = true; const worldVerticesLength = clippingAttachment.worldVerticesLength; - if (this.clippingVertAux.length < worldVerticesLength) this.clippingVertAux = new Float32Array(worldVerticesLength); - clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2); - mask.clear().lineStyle(0).beginFill(0x000000); - mask.moveTo(this.clippingVertAux[0], this.clippingVertAux[1]); - for (let i = 2; i < worldVerticesLength; i += 2) { - mask.lineTo(this.clippingVertAux[i], this.clippingVertAux[i + 1]); - } - mask.finishPoly(); + const vertices = currentClippingSlot.vertices; + clippingAttachment.computeWorldVertices(slotClipping, 0, worldVerticesLength, vertices, 0, 2); + mask.clear().lineStyle(0).beginFill(0x000000).drawPolygon(vertices).endFill(); } - pixiObject.mask = mask; - } else if (pixiObject.mask) { - pixiObject.mask = null; + + slotObject.container.mask = mask; + } else if (slotObject?.container.mask) { + // remove the mask, if slot object has a mask, but currentClippingSlot is undefined + slotObject.container.mask = null; + } + + // if current slot is the ending one of the currentClippingSlot mask, set currentClippingSlot to undefined + if (currentClippingSlot && (currentClippingSlot.slot.attachment as ClippingAttachment).endSlot == slot.data) { + this.currentClippingSlot = undefined; + } + + // clean up unused masks + if (last) { + for (const key in this.clippingSlotToPixiMasks) { + const clippingSlotToPixiMask = this.clippingSlotToPixiMasks[key]; + if ((!(clippingSlotToPixiMask.slot.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) { + this.removeChild(clippingSlotToPixiMask.mask); + maskPool.free(clippingSlotToPixiMask.mask); + clippingSlotToPixiMask.mask = undefined; + } + } + this.currentClippingSlot = undefined; } } @@ -658,7 +683,7 @@ export class Spine extends Container { 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++) { @@ -670,14 +695,13 @@ export class Spine extends Container { if (pixiObject) { this.updateSlotObject(pixiObject, slot, zIndex + 1); slotObjectsCounter++; - this.updateAndSetPixiMask(pixiMaskSource, pixiObject.container); } + this.updateAndSetPixiMask(slot, i === drawOrder.length - 1); 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(); @@ -705,14 +729,12 @@ 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 { if (this.hasMeshForSlot(slot)) { this.getMeshForSlot(slot).visible = false; } Spine.clipper.clipEndWithSlot(slot); - this.pixiMaskCleanup(slot); continue; } if (texture != null) { @@ -788,7 +810,6 @@ export class Spine extends Container { } Spine.clipper.clipEndWithSlot(slot); - this.pixiMaskCleanup(slot); } Spine.clipper.clipEnd(); } @@ -997,10 +1018,14 @@ export class Spine extends Container { } } -type PixiMaskSource = { +interface SlotsToClipping { slot: Slot, - computed: boolean, // prevent to reculaculate vertices for a mask clipping multiple pixi objects -} + mask?: Graphics, + maskComputed?: boolean, + vertices: Array, +}; + +const maskPool = new Pool(() => new Graphics); Skeleton.yDown = true; diff --git a/spine-ts/spine-pixi-v8/src/Spine.ts b/spine-ts/spine-pixi-v8/src/Spine.ts index a180a2507..fdc299284 100644 --- a/spine-ts/spine-pixi-v8/src/Spine.ts +++ b/spine-ts/spine-pixi-v8/src/Spine.ts @@ -568,7 +568,7 @@ export class Spine extends ViewContainer { if (attachment && attachment instanceof ClippingAttachment) { const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array() }); clip.maskComputed = false; - this.currentClippingSlot = this.clippingSlotToPixiMasks[slot.data.name]; + this.currentClippingSlot = clip; return; } @@ -576,11 +576,8 @@ export class Spine extends ViewContainer { let currentClippingSlot = this.currentClippingSlot; let slotObject = this._slotsObject[slot.data.name]; if (currentClippingSlot && slotObject) { - let slotClipping = currentClippingSlot.slot; - let clippingAttachment = slotClipping.attachment as ClippingAttachment; - // create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot - let mask = currentClippingSlot.mask as Graphics; + let mask = currentClippingSlot.mask; if (!mask) { mask = maskPool.obtain(); currentClippingSlot.mask = mask; @@ -589,6 +586,8 @@ export class Spine extends ViewContainer { // compute the pixi mask polygon, if the clipped slot is the first one clipped by this currentClippingSlot if (!currentClippingSlot.maskComputed) { + let slotClipping = currentClippingSlot.slot; + let clippingAttachment = slotClipping.attachment as ClippingAttachment; currentClippingSlot.maskComputed = true; const worldVerticesLength = clippingAttachment.worldVerticesLength; const vertices = currentClippingSlot.vertices; @@ -896,7 +895,6 @@ export class Spine extends ViewContainer { container.includeInBuild = false; - // TODO only add once?? this.addChild(container); const slotObject = {