[ts] Add SkeletonRendererCore.

This commit is contained in:
Davide Tantillo 2025-08-12 10:32:17 +02:00
parent 75421b2a41
commit 3fa97353f1
4 changed files with 191 additions and 232 deletions

View File

@ -34,20 +34,14 @@ import {
AnimationStateData,
AtlasAttachmentLoader,
BlendMode,
ClippingAttachment,
MathUtils,
MeshAttachment,
type NumberArrayLike,
Physics,
RegionAttachment,
Skeleton,
SkeletonBinary,
SkeletonClipping,
type SkeletonData,
SkeletonJson,
SkeletonRendererCore,
Texture,
TextureAtlas,
Utils,
} from "@esotericsoftware/spine-core";
import type { Canvas, CanvasKit, Image, Paint, Shader } from "canvaskit-wasm";
@ -223,11 +217,7 @@ export class SkeletonDrawable {
* Renders a {@link Skeleton} or {@link SkeletonDrawable} to a CanvasKit {@link Canvas}.
*/
export class SkeletonRenderer {
private clipper = new SkeletonClipping();
private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
private scratchPositions = Utils.newFloatArray(100);
private scratchUVs = Utils.newFloatArray(100);
private scratchColors = new Uint32Array(100 / 4);
private skeletonRenderer = new SkeletonRendererCore();
/**
* Creates a new skeleton renderer.
@ -235,119 +225,34 @@ export class SkeletonRenderer {
*/
constructor (private ck: CanvasKit) { }
/**
* Renders a skeleton or skeleton drawable in its current pose to the canvas.
* @param canvas the canvas to render to.
* @param skeleton the skeleton or drawable to render.
*/
render (canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) {
if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton;
const clipper = this.clipper;
const drawOrder = skeleton.drawOrder;
const skeletonColor = skeleton.color;
let command = this.skeletonRenderer.render(skeleton);
while (command) {
const { positions, uvs, colors, indices } = command;
const ckImage = command.texture.getImage();
const image = ckImage.image;
const width = image.width();
const height = image.height();
for (let i = 0, n = drawOrder.length; i < n; i++) {
const slot = drawOrder[i];
if (!slot.bone.active) {
clipper.clipEnd(slot);
continue;
for (let i = 0; i < uvs.length; i += 2) {
uvs[i] = uvs[i] * width;
uvs[i + 1] = uvs[i + 1] * height;
}
const pose = slot.applied;
const attachment = pose.attachment;
let positions = this.scratchPositions;
let triangles: Array<number>;
let numVertices = 4;
if (attachment instanceof RegionAttachment) {
attachment.computeWorldVertices(slot, positions, 0, 2);
triangles = SkeletonRenderer.QUAD_TRIANGLES;
} else if (attachment instanceof MeshAttachment) {
if (positions.length < attachment.worldVerticesLength) {
this.scratchPositions = Utils.newFloatArray(attachment.worldVerticesLength);
positions = this.scratchPositions;
}
numVertices = attachment.worldVerticesLength >> 1;
attachment.computeWorldVertices(
skeleton,
slot,
0,
attachment.worldVerticesLength,
positions,
0,
2
);
triangles = attachment.triangles;
} else if (attachment instanceof ClippingAttachment) {
clipper.clipStart(skeleton, slot, attachment);
continue;
} else {
clipper.clipEnd(slot);
continue;
}
const texture = attachment.region?.texture as CanvasKitTexture;
if (texture) {
let uvs = attachment.uvs;
let scaledUvs: NumberArrayLike;
let colors = this.scratchColors;
if (clipper.isClipping()) {
clipper.clipTrianglesUnpacked(positions, triangles, triangles.length, uvs);
if (clipper.clippedVertices.length <= 0) {
clipper.clipEnd(slot);
continue;
}
positions = clipper.clippedVertices;
uvs = scaledUvs = clipper.clippedUVs;
triangles = clipper.clippedTriangles;
numVertices = clipper.clippedVertices.length / 2;
colors = new Uint32Array(numVertices);
} else {
scaledUvs = this.scratchUVs;
if (this.scratchUVs.length < uvs.length)
scaledUvs = this.scratchUVs = Utils.newFloatArray(uvs.length);
if (colors.length < numVertices)
colors = this.scratchColors = new Uint32Array(numVertices);
}
const ckImage = texture.getImage();
const image = ckImage.image;
const width = image.width();
const height = image.height();
for (let i = 0; i < uvs.length; i += 2) {
scaledUvs[i] = uvs[i] * width;
scaledUvs[i + 1] = uvs[i + 1] * height;
}
const attachmentColor = attachment.color;
const slotColor = pose.color;
// using Uint32Array for colors allows to avoid canvaskit to allocate one each time
// but colors need to be in canvaskit format.
// See: https://github.com/google/skia/blob/bb8c36fdf7b915a8c096e35e2f08109e477fe1b8/modules/canvaskit/color.js#L163
const finalColor = (
MathUtils.clamp(skeletonColor.a * slotColor.a * attachmentColor.a * 255, 0, 255) << 24 |
MathUtils.clamp(skeletonColor.r * slotColor.r * attachmentColor.r * 255, 0, 255) << 16 |
MathUtils.clamp(skeletonColor.g * slotColor.g * attachmentColor.g * 255, 0, 255) << 8 |
MathUtils.clamp(skeletonColor.b * slotColor.b * attachmentColor.b * 255, 0, 255) << 0
) >>> 0;
for (let i = 0, n = numVertices; i < n; i++) colors[i] = finalColor;
const vertices = this.ck.MakeVertices(
this.ck.VertexMode.Triangles,
positions,
scaledUvs,
colors,
triangles,
false
);
const ckPaint = ckImage.paintPerBlendMode.get(slot.data.blendMode);
if (ckPaint) canvas.drawVertices(vertices, this.ck.BlendMode.Modulate, ckPaint);
vertices.delete();
}
clipper.clipEnd(slot);
const vertices = this.ck.MakeVertices(
this.ck.VertexMode.Triangles,
positions,
uvs,
colors,
indices as any as number[],
false
);
const ckPaint = ckImage.paintPerBlendMode.get(command.blendMode);
if (ckPaint) canvas.drawVertices(vertices, this.ck.BlendMode.Modulate, ckPaint);
vertices.delete();
command = command.next;
}
clipper.clipEnd();
}
}

View File

@ -43,6 +43,17 @@ export class SkeletonClipping {
clippedUVs = new Array<number>();
clippedTriangles = new Array<number>();
_clippedVerticesTyped = new Float32Array(1024);
_clippedUVsTyped = new Float32Array(1024);
_clippedTrianglesTyped = new Uint32Array(1024);
clippedVerticesTyped = new Float32Array(0);
clippedUVsTyped = new Float32Array(0);
clippedTrianglesTyped = new Uint32Array(0);
clippedVerticesLength = 0;
clippedUVsLength = 0;
clippedTrianglesLength = 0;
private scratch = new Array<number>();
private clipAttachment: ClippingAttachment | null = null;
@ -76,6 +87,9 @@ export class SkeletonClipping {
this.clippedVertices.length = 0;
this.clippedTriangles.length = 0;
this.clippingPolygon.length = 0;
this.clippedVerticesLength = 0;
this.clippedUVsLength = 0;
this.clippedTrianglesLength = 0;
}
isClipping (): boolean {
@ -308,55 +322,82 @@ export class SkeletonClipping {
return clipOutputItems != null;
}
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number, uvs: NumberArrayLike) {
let clipOutput = this.clipOutput, clippedVertices = this.clippedVertices, clippedUVs = this.clippedUVs;
let clippedTriangles = this.clippedTriangles;
let polygons = this.clippingPolygons!;
let polygonsCount = polygons.length;
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike | Uint32Array, trianglesLength: number, uvs: NumberArrayLike) {
const clipOutput = this.clipOutput;
let clippedVertices = this._clippedVerticesTyped, clippedUVs = this._clippedUVsTyped, clippedTriangles = this._clippedTrianglesTyped;
const polygons = this.clippingPolygons!;
const polygonsCount = polygons.length;
let index = 0;
clippedVertices.length = 0;
clippedUVs.length = 0;
clippedTriangles.length = 0;
this.clippedVerticesLength = 0;
this.clippedUVsLength = 0;
this.clippedTrianglesLength = 0;
this._clippedVerticesTyped;
this._clippedUVsTyped;
this._clippedTrianglesTyped;
let clipped = false;
for (let i = 0; i < trianglesLength; i += 3) {
let v = triangles[i] << 1;
let x1 = vertices[v], y1 = vertices[v + 1];
let u1 = uvs[v], v1 = uvs[v + 1];
const x1 = vertices[v], y1 = vertices[v + 1];
const u1 = uvs[v], v1 = uvs[v + 1];
v = triangles[i + 1] << 1;
let x2 = vertices[v], y2 = vertices[v + 1];
let u2 = uvs[v], v2 = uvs[v + 1];
const x2 = vertices[v], y2 = vertices[v + 1];
const u2 = uvs[v], v2 = uvs[v + 1];
v = triangles[i + 2] << 1;
let x3 = vertices[v], y3 = vertices[v + 1];
let u3 = uvs[v], v3 = uvs[v + 1];
const x3 = vertices[v], y3 = vertices[v + 1];
const u3 = uvs[v], v3 = uvs[v + 1];
for (let p = 0; p < polygonsCount; p++) {
let s = clippedVertices.length;
let s = this.clippedVerticesLength;
if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) {
let clipOutputLength = clipOutput.length;
if (clipOutputLength == 0) continue;
let d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1;
let d = 1 / (d0 * d2 + d1 * (y1 - y3));
const clipOutputLength = clipOutput.length;
if (clipOutputLength === 0) continue;
clipped = true;
const d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1;
const d = 1 / (d0 * d2 + d1 * (y1 - y3));
let clipOutputCount = clipOutputLength >> 1;
let clipOutputItems = this.clipOutput;
let clippedVerticesItems = Utils.setArraySize(clippedVertices, s + clipOutputCount * 2);
let clippedUVsItems = Utils.setArraySize(clippedUVs, s + clipOutputCount * 2);
const clipOutputItems = this.clipOutput;
const newLength = s + clipOutputCount * 2;
if (clippedVertices.length < newLength) {
this._clippedVerticesTyped = new Float32Array(newLength * 2);
this._clippedVerticesTyped.set(clippedVertices.subarray(0, s));
this._clippedUVsTyped = new Float32Array(newLength * 2);
this._clippedUVsTyped.set(clippedUVs.subarray(0, s));
clippedVertices = this._clippedVerticesTyped;
clippedUVs = this._clippedUVsTyped;
}
const clippedVerticesItems = clippedVertices;
const clippedUVsItems = clippedUVs;
this.clippedVerticesLength = newLength;
this.clippedUVsLength = newLength;
for (let ii = 0; ii < clipOutputLength; ii += 2, s += 2) {
let x = clipOutputItems[ii], y = clipOutputItems[ii + 1];
const x = clipOutputItems[ii], y = clipOutputItems[ii + 1];
clippedVerticesItems[s] = x;
clippedVerticesItems[s + 1] = y;
let c0 = x - x3, c1 = y - y3;
let a = (d0 * c0 + d1 * c1) * d;
let b = (d4 * c0 + d2 * c1) * d;
let c = 1 - a - b;
const c0 = x - x3, c1 = y - y3;
const a = (d0 * c0 + d1 * c1) * d;
const b = (d4 * c0 + d2 * c1) * d;
const c = 1 - a - b;
clippedUVsItems[s] = u1 * a + u2 * b + u3 * c;
clippedUVsItems[s + 1] = v1 * a + v2 * b + v3 * c;
}
s = clippedTriangles.length;
let clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3 * (clipOutputCount - 2));
s = this.clippedTrianglesLength;
const newLengthTriangles = s + 3 * (clipOutputCount - 2)
if (clippedTriangles.length < newLengthTriangles) {
this._clippedTrianglesTyped = new Uint32Array(newLengthTriangles * 2);
this._clippedTrianglesTyped.set(clippedTriangles.subarray(0, s));
clippedTriangles = this._clippedTrianglesTyped;
}
this.clippedTrianglesLength = newLengthTriangles;
const clippedTrianglesItems = clippedTriangles;
clipOutputCount--;
for (let ii = 1; ii < clipOutputCount; ii++, s += 3) {
clippedTrianglesItems[s] = index;
@ -366,32 +407,58 @@ export class SkeletonClipping {
index += clipOutputCount + 1;
} else {
let clippedVerticesItems = Utils.setArraySize(clippedVertices, s + 3 * 2);
clippedVerticesItems[s] = x1;
clippedVerticesItems[s + 1] = y1;
clippedVerticesItems[s + 2] = x2;
clippedVerticesItems[s + 3] = y2;
clippedVerticesItems[s + 4] = x3;
clippedVerticesItems[s + 5] = y3;
let clippedUVSItems = Utils.setArraySize(clippedUVs, s + 3 * 2);
clippedUVSItems[s] = u1;
clippedUVSItems[s + 1] = v1;
clippedUVSItems[s + 2] = u2;
clippedUVSItems[s + 3] = v2;
clippedUVSItems[s + 4] = u3;
clippedUVSItems[s + 5] = v3;
let newLength = s + 3 * 2;
if (clippedVertices.length < newLength) {
this._clippedVerticesTyped = new Float32Array(newLength * 2);
this._clippedVerticesTyped.set(clippedVertices.subarray(0, s));
clippedVertices = this._clippedVerticesTyped;
}
clippedVertices[s] = x1;
clippedVertices[s + 1] = y1;
clippedVertices[s + 2] = x2;
clippedVertices[s + 3] = y2;
clippedVertices[s + 4] = x3;
clippedVertices[s + 5] = y3;
s = clippedTriangles.length;
let clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3);
clippedTrianglesItems[s] = index;
clippedTrianglesItems[s + 1] = (index + 1);
clippedTrianglesItems[s + 2] = (index + 2);
if (clippedUVs.length < newLength) {
this._clippedUVsTyped = new Float32Array(newLength * 2);
this._clippedUVsTyped.set(clippedUVs.subarray(0, s));
clippedUVs = this._clippedUVsTyped;
}
clippedUVs[s] = u1;
clippedUVs[s + 1] = v1;
clippedUVs[s + 2] = u2;
clippedUVs[s + 3] = v2;
clippedUVs[s + 4] = u3;
clippedUVs[s + 5] = v3;
this.clippedVerticesLength = newLength;
this.clippedUVsLength = newLength;
s = this.clippedTrianglesLength;
newLength = s + 3;
if (clippedTriangles.length < newLength) {
this._clippedTrianglesTyped = new Uint32Array(newLength * 2);
this._clippedTrianglesTyped.set(clippedTriangles.subarray(0, s));
clippedTriangles = this._clippedTrianglesTyped;
}
clippedTriangles[s] = index;
clippedTriangles[s + 1] = (index + 1);
clippedTriangles[s + 2] = (index + 2);
index += 3;
this.clippedTrianglesLength = newLength;
break;
}
}
}
this.clippedVerticesTyped = this._clippedVerticesTyped.subarray(0, this.clippedVerticesLength)
this.clippedUVsTyped = this._clippedUVsTyped.subarray(0, this.clippedUVsLength)
this.clippedTrianglesTyped = this._clippedTrianglesTyped.subarray(0, this.clippedTrianglesLength)
return clipped;
}
/** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping

View File

@ -1,8 +1,18 @@
export * from './Animation.js';
export * from './AnimationState.js';
export * from './AnimationStateData.js';
export * from './AtlasAttachmentLoader.js';
export * from './AssetManagerBase.js';
export * from './AtlasAttachmentLoader.js';
export * from './attachments/Attachment.js';
export * from './attachments/AttachmentLoader.js';
export * from './attachments/BoundingBoxAttachment.js';
export * from './attachments/ClippingAttachment.js';
export * from './attachments/HasTextureRegion.js';
export * from './attachments/MeshAttachment.js';
export * from './attachments/PathAttachment.js';
export * from './attachments/PointAttachment.js';
export * from './attachments/RegionAttachment.js';
export * from './attachments/Sequence.js';
export * from './Bone.js';
export * from './BoneData.js';
export * from './BoneLocal.js';
@ -25,12 +35,14 @@ export * from './Pose.js';
export * from './Posed.js';
export * from './PosedActive.js';
export * from './PosedData.js';
export * from './polyfills.js';
export * from './Skeleton.js';
export * from './SkeletonBinary.js';
export * from './SkeletonBounds.js';
export * from './SkeletonClipping.js';
export * from './SkeletonData.js';
export * from './SkeletonJson.js';
export * from './SkeletonRendererCore.js';
export * from './Skin.js';
export * from './Slider.js';
export * from './SliderData.js';
@ -46,14 +58,3 @@ export * from './TransformConstraintPose.js';
export * from './Triangulator.js';
export * from './Update.js';
export * from './Utils.js';
export * from './polyfills.js';
export * from './attachments/Attachment.js';
export * from './attachments/AttachmentLoader.js';
export * from './attachments/BoundingBoxAttachment.js';
export * from './attachments/ClippingAttachment.js';
export * from './attachments/HasTextureRegion.js';
export * from './attachments/MeshAttachment.js';
export * from './attachments/PathAttachment.js';
export * from './attachments/PointAttachment.js';
export * from './attachments/RegionAttachment.js';
export * from './attachments/Sequence.js';

View File

@ -27,27 +27,12 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import {
Assets,
Bounds,
Cache,
Container,
ContainerOptions,
DestroyOptions,
fastCopy,
Graphics,
PointData,
Texture,
Ticker,
ViewContainer,
} from 'pixi.js';
import { ISpineDebugRenderer } from './SpineDebugRenderer.js';
import {
AnimationState,
AnimationStateData,
AtlasAttachmentLoader,
Attachment,
Bone,
type Attachment,
type Bone,
ClippingAttachment,
Color,
MeshAttachment,
@ -61,11 +46,26 @@ import {
SkeletonData,
SkeletonJson,
Skin,
Slot,
type Slot,
type TextureAtlas,
TrackEntry,
type TrackEntry,
Vector2,
} from '@esotericsoftware/spine-core';
import {
Assets,
type Bounds,
Cache,
Container,
type ContainerOptions,
type DestroyOptions,
fastCopy,
Graphics,
type PointData,
Texture,
Ticker,
ViewContainer,
} from 'pixi.js';
import type { ISpineDebugRenderer } from './SpineDebugRenderer.js';
/**
* Options to create a {@link Spine} using {@link Spine.from}.
@ -142,7 +142,7 @@ export class SetupPoseBoundsProvider implements SpineBoundsProvider {
skeleton.setupPose();
skeleton.updateWorldTransform(Physics.update);
const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined);
return bounds.width == Number.NEGATIVE_INFINITY
return bounds.width === Number.NEGATIVE_INFINITY
? { x: 0, y: 0, width: 0, height: 0 }
: bounds;
}
@ -180,7 +180,7 @@ export class SkinsAndAnimationBoundsProvider
const clipper = this.clipping ? new SkeletonClipping() : undefined;
const data = skeleton.data;
if (this.skins.length > 0) {
let customSkin = new Skin("custom-skin");
const customSkin = new Skin("custom-skin");
for (const skinName of this.skins) {
const skin = data.findSkin(skinName);
if (skin == null) continue;
@ -190,12 +190,12 @@ export class SkinsAndAnimationBoundsProvider
}
skeleton.setupPose();
const animation = this.animation != null ? data.findAnimation(this.animation!) : null;
const animation = this.animation != null ? data.findAnimation(this.animation) : null;
if (animation == null) {
skeleton.updateWorldTransform(Physics.update);
const bounds = skeleton.getBoundsRect(clipper);
return bounds.width == Number.NEGATIVE_INFINITY
return bounds.width === Number.NEGATIVE_INFINITY
? { x: 0, y: 0, width: 0, height: 0 }
: bounds;
} else {
@ -225,7 +225,7 @@ export class SkinsAndAnimationBoundsProvider
width: maxX - minX,
height: maxY - minY,
};
return bounds.width == Number.NEGATIVE_INFINITY
return bounds.width === Number.NEGATIVE_INFINITY
? { x: 0, y: 0, width: 0, height: 0 }
: bounds;
}
@ -414,10 +414,10 @@ export class Spine extends ViewContainer {
/** If {@link Spine.autoUpdate} is `false`, this method allows to update the AnimationState and the Skeleton with the given delta. */
public update (dt: number): void {
this.internalUpdate(0, dt);
this.internalUpdate(undefined, dt);
}
protected internalUpdate (_deltaFrame: any, deltaSeconds?: number): void {
protected internalUpdate (ticker?: Ticker, deltaSeconds?: number): void {
// Because reasons, pixi uses deltaFrames at 60fps.
// We ignore the default deltaFrames and use the deltaSeconds from pixi ticker.
this._updateAndApplyState(deltaSeconds ?? Ticker.shared.deltaMS / 1000);
@ -568,18 +568,18 @@ export class Spine extends ViewContainer {
const pose = slot.applied;
const attachment = pose.attachment;
if (attachment && attachment instanceof ClippingAttachment) {
const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array<number>() });
const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: [] as number[] });
clip.maskComputed = false;
this.currentClippingSlot = this.clippingSlotToPixiMasks[slot.data.name];
return;
}
// assign the currentClippingSlot mask to the slot object
let currentClippingSlot = this.currentClippingSlot;
let slotObject = this._slotsObject[slot.data.name];
const currentClippingSlot = this.currentClippingSlot;
const slotObject = this._slotsObject[slot.data.name];
if (currentClippingSlot && slotObject) {
let slotClipping = currentClippingSlot.slot;
let clippingAttachment = slotClipping.pose.attachment as ClippingAttachment;
const slotClipping = currentClippingSlot.slot;
const clippingAttachment = slotClipping.pose.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;
@ -604,7 +604,7 @@ export class Spine extends ViewContainer {
}
// if current slot is the ending one of the currentClippingSlot mask, set currentClippingSlot to undefined
if (currentClippingSlot && (currentClippingSlot.slot.applied.attachment as ClippingAttachment).endSlot == slot.data) {
if (currentClippingSlot && (currentClippingSlot.slot.applied.attachment as ClippingAttachment).endSlot === slot.data) {
this.currentClippingSlot = undefined;
}
@ -711,10 +711,10 @@ export class Spine extends ViewContainer {
cacheData.uvs,
);
const { clippedVertices, clippedUVs, clippedTriangles } = clipper;
const { clippedVerticesTyped, clippedUVsTyped, clippedTrianglesTyped } = clipper;
const verticesCount = clippedVertices.length / 2;
const indicesCount = clippedTriangles.length;
const verticesCount = clipper.clippedVerticesLength / 2;
const indicesCount = clipper.clippedTrianglesLength;
if (!cacheData.clippedData) {
cacheData.clippedData = {
@ -749,24 +749,10 @@ export class Spine extends ViewContainer {
}
const { vertices, uvs, indices } = clippedData;
for (let i = 0; i < verticesCount; i++) {
vertices[i * 2] = clippedVertices[i * 2];
vertices[(i * 2) + 1] = clippedVertices[(i * 2) + 1];
uvs[i * 2] = clippedUVs[(i * 2)];
uvs[(i * 2) + 1] = clippedUVs[(i * 2) + 1];
}
vertices.set(clippedVerticesTyped);
uvs.set(clippedUVsTyped);
indices.set(clippedTrianglesTyped);
clippedData.vertexCount = verticesCount;
for (let i = 0; i < indicesCount; i++) {
if (indices[i] !== clippedTriangles[i]) {
this.spineAttachmentsDirty = true;
indices[i] = clippedTriangles[i];
}
}
clippedData.indicesCount = indicesCount;
}