[ts][canvaskit] Use Uint32Array for colors to avoid canvaskit to allocate one each vertices creation.

This commit is contained in:
Davide Tantillo 2025-07-22 15:42:50 +02:00
parent 532fdb87eb
commit c7cf509071
2 changed files with 59 additions and 75 deletions

View File

@ -1,9 +1,9 @@
import * as fs from "fs"
import { fileURLToPath } from 'url';
import path from 'path';
import CanvasKitInit from "canvaskit-wasm";
import UPNG from "@pdf-lib/upng"
import {loadTextureAtlas, SkeletonRenderer, Skeleton, SkeletonBinary, AnimationState, AnimationStateData, AtlasAttachmentLoader, Physics, loadSkeletonData, SkeletonDrawable} from "../dist/index.js"
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readFileSync, writeFileSync } from "node:fs"
import { loadTextureAtlas, SkeletonRenderer, loadSkeletonData, SkeletonDrawable } from "../dist/index.js"
// Get the current directory
const __filename = fileURLToPath(import.meta.url);
@ -19,10 +19,10 @@ async function main() {
if (!surface) throw new Error();
// Load atlas
const atlas = await loadTextureAtlas(ck, __dirname + "/../../assets/spineboy.atlas", async (path) => fs.readFileSync(path));
const atlas = await loadTextureAtlas(ck, `${__dirname}/../../assets/spineboy.atlas`, async (path) => readFileSync(path));
// Load the skeleton data
const skeletonData = await loadSkeletonData(__dirname + "/../../assets/spineboy-pro.skel", atlas, async (path) => fs.readFileSync(path));
const skeletonData = await loadSkeletonData(`${__dirname}/../../assets/spineboy-pro.skel`, atlas, async (path) => readFileSync(path));
// Create a SkeletonDrawable
const drawable = new SkeletonDrawable(skeletonData);
@ -68,7 +68,7 @@ async function main() {
}
const apng = UPNG.default.encode(frames, 600, 400, 0, frames.map(() => FRAME_TIME * 1000));
fs.writeFileSync('output.png', Buffer.from(apng));
writeFileSync('output.png', Buffer.from(apng));
}
main();

View File

@ -35,7 +35,8 @@ import {
AtlasAttachmentLoader,
BlendMode,
ClippingAttachment,
Color,
type Color,
MathUtils,
MeshAttachment,
type NumberArrayLike,
Physics,
@ -194,9 +195,9 @@ export class SkeletonDrawable {
public readonly skeleton: Skeleton;
public readonly animationState: AnimationState;
/**
* Constructs a new drawble from the skeleton data.
*/
/**
* Constructs a new drawble from the skeleton data.
*/
constructor (skeletonData: SkeletonData) {
this.skeleton = new Skeleton(skeletonData);
this.animationState = new AnimationState(
@ -204,13 +205,13 @@ export class SkeletonDrawable {
);
}
/**
* Updates the animation state and skeleton time by the delta time. Applies the
* animations to the skeleton and calculates the final pose of the skeleton.
*
* @param deltaTime the time since the last update in seconds
* @param physicsUpdate optional {@link Physics} update mode.
*/
/**
* Updates the animation state and skeleton time by the delta time. Applies the
* animations to the skeleton and calculates the final pose of the skeleton.
*
* @param deltaTime the time since the last update in seconds
* @param physicsUpdate optional {@link Physics} update mode.
*/
update (deltaTime: number, physicsUpdate: Physics = Physics.update) {
this.animationState.update(deltaTime);
this.skeleton.update(deltaTime);
@ -224,24 +225,22 @@ export class SkeletonDrawable {
*/
export class SkeletonRenderer {
private clipper = new SkeletonClipping();
private tempColor = new Color();
private tempColor2 = new Color();
private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
private scratchPositions = Utils.newFloatArray(100);
private scratchColors = Utils.newFloatArray(100);
private scratchUVs = Utils.newFloatArray(100);
private scratchColors = new Uint32Array(100 / 4);
/**
* Creates a new skeleton renderer.
* @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`.
*/
/**
* Creates a new skeleton renderer.
* @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`.
*/
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.
*/
/**
* 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;
@ -257,50 +256,40 @@ export class SkeletonRenderer {
const attachment = slot.getAttachment();
let positions = this.scratchPositions;
let colors = this.scratchColors;
let uvs: NumberArrayLike;
let texture: CanvasKitTexture;
let triangles: Array<number>;
let attachmentColor: Color;
let numVertices = 0;
let numVertices = 4;
if (attachment instanceof RegionAttachment) {
const region = attachment;
numVertices = 4;
region.computeWorldVertices(slot, positions, 0, 2);
attachment.computeWorldVertices(slot, positions, 0, 2);
triangles = SkeletonRenderer.QUAD_TRIANGLES;
uvs = region.uvs as Float32Array;
texture = region.region ?.texture as CanvasKitTexture;
attachmentColor = region.color;
} else if (attachment instanceof MeshAttachment) {
const mesh = attachment as MeshAttachment;
if (positions.length < mesh.worldVerticesLength) {
this.scratchPositions = Utils.newFloatArray(mesh.worldVerticesLength);
if (positions.length < attachment.worldVerticesLength) {
this.scratchPositions = Utils.newFloatArray(attachment.worldVerticesLength);
positions = this.scratchPositions;
}
numVertices = mesh.worldVerticesLength >> 1;
mesh.computeWorldVertices(
numVertices = attachment.worldVerticesLength >> 1;
attachment.computeWorldVertices(
slot,
0,
mesh.worldVerticesLength,
attachment.worldVerticesLength,
positions,
0,
2
);
triangles = mesh.triangles;
texture = mesh.region ?.texture as CanvasKitTexture;
uvs = mesh.uvs as Float32Array;
attachmentColor = mesh.color;
triangles = attachment.triangles;
} else if (attachment instanceof ClippingAttachment) {
const clip = attachment as ClippingAttachment;
clipper.clipStart(slot, clip);
clipper.clipStart(slot, attachment);
continue;
} else {
clipper.clipEndWithSlot(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) {
@ -308,21 +297,16 @@ export class SkeletonRenderer {
continue;
}
positions = clipper.clippedVertices;
uvs = clipper.clippedUVs;
scaledUvs = clipper.clippedUVs;
uvs = scaledUvs = clipper.clippedUVs;
triangles = clipper.clippedTriangles;
numVertices = clipper.clippedVertices.length / 2;
colors = Utils.newFloatArray(numVertices * 4);
colors = new Uint32Array(numVertices);
} else {
scaledUvs = this.scratchUVs;
if (this.scratchUVs.length < uvs.length) {
this.scratchUVs = Utils.newFloatArray(uvs.length);
scaledUvs = this.scratchUVs;
}
if (colors.length / 4 < numVertices) {
this.scratchColors = Utils.newFloatArray(numVertices * 4);
colors = this.scratchColors;
}
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();
@ -334,19 +318,19 @@ export class SkeletonRenderer {
scaledUvs[i + 1] = uvs[i + 1] * height;
}
const attachmentColor = attachment.color;
const slotColor = slot.color;
const finalColor = this.tempColor;
finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r;
finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g;
finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b;
finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a;
for (let i = 0, n = numVertices * 4; i < n; i += 4) {
colors[i] = finalColor.r;
colors[i + 1] = finalColor.g;
colors[i + 2] = finalColor.b;
colors[i + 3] = finalColor.a;
}
// 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,