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 tx = 0;
public ty = 0; public ty = 0;
public prevX = Infinity;
public prevY = Infinity;
public prevAngle = Infinity;
private tempPoint = new Vector2(); private tempPoint = new Vector2();
public update (x: number, y: number, angle: number) { 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 cos = Math.cos(angle);
const sin = Math.sin(angle); const sin = Math.sin(angle);
this.a = cos; this.a = cos;
@ -49,6 +57,7 @@ export class C3Matrix {
this.d = cos; this.d = cos;
this.tx = x; this.tx = x;
this.ty = y; this.ty = y;
return true;
} }
public gameToSkeleton (x: number, y: number) { public gameToSkeleton (x: number, y: number) {

View File

@ -47,6 +47,14 @@ abstract class C3SkeletonRenderer<
private tempArray = [] as number[]; private tempArray = [] as number[];
private inv255 = 1 / 255; 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 ( constructor (
protected renderer: Renderer, protected renderer: Renderer,
protected matrix: C3Matrix, protected matrix: C3Matrix,
@ -54,45 +62,51 @@ abstract class C3SkeletonRenderer<
super(); 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 { matrix, inv255 } = this;
const { a, b, c, d, tx, ty, prevX, prevY, prevAngle } = matrix;
this.command = (isPlaying || !this.command) const requestRedrawForMatrix = this.prevX !== prevX || this.prevY !== prevY || this.prevAngle !== prevAngle;
? this.render(skeleton, true, [...inColors, opacity]) this.prevX = prevX;
: this.command; 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; let command = this.command;
const { a, b, c, d, tx, ty } = matrix;
while (command) { while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = 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 const c3colors = this.tempColors.length < numVertices * 4
? (this.tempColors = new Float32Array(numVertices * 4)) ? (this.tempColors = new Float32Array(numVertices * 4))
: this.tempColors; : this.tempColors;
for (let i = 0; i < numVertices; i++) { if (newCommand) {
const srcIndex = i * 2; for (let i = 0; i < numVertices; i++) {
const dstIndex = i * 3; const index = i * 3;
const x = positions[srcIndex]; const x = positions[index];
const y = positions[srcIndex + 1]; const y = positions[index + 1];
vertices[dstIndex] = a * x + c * y + tx; positions[index] = a * x + c * y + tx;
vertices[dstIndex + 1] = b * x + d * y + ty; positions[index + 1] = b * x + d * y + ty;
vertices[dstIndex + 2] = 0;
const color = colors[i]; const color = colors[i];
const colorDst = i * 4; const colorDst = i * 4;
c3colors[colorDst] = (color >>> 16 & 0xFF) * inv255; c3colors[colorDst] = (color >>> 16 & 0xFF) * inv255;
c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * inv255; c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * inv255;
c3colors[colorDst + 2] = (color & 0xFF) * inv255; c3colors[colorDst + 2] = (color & 0xFF) * inv255;
c3colors[colorDst + 3] = (color >>> 24) * inv255; c3colors[colorDst + 3] = (color >>> 24) * inv255;
}
} }
this.renderSkeleton( this.renderSkeleton(
vertices.subarray(0, numVertices * 3), positions.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2), uvs.subarray(0, numVertices * 2),
indices.subarray(0, numIndices), indices.subarray(0, numIndices),
c3colors.subarray(0, numVertices * 4), c3colors.subarray(0, numVertices * 4),

View File

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

View File

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

View File

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