Browse Source

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

Davide Tantillo 4 days ago
parent
commit
c7cf509071
2 changed files with 59 additions and 75 deletions
  1. 7 7
      spine-ts/spine-canvaskit/example/headless.js
  2. 52 68
      spine-ts/spine-canvaskit/src/index.ts

+ 7 - 7
spine-ts/spine-canvaskit/example/headless.js

@@ -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();

+ 52 - 68
spine-ts/spine-canvaskit/src/index.ts

@@ -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,