• Runtimes
  • Using the same assets in Pixi and Spine

michalvadak

For Spine GameObjects, it's always best to have all character images on a single texture atlas page whenever possible. This ensures the character can be rendered without breaking batching.

The addSlotObjects method is merely a convenience function and should be avoided when possible. Typically, the Pixi object drawn using addSlotObjects comes from a different texture, which breaks batching. If the slot is in the middle, rendering the character would now require three draw calls.

Spine atlases are primarily designed for Spine GameObjects, but nothing prevents you from reusing the character’s images for other purposes. However, individual character images are not recreated as Pixi sub-textures from the texture atlas PNG. Instead, meshes and UVs are used to achieve this.

If you want to reuse an image packed into an atlas page outside of a Spine GameObject, it’s your responsibility to create a Pixi sub-texture for it. You can use the atlas data to determine the image’s position and size within the respective texture atlas page.

I did a quick experiment with this—here’s the code:

// load the atlas and automatically the textures
const atlas = await PIXI.Assets.load("https://esotericsoftware.com/files/examples/latest/spineboy/export/spineboy-pma.atlas");

// util to get a new sprite, given an attachment name
const getSprite = attachmentName => {
    // find the region with the given attachment name
    const region = atlas.regions.find(element => element.name === attachmentName);

    // get the respective page texture
    const originalTexture = region.texture.texture;

    // determine the position of the desired attachment, considering rotation
    const frame = {
        x: region.x,
        y: region.y,
        width: region.rotate === undefined ? region.width : region.height,
        height: region.rotate === undefined ? region.height : region.width,
    };

    // creating the new sub texture
        const texture = new PIXI.Texture({
        source: originalTexture.baseTexture,
        crop: new PIXI.Rectangle(frame.x, frame.y, frame.width, frame.height),
        frame,
    });

    // creating a new sprite
    return new PIXI.Sprite(texture);
}

const miniSpineBoy = new PIXI.Container();

const footR = getSprite("front-foot");
footR.x = 85;
footR.y = 135;
footR.scale.set(.70)
miniSpineBoy.addChild(footR);

const head = getSprite("head");
miniSpineBoy.addChild(head);

const eyeL = getSprite("eye-surprised");
eyeL.x = 45;
eyeL.y = 75;
eyeL.scale.set(.75)
miniSpineBoy.addChild(eyeL);

const eyeR = getSprite("eye-indifferent");
eyeR.x = 70;
eyeR.y = 80;
eyeR.scale.set(.75)
miniSpineBoy.addChild(eyeR);

const mouth = getSprite("mouth-oooo");
mouth.x = 50;
mouth.y = 120;
miniSpineBoy.addChild(mouth);

const footL = getSprite("front-foot");
footL.x = 25;
footL.y = 135;
footL.scale.set(.70)
miniSpineBoy.addChild(footL);

miniSpineBoy.x = window.innerWidth / 2 - miniSpineBoy.width / 2;
miniSpineBoy.y = window.innerHeight / 2 - miniSpineBoy.height / 2;;
app.stage.addChild(miniSpineBoy);

And here the result:

