azerion / phaser-spine

A plugin for Phaser 2 that adds Spine support
MIT License
121 stars 57 forks source link

[Spine-TS] Multipage Texture Bug Fix/Support #70

Closed shibekin69 closed 6 years ago

shibekin69 commented 6 years ago

@AleBles,

I don't know how this pull request thing works. Anyway, I'll put my code changes here. Like before, I applied this to the JS output. It's currently working well. Code isn't elegant so if you can integrate this into the main code base so it won't break anything, that'd be cool! I made it look for 20 pages and assigned filenames as key names since this is what the textureLoader is being fed from the atlas text file. Ex:

textureLoader(myanim.png); textureLoader(myanim1.png); textureLoader(myanim2.png); ...

Around Line 9217:

SpinePlugin.prototype.addSpineLoader = function () {
    Phaser.Loader.prototype.spine = function (key, url, scalingVariants) {
        var path = url.substr(0, url.lastIndexOf('.'));
        this.text(SpinePlugin.SPINE_NAMESPACE + key, path + '.atlas');
        this.json(SpinePlugin.SPINE_NAMESPACE + key, path + '.json');
        this.image(SpinePlugin.SPINE_NAMESPACE + key, path + '.png');

        //SHIBEKIN69 ADDITION: Load up spine parts. Use filenames as key names. Try to load up to 20 pages.
        this.image(key + '.png', path + '.png');
        try {
          for (var spicount = 2; spicount < 20; spicount++) {
            this.image(key + String(spicount) + '.png', path + String(spicount) + '.png');
          }
        } catch(e) {}
    };
};

Around Line 9300 - 9320:

    Spine.prototype.createSkeleton = function (key) {
        var _this = this;
        var atlas = new spine.TextureAtlas(this.game.cache.getText(PhaserSpine.SpinePlugin.SPINE_NAMESPACE + key), function (path) {
            if (_this.game.renderType === Phaser.CANVAS) {
                //return new PhaserSpine.Canvas.Texture(_this.game.cache.getImage(PhaserSpine.SpinePlugin.SPINE_NAMESPACE + key));
                //SHIBEKIN69 CHANGE: Use path instead.
                return new PhaserSpine.Canvas.Texture(_this.game.cache.getImage(path));
            }
            //return new PhaserSpine.WebGL.Texture(_this.game.renderer.gl, _this.game.cache.getImage(PhaserSpine.SpinePlugin.SPINE_NAMESPACE + key));
            //SHIBEKIN69 CHANGE: Use path instead. 
            return new PhaserSpine.WebGL.Texture(_this.game.renderer.gl, _this.game.cache.getImage(path));
        });
        var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
        var skeletonJson = new spine.SkeletonJson(atlasLoader);
        var skeletonData = skeletonJson.readSkeletonData(this.game.cache.getJSON(PhaserSpine.SpinePlugin.SPINE_NAMESPACE + key));
        return new spine.Skeleton(skeletonData);
    };
shibekin69 commented 6 years ago

Ok, this change worked for me:

In the function createCombinedSkin, around line 9400:

for (var key in skin.attachments) {
    var slotKeyPair = key.split(':');
    var slotIndex = parseInt(slotKeyPair[0]);
    //var attachmentName = slotKeyPair[1];
    //CHANGE
    var attachmentName = Object.keys(skin.attachments[key])[0];
    //var attachment = skin.attachments[key];
    //CHANGE
    var attachment = skin.attachments[key][attachmentName];
    console.log(key);
    console.log(attachment);
    if (undefined === slotIndex || undefined === attachmentName) {
        console.warn('something went wrong with reading the attachments index and/or name');
        return;
    }
    if (newSkin.getAttachment(slotIndex, attachmentName) !== undefined) {
        //CHANGE: IMPORTANT! COMMENT THESE OUT! Need duplicate attachments or I won't see anything.
        //console.warn('Found double attachment for: ' + skinName + '. Skipping');
        //continue;
    }
    newSkin.addAttachment(slotIndex, attachmentName, attachment);
    console.log(newSkin);
}
AleBles commented 6 years ago

@shibekin69 Could you add a simple multipage export that I can add to examples?

shibekin69 commented 6 years ago

@AleBles just the output with multiple pages right?

shibekin69 commented 6 years ago

BTW: The rough fix above will produce a lot of console errors, since it tries to load atlas page images that may or may not be there for each spine load. So this'll definitely need a more elegant fix.

@Simple example of multipage output: The Spine project and output is inside this directory. It's a simple Spine animation consisting of 8 circles of different colors. I set the atlas page size to a max of 512px. Since each circle is 300px, the output will be forced to put each of the circles in separate pages.

multipage-test.zip

shibekin69 commented 6 years ago

I have something that goes through the textureAtlas, but I couldn't complete this solution since I didn't know how to call the game object or pass the appropriate stuff to the textureLoader. Maybe you can make use of it. I commented out my code here:

