[canvaskit] Fix JSON loading.

This commit is contained in:
Mario Zechner 2024-07-09 14:22:11 +02:00
parent 1b1e358f85
commit e9bcaa7a9b
5 changed files with 253 additions and 155 deletions

View File

@ -34,7 +34,6 @@
canvasElement.width = canvasElement.clientWidth * dpr; canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr; canvasElement.height = canvasElement.clientHeight * dpr;
// Initialize CanvasKit and create a surface from the Canvas element to draw to // Initialize CanvasKit and create a surface from the Canvas element to draw to
const ck = await CanvasKitInit(); const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo'); const surface = ck.MakeCanvasSurface('foo');

View File

@ -1,103 +1,170 @@
export * from "@esotericsoftware/spine-core"; export * from "@esotericsoftware/spine-core";
import { AnimationState, AnimationStateData, AtlasAttachmentLoader, BlendMode, ClippingAttachment, Color, MeshAttachment, NumberArrayLike, Physics, RegionAttachment, Skeleton, SkeletonBinary, SkeletonClipping, SkeletonData, SkeletonJson, Texture, TextureAtlas, TextureFilter, TextureWrap, Utils } from "@esotericsoftware/spine-core"; import {
import { Canvas, Surface, CanvasKit, Image, Paint, Shader, BlendMode as CanvasKitBlendMode } from "canvaskit-wasm"; AnimationState,
AnimationStateData,
AtlasAttachmentLoader,
BlendMode,
ClippingAttachment,
Color,
MeshAttachment,
NumberArrayLike,
Physics,
RegionAttachment,
Skeleton,
SkeletonBinary,
SkeletonClipping,
SkeletonData,
SkeletonJson,
Texture,
TextureAtlas,
TextureFilter,
TextureWrap,
Utils,
} from "@esotericsoftware/spine-core";
import {
Canvas,
Surface,
CanvasKit,
Image,
Paint,
Shader,
BlendMode as CanvasKitBlendMode,
} from "canvaskit-wasm";
Skeleton.yDown = true; Skeleton.yDown = true;
type CanvasKitImage = { shaders: Shader[], paintPerBlendMode: Map<BlendMode, Paint>, image: Image }; type CanvasKitImage = {
shaders: Shader[];
paintPerBlendMode: Map<BlendMode, Paint>;
image: Image;
};
// CanvasKit blend modes for premultiplied alpha // CanvasKit blend modes for premultiplied alpha
function toCkBlendMode(ck: CanvasKit, blendMode: BlendMode) { function toCkBlendMode (ck: CanvasKit, blendMode: BlendMode) {
switch(blendMode) { switch (blendMode) {
case BlendMode.Normal: return ck.BlendMode.SrcOver; case BlendMode.Normal:
case BlendMode.Additive: return ck.BlendMode.Plus; return ck.BlendMode.SrcOver;
case BlendMode.Multiply: return ck.BlendMode.SrcOver; case BlendMode.Additive:
case BlendMode.Screen: return ck.BlendMode.Screen; return ck.BlendMode.Plus;
default: return ck.BlendMode.SrcOver; case BlendMode.Multiply:
} return ck.BlendMode.SrcOver;
case BlendMode.Screen:
return ck.BlendMode.Screen;
default:
return ck.BlendMode.SrcOver;
}
} }
function bufferToUtf8String(buffer: any) { function bufferToUtf8String (buffer: any) {
if (typeof Buffer !== 'undefined') { if (typeof Buffer !== "undefined") {
return buffer.toString('utf-8'); return buffer.toString("utf-8");
} else if (typeof TextDecoder !== 'undefined') { } else if (typeof TextDecoder !== "undefined") {
return new TextDecoder('utf-8').decode(buffer); return new TextDecoder("utf-8").decode(buffer);
} else { } else {
throw new Error('Unsupported environment'); throw new Error("Unsupported environment");
} }
} }
class CanvasKitTexture extends Texture { class CanvasKitTexture extends Texture {
getImage(): CanvasKitImage { getImage (): CanvasKitImage {
return this._image; return this._image;
} }
setFilters(minFilter: TextureFilter, magFilter: TextureFilter): void { setFilters (minFilter: TextureFilter, magFilter: TextureFilter): void { }
}
setWraps(uWrap: TextureWrap, vWrap: TextureWrap): void { setWraps (uWrap: TextureWrap, vWrap: TextureWrap): void { }
}
dispose(): void { dispose (): void {
const data: CanvasKitImage = this._image; const data: CanvasKitImage = this._image;
for (const paint of data.paintPerBlendMode.values()) { for (const paint of data.paintPerBlendMode.values()) {
paint.delete(); paint.delete();
} }
for (const shader of data.shaders) { for (const shader of data.shaders) {
shader.delete(); shader.delete();
} }
data.image.delete(); data.image.delete();
this._image = null; this._image = null;
} }
static async fromFile(ck: CanvasKit, path: string, readFile: (path: string) => Promise<any>): Promise<CanvasKitTexture> { static async fromFile (
const imgData = await readFile(path); ck: CanvasKit,
if (!imgData) throw new Error(`Could not load image ${path}`); path: string,
const image = ck.MakeImageFromEncoded(imgData); readFile: (path: string) => Promise<any>
if (!image) throw new Error(`Could not load image ${path}`); ): Promise<CanvasKitTexture> {
const paintPerBlendMode = new Map<BlendMode, Paint>(); const imgData = await readFile(path);
const shaders: Shader[] = []; if (!imgData) throw new Error(`Could not load image ${path}`);
for (const blendMode of [BlendMode.Normal, BlendMode.Additive, BlendMode.Multiply, BlendMode.Screen]) { const image = ck.MakeImageFromEncoded(imgData);
const paint = new ck.Paint(); if (!image) throw new Error(`Could not load image ${path}`);
const shader = image.makeShaderOptions(ck.TileMode.Clamp, ck.TileMode.Clamp, ck.FilterMode.Linear, ck.MipmapMode.Linear); const paintPerBlendMode = new Map<BlendMode, Paint>();
paint.setShader(shader); const shaders: Shader[] = [];
paint.setBlendMode(toCkBlendMode(ck, blendMode)); for (const blendMode of [
paintPerBlendMode.set(blendMode, paint); BlendMode.Normal,
shaders.push(shader); BlendMode.Additive,
} BlendMode.Multiply,
return new CanvasKitTexture({ shaders, paintPerBlendMode, image }); BlendMode.Screen,
} ]) {
const paint = new ck.Paint();
const shader = image.makeShaderOptions(
ck.TileMode.Clamp,
ck.TileMode.Clamp,
ck.FilterMode.Linear,
ck.MipmapMode.Linear
);
paint.setShader(shader);
paint.setBlendMode(toCkBlendMode(ck, blendMode));
paintPerBlendMode.set(blendMode, paint);
shaders.push(shader);
}
return new CanvasKitTexture({ shaders, paintPerBlendMode, image });
}
} }
/** /**
* Loads a {@link TextureAtlas} and its atlas page images from the given file path using the `readFile(path: string): Promise<Buffer>` function. * Loads a {@link TextureAtlas} and its atlas page images from the given file path using the `readFile(path: string): Promise<Buffer>` function.
* Throws an `Error` if the file or one of the atlas page images could not be loaded. * Throws an `Error` if the file or one of the atlas page images could not be loaded.
*/ */
export async function loadTextureAtlas(ck: CanvasKit, atlasFile: string, readFile: (path: string) => Promise<Buffer>): Promise<TextureAtlas> { export async function loadTextureAtlas (
const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile))); ck: CanvasKit,
const slashIndex = atlasFile.lastIndexOf("/"); atlasFile: string,
const parentDir = slashIndex >= 0 ? atlasFile.substring(0, slashIndex + 1) + "/" : ""; readFile: (path: string) => Promise<Buffer>
for (const page of atlas.pages) { ): Promise<TextureAtlas> {
const texture = await CanvasKitTexture.fromFile(ck, parentDir + page.name, readFile); const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile)));
page.setTexture(texture); const slashIndex = atlasFile.lastIndexOf("/");
} const parentDir =
return atlas; slashIndex >= 0 ? atlasFile.substring(0, slashIndex + 1) + "/" : "";
for (const page of atlas.pages) {
const texture = await CanvasKitTexture.fromFile(
ck,
parentDir + page.name,
readFile
);
page.setTexture(texture);
}
return atlas;
} }
/** /**
* Loads a {@link SkeletonData} from the given file path (`.json` or `.skel`) using the `readFile(path: string): Promise<Buffer>` function. * Loads a {@link SkeletonData} from the given file path (`.json` or `.skel`) using the `readFile(path: string): Promise<Buffer>` function.
* Attachments will be looked up in the provided atlas. * Attachments will be looked up in the provided atlas.
*/ */
export async function loadSkeletonData(skeletonFile: string, atlas: TextureAtlas, readFile: (path: string) => Promise<Buffer>): Promise<SkeletonData> { export async function loadSkeletonData (
const attachmentLoader = new AtlasAttachmentLoader(atlas); skeletonFile: string,
const loader = skeletonFile.endsWith(".json") ? new SkeletonJson(attachmentLoader) : new SkeletonBinary(attachmentLoader); atlas: TextureAtlas,
let data = await readFile(skeletonFile); readFile: (path: string) => Promise<Buffer>,
if (skeletonFile.endsWith(".json")) { scale = 1
data = bufferToUtf8String(data); ): Promise<SkeletonData> {
} const attachmentLoader = new AtlasAttachmentLoader(atlas);
const skeletonData = loader.readSkeletonData(data); const loader = skeletonFile.endsWith(".json")
return skeletonData; ? new SkeletonJson(attachmentLoader)
: new SkeletonBinary(attachmentLoader);
loader.scale = scale;
let data = await readFile(skeletonFile);
if (skeletonFile.endsWith(".json")) {
data = bufferToUtf8String(data);
}
const skeletonData = loader.readSkeletonData(data);
return skeletonData;
} }
/** /**
@ -105,58 +172,60 @@ export async function loadSkeletonData(skeletonFile: string, atlas: TextureAtlas
* be shared by any number of drawables. * be shared by any number of drawables.
*/ */
export class SkeletonDrawable { export class SkeletonDrawable {
public readonly skeleton: Skeleton; public readonly skeleton: Skeleton;
public readonly animationState: AnimationState; public readonly animationState: AnimationState;
/** /**
* Constructs a new drawble from the skeleton data. * Constructs a new drawble from the skeleton data.
*/ */
constructor(skeletonData: SkeletonData) { constructor (skeletonData: SkeletonData) {
this.skeleton = new Skeleton(skeletonData); this.skeleton = new Skeleton(skeletonData);
this.animationState = new AnimationState(new AnimationStateData(skeletonData)); this.animationState = new AnimationState(
} new AnimationStateData(skeletonData)
);
}
/** /**
* Updates the animation state and skeleton time by the delta time. Applies the * 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. * animations to the skeleton and calculates the final pose of the skeleton.
* *
* @param deltaTime the time since the last update in seconds * @param deltaTime the time since the last update in seconds
* @param physicsUpdate optional {@link Physics} update mode. * @param physicsUpdate optional {@link Physics} update mode.
*/ */
update(deltaTime: number, physicsUpdate: Physics = Physics.update) { update (deltaTime: number, physicsUpdate: Physics = Physics.update) {
this.animationState.update(deltaTime); this.animationState.update(deltaTime);
this.skeleton.update(deltaTime); this.skeleton.update(deltaTime);
this.animationState.apply(this.skeleton); this.animationState.apply(this.skeleton);
this.skeleton.updateWorldTransform(physicsUpdate); this.skeleton.updateWorldTransform(physicsUpdate);
} }
} }
/** /**
* Renders a {@link Skeleton} or {@link SkeletonDrawable} to a CanvasKit {@link Canvas}. * Renders a {@link Skeleton} or {@link SkeletonDrawable} to a CanvasKit {@link Canvas}.
*/ */
export class SkeletonRenderer { export class SkeletonRenderer {
private clipper = new SkeletonClipping(); private clipper = new SkeletonClipping();
private tempColor = new Color(); private tempColor = new Color();
private tempColor2 = new Color(); private tempColor2 = new Color();
private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
private scratchPositions = Utils.newFloatArray(100); private scratchPositions = Utils.newFloatArray(100);
private scratchColors = Utils.newFloatArray(100); private scratchColors = Utils.newFloatArray(100);
private scratchUVs = Utils.newFloatArray(100); private scratchUVs = Utils.newFloatArray(100);
/** /**
* Creates a new skeleton renderer. * Creates a new skeleton renderer.
* @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`. * @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`.
*/ */
constructor(private ck: CanvasKit) {} constructor (private ck: CanvasKit) { }
/** /**
* Renders a skeleton or skeleton drawable in its current pose to the canvas. * Renders a skeleton or skeleton drawable in its current pose to the canvas.
* @param canvas the canvas to render to. * @param canvas the canvas to render to.
* @param skeleton the skeleton or drawable to render. * @param skeleton the skeleton or drawable to render.
*/ */
render(canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) { render (canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) {
if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton; if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton;
let clipper = this.clipper; let clipper = this.clipper;
let drawOrder = skeleton.drawOrder; let drawOrder = skeleton.drawOrder;
let skeletonColor = skeleton.color; let skeletonColor = skeleton.color;
@ -165,20 +234,20 @@ export class SkeletonRenderer {
if (!slot.bone.active) { if (!slot.bone.active) {
clipper.clipEndWithSlot(slot); clipper.clipEndWithSlot(slot);
continue; continue;
} }
let attachment = slot.getAttachment(); let attachment = slot.getAttachment();
let positions = this.scratchPositions; let positions = this.scratchPositions;
let colors = this.scratchColors; let colors = this.scratchColors;
let uvs: NumberArrayLike; let uvs: NumberArrayLike;
let texture: CanvasKitTexture; let texture: CanvasKitTexture;
let triangles: Array<number>; let triangles: Array<number>;
let attachmentColor: Color; let attachmentColor: Color;
let numVertices = 0; let numVertices = 0;
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
let region = attachment as RegionAttachment; let region = attachment as RegionAttachment;
positions = positions.length < 8 ? Utils.newFloatArray(8) : positions; positions = positions.length < 8 ? Utils.newFloatArray(8) : positions;
numVertices = 4; numVertices = 4;
region.computeWorldVertices(slot, positions, 0, 2); region.computeWorldVertices(slot, positions, 0, 2);
triangles = SkeletonRenderer.QUAD_TRIANGLES; triangles = SkeletonRenderer.QUAD_TRIANGLES;
uvs = region.uvs as Float32Array; uvs = region.uvs as Float32Array;
@ -186,9 +255,19 @@ export class SkeletonRenderer {
attachmentColor = region.color; attachmentColor = region.color;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
let mesh = attachment as MeshAttachment; let mesh = attachment as MeshAttachment;
positions = positions.length < mesh.worldVerticesLength ? Utils.newFloatArray(mesh.worldVerticesLength) : positions; positions =
numVertices = mesh.worldVerticesLength >> 1; positions.length < mesh.worldVerticesLength
mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, positions, 0, 2); ? Utils.newFloatArray(mesh.worldVerticesLength)
: positions;
numVertices = mesh.worldVerticesLength >> 1;
mesh.computeWorldVertices(
slot,
0,
mesh.worldVerticesLength,
positions,
0,
2
);
triangles = mesh.triangles; triangles = mesh.triangles;
texture = mesh.region?.texture as CanvasKitTexture; texture = mesh.region?.texture as CanvasKitTexture;
uvs = mesh.uvs as Float32Array; uvs = mesh.uvs as Float32Array;
@ -204,43 +283,63 @@ export class SkeletonRenderer {
if (texture) { if (texture) {
if (clipper.isClipping()) { if (clipper.isClipping()) {
clipper.clipTrianglesUnpacked(positions, triangles, triangles.length, uvs); clipper.clipTrianglesUnpacked(
positions = clipper.clippedVertices; positions,
uvs = clipper.clippedUVs; triangles,
triangles = clipper.clippedTriangles; triangles.length,
uvs
);
positions = clipper.clippedVertices;
uvs = clipper.clippedUVs;
triangles = clipper.clippedTriangles;
} }
let slotColor = slot.color; let slotColor = slot.color;
let finalColor = this.tempColor; let finalColor = this.tempColor;
finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r;
finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g;
finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b;
finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a; finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a;
if (colors.length / 4 < numVertices) colors = Utils.newFloatArray(numVertices * 4); if (colors.length / 4 < numVertices)
for (let i = 0, n = numVertices * 4; i < n; i += 4) { colors = Utils.newFloatArray(numVertices * 4);
colors[i] = finalColor.r; for (let i = 0, n = numVertices * 4; i < n; i += 4) {
colors[i + 1] = finalColor.g; colors[i] = finalColor.r;
colors[i + 2] = finalColor.b; colors[i + 1] = finalColor.g;
colors[i + 3] = finalColor.a; colors[i + 2] = finalColor.b;
} colors[i + 3] = finalColor.a;
}
const scaledUvs = this.scratchUVs.length < uvs.length ? Utils.newFloatArray(uvs.length) : this.scratchUVs; const scaledUvs =
const width = texture.getImage().image.width(); this.scratchUVs.length < uvs.length
const height = texture.getImage().image.height(); ? Utils.newFloatArray(uvs.length)
for (let i = 0; i < uvs.length; i+=2) { : this.scratchUVs;
scaledUvs[i] = uvs[i] * width; const width = texture.getImage().image.width();
scaledUvs[i + 1] = uvs[i + 1] * height; const height = texture.getImage().image.height();
} for (let i = 0; i < uvs.length; i += 2) {
scaledUvs[i] = uvs[i] * width;
scaledUvs[i + 1] = uvs[i + 1] * height;
}
const blendMode = slot.data.blendMode; const blendMode = slot.data.blendMode;
const vertices = this.ck.MakeVertices(this.ck.VertexMode.Triangles, positions, scaledUvs, colors, triangles, false); const vertices = this.ck.MakeVertices(
canvas.drawVertices(vertices, this.ck.BlendMode.Modulate, texture.getImage().paintPerBlendMode.get(blendMode)!); this.ck.VertexMode.Triangles,
vertices.delete(); positions,
scaledUvs,
colors,
triangles,
false
);
canvas.drawVertices(
vertices,
this.ck.BlendMode.Modulate,
texture.getImage().paintPerBlendMode.get(blendMode)!
);
vertices.delete();
} }
clipper.clipEndWithSlot(slot); clipper.clipEndWithSlot(slot);
} }
clipper.clipEnd(); clipper.clipEnd();
} }
} }

