• Runtimes
  • How to Load Embedded .skel and .atlas Files in Base64 in PixiJS + Spine?

Hi everyone,

I'm working on integrating a Spine animation in PixiJS, and everything works perfectly when I load the files normally like this:

PIXI.Assets.add({ alias: "spineboyData", src: "chibi-stickers.txt" });
PIXI.Assets.add({ alias: "spineboyAtlas", src: "chibi-stickers-pro.atlas" });
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);

const spineboy = spine.Spine.from({
skeleton: "spineboyData",
atlas: "spineboyAtlas",
scale: 0.5,
});

However, I need to embed the .skel and .atlas files as Base64 instead of loading them from external files. I tried converting the .skel binary file to Base64 and decoding it back to a Uint8Array, but it doesn't seem to work.

I also embedded the .atlas as a Base64 string inside a Blob and created an object URL, but Spine cannot find the region textures.
However, I need to embed the .skel, .atlas, and .png files in Base64 instead of loading them as external files.

For the .skel binary file, I tried converting it to Base64 and then decoding it back into a Uint8Array, but it doesn’t seem to work properly.
For the .atlas file, I embedded it as a Base64 string inside a Blob and created a URL object, but Spine can’t find the texture regions when loading the skeleton.
For the .png file, I know it must be converted into Base64, but I’m not sure how to correctly reference it inside the .atlas file.

