Skip to content

Commit 632fb22

Browse files
committed
[ts] Add support across all runtimes for premultiplying non-PMA textures on upload.
1 parent 86981a0 commit 632fb22

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+298
-313
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,8 @@
758758
- Added `Skeleton` properties `windX`, `windY`, `gravityX`, `gravityY` to allow rotating physics force directions
759759
- Added `SequenceTimeline` for sequence animation
760760
- Added `allowMissingRegions` parameter to `AtlasAttachmentLoader` constructor to support skeletons exported with per-skin atlases
761+
- Added `TextureLoader` type with optional `pma?: boolean` parameter to `AssetManagerBase`. `AssetManagerBase` now tracks and passes PMA metadata from atlas pages to texture loaders, allowing runtimes to automatically premultiply textures on upload
762+
- Added `SkeletonRendererCore` class to reduce complexity of runtime-specific render code
761763

762764
- **Breaking changes**
763765
- `Bone` now extends `PosedActive` with separate pose, constrained, and applied states
@@ -869,6 +871,12 @@
869871

870872
- **Breaking changes**
871873
- Updated to use new TypeScript/JavaScript runtime
874+
- `GLTexture` constructor now requires `pma: boolean` parameter (automatically read from atlas page metadata)
875+
- Removed `GLTexture.DISABLE_UNPACK_PREMULTIPLIED_ALPHA_WEBGL` static property
876+
- `SkeletonRenderer` and `SkeletonDebugRenderer` no longer have `premultipliedAlpha` property - PMA is handled automatically
877+
- `SceneRenderer.drawSkeleton()` and `drawSkeletonDebug()` no longer take `premultipliedAlpha` parameter
878+
- `PolygonBatcher.setBlendMode()` no longer takes `premultipliedAlpha` parameter
879+
- `LoadingScreen` no longer accepts PMA parameters
872880

873881
### Canvas backend
874882

@@ -885,11 +893,13 @@
885893

886894
- **Breaking changes**
887895
- Updated to use new TypeScript/JavaScript runtime
896+
- `AssetManager` constructor no longer takes `pma` parameter - PMA is handled automatically
888897

889898
### Player
890899

891900
- **Breaking changes**
892901
- Updated to use new TypeScript/JavaScript runtime
902+
- Removed `premultipliedAlpha` option from `SpinePlayerConfig` - PMA is now handled automatically
893903

894904
### Pixi v7
895905

@@ -917,17 +927,22 @@
917927

918928
- **Breaking changes**
919929
- Updated to use new TypeScript/JavaScript runtime
930+
- `SpinePlugin.spineAtlas()` loader no longer takes `premultipliedAlpha` parameter - PMA is handled automatically
931+
- `SpinePlugin.createSkeleton()` no longer takes `premultipliedAlpha` parameter
920932

921933
### Phaser v4
922934

923935
- **Breaking changes**
924936
- Updated to use new TypeScript/JavaScript runtime
937+
- `SpinePlugin.spineAtlas()` loader no longer takes `premultipliedAlpha` parameter - PMA is handled automatically
938+
- `SpinePlugin.createSkeleton()` no longer takes `premultipliedAlpha` parameter
925939

926940
### Web Components
927941

928942
- **Breaking changes**
929943
- Updated to use new TypeScript/JavaScript runtime
930944
- Updated skeleton and overlay component implementations
945+
- Removed `pma` property from `SpineWebComponentSkeleton` - PMA is handled automatically
931946

932947
# 4.2
933948

spine-ts/spine-core/src/AssetManagerBase.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,19 @@ type AssetData = (Uint8Array | string | Texture | TextureAtlas | object) & Parti
3535
type AssetCallback<T extends AssetData> = (path: string, data: T) => void;
3636
type ErrorCallback = (path: string, message: string) => void;
3737

