Do not reuse the same command if position or color change.

This commit is contained in:
Davide Tantillo 2025-12-10 17:10:45 +01:00
parent 4e2d01d67a
commit 829c3c3e6a
5 changed files with 110 additions and 81 deletions

View File

@ -38,9 +38,17 @@ export class C3Matrix {
public tx = 0;
public ty = 0;
public prevX = Infinity;
public prevY = Infinity;
public prevAngle = Infinity;
private tempPoint = new Vector2();
public update (x: number, y: number, angle: number) {
if (this.prevX === x && this.prevY === y && this.prevAngle === angle) return false;
this.prevX = x;
this.prevY = y;
this.prevAngle = angle;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
this.a = cos;
@ -49,6 +57,7 @@ export class C3Matrix {
this.d = cos;
this.tx = x;
this.ty = y;
return true;
}
public gameToSkeleton (x: number, y: number) {

View File

@ -47,6 +47,14 @@ abstract class C3SkeletonRenderer<
private tempArray = [] as number[];
private inv255 = 1 / 255;
private prevX = Infinity;
private prevY = Infinity;
private prevAngle = Infinity;
private prevRed = -1;
private prevGreen = -1;
private prevBlue = -1;
private prevAlpha = -1;
constructor (
protected renderer: Renderer,
protected matrix: C3Matrix,
@ -54,34 +62,39 @@ abstract class C3SkeletonRenderer<
super();
}
draw (skeleton: Skeleton, inColors: [number, number, number], opacity = 1, isPlaying = true, fromUpdate = true) {
draw (skeleton: Skeleton, inColors: [number, number, number], opacity = 1, requestRedraw = true) {
const { matrix, inv255 } = this;
const { a, b, c, d, tx, ty, prevX, prevY, prevAngle } = matrix;
this.command = (isPlaying || !this.command)
? this.render(skeleton, true, [...inColors, opacity])
: this.command;
const requestRedrawForMatrix = this.prevX !== prevX || this.prevY !== prevY || this.prevAngle !== prevAngle;
this.prevX = prevX;
this.prevY = prevY;
this.prevAngle = prevAngle;
const requestRedrawForColor = this.prevRed !== inColors[0] || this.prevGreen !== inColors[1] || this.prevBlue !== inColors[2] || this.prevAlpha !== opacity;
this.prevRed = inColors[0];
this.prevGreen = inColors[1];
this.prevBlue = inColors[2];
this.prevAlpha = opacity;
const newCommand = (requestRedraw || requestRedrawForColor || requestRedrawForMatrix || !this.command);
this.command = newCommand ? this.render(skeleton, true, [...inColors, opacity], 3) : this.command;
let command = this.command;
const { a, b, c, d, tx, ty } = matrix;
while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
const vertices = this.tempVertices.length < numVertices * 3
? (this.tempVertices = new Float32Array(numVertices * 3))
: this.tempVertices;
const c3colors = this.tempColors.length < numVertices * 4
? (this.tempColors = new Float32Array(numVertices * 4))
: this.tempColors;
if (newCommand) {
for (let i = 0; i < numVertices; i++) {
const srcIndex = i * 2;
const dstIndex = i * 3;
const x = positions[srcIndex];
const y = positions[srcIndex + 1];
vertices[dstIndex] = a * x + c * y + tx;
vertices[dstIndex + 1] = b * x + d * y + ty;
vertices[dstIndex + 2] = 0;
const index = i * 3;
const x = positions[index];
const y = positions[index + 1];
positions[index] = a * x + c * y + tx;
positions[index + 1] = b * x + d * y + ty;
const color = colors[i];
const colorDst = i * 4;
@ -90,9 +103,10 @@ abstract class C3SkeletonRenderer<
c3colors[colorDst + 2] = (color & 0xFF) * inv255;
c3colors[colorDst + 3] = (color >>> 24) * inv255;
}
}
this.renderSkeleton(
vertices.subarray(0, numVertices * 3),
positions.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2),
indices.subarray(0, numIndices),
c3colors.subarray(0, numVertices * 4),

View File

@ -41,6 +41,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
private assetLoader: AssetLoader;
private skeletonRenderer?: C3RendererRuntime;
private matrix: C3Matrix;
private requestRedraw = false;
private verticesTemp = spine.Utils.newFloatArray(2 * 1024);
@ -116,13 +117,8 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
this.y + this.propOffsetY,
this.angle + this.propOffsetAngle);
if (this.isPlaying) {
this.update(this.dt);
this.runtime.sdk.updateRender();
if (this.isPlaying) this.update(this.dt);
}
}
private fromUpdate = false;
private update (delta: number) {
const { state, skeleton, animationSpeed, physicsMode, matrix } = this;
@ -140,7 +136,8 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
this.updateBoneFollowers(matrix);
this.fromUpdate = true;
this.runtime.sdk.updateRender();
this.requestRedraw = true;
}
_draw (renderer: IRenderer) {
@ -153,8 +150,8 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
if (!skeleton) return;
this.skeletonRenderer ||= new spine.C3RendererRuntime(renderer, this.matrix);
this.skeletonRenderer.draw(skeleton, this.colorRgb, this.opacity, this.isPlaying, this.fromUpdate);
this.fromUpdate = false;
this.skeletonRenderer.draw(skeleton, this.colorRgb, this.opacity, this.requestRedraw);
this.requestRedraw = false;
if (this.propDebugSkeleton) this.skeletonRenderer.drawDebug(skeleton, this.x, this.y, this.getBoundingQuad(false));
this.renderDragHandles();
@ -410,7 +407,6 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
this.skeleton.scaleY = this.propScaleY;
this.update(0);
this.runtime.sdk.updateRender();
this.skeletonLoaded = true;
this._trigger(C3.Plugins.EsotericSoftware_SpineConstruct3.Cnds.OnSkeletonLoaded);

View File

@ -324,7 +324,7 @@ export class SkeletonClipping {
return clipOutputItems != null;
}
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike | Uint32Array, trianglesLength: number, uvs: NumberArrayLike) {
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike | Uint32Array, trianglesLength: number, uvs: NumberArrayLike, stride = 2) {
const clipOutput = this.clipOutput;
let clippedVertices = this._clippedVerticesTyped, clippedUVs = this._clippedUVsTyped, clippedTriangles = this._clippedTrianglesTyped;
// biome-ignore lint/style/noNonNullAssertion: clipStart define it
@ -343,17 +343,23 @@ export class SkeletonClipping {
let clipped = false;
for (let i = 0; i < trianglesLength; i += 3) {
let v = triangles[i] << 1;
let t = triangles[i];
let v = t * stride;
const x1 = vertices[v], y1 = vertices[v + 1];
const u1 = uvs[v], v1 = uvs[v + 1];
let uv = t << 1;
const u1 = uvs[uv], v1 = uvs[uv + 1];
v = triangles[i + 1] << 1;
t = triangles[i + 1];
v = t * stride;
const x2 = vertices[v], y2 = vertices[v + 1];
const u2 = uvs[v], v2 = uvs[v + 1];
uv = t << 1;
const u2 = uvs[uv], v2 = uvs[uv + 1];
v = triangles[i + 2] << 1;
t = triangles[i + 2];
v = t * stride;
const x3 = vertices[v], y3 = vertices[v + 1];
const u3 = uvs[v], v3 = uvs[v + 1];
uv = t << 1;
const u3 = uvs[uv], v3 = uvs[uv + 1];
for (let p = 0; p < polygonsCount; p++) {
let s = this.clippedVerticesLength;
@ -367,20 +373,22 @@ export class SkeletonClipping {
let clipOutputCount = clipOutputLength >> 1;
const clipOutputItems = this.clipOutput;
const newLength = s + clipOutputCount * 2;
const newLength = s + clipOutputCount * stride;
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));
this._clippedUVsTyped = new Float32Array((this.clippedUVsLength + clipOutputCount * 2) * 2);
this._clippedUVsTyped.set(clippedUVs.subarray(0, this.clippedUVsLength));
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 uvIndex = this.clippedUVsLength;
this.clippedUVsLength = uvIndex + clipOutputCount * 2;
for (let ii = 0; ii < clipOutputLength; ii += 2, s += stride, uvIndex += 2) {
const x = clipOutputItems[ii], y = clipOutputItems[ii + 1];
clippedVerticesItems[s] = x;
clippedVerticesItems[s + 1] = y;
@ -388,8 +396,8 @@ export class SkeletonClipping {
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;
clippedUVsItems[uvIndex] = u1 * a + u2 * b + u3 * c;
clippedUVsItems[uvIndex + 1] = v1 * a + v2 * b + v3 * c;
}
s = this.clippedTrianglesLength;
@ -411,7 +419,7 @@ export class SkeletonClipping {
} else {
let newLength = s + 3 * 2;
let newLength = s + 3 * stride;
if (clippedVertices.length < newLength) {
this._clippedVerticesTyped = new Float32Array(newLength * 2);
this._clippedVerticesTyped.set(clippedVertices.subarray(0, s));
@ -419,25 +427,27 @@ export class SkeletonClipping {
}
clippedVertices[s] = x1;
clippedVertices[s + 1] = y1;
clippedVertices[s + 2] = x2;
clippedVertices[s + 3] = y2;
clippedVertices[s + 4] = x3;
clippedVertices[s + 5] = y3;
clippedVertices[s + stride] = x2;
clippedVertices[s + stride + 1] = y2;
clippedVertices[s + stride * 2] = x3;
clippedVertices[s + stride * 2 + 1] = y3;
if (clippedUVs.length < newLength) {
this._clippedUVsTyped = new Float32Array(newLength * 2);
this._clippedUVsTyped.set(clippedUVs.subarray(0, s));
let uvLength = this.clippedUVsLength + 3 * 2;
if (clippedUVs.length < uvLength) {
this._clippedUVsTyped = new Float32Array(uvLength * 2);
this._clippedUVsTyped.set(clippedUVs.subarray(0, this.clippedUVsLength));
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;
let uvIndex = this.clippedUVsLength;
clippedUVs[uvIndex] = u1;
clippedUVs[uvIndex + 1] = v1;
clippedUVs[uvIndex + 2] = u2;
clippedUVs[uvIndex + 3] = v2;
clippedUVs[uvIndex + 4] = u3;
clippedUVs[uvIndex + 5] = v3;
this.clippedVerticesLength = newLength;
this.clippedUVsLength = newLength;
this.clippedUVsLength = uvLength;
s = this.clippedTrianglesLength;
newLength = s + 3;

View File

@ -40,7 +40,7 @@ export class SkeletonRendererCore {
private clipping = new SkeletonClipping();
private renderCommands: RenderCommand[] = [];
render (skeleton: Skeleton, pma = false, inColor?: [number, number, number, number]): RenderCommand | undefined {
render (skeleton: Skeleton, pma = false, inColor?: [number, number, number, number], stride = 2): RenderCommand | undefined {
this.commandPool.reset();
this.renderCommands.length = 0;
@ -80,7 +80,7 @@ export class SkeletonRendererCore {
continue;
}
attachment.computeWorldVertices(slot, this.worldVertices, 0, 2);
attachment.computeWorldVertices(slot, this.worldVertices, 0, stride);
vertices = this.worldVertices;
verticesCount = 4;
uvs = attachment.uvs as Float32Array;
@ -99,7 +99,7 @@ export class SkeletonRendererCore {
if (this.worldVertices.length < attachment.worldVerticesLength)
this.worldVertices = new Float32Array(attachment.worldVerticesLength);
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, this.worldVertices, 0, 2);
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, this.worldVertices, 0, stride);
vertices = this.worldVertices;
verticesCount = attachment.worldVerticesLength >> 1;
uvs = attachment.uvs as Float32Array;
@ -163,19 +163,19 @@ export class SkeletonRendererCore {
}
if (clipper.isClipping()) {
clipper.clipTrianglesUnpacked(vertices, indices, indicesCount, uvs);
clipper.clipTrianglesUnpacked(vertices, indices, indicesCount, uvs, stride);
vertices = clipper.clippedVerticesTyped;
verticesCount = clipper.clippedVerticesLength >> 1;
verticesCount = clipper.clippedVerticesLength / stride;
uvs = clipper.clippedUVsTyped;
indices = clipper.clippedTrianglesTyped;
indicesCount = clipper.clippedTrianglesLength;
}
const cmd = this.commandPool.getCommand(verticesCount, indicesCount);
const cmd = this.commandPool.getCommand(verticesCount, indicesCount, stride);
cmd.blendMode = slot.data.blendMode;
cmd.texture = texture;
cmd.positions.set(vertices.subarray(0, verticesCount << 1));
cmd.positions.set(vertices.subarray(0, verticesCount * stride));
cmd.uvs.set(uvs.subarray(0, verticesCount << 1));
for (let j = 0; j < verticesCount; j++) {
@ -194,14 +194,14 @@ export class SkeletonRendererCore {
}
clipper.clipEnd();
return this.batchCommands();
return this.batchCommands(stride);
}
private batchSubCommands (commands: RenderCommand[], first: number, last: number,
numVertices: number, numIndices: number): RenderCommand {
numVertices: number, numIndices: number, stride: number): RenderCommand {
const firstCmd = commands[first];
const batched = this.commandPool.getCommand(numVertices, numIndices);
const batched = this.commandPool.getCommand(numVertices, numIndices, stride);
batched.blendMode = firstCmd.blendMode;
batched.texture = firstCmd.texture;
@ -216,7 +216,7 @@ export class SkeletonRendererCore {
const cmd = commands[i];
batched.positions.set(cmd.positions, positionsOffset);
positionsOffset += cmd.numVertices << 1;
positionsOffset += cmd.numVertices * stride;
batched.uvs.set(cmd.uvs, uvsOffset);
uvsOffset += cmd.numVertices << 1;
@ -236,7 +236,7 @@ export class SkeletonRendererCore {
return batched;
}
private batchCommands (): RenderCommand | undefined {
private batchCommands (stride: number): RenderCommand | undefined {
if (this.renderCommands.length === 0) return undefined;
let root: RenderCommand | undefined;
@ -267,7 +267,7 @@ export class SkeletonRendererCore {
numIndices += cmd.numIndices;
} else {
const batched = this.batchSubCommands(this.renderCommands, startIndex, i - 1,
numVertices, numIndices);
numVertices, numIndices, stride);
if (!last) {
root = last = batched;
@ -315,17 +315,17 @@ class CommandPool {
private pool: RenderCommand[] = [];
private inUse: RenderCommand[] = [];
getCommand (numVertices: number, numIndices: number): RenderCommand {
getCommand (numVertices: number, numIndices: number, stride: number): RenderCommand {
let cmd: RenderCommand | undefined;
for (const c of this.pool) {
if (c._positions.length >= numVertices << 1 && c._indices.length >= numIndices) {
if (c._positions.length >= numVertices * stride && c._indices.length >= numIndices) {
cmd = c;
break;
}
}
if (!cmd) {
const _positions = new Float32Array(numVertices << 1);
const _positions = new Float32Array(numVertices * stride);
const _uvs = new Float32Array(numVertices << 1);
const _colors = new Uint32Array(numVertices);
const _darkColors = new Uint32Array(numVertices);
@ -352,8 +352,8 @@ class CommandPool {
cmd.numVertices = numVertices;
cmd.numIndices = numIndices;
cmd.positions = cmd._positions.subarray(0, numVertices << 1);
cmd.uvs = cmd._uvs.subarray(0, numVertices * 2);
cmd.positions = cmd._positions.subarray(0, numVertices * stride);
cmd.uvs = cmd._uvs.subarray(0, numVertices << 1);
cmd.colors = cmd._colors.subarray(0, numVertices);
cmd.darkColors = cmd._darkColors.subarray(0, numVertices);
cmd.indices = cmd._indices.subarray(0, numIndices);