My Questions:
How should I embed the .skel binary file in Base64? Do I need to change the way I load it in PixiJS?
How should I embed the .atlas file correctly? Does anything need to be modified to keep the texture references valid?
How do I reference a .png in Base64 inside the .atlas file? Should I replace the image name with the Base64 string directly, or is there another way?
If anyone has successfully done this, I’d really appreciate some guidance. Thanks in advance!

  • Davide ответили на это сообщение.
    Related Discussions
    ...

    jorgeds

    Currently, the Spine asset loaders do not allow passing a base64 version of the assets. However, you can bypass their usage by loading the base64 version of the assets separately and adding them to the Pixi Asset cache. The final step is to load the atlas images in the spine atlas instance.

    I have not yet validated this solution, but I will try it tomorrow. In any case, this is an interesting feature that we could potentially add. However, I first need to better understand the capabilities of Pixi Asset loaders. If it is feasible, I will open an issue on our GitHub tracker tomorrow.

    I just remember that I provided a solution for a user for Pixi v7. The idea is the same that I described above, but here you have also a code example, even though you have to adapt it to Pixi v8.

    Hi Davide,thank you!

    ✅ What I Have Done
    Converted my .json, .atlas, and .png to Base64.
    Loaded the assets using PIXI.Assets.add().
    Replaced chibi-stickers-pro.png in the .atlas file with "spineImage".
    Created a TextureAtlas from the .atlas file.
    Assigned the correct textures from PIXI.Assets.get().
    Initialized the Spine animation with spine.Spine.from(skeleton, textureAtlas, { autoUpdate: true }).
    🔴 The Problem
    I keep getting this error in the console:

    Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'length')
    at Bm.get (Assets.ts:732:34)
    at Spine.from (Spine.ts:892:29)
    It seems that either skeleton or textureAtlas is not properly loaded, but when I log them to the console, they appear to be valid.

    📜 My Current Code

    const atlas_base64 = "INSERT_YOUR_ATLAS_BASE64_HERE";
    const json_base64 = "INSERT_YOUR_JSON_BASE64_HERE";
    const sprite_base64 = "INSERT_YOUR_PNG_BASE64_HERE";

    (async function () {
    var app = new PIXI.Application();
    await app.init({
    resizeTo: document.body,
    backgroundAlpha: 0.0,
    });

    document.body.appendChild(app.view);
    
    // ✅ Load the Base64 PNG as a texture
    PIXI.Assets.add({
        alias: 'spineImage',
        src: `data:image/png;base64,${sprite_base64}`,
    });
    
    // ✅ Decode the Atlas and replace the original image name
    const decodedAtlas = atob(atlas_base64).replaceAll('chibi-stickers-pro.png', 'spineImage');
    const encodedAtlas = btoa(decodedAtlas);
    
    PIXI.Assets.add({
        alias: 'atlasTxt',
        src: `data:plain/text;base64,${encodedAtlas}`,
        loadParser: 'loadTxt',
    });
    
    // ✅ Load the JSON skeleton
    PIXI.Assets.add({
        alias: 'skeleton',
        src: `data:application/json;base64,${json_base64}`,
        loadParser: 'loadJson',
    });
    
    // 🟢 Load all assets
    const { atlasTxt: loadedAtlasTxt, skeleton } = await PIXI.Assets.load(['atlasTxt', 'skeleton', 'spineImage']);
    
    // 🚨 Debugging logs
    console.log("Checking if assets are loaded:");
    console.log("Skeleton:", skeleton);
    console.log("Atlas Text:", loadedAtlasTxt);
    
    if (!skeleton) {
        console.error("❌ Error: Skeleton asset is missing.");
        return;
    }
    
    if (!loadedAtlasTxt || loadedAtlasTxt.length === 0) {
        console.error("❌ Error: Atlas text is empty or failed to load.");
        return;
    }
    
    // ✅ Create the TextureAtlas
    const textureAtlas = new spine.TextureAtlas(loadedAtlasTxt, (path) => {
        console.log(`🔍 Looking for texture: ${path}`);
        return PIXI.Assets.get("spineImage")?.baseTexture || null;
    });
    
    PIXI.Assets.cache.set('atlas', textureAtlas);
    
    // 🚨 Check if TextureAtlas has pages
    console.log("Checking TextureAtlas:", textureAtlas);
    console.log("TextureAtlas Pages:", textureAtlas.pages);
    
    if (!textureAtlas.pages || textureAtlas.pages.length === 0) {
        console.error("❌ Error: TextureAtlas has no pages loaded.");
        return;
    }
    
    // ✅ Assign textures to the atlas pages
    for (const page of textureAtlas.pages) {
        const sprite = PIXI.Assets.get("spineImage");
        if (!sprite) {
            console.error(`❌ Error: Texture not found for page: ${page.name}`);
            continue;
        }
        console.log(`✅ Assigning texture to page: ${page.name}`);
        page.setTexture(spine.SpineTexture.from(sprite.baseTexture));
    }
    
    // 🟢 Create the Spine instance
    const spineboy = spine.Spine.from(skeleton, textureAtlas, { autoUpdate: true });
    
    spineboy.x = app.screen.width / 2;
    spineboy.y = app.screen.height / 2;
    spineboy.scale.set(0.5);
    
    // 🔹 Set animations
    spineboy.skeleton.setSkinByName("soeren");
    spineboy.skeleton.setSlotsToSetupPose();
    spineboy.state.setAnimation(0, "wave", true);
    
    // ✅ Add to stage
    app.stage.addChild(spineboy);

    })();

    🔍 What I Have Checked
    Skeleton is loaded correctly (confirmed in console logs).
    Atlas text is loaded and contains valid Spine atlas data.
    TextureAtlas is created, but the error still happens when calling Spine.from().

    The .skel file is a binary and Pixi default loaders aren't able to get it. So the solution is to use the json version of the skeleton or to manually get the skel binary daya.

    Please, have a look at the following code. I've used a 1px white square as texture to reduce the base64 size.
    I've used the Pixi DOMAdapter to fetch the skel binary data and manually added it to the cache. If you preferer to use the json version, uncomment the section above the skel file loading.

    (async () => {
        const app = new PIXI.Application();
        await app.init({
            width: window.innerWidth,
            height: window.innerHeight,
            backgroundColor: 0x2c3e50
        })
        document.body.appendChild(app.canvas);
    
        const skel = "/B8S/IqaXgYHNC4yLjM5wkgAAMJIAABCyAAAQsgAAELIAAAAAQRkb3QCBXJvb3QAAAAAAAAAAAAAAAA/gAAAP4AAAAAAAAAAAAAAAAAAAAAABGRvdAAAAAAAAAAAAAAAAABCyAAAQsgAAAAAAAAAAAAAAAAAAAAAAQRkb3QB//////////8BAAAAAAABAAEBACWwfdcAAAAAP4AAAD+AAAA/gAAAP4AAAAAAAQphbmltYXRpb24BAQABAQMAAAAAAP////8/gAAA/wAA/wBAAAAA/////wAAAAAAAAAAAA==";
        const atlas = "aW5saW5lLnBuZwpzaXplOjE2LDE2CmZpbHRlcjpMaW5lYXIsTGluZWFyCnBtYTp0cnVlCmRvdApib3VuZHM6MCwwLDEsMQo=";
        const sprite1_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAApJREFUeJxjZAAAAAQAAiFkrWoAAAAASUVORK5CYII="
        const json_base64 = "ewoic2tlbGV0b24iOiB7CgkiaGFzaCI6ICJzYXV5V3dzeXVmOCIsCgkic3BpbmUiOiAiNC4yLjM5IiwKCSJ4IjogLTUwLAoJInkiOiAtNTAsCgkid2lkdGgiOiAxMDAsCgkiaGVpZ2h0IjogMTAwLAoJImltYWdlcyI6ICIuL2ltYWdlcyIsCgkiYXVkaW8iOiAiLi9hdWRpbyIKfSwKImJvbmVzIjogWwoJeyAibmFtZSI6ICJyb290IiB9LAoJeyAibmFtZSI6ICJkb3QiLCAicGFyZW50IjogInJvb3QiLCAic2NhbGVYIjogMTAwLCAic2NhbGVZIjogMTAwIH0KXSwKInNsb3RzIjogWwoJeyAibmFtZSI6ICJkb3QiLCAiYm9uZSI6ICJkb3QiLCAiYXR0YWNobWVudCI6ICJkb3QiIH0KXSwKInNraW5zIjogWwoJewoJCSJuYW1lIjogImRlZmF1bHQiLAoJCSJhdHRhY2htZW50cyI6IHsKCQkJImRvdCI6IHsKCQkJCSJkb3QiOiB7ICJ3aWR0aCI6IDEsICJoZWlnaHQiOiAxIH0KCQkJfQoJCX0KCX0KXSwKImFuaW1hdGlvbnMiOiB7CgkiYW5pbWF0aW9uIjogewoJCSJzbG90cyI6IHsKCQkJImRvdCI6IHsKCQkJCSJyZ2JhIjogWwoJCQkJCXsgImNvbG9yIjogImZmZmZmZmZmIiB9LAoJCQkJCXsgInRpbWUiOiAxLCAiY29sb3IiOiAiZmYwMDAwZmYiIH0sCgkJCQkJeyAidGltZSI6IDIsICJjb2xvciI6ICJmZmZmZmZmZiIgfQoJCQkJXQoJCQl9CgkJfQoJfQp9Cn0="
    
        // create a map that maps the page name to the respective base64 image
        const spriteMap = {
            "inline.png": `data:image/png;base64,${sprite1_base64}`
        }
    
        // add the atlas pages to the assets to load
        const assetsToLoad = Object.keys(spriteMap).map(pageName => {
            PIXI.Assets.add({
            alias: pageName,
            src: spriteMap[pageName],
            });
            return pageName;
        })
    
        // add the base64 atlas to the pixi asset manager
        PIXI.Assets.add({
            alias: 'atlasTxt',
            src: `data:plain/text;base64,${atlas}`,
            loadParser: 'loadTxt'
        });
        assetsToLoad.push('atlasTxt');
    
        /////////////////////////////////
        //// JSON SKELETON LOADING START
        /////////////////////////////////
        // add the base64 skeleton to the pixi asset manager
        // PIXI.Assets.add({
        //     alias: 'skeleton',
        //     src: `data:application/json;base64,${json_base64}`,
        //     loadParser: 'loadJson'
        // });
        // assetsToLoad.push('skeleton');
        /////////////////////////////////
        //// JSON SKELETON LOADING END
        /////////////////////////////////
    
    
        /////////////////////////////////
        //// SKEL SKELETON LOADING START
        /////////////////////////////////
        // manually load .skel since there's not octet-stream loader
        const response = await PIXI.DOMAdapter.get().fetch(`data:application/octet-stream;base64,${skel}`);
        const buffer = new Uint8Array(await response.arrayBuffer());
        PIXI.Assets.cache.set("skeleton", buffer);
        /////////////////////////////////
        //// SKEL SKELETON LOADING END
        /////////////////////////////////
    
        // load the assets
        const { atlasTxt } = await PIXI.Assets.load(assetsToLoad);
    
        // initialize texture atlas and add it to the Pixi cache
        const textureAtlas = new spine.TextureAtlas(atlasTxt);
        PIXI.Assets.cache.set('atlas', textureAtlas)
    
    
        // loop through each atlas page
        for (const page of textureAtlas.pages) {
    
            // get the sprite associated to the name of this page
            const sprite = PIXI.Assets.get(page.name);
    
            // set the sprite to the page and all its regions
            page.setTexture(spine.SpineTexture.from(sprite.source));
        }
    
        // Create the spine display object
        const square = spine.Spine.from({skeleton: "skeleton", atlas: "atlas", scale: 0.5 });
    
        // Set the default mix time to use when transitioning
        // from one animation to the next.
        square.state.data.defaultMix = 0.2;
    
        // Set animation "run" on track 0, looped.
        square.state.setAnimation(0, "animation", true);
    
        // Center the spine object on screen.
        square.x = window.innerWidth / 2;
        square.y = window.innerHeight / 2 + square.getBounds().height / 2;
    
        // Add the display object to the stage.
        app.stage.addChild(square);
    })();

    For future references, if the playground won't work anymore, I have used the following versions of the library:

    <script src="https://cdn.jsdelivr.net/npm/pixi.js@8.4.1/dist/pixi.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@esotericsoftware/spine-pixi-v8@4.2.71/dist/iife/spine-pixi-v8.min.js"></script>

    Thanks, Davide! Your explanation was really helpful.

    • Davide ответили на это сообщение.

      jorgeds

      You're very welcome 🙂
      Since you are not the first user asking this, I've opened an issue to make this process easier.