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/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( 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,