View File

@ -64,7 +64,7 @@ export class SkeletonBinary {
this.attachmentLoader = attachmentLoader; this.attachmentLoader = attachmentLoader;
} }
readSkeletonData (binary: Uint8Array | ArrayBuffer): SkeletonData { readSkeletonData (binary: Uint8Array | ArrayBuffer): SkeletonData {
let scale = this.scale; let scale = this.scale;
let skeletonData = new SkeletonData(); let skeletonData = new SkeletonData();

View File

@ -95,7 +95,7 @@ void USpineAtlasAsset::BeginDestroy() {
class UETextureLoader : public TextureLoader { class UETextureLoader : public TextureLoader {
void load(AtlasPage &page, const String &path) { void load(AtlasPage &page, const String &path) {
page.texture = (void*)(uintptr_t)page.index; page.texture = (void *) (uintptr_t) page.index;
} }
void unload(void *texture) { void unload(void *texture) {

View File

@ -268,7 +268,7 @@ void USpineSkeletonRendererComponent::UpdateMesh(USpineSkeletonComponent *compon
// to the correct skeleton data yet, we won't find any regions. // to the correct skeleton data yet, we won't find any regions.
// ignore regions for which we can't find a material // ignore regions for which we can't find a material
UMaterialInstanceDynamic *material = nullptr; UMaterialInstanceDynamic *material = nullptr;
int foundPageIndex = (int)(intptr_t)attachmentAtlasRegion->rendererObject; int foundPageIndex = (int) (intptr_t) attachmentAtlasRegion->rendererObject;
if (foundPageIndex == -1) { if (foundPageIndex == -1) {
clipper.clipEnd(*slot); clipper.clipEnd(*slot);
continue; continue;