As you can see, I’m not even creating a Spine GameObject here, just loading the atlas. I haven’t considered any special cases related to how the texture packing is done, but for reusing a few images, this approach should work.

    Related Discussions
    ...

    That's a cool example! Thank you!

    How about I will have multiple skins for my game... but each skin should be its own game instance and the textures for each skin should be packed separately as I don't want to pollute the atlas for skin1 with assets for skin2 as this would increase the size of the entire game.

    Would this be a case where it would be better to do all the spine animations with just slots so Pixi can manage this and only load the necessary assets? Or is there a way to do this in spine without having to create a duplicate of the spine project?

    • Davide ha risposto a questo messaggio

      michalvadak

      I'm not entirely sure I understand how your game and game instances work.
      From what I gather, each game instance uses only one skin. Because of this, you want to pack the skin images into separate atlases. Please correct me if I'm wrong.

      Keep in mind that you don’t need to duplicate your projects to create separate atlas pages.
      If you organize the skin images into different folders, the Spine Texture Packer is smart enough to pack them into separate atlas pages, as explained in this user guide section.

      However, even if you use multiple atlas pages, once you load your atlas file (txt), all atlas pages (pngs) are loaded into memory.
      If you want to load only specific pages into memory, you'll need to modify our atlas loader and implement a deferred loading strategy.
      If in your game instance you never need of other skins, you could also just remove the atlas page info not needed from the atlas text file.

      Additionally, if you have multiple skin variations, you don’t need to add all of them directly into Spine. Instead, create a single dummy version in Spine, and generate the variations externally. Then, you can use the texture packer separately to create different atlas variations. I recommend reading the full Spine Texture Packer user guide for a deeper understanding.

      You can also use the CLI to pack your textures, creating a streamlined pipeline for generating skin variations.

      As a last resort, you could use an external Texture Packer to provide assets to the Spine skeleton.
      If you use addSlotObjects, it will generate a large number of Pixi elements. Alternatively, you can create a custom AttachmentLoader to feed images directly to the skeleton.

        Let me give you some more context.

        Let's say I have 3 pixi games (same game code, just different assets). All of them will use a single Spine project as the game is the same, just with a different skin.

        If I leave all the assets in Spine it will pack them into atlas pages (png) together with info about all in the atlas file (txt). I don't want this as it will increase the size of the games with each skin.

        What I need is to have atlas pages (png) with assets only for game1 and atlas file (txt) with information about assets only for game1 so I can load only that specific game skin.

        Same for game2, game3 etc.

        Davide Additionally, if you have multiple skin variations, you don’t need to add all of them directly into Spine. Instead, create a single dummy version in Spine, and generate the variations externally. Then, you can use the texture packer separately to create different atlas variations. I recommend reading the full Spine Texture Packer user guide for a deeper understanding.

        That would mean I would have a single Spine project with the "base" skin and that would provide me with just the JSON files and I would be able to generate atlas pages (png) and atlas file (txt) for each skin separately using the CLI?

        • Davide ha risposto a questo messaggio
          • Modificato

          michalvadak

          The easiest approach in your case is to export the skeleton three times, enabling the export flag only for the skin you want to export each time. This way, at runtime, your skeleton will require only the regions for the selected skin.

          Next, use the texture packer separately, providing as input a folder containing only the attachments of that skin. For example, to create the atlas for skin1, if your image folder is structured as follows:

          • images/
            • skin1/
            • skin2/
            • skin3/

          You can create a temporary folder containing only skin1:

          • temp/
            • skin1/

          Then, provide this temp/ folder to the standalone texture packer.

          If there are shared elements across skins stored in a separate common/ folder:

          • images/
            • common/
            • skin1/
            • skin2/
            • skin3/

          Copy the common/ folder as well:

          • temp/
            • common/
            • skin1/

          Make sure to select Combine subdirectories so the texture packer doesn’t generate a separate atlas page for each folder.

          Note that, as stated in the export flag documentation:

          If a mesh attachment is not exported, none of its linked meshes will be exported either.

          This means that if you export skin1, and skin1 contains a linked mesh that references a mesh in skin2, you must also export skin2 to preserve the dependency.
          Alternatively, to overcome this limitation, you can export a JSON containing all the skins (or the skins for which there are dependencies). However, you’ll need to create a custom AttachmentLoader that doesn’t throw an error when a region is missing from the atlas.
          In this case, the custom AttachmentLoader would function similarly to the AtlasAttachmentLoader, but with the throw declarations removed.
          If you take this approach, ensure that at runtime, you only set skins that are present in your atlas to avoid errors.
          You probably want to use CLI in this case to automatize this process.

            Davide The easiest approach in your case is to export the skeleton three times, enabling the export flag only for the skin you want to export each time. This way, at runtime, your skeleton will require only the regions for the selected skin.

            Thanks this seems to work!

            Davide Next, use the texture packer separately, providing as input a folder containing only the attachments of that skin. For example, to create the atlas for skin1, if your image folder is structured as follows:...

            I'm not sure why I need this? Doesn't the first part cover all my needs? Is this a different approach or an extension?

            • Davide ha risposto a questo messaggio

              Glad it worked!

              michalvadak I'm not sure why I need this. Doesn't the first part cover all my needs? Is this a different approach or an extension?

              If your skin has no dependencies, the first step should be enough.
              If there are dependencies, as explained below, you also need to export the skin you depend on. In that case, its images will be included in your atlas. To avoid that you need to export the skeleton and the atlas separately.

              5 giorni dopo

              Davide // load the atlas and automatically the textures
              const atlas = await PIXI.Assets.load("https://esotericsoftware.com/files/examples/latest/spineboy/export/spineboy-pma.atlas");

              // util to get a new sprite, given an attachment name
              const getSprite = attachmentName => {
              // find the region with the given attachment name
              const region = atlas.regions.find(element => element.name === attachmentName);

              // get the respective page texture
              const originalTexture = region.texture.texture;
              
              // determine the position of the desired attachment, considering rotation
              const frame = {
                  x: region.x,
                  y: region.y,
                  width: region.rotate === undefined ? region.width : region.height,
                  height: region.rotate === undefined ? region.height : region.width,
              };
              
              // creating the new sub texture
                  const texture = new PIXI.Texture({
                  source: originalTexture.baseTexture,
                  crop: new PIXI.Rectangle(frame.x, frame.y, frame.width, frame.height),
                  frame,
              });
              
              // creating a new sprite
              return new PIXI.Sprite(texture);

              }

              const miniSpineBoy = new PIXI.Container();

              const footR = getSprite("front-foot");
              footR.x = 85;
              footR.y = 135;
              footR.scale.set(.70)
              miniSpineBoy.addChild(footR);

              const head = getSprite("head");
              miniSpineBoy.addChild(head);

              const eyeL = getSprite("eye-surprised");
              eyeL.x = 45;
              eyeL.y = 75;
              eyeL.scale.set(.75)
              miniSpineBoy.addChild(eyeL);

              const eyeR = getSprite("eye-indifferent");
              eyeR.x = 70;
              eyeR.y = 80;
              eyeR.scale.set(.75)
              miniSpineBoy.addChild(eyeR);

              const mouth = getSprite("mouth-oooo");
              mouth.x = 50;
              mouth.y = 120;
              miniSpineBoy.addChild(mouth);

              const footL = getSprite("front-foot");
              footL.x = 25;
              footL.y = 135;
              footL.scale.set(.70)
              miniSpineBoy.addChild(footL);

              miniSpineBoy.x = window.innerWidth / 2 - miniSpineBoy.width / 2;
              miniSpineBoy.y = window.innerHeight / 2 - miniSpineBoy.height / 2;;
              app.stage.addChild(miniSpineBoy);

              The only thing is I can't get the script to work correctly in Spine 7 with rotated assets and white strip on :/

              • Davide ha risposto a questo messaggio

                michalvadak The only thing is I can't get the script to work correctly in Spine 7 with rotated assets and white strip on :/

                The script above was just a quick experiment. I did not test any special use cases.

                But what's the problem? In my example, I also had rotated textures and white strips.
                Just knowing that it doesn't work for you won't help me in helping you 🙂
                Could you describe your issue better?

                Got it! We made it to work now... but we have calculate the offsets and rotate the Texture at the end which I'm not sure is the correct approach.

                export const createTexturesFromSpineAtlas = (
                  atlas: string,
                  assets: string[]
                ) => {
                  const regions = Assets.get(atlas)['regions'];
                
                  regions
                    .filter((region: TextureAtlasRegion) => assets.includes(region.name))
                    .forEach((region: TextureAtlasRegion) => {
                      const frame = new Rectangle(
                        region.x,
                        region.y,
                        region.degrees !== 0 ? region.height : region.width,
                        region.degrees !== 0 ? region.width : region.height
                      );
                
                      const orig = new Rectangle(
                        0,
                        0,
                        region.originalWidth,
                        region.originalHeight
                      );
                
                      const offsetY = region.originalHeight - region.height - region.offsetY; // spine stores data from the bottom-left corner, and we need to adjust for that
                
                      const trim = new Rectangle(
                        region.offsetX,
                        offsetY,
                        region.width,
                        region.height
                      );
                
                      const texture = new Texture(
                        region.texture.texture,
                        frame,
                        orig,
                        trim,
                        Pixi.groupD8.N
                      );
                
                      Texture.addToCache(texture, region.name);
                    });
                };
                • Davide ha risposto a questo messaggio