[ts] Add SkeletonRendererCore (missing file).

This commit is contained in:
Davide Tantillo 2025-08-12 10:34:02 +02:00
parent 3fa97353f1
commit 7c28c3c1c7

View File

@ -0,0 +1,333 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { ClippingAttachment, MeshAttachment, RegionAttachment } from "./attachments";
import type { Skeleton } from "./Skeleton";
import { SkeletonClipping } from "./SkeletonClipping";
import { BlendMode } from "./SlotData";
import type { Color, NumberArrayLike } from "./Utils";
export class SkeletonRendererCore {
private commandPool = new CommandPool();
private worldVertices = new Float32Array(12 * 1024);
private quadIndices = new Uint32Array([0, 1, 2, 2, 3, 0]);
private clipping = new SkeletonClipping();
private renderCommands: RenderCommand[] = [];
render (skeleton: Skeleton): RenderCommand | undefined {
this.commandPool.reset();
this.renderCommands.length = 0;
const clipper = this.clipping;
for (let i = 0; i < skeleton.slots.length; i++) {
const slot = skeleton.drawOrder[i];
const attachment = slot.applied.attachment;
if (!attachment) {
clipper.clipEnd(slot);
continue;
}
const slotApplied = slot.applied;
const color = slotApplied.color;
const alpha = color.a;
if ((alpha === 0 || !slot.bone.active) && !(attachment instanceof ClippingAttachment)) {
clipper.clipEnd(slot);
continue;
}
let vertices: NumberArrayLike;
let verticesCount: number;
let uvs: NumberArrayLike;
let indices: number[] | Uint32Array;
let indicesCount: number;
let attachmentColor: Color;
let texture: any;
if (attachment instanceof RegionAttachment) {
attachmentColor = attachment.color;
if (attachmentColor.a === 0) {
clipper.clipEnd(slot);
continue;
}
attachment.computeWorldVertices(slot, this.worldVertices, 0, 2);
vertices = this.worldVertices;
verticesCount = 4;
uvs = attachment.uvs as Float32Array;
indices = this.quadIndices;
indicesCount = 6;
texture = attachment.region?.texture;
} else if (attachment instanceof MeshAttachment) {
attachmentColor = attachment.color;
if (attachmentColor.a === 0) {
clipper.clipEnd(slot);
continue;
}
if (this.worldVertices.length < attachment.worldVerticesLength)
this.worldVertices = new Float32Array(attachment.worldVerticesLength);
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, this.worldVertices, 0, 2);
vertices = this.worldVertices;
verticesCount = attachment.worldVerticesLength >> 1;
uvs = attachment.uvs as Float32Array;
indices = attachment.triangles;
indicesCount = indices.length;
texture = attachment.region?.texture;
} else if (attachment instanceof ClippingAttachment) {
clipper.clipStart(skeleton, slot, attachment);
continue;
} else {
continue;
}
const skelColor = skeleton.color;
const r = Math.floor(skelColor.r * slotApplied.color.r * attachmentColor.r * 255);
const g = Math.floor(skelColor.g * slotApplied.color.g * attachmentColor.g * 255);
const b = Math.floor(skelColor.b * slotApplied.color.b * attachmentColor.b * 255);
const a = Math.floor(skelColor.a * slotApplied.color.a * attachmentColor.a * 255);
let darkColor = 0xff000000;
if (slotApplied.darkColor) {
const { r, g, b } = slotApplied.darkColor;
darkColor = 0xff000000 |
(Math.floor(r * 255) << 16) |
(Math.floor(g * 255) << 8) |
Math.floor(b * 255);
}
if (clipper.isClipping()) {
clipper.clipTrianglesUnpacked(vertices, indices, indicesCount, uvs);
vertices = clipper.clippedVerticesTyped;
verticesCount = clipper.clippedVerticesLength >> 1;
uvs = clipper.clippedUVsTyped;
indices = clipper.clippedTrianglesTyped;
indicesCount = clipper.clippedTrianglesLength;
}
const cmd = this.commandPool.getCommand(verticesCount, indicesCount);
cmd.blendMode = slot.data.blendMode;
cmd.texture = texture;
cmd.positions.set(vertices.subarray(0, verticesCount << 1));
cmd.uvs.set(uvs.subarray(0, verticesCount << 1));
for (let j = 0; j < verticesCount; j++) {
cmd.colors[j] = (a << 24) | (r << 16) | (g << 8) | b;
cmd.darkColors[j] = darkColor;
}
if (indices instanceof Uint16Array) {
cmd.indices.set(indices.subarray(0, indicesCount));
} else {
cmd.indices.set(indices.slice(0, indicesCount));
}
this.renderCommands.push(cmd);
clipper.clipEnd(slot);
}
clipper.clipEnd();
return this.batchCommands();
}
private batchSubCommands (commands: RenderCommand[], first: number, last: number,
numVertices: number, numIndices: number): RenderCommand {
const firstCmd = commands[first];
const batched = this.commandPool.getCommand(numVertices, numIndices);
batched.blendMode = firstCmd.blendMode;
batched.texture = firstCmd.texture;
let positionsOffset = 0;
let uvsOffset = 0;
let colorsOffset = 0;
let indicesOffset = 0;
let vertexOffset = 0;
for (let i = first; i <= last; i++) {
const cmd = commands[i];
batched.positions.set(cmd.positions, positionsOffset);
positionsOffset += cmd.numVertices << 1;
batched.uvs.set(cmd.uvs, uvsOffset);
uvsOffset += cmd.numVertices << 1;
batched.colors.set(cmd.colors, colorsOffset);
batched.darkColors.set(cmd.darkColors, colorsOffset);
colorsOffset += cmd.numVertices;
// cannot fast copy - indices need vertex offset adjustment
for (let j = 0; j < cmd.numIndices; j++)
batched.indices[indicesOffset + j] = cmd.indices[j] + vertexOffset;
indicesOffset += cmd.numIndices;
vertexOffset += cmd.numVertices;
}
return batched;
}
private batchCommands (): RenderCommand | undefined {
if (this.renderCommands.length === 0) return undefined;
let root: RenderCommand | undefined;
let last: RenderCommand | undefined;
let first = this.renderCommands[0];
let startIndex = 0;
let i = 1;
let numVertices = first.numVertices;
let numIndices = first.numIndices;
while (i <= this.renderCommands.length) {
const cmd = i < this.renderCommands.length ? this.renderCommands[i] : null;
if (cmd && cmd.numVertices === 0 && cmd.numIndices === 0) {
i++;
continue;
}
const canBatch = cmd !== null &&
cmd.texture === first.texture &&
cmd.blendMode === first.blendMode &&
cmd.colors[0] === first.colors[0] &&
cmd.darkColors[0] === first.darkColors[0] &&
numIndices + cmd.numIndices < 0xffff;
if (canBatch) {
numVertices += cmd.numVertices;
numIndices += cmd.numIndices;
} else {
const batched = this.batchSubCommands(this.renderCommands, startIndex, i - 1,
numVertices, numIndices);
if (!last) {
root = last = batched;
} else {
last.next = batched;
last = batched;
}
if (i === this.renderCommands.length) break;
first = this.renderCommands[i];
startIndex = i;
numVertices = first.numVertices;
numIndices = first.numIndices;
}
i++;
}
return root;
}
}
interface RenderCommand {
positions: Float32Array;
uvs: Float32Array;
colors: Uint32Array;
darkColors: Uint32Array;
indices: Uint16Array;
_positions: Float32Array;
_uvs: Float32Array;
_colors: Uint32Array;
_darkColors: Uint32Array;
_indices: Uint16Array;
numVertices: number;
numIndices: number;
blendMode: BlendMode;
texture: any;
next?: RenderCommand;
}
class CommandPool {
private pool: RenderCommand[] = [];
private inUse: RenderCommand[] = [];
getCommand (numVertices: number, numIndices: number): RenderCommand {
let cmd: RenderCommand | undefined;
for (const c of this.pool) {
if (c._positions.length >= numVertices << 1 && c._indices.length >= numIndices) {
cmd = c;
break;
}
}
if (!cmd) {
const _positions = new Float32Array(numVertices << 1);
const _uvs = new Float32Array(numVertices << 1);
const _colors = new Uint32Array(numVertices);
const _darkColors = new Uint32Array(numVertices);
const _indices = new Uint16Array(numIndices);
cmd = {
positions: _positions,
uvs: _uvs,
colors: _colors,
darkColors: _darkColors,
indices: _indices,
_positions,
_uvs,
_colors,
_darkColors,
_indices,
numVertices,
numIndices,
blendMode: BlendMode.Normal,
texture: null
};
} else {
this.pool.splice(this.pool.indexOf(cmd), 1);
cmd.next = undefined;
cmd.numVertices = numVertices;
cmd.numIndices = numIndices;
cmd.positions = cmd._positions.subarray(0, numVertices << 1);
cmd.uvs = cmd._uvs.subarray(0, numVertices * 2);
cmd.colors = cmd._colors.subarray(0, numVertices);
cmd.darkColors = cmd._darkColors.subarray(0, numVertices);
cmd.indices = cmd._indices.subarray(0, numIndices);
}
this.inUse.push(cmd);
return cmd;
}
reset (): void {
this.pool.push(...this.inUse);
this.inUse.length = 0;
}
}