TextureAtlas.prototype.load = function (atlasText, textureLoader) {
  if (textureLoader == null)
    throw new Error("textureLoader cannot be null.");
  var reader = new TextureAtlasReader(atlasText);
  var tuple = new Array(4);
  var page = null;
  /*
  var pagekey = null;
  var url = null;
  var path = null;
  */
  while (true) {
    var line = reader.readLine();
    if (line == null)
      break;
    line = line.trim();
    if (line.length == 0)
      page = null;
    else if (!page) {
      page = new TextureAtlasPage();
      page.name = line;
      if (reader.readTuple(tuple) == 2) {
        page.width = parseInt(tuple[0]);
        page.height = parseInt(tuple[1]);
        reader.readTuple(tuple);
      }
      reader.readTuple(tuple);
      page.minFilter = spine.Texture.filterFromString(tuple[0]);
      page.magFilter = spine.Texture.filterFromString(tuple[1]);
      var direction = reader.readValue();
      page.uWrap = spine.TextureWrap.ClampToEdge;
      page.vWrap = spine.TextureWrap.ClampToEdge;
      if (direction == "x")
        page.uWrap = spine.TextureWrap.Repeat;
      else if (direction == "y")
        page.vWrap = spine.TextureWrap.Repeat;
      else if (direction == "xy")
        page.uWrap = page.vWrap = spine.TextureWrap.Repeat;
      page.texture = textureLoader(line);
      page.texture.setFilters(page.minFilter, page.magFilter);
      page.texture.setWraps(page.uWrap, page.vWrap);
      page.width = page.texture.getImage().width;
      page.height = page.texture.getImage().height;

      /*
      console.log('SPINE PAGES: ');
      console.log(textureLoader);
      console.log(this.pages);
      if (vnManager.gamestate.game.cache.checkImageKey(PhaserSpine.SpinePlugin.SPINE_NAMESPACE + page.name.substr(0, page.name.lastIndexOf('.')) )) {
        pagekey = PhaserSpine.SpinePlugin.SPINE_NAMESPACE + page.name.substr(0, page.name.lastIndexOf('.'));
        console.log(vnManager.gamestate.game.cache._cache.image[pagekey]);
        url = vnManager.gamestate.game.cache._cache.image[pagekey].url;
        path = url.substr(0, url.lastIndexOf('/'));
      }
      console.log(page.name);
      console.log(path + '/' + page.name);
      vnManager.gamestate.game.load.image(page.name, path + '/' + page.name);
      vnManager.gamestate.game.load.start();

      page.texture._image.currentSrc = page.texture._image.baseURI + path + '/' + page.name + '?v=' + parseInt(Math.random() * 1000000);
      */

      this.pages.push(page);
    }
    ...
AleBles commented 6 years ago

I've just comitted a solution that doesn'y modify the original spine-ts code, you're welcome to try it out =)

shibekin69 commented 6 years ago

@AleBles EDIT: Ignore this comment. Please see below instead.

I got feedback on this one. It seems to work in general, but I think it also tries to load up some similarly named images in the cache, which it isn't supposed to (I'm getting some Phaser.Loader errors). For example, if my spine output project is named:

xxxxxbase01.json xxxxxbase01.png xxxxxbase01.atlas

I see Phaser.Loader errors where it's trying to load other files that's kind of similar in terms of file name that's already in the cache. Files like:

xxxxxtunic.png xxxxxdress.png xxxxxarmor.png xxxxxbikini.png

etc...

When it's supposed to load in only multipage files following the output format:

[filename][pagenumber].png

xxxxxbase012.png <- Page 2 xxxxxbase013.png xxxxxbase014.png ... xxxxxbase019.png xxxxxbase0110.png <- Page 10 xxxxxbase0123.png <- Page 23

Maybe you can just check the entire filename of the first file?

shibekin69 commented 6 years ago

Ah hold on. Let me look into my stuff more. I think I know what's causing this.

shibekin69 commented 6 years ago

I'll investigate this one further, but so far from the loader errors I've seen, when I load multiple spine projects into the game, it thinks all of them are in the same directory as the first one. So for example, if I organize my spine files like these:

CharA/charaspine.json CharA/charaspine.atlas CharA/charaspine.png

CharB/charbspine.json CharB/charbspine.atlas CharB/charbspine.png CharB/charbspine2.png

CharC/charcspine.json CharC/charcspine.atlas CharC/charcspine.png CharC/charcspine2.png

It tries to load charbspine and charcspine images using CharA's directory

shibekin69 commented 6 years ago

O, I think I figured it out. Please see proposed fix below. Added stuff are below the //CHANGE comment lines. I fixed it up to use a corrected path variable. Then I made it check for just the filename against the current file that's just been loaded if they're from the same spine files, if so, attempt to load whatever filenames are in the atlas using the path.

SpinePlugin.prototype.addSpineLoader = function () {
    Phaser.Loader.prototype.spine = function (key, url, scalingVariants) {
        var _this = this;
        var path = url.substr(0, url.lastIndexOf('.'));
        //CHANGE Added pathonly and filenameonly. (use substring here, this works. substr doesn't want to stop at the last period.)
        var pathonly = url.substr(0, url.lastIndexOf('/'));
        var filenameonly = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'));

        this.text('atlas_' + SpinePlugin.SPINE_NAMESPACE + '_' + key, path + '.atlas');
        this.json(SpinePlugin.SPINE_NAMESPACE + key, path + '.json');
        this.onFileComplete.add(function (progress, name) {
            if (name.indexOf('atlas_spine_') === 0) {
                var atlas = _this.game.cache.getText(name);
                var firstImageName = null;
                atlas.split(/\r\n|\r|\n/).forEach(function (line, idx) {
                    if (line.length === 0 || line.indexOf(':') !== -1) {
                        return;
                    }
                    if (firstImageName === null) {
                        firstImageName = line.substr(0, line.lastIndexOf('.'));
                    }
                    if (firstImageName !== null && line.indexOf(firstImageName) !== -1 && line.indexOf('.') !== -1) {
                        //this.image(line, url.substr(0, url.lastIndexOf('/') + 1) + line);
                        //CHANGE
                        if (filenameonly === name.replace('atlas_spine_', '')) {
                          this.image(line, pathonly + '/' + line);
                          console.log('SPINE PATH: ');
                          console.log(pathonly + '/' + line);
                        }
                    }
                }.bind(_this));
            }
        });
    };
};
AleBles commented 6 years ago

@shibekin69 this fix doesn't work when you load up the spine with a different keyname than the filename (see the multipart-atlas example)

shibekin69 commented 6 years ago

Ah Hmm..

I'm relying on comparing the filename against the generated atlas_spine_key text name assuming they're the same. Hm..

The URL contains the directory path to the json file..

key: alice path: /image/spine/alice/alice.json

So we need to take note of the path and filename for comparison later when it looks for atlas_spine_keyname:

pathonly: /image/spine/alice/ filenameonly: alice

comparison: alice === (atlasspine)alice <-- WILL WORK

So the problem is, the keyname can be different from the filename.

key: alicegiant path: /image/spine/alice/alice.json

pathonly: /image/spine/alice/ filenameonly: alice

comparison: alice === (atlasspine)alicegiant <-- WILL MISS THIS ONE.

What if we just do the comparison for both the filename and the keyname?

comparison: filenameonly === (atlasspine)keyname || keyname === (atlasspine)keyname

shibekin69 commented 6 years ago

How about this? This one compares both the filename (without the extension) and the keyname against (atlasspine)keyname.

Proposed solution: Change the above line:

if (filenameonly === name.replace('atlas_spine_', '')) {

to

if (filenameonly === name.replace('atlas_spine_', '') || key === name.replace('atlas_spine_', '')) {

SpinePlugin.prototype.addSpineLoader = function () {
    Phaser.Loader.prototype.spine = function (key, url, scalingVariants) {
        var _this = this;
        var path = url.substr(0, url.lastIndexOf('.'));
        //CHANGE Added pathonly and filenameonly. (use substring here, this works. substr doesn't want to stop at the last period.)
        var pathonly = url.substr(0, url.lastIndexOf('/'));
        var filenameonly = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'));

        this.text('atlas_' + SpinePlugin.SPINE_NAMESPACE + '_' + key, path + '.atlas');
        this.json(SpinePlugin.SPINE_NAMESPACE + key, path + '.json');
        this.onFileComplete.add(function (progress, name) {
            if (name.indexOf('atlas_spine_') === 0) {
                var atlas = _this.game.cache.getText(name);
                var firstImageName = null;
                atlas.split(/\r\n|\r|\n/).forEach(function (line, idx) {
                    if (line.length === 0 || line.indexOf(':') !== -1) {
                        return;
                    }
                    if (firstImageName === null) {
                        firstImageName = line.substr(0, line.lastIndexOf('.'));
                    }
                    if (firstImageName !== null && line.indexOf(firstImageName) !== -1 && line.indexOf('.') !== -1) {
                        //this.image(line, url.substr(0, url.lastIndexOf('/') + 1) + line);
                        //CHANGE Only load up atlas images if filename or keyname matches text atlas key [atlas_spine_keyname] are of the same spine project
                        //Assumes each spine project is in its own separate directory. Filename or keyname must match text atlas key! 
                        if (filenameonly === name.replace('atlas_spine_', '') || key === name.replace('atlas_spine_', '')) {
                          this.image(line, pathonly + '/' + line);
                          //console.log('SPINE PATH: ');
                          //console.log(pathonly + '/' + line);
                        }
                    }
                }.bind(_this));
            }
        });
    };
};
AleBles commented 6 years ago

Appears to work, thanks dude!