38+
export type TextureLoader = (image: HTMLImageElement | ImageBitmap, pma?: boolean) => Texture;
39+
3840
export class AssetManagerBase implements Disposable {
39-
private pathPrefix: string = "";
40-
private textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture;
41-
private downloader: Downloader;
42-
private cache: AssetCache;
4341
private errors: StringMap<string> = {};
4442
private toLoad = 0;
4543
private loaded = 0;
44+
private texturePmaInfo: Record<string, boolean> = {};
4645

47-
constructor (textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader = new Downloader(), cache = new AssetCache()) {
48-
this.textureLoader = textureLoader;
49-
this.pathPrefix = pathPrefix;
50-
this.downloader = downloader;
51-
this.cache = cache;
46+
constructor (
47+
private textureLoader: TextureLoader,
48+
private pathPrefix: string = "",
49+
private downloader = new Downloader(),
50+
private cache = new AssetCache()) {
5251
}
5352

5453
private start (path: string): string {
@@ -175,6 +174,7 @@ export class AssetManagerBase implements Disposable {
175174

176175
if (this.reuseAssets(path, success, error)) return;
177176

177+
const pma = this.texturePmaInfo[path];
178178
this.cache.assetsLoaded[path] = new Promise<Texture>((resolve, reject) => {
179179
const isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document);
180180
const isWebWorker = !isBrowser; // && typeof importScripts !== 'undefined';
@@ -188,7 +188,7 @@ export class AssetManagerBase implements Disposable {
188188
return blob ? createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }) : null;
189189
}).then((bitmap) => {
190190
if (bitmap) {
191-
const texture = this.createTexture(path, bitmap);
191+
const texture = this.createTexture(path, pma, bitmap);
192192
this.success(success, path, texture);
193193
resolve(texture);
194194
};
@@ -197,7 +197,7 @@ export class AssetManagerBase implements Disposable {
197197
const image = new Image();
198198
image.crossOrigin = "anonymous";
199199
image.onload = () => {
200-
const texture = this.createTexture(path, image);
200+
const texture = this.createTexture(path, pma, image);
201201
this.success(success, path, texture);
202202
resolve(texture);
203203
};
@@ -216,7 +216,7 @@ export class AssetManagerBase implements Disposable {
216216
path: string,
217217
success: AssetCallback<TextureAtlas> = () => { },
218218
error: ErrorCallback = () => { },
219-
fileAlias?: { [keyword: string]: string }
219+
fileAlias?: Record<string, string>
220220
) {
221221
const index = path.lastIndexOf("/");
222222
const parent = index >= 0 ? path.substring(0, index + 1) : "";
@@ -227,10 +227,11 @@ export class AssetManagerBase implements Disposable {
227227
this.cache.assetsLoaded[path] = new Promise<TextureAtlas>((resolve, reject) => {
228228
this.downloader.downloadText(path, (atlasText: string): void => {
229229
try {
230-
const atlas = this.createTextureAtlas(path, atlasText);
230+
const atlas = this.createTextureAtlas(atlasText, parent, path, fileAlias);
231231
let toLoad = atlas.pages.length, abort = false;
232232
for (const page of atlas.pages) {
233-
this.loadTexture(!fileAlias ? parent + page.name : fileAlias[page.name],
233+
this.loadTexture(
234+
this.texturePath(parent, page.name, fileAlias),
234235
(imagePath: string, texture: Texture) => {
235236
if (!abort) {
236237
page.setTexture(texture);
@@ -268,14 +269,16 @@ export class AssetManagerBase implements Disposable {
268269
success: AssetCallback<TextureAtlas> = () => { },
269270
error: ErrorCallback = () => { },
270271
) {
272+
const index = path.lastIndexOf("/");
273+
const parent = index >= 0 ? path.substring(0, index + 1) : "";
271274
path = this.start(path);
272275

273276
if (this.reuseAssets(path, success, error)) return;
274277

275278
this.cache.assetsLoaded[path] = new Promise<TextureAtlas>((resolve, reject) => {
276279
this.downloader.downloadText(path, (atlasText: string): void => {
277280
try {
278-
const atlas = this.createTextureAtlas(path, atlasText);
281+
const atlas = this.createTextureAtlas(atlasText, parent, path);
279282
this.success(success, path, atlas);
280283
resolve(atlas);
281284
} catch (e) {
@@ -291,7 +294,6 @@ export class AssetManagerBase implements Disposable {
291294
});
292295
}
293296

294-
// Promisified versions of load function
295297
async loadBinaryAsync (path: string) {
296298
return new Promise((resolve, reject) => {
297299
this.loadBinary(path,
@@ -413,7 +415,7 @@ export class AssetManagerBase implements Disposable {
413415
}
414416
}
415417

416-
private createTextureAtlas (path: string, atlasText: string): TextureAtlas {
418+
private createTextureAtlas (atlasText: string, parentPath: string, path: string, fileAlias?: Record<string, string>): TextureAtlas {
417419
const atlas = new TextureAtlas(atlasText);
418420
atlas.dispose = () => {
419421
if (this.cache.assetsRefCount[path] <= 0) return;
@@ -422,17 +424,26 @@ export class AssetManagerBase implements Disposable {
422424
page.texture?.dispose();
423425
}
424426
}
427+
for (const page of atlas.pages) {
428+
const texturePath = this.texturePath(parentPath, page.name, fileAlias);
429+
this.texturePmaInfo[this.pathPrefix + texturePath] = page.pma;
430+
}
425431
return atlas;
426432
}
427433

428-
private createTexture (path: string, image: HTMLImageElement | ImageBitmap): Texture {
429-
const texture = this.textureLoader(image);
434+
private createTexture (path: string, pma: boolean, image: HTMLImageElement | ImageBitmap): Texture {
435+
const texture = this.textureLoader(image, pma);
430436
const textureDispose = texture.dispose.bind(texture);
431437
texture.dispose = () => {
432438
if (this.disposeAssetInternal(path)) textureDispose();
433439
}
434440
return texture;
435441
}
442+
443+
private texturePath (parentPath: string, pageName: string, fileAlias?: Record<string, string>) {
444+
if (!fileAlias) return parentPath + pageName;
445+
return fileAlias[pageName];
446+
}
436447
}
437448

438449
export class AssetCache {

spine-ts/spine-core/src/SkeletonClipping.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export class SkeletonClipping {
324324
return clipOutputItems != null;
325325
}
326326

327-
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike | Uint32Array, trianglesLength: number, uvs: NumberArrayLike) {
327+
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike | Uint32Array, trianglesLength: number, uvs: NumberArrayLike, stride = 2) {
328328
const clipOutput = this.clipOutput;
329329
let clippedVertices = this._clippedVerticesTyped, clippedUVs = this._clippedUVsTyped, clippedTriangles = this._clippedTrianglesTyped;
330330
// biome-ignore lint/style/noNonNullAssertion: clipStart define it
@@ -343,17 +343,23 @@ export class SkeletonClipping {
343343
let clipped = false;
344344

345345
for (let i = 0; i < trianglesLength; i += 3) {
346-
let v = triangles[i] << 1;
346+
let t = triangles[i];
347+
let v = t * stride;
347348
const x1 = vertices[v], y1 = vertices[v + 1];
348-
const u1 = uvs[v], v1 = uvs[v + 1];
349+
let uv = t << 1;
350+
const u1 = uvs[uv], v1 = uvs[uv + 1];
349351

350-
v = triangles[i + 1] << 1;
352+
t = triangles[i + 1];
353+
v = t * stride;
351354
const x2 = vertices[v], y2 = vertices[v + 1];
352-
const u2 = uvs[v], v2 = uvs[v + 1];
355+
uv = t << 1;
356+
const u2 = uvs[uv], v2 = uvs[uv + 1];
353357

354-
v = triangles[i + 2] << 1;
358+
t = triangles[i + 2];
359+
v = t * stride;
355360
const x3 = vertices[v], y3 = vertices[v + 1];
356-
const u3 = uvs[v], v3 = uvs[v + 1];
361+
uv = t << 1;
362+
const u3 = uvs[uv], v3 = uvs[uv + 1];
357363

358364
for (let p = 0; p < polygonsCount; p++) {
359365
let s = this.clippedVerticesLength;
@@ -367,29 +373,31 @@ export class SkeletonClipping {
367373
let clipOutputCount = clipOutputLength >> 1;
368374
const clipOutputItems = this.clipOutput;
369375

370-
const newLength = s + clipOutputCount * 2;
376+
const newLength = s + clipOutputCount * stride;
371377
if (clippedVertices.length < newLength) {
372378
this._clippedVerticesTyped = new Float32Array(newLength * 2);
373379
this._clippedVerticesTyped.set(clippedVertices.subarray(0, s));
374-
this._clippedUVsTyped = new Float32Array(newLength * 2);
375-
this._clippedUVsTyped.set(clippedUVs.subarray(0, s));
380+
this._clippedUVsTyped = new Float32Array((this.clippedUVsLength + clipOutputCount * 2) * 2);
381+
this._clippedUVsTyped.set(clippedUVs.subarray(0, this.clippedUVsLength));
376382
clippedVertices = this._clippedVerticesTyped;
377383
clippedUVs = this._clippedUVsTyped;
378384
}
379385
const clippedVerticesItems = clippedVertices;
380386
const clippedUVsItems = clippedUVs;
381387
this.clippedVerticesLength = newLength;
382-
this.clippedUVsLength = newLength;
383-
for (let ii = 0; ii < clipOutputLength; ii += 2, s += 2) {
388+
389+
let uvIndex = this.clippedUVsLength;
390+
this.clippedUVsLength = uvIndex + clipOutputCount * 2;
391+
for (let ii = 0; ii < clipOutputLength; ii += 2, s += stride, uvIndex += 2) {
384392
const x = clipOutputItems[ii], y = clipOutputItems[ii + 1];
385393
clippedVerticesItems[s] = x;
386394
clippedVerticesItems[s + 1] = y;
387395
const c0 = x - x3, c1 = y - y3;
388396
const a = (d0 * c0 + d1 * c1) * d;
389397
const b = (d4 * c0 + d2 * c1) * d;
390398
const c = 1 - a - b;
391-
clippedUVsItems[s] = u1 * a + u2 * b + u3 * c;
392-
clippedUVsItems[s + 1] = v1 * a + v2 * b + v3 * c;
399+
clippedUVsItems[uvIndex] = u1 * a + u2 * b + u3 * c;
400+
clippedUVsItems[uvIndex + 1] = v1 * a + v2 * b + v3 * c;
393401
}
394402

395403
s = this.clippedTrianglesLength;
@@ -411,33 +419,35 @@ export class SkeletonClipping {
411419

412420
} else {
413421

414-
let newLength = s + 3 * 2;
422+
let newLength = s + 3 * stride;
415423
if (clippedVertices.length < newLength) {
416424
this._clippedVerticesTyped = new Float32Array(newLength * 2);
417425
this._clippedVerticesTyped.set(clippedVertices.subarray(0, s));
418426
clippedVertices = this._clippedVerticesTyped;
419427
}
420428
clippedVertices[s] = x1;
421429
clippedVertices[s + 1] = y1;
422-
clippedVertices[s + 2] = x2;
423-
clippedVertices[s + 3] = y2;
424-
clippedVertices[s + 4] = x3;
425-
clippedVertices[s + 5] = y3;
426-
427-
if (clippedUVs.length < newLength) {
428-
this._clippedUVsTyped = new Float32Array(newLength * 2);
429-
this._clippedUVsTyped.set(clippedUVs.subarray(0, s));
430+
clippedVertices[s + stride] = x2;
431+
clippedVertices[s + stride + 1] = y2;
432+
clippedVertices[s + stride * 2] = x3;
433+
clippedVertices[s + stride * 2 + 1] = y3;
434+
435+
let uvLength = this.clippedUVsLength + 3 * 2;
436+
if (clippedUVs.length < uvLength) {
437+
this._clippedUVsTyped = new Float32Array(uvLength * 2);
438+
this._clippedUVsTyped.set(clippedUVs.subarray(0, this.clippedUVsLength));
430439
clippedUVs = this._clippedUVsTyped;
431440
}
432-
clippedUVs[s] = u1;
433-
clippedUVs[s + 1] = v1;
434-
clippedUVs[s + 2] = u2;
435-
clippedUVs[s + 3] = v2;
436-
clippedUVs[s + 4] = u3;
437-
clippedUVs[s + 5] = v3;
441+
let uvIndex = this.clippedUVsLength;
442+
clippedUVs[uvIndex] = u1;
443+
clippedUVs[uvIndex + 1] = v1;
444+
clippedUVs[uvIndex + 2] = u2;
445+
clippedUVs[uvIndex + 3] = v2;
446+
clippedUVs[uvIndex + 4] = u3;
447+
clippedUVs[uvIndex + 5] = v3;
438448

439449
this.clippedVerticesLength = newLength;
440-
this.clippedUVsLength = newLength;
450+
this.clippedUVsLength = uvLength;
441451

442452
s = this.clippedTrianglesLength;
443453
newLength = s + 3;

0 commit comments

Comments
 (0)