From d804781c526e44318bd9d73da51339dfa3e03460 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 14:52:36 +0000 Subject: [PATCH 1/2] Fix Spine objects showing dark halos with premultiplied-alpha atlases The migration to @esotericsoftware/spine-pixi-v7 shares the textures already loaded by the engine's ImageManager (via `data.images`) with the spine atlas loader. When a texture is provided this way, the spine loader reuses it as-is and does NOT set its alpha mode, unlike the path where it loads the pages itself (where it uses `PMA` for premultiplied atlases and `UNPACK` otherwise). The shared textures are uploaded with PIXI's default `UNPACK` mode (premultiply-on-upload), while the spine-pixi-v7 tint shader always samples textures as if they were premultiplied. For atlases exported with premultiplied alpha (`pma: true`, which is a common/default Spine export setting), the texture ends up premultiplied twice, producing dark fringes/halos that look like "shadows" around the rendered parts. Align each shared atlas page texture's alpha mode with the `pma` flag declared by the atlas, in both the runtime SpineAtlasManager and the editor's PixiResourcesLoader. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01C9Py2JHcqk5zKaWyfYhxJv --- .../managers/pixi-spine-atlas-manager.ts | 33 ++++++++++++++++++- .../ObjectsRendering/PixiResourcesLoader.js | 26 +++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Extensions/Spine/managers/pixi-spine-atlas-manager.ts b/Extensions/Spine/managers/pixi-spine-atlas-manager.ts index aa8038bf3477..a40ca5b39923 100644 --- a/Extensions/Spine/managers/pixi-spine-atlas-manager.ts +++ b/Extensions/Spine/managers/pixi-spine-atlas-manager.ts @@ -113,7 +113,38 @@ namespace gdjs { : 'anonymous', }); PIXI.Assets.add({ alias, src: url, data: { images } }); - return PIXI.Assets.load(alias); + const atlas = await PIXI.Assets.load(alias); + + // The spine-pixi-v7 tint shader always samples atlas textures as if they + // were premultiplied (see the runtime comment in `renderMeshes`). When the + // spine loader loads the atlas pages itself, it sets each page texture's + // alpha mode accordingly (`PMA` for premultiplied atlases, `UNPACK` + // otherwise). Here we instead share the textures already loaded by the + // ImageManager, which are uploaded to the GPU with PIXI's default `UNPACK` + // mode (premultiply-on-upload). For atlases exported with premultiplied + // alpha (`pma: true`), this premultiplies the texture a second time, which + // produces dark fringes/halos ("shadows") around the rendered parts. + // Align each shared texture's alpha mode with what the atlas page declares. + const imageNames = Object.keys(images); + for (const page of atlas.pages) { + const baseTexture = + images[page.name] || + (atlas.pages.length === 1 && imageNames.length === 1 + ? images[imageNames[0]] + : undefined); + if (!baseTexture) continue; + + const expectedAlphaMode = page.pma + ? PIXI.ALPHA_MODES.PMA + : PIXI.ALPHA_MODES.UNPACK; + if (baseTexture.alphaMode !== expectedAlphaMode) { + baseTexture.alphaMode = expectedAlphaMode; + // Force a re-upload to the GPU so the new alpha mode takes effect. + baseTexture.update(); + } + } + + return atlas; } /** diff --git a/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js b/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js index 5fd1874abb5f..d07f4512f3b1 100644 --- a/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js +++ b/newIDE/app/src/ObjectsRendering/PixiResourcesLoader.js @@ -1019,6 +1019,32 @@ export default class PixiResourcesLoader { }); PIXI.Assets.load(spineTextureAtlasName).then( textureAtlas => { + // The spine-pixi-v7 tint shader always samples atlas textures as if + // they were premultiplied. The shared textures loaded by the engine + // are uploaded with PIXI's default UNPACK mode (premultiply-on-upload), + // so atlases exported with premultiplied alpha (`pma: true`) would be + // premultiplied a second time, producing dark fringes/halos + // ("shadows") around the rendered parts. Align each shared texture's + // alpha mode with what the atlas page declares. + const imageNames = Object.keys(images); + for (const page of textureAtlas.pages) { + const baseTexture = + images[page.name] || + (textureAtlas.pages.length === 1 && imageNames.length === 1 + ? images[imageNames[0]] + : undefined); + if (!baseTexture) continue; + + const expectedAlphaMode = page.pma + ? PIXI.ALPHA_MODES.PMA + : PIXI.ALPHA_MODES.UNPACK; + if (baseTexture.alphaMode !== expectedAlphaMode) { + baseTexture.alphaMode = expectedAlphaMode; + // Force a re-upload to the GPU so the new alpha mode takes effect. + baseTexture.update(); + } + } + resolve({ textureAtlas, atlasAlias: spineTextureAtlasName, From 6a402aa728aa0bed3f316aa7d5a021bbe7814931 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 14:54:27 +0000 Subject: [PATCH 2/2] Fix Spine point attachment local rotation ignoring the attachment angle `getPointAttachmentRotationLocal` returned only `slot.bone.rotation`, which ignored the point attachment's own rotation (the angle set on the point in the Spine editor) as well as its parent bone chain. As a result the "Point attachment local rotation" expression did not reflect the point's configured angle, and was inconsistent with the world variant (which already used `computeWorldRotation`, accounting for the attachment rotation). Use `attachment.computeWorldRotation(slot.bone)` for the skeleton-space angle in both cases so that the attachment's own rotation and bone chain are taken into account, with: world rotation = local rotation + object angle. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01C9Py2JHcqk5zKaWyfYhxJv --- .../Spine/spineruntimeobject-pixi-renderer.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Extensions/Spine/spineruntimeobject-pixi-renderer.ts b/Extensions/Spine/spineruntimeobject-pixi-renderer.ts index 7f32d53c4429..c12843e4e0c3 100644 --- a/Extensions/Spine/spineruntimeobject-pixi-renderer.ts +++ b/Extensions/Spine/spineruntimeobject-pixi-renderer.ts @@ -254,14 +254,22 @@ namespace gdjs { this._rendererObject ); + // Rotation of the point attachment within the skeleton, in degrees. + // `computeWorldRotation` accounts for both the attachment's own rotation + // (the angle set on the point in the Spine editor) and the full bone chain + // it is attached to. + // Note: the previous code returned only `slot.bone.rotation` for the local + // case, which ignored the attachment's own rotation entirely - so a point's + // configured angle had no effect on the "local rotation" expression. + const localRotation = attachment.computeWorldRotation(slot.bone); + if (isWorld) { - return ( - gdjs.toDegrees(this._rendererObject.rotation) + - attachment.computeWorldRotation(slot.bone) - ); + // Add the object's own rotation to express the angle in scene space, so + // that: world rotation = local rotation + object angle. + return gdjs.toDegrees(this._rendererObject.rotation) + localRotation; } - return slot.bone.rotation; + return localRotation; } getPointAttachmentScale(