pixijs / spine

Pixi.js plugin that enables Spine support.
Other
569 stars 217 forks source link

Load spine atlas with texture iso image #169

Open qinxgee opened 7 years ago

qinxgee commented 7 years ago

Hi!

I would like to use a spine animation with a sprite sheet texture that has been loaded earlier. I have an atlas file, but the image referenced in it is not the sprite sheet that I want to use. Can I load the atlas, but use the texture instead of the referenced file? I have read the docs on how to load a spine animation without an atlas, but I'd still need the regions that are defined in the atlas. Can I add those somehow? Or can I load the spine with atlas and then change the sprite sheet?

Little bit of background: the original animation uses a PNG, but it is very big. JPG would be more suitable, but I need transparency. So instead I want to load the transparencies in one image (PNG) and the textures in another (JPG). This would reduce the download size to about 1/3rd. With additive blending, the resulting texture is exactly what I want. In the past I've just put two animations on top of each other, so that the result is correct, but that only seems to work when the animation does not use any fading (alpha has to be either 0 or 1).

ivanpopelyshev commented 7 years ago

Congratulations! You found the case we havent cover yet.

1.

Please look at https://github.com/pixijs/pixi-spine/blob/master/src/loaders.ts . As you see, "imageLoaderAdapter" is fixed, but i think we can add custom function in "imageOptions" that will be used instead. Would you to do that and rebuild and test pixi-spine? Or do you want me to do it?

Please specify your target system. If its desktop WebGL, you can use gzipped dds instead of png, and then https://github.com/pixijs/pixi-compressed-textures will help you. You can even use crunch compression if you arent afraid of extra time for unpacking on js side. However, that way alpha wont work like before, we have to add "non-premultiplied" blendmodes into pixi first. Also, texture has to be "prepared" for it, we need to extrude colors from opaque pixels to alpha pixels.

ivanpopelyshev commented 7 years ago

OK, try latest pixi-spine from "bin" folder, and this thing: https://github.com/pixijs/pixi-spine/blob/master/examples/preload_atlas_image.md

If it works for you, I re-publish pixi-spine in npm

qinxgee commented 7 years ago

Wow that was fast... I'm going to need more time to try that out. Thanks!

1) I can certainly have a go at adding this functionality, but I have not used type script before. No better time to start than the present, but maybe not when it is affecting so many people ;-)

2) Target is both mobile and desktop, but mainly mobile. I will still have a look at your suggestion because we are using different resources depending on platform, and there is nothing wrong with limiting the download size on desktop. So we can still try and use gzip there.

ivanpopelyshev commented 7 years ago

OK, then do 2., that'll work better than png+jpg. And for 1. you can try that thing i've already made in previous comment.

lucap86 commented 7 years ago

how to use this in https://github.com/pixijs/pixi-spine/blob/master/examples/preloaded_json.md

ivanpopelyshev commented 7 years ago

@lucap86 line 5

callback(myAweSomeBaseTextureThatWasGeneratedOrPreloaded)

You dont need latest version for that.

lucap86 commented 7 years ago

you made my day :)

ivanpopelyshev commented 7 years ago

@qinxgee Im working on better solution, I want to add your algorithm (png+jpg) to pixi loader itself.

qinxgee commented 7 years ago

OK, try latest pixi-spine from "bin" folder, and this thing: https://github.com/pixijs/pixi-spine/blob/master/examples/preload_atlas_image.md

Hi!

i have just done this but I'm thinking that I'm maybe not understanding what is going on exactly when I do this. This is now my advancedImageLoader:

    advancedImageLoader : function(loader, namePrefix, baseUrl, imageOptions) {
        if (baseUrl && baseUrl.lastIndexOf("/") !== (baseUrl.length - 1)) {
            baseUrl += "/";
        }

        return function(line, callback) {
            var lineAlpha = line.replace('.jpg', '.png');
            var name = namePrefix + line;
            var url = baseUrl + line;
            var urlAlpha = baseUrl + lineAlpha;

            loader.add(name, url, imageOptions, function(resource) {
                var container = new PIXI.Container(),
                    alphaSprite = new PIXI.Sprite.fromImage(urlAlpha),
                    textureSprite = new PIXI.Sprite.fromImage(url),
                    renderTexture;

                textureSprite.blendMode = PIXI.BLEND_MODES.ADD;
                container.addChild(alphaSprite);
                container.addChild(textureSprite);

                renderTexture = PIXI.RenderTexture.create(container.width, container.height);
                renderer.render(container, renderTexture);

                callback(renderTexture.baseTexture);
            })
        }
    }

(renderer is defined elsewhere in my code).

I create two new sprites, one with the alpha values, one with the textures, put them in a container and render the container back to a texture. Instead of now returning the resource's texture's baseTexture, I return the baseTexture of the rendered one.

As far as I can tell, the animation ends up using the original texture. I have switched the two sprites in the container in which case I'd expect to see a black screen (the silhouettes from the PNG on top, and the black background from the JPG showing through the transparent parts). But instead I'm seeing the original picture.

ivanpopelyshev commented 7 years ago

Do you use latest "pixi-spine.js" from "bin" folder? Make sure this line exist: https://github.com/pixijs/pixi-spine/blob/master/bin/pixi-spine.js#L5363 . It takes different fields from metadata and tries to create a loader out of them.

I didn't test it yet, I did add "metadata.imageLoader" there

qinxgee commented 7 years ago

I downloaded pixi-spine.js from master. The code is identical. I should have more time in a bit, I'll add a means for me to verify that the rendered texture is actually what I expect it to be.

ivanpopelyshev commented 7 years ago

then put breakpoint in your advancedImageLoader. If its not called, then i screwed up somewhere.

qinxgee commented 7 years ago

advancedImageLoader is called, so I need to check my (rendered) texture.

ivanpopelyshev commented 7 years ago

Are you sure its ADD and not multiply? I think you need different blendMode. Im adding support for gl.blendFuncSeparate to pixi in https://github.com/pixijs/pixi.js/pull/4033

qinxgee commented 7 years ago

I don't think it is your code, it is something with my pixi renderer: if I display the container with the two sprites it looks as I would expect: like the JPG, but with the transparency from the PNG. When I create a sprite from the renderTexture, the image is back to being opaque.

ivanpopelyshev commented 7 years ago

Try "render(container, renderTexture, false)". Pixi clears renderTexture with the "backgroundColor" of your renderer by default, but if we specify clear=false, it might work.

qinxgee commented 7 years ago

I tried both true and false, but it does not seem to make a difference. The 'transparent' option when setting up the renderer also does not seem to change anything, but I must admit that I am not sure what that setting is supposed to do.

ivanpopelyshev commented 7 years ago

I'm sure ADD cant combine alpha=1 and alpha=1 to alpha=0. How exactly that png looks, can you give me it?

qinxgee commented 7 years ago

I tried m=both ADD and MULTIPLY, but MULTIPLY made the picture pretty much disappear (since the texture contains a lot of black as well: multiply by 0, means 0.

Now I'm not a graphics guy by any stretch of the word, but how it was explained is that you add 0 from the JPG to transparency from the PNG, you should end up with transparency. When you add any colour (0 - FF) from the JPG to the solid black of the PNG, you end up with whatever colour was in the JPG.

I cannot give you the actual artwork (my boss would not be happy), but I can provide a similar idea (just a wallpaper that I downloaded from the internet and messed about with). The idea is similar: just add both sprites to a container, put an ADD mode on the JPG, and you should end up with a transparency of the JPG. When I change the blend mode to MULTIPLY, I end up with a solid black container.

flower flower

ivanpopelyshev commented 7 years ago

No, unfortunately that's not how blendmodes work. When you ADD alpha=0 and alpha=1 it will end with alpha=1. I dont even think that multiply can take alpha out. There are no blendmodes in curernt version of pixi that will work on that.

I have few ideas in my mind, but they are all too hacky for your case - they are using pure webgl, I wont be able to explain that. You can ask me to implement them right away if you need it working right now/

Otherwise, I'll add special blendmode after https://github.com/pixijs/pixi.js/pull/4033 gets approved, hope it will be tomorrow.

qinxgee commented 7 years ago

That is a shame... it's odd that it would work with plain containers (see those work on my screen right now), but maybe the renderer just isn't able to keep the transparency when rendering a DisplayObject? I have left a question about that at the pixi.js hub, in a thread that someone started there with seemingly a similar issue.

Anyway, as a fallback we always have the original PNGs to work with. It'll be a hefty download, but we'll make it work. And I'm keeping an eye our for your work. We still have a some time before feature complete to work on this :-) (not to mention future projects)

Thanks a ton for all your help!

ivanpopelyshev commented 7 years ago

It cant work with plain containers. Unless you are using something like VoidFilter on it. Do you? I know how to make that solution with less memory, for mobiles, but I said that its very hacky in current version of pixi, I'm trying to introduce some changes right now :)

qinxgee commented 7 years ago

Ehhm... no... here's the entire code that I use to display it on top of my game:

                var container = new PIXI.Container(),
                    alphaSprite = new PIXI.Sprite.fromImage(urlAlpha),
                    textureSprite = new PIXI.Sprite.fromImage(url);

                textureSprite.blendMode = PIXI.BLEND_MODES.ADD;
                container.addChild(alphaSprite);
                container.addChild(textureSprite);
                layer.addChild(sprite);

"layer" is our top rootcontainer that gets rendered to the screen. I attached a screen shot. The white that you see is an underlying pixi graphics rectangle, the rest is a combination of the jpg and the png...

screenshot

ivanpopelyshev commented 7 years ago

Make it green and not white.

qinxgee commented 7 years ago

Would that help? The white (or green) is not part of the container. If I make it part of the container and try to render it, the resulting texture is solid white (or solid green).

ivanpopelyshev commented 7 years ago

Ah, suddenly I understand how it works. First png draws black on the place where flower will be drawn, and then you paste jpg on it with ADD. It won't work for alpha=0.5 properly. And its not associative operation, it cant be just put into renderTexture, it needs something below it. I'll think about how to help you tomorrow.

qinxgee commented 7 years ago

Yes, that is how it works. I was hoping to create a full texture, and let the animation deal with that, just as it would deal with a texture that was loaded directly from a png. I'm a little bit worried about the "it will not work for alpha = 0.5". The pasting of two animations on top of each other also doesn't work for alpha different than 0 or 1, and I was hoping to solve that by rendering the combined texture first, then use that in an animation.

ivanpopelyshev commented 7 years ago

We can solve it. It just requires a bit of changes in pixi, or huge hacks from my side, and I'm kinda tired after working day. Anyway, you shown me a good trick i totally forgot about.

gunbaranai commented 5 years ago

@lucap86 line 5

callback(myAweSomeBaseTextureThatWasGeneratedOrPreloaded)

You dont need latest version for that.

sorry for the necro, how do i add multiple images like in this example? it has two .png files, and i would like to do similar thing but with preloaded textures. i'm pretty new to pixi (perhaps js too), i'm not sure how the iteration works inside the loader

ivanpopelyshev commented 5 years ago

i'm pretty new to pixi (perhaps js too), i'm not sure how the iteration works inside the loader

Looks like this is your case: https://github.com/pixijs/pixi-spine/blob/master/examples/preload_atlas_image.md

There are too many ways to load things in pixi-spine and we moved it to examples folder. When someone adds one more good way I accept his changes and add his example to the folder.

Also prepare to debug https://github.com/pixijs/pixi-spine/blob/master/src/loaders.ts if something goes wrong.

Its also the reason why PixiJS doesnt grow to framework, we dont have enough resources to maintain code like that and it'll be like 90% of framework code ;)

gunbaranai commented 5 years ago

Looks like this is your case: https://github.com/pixijs/pixi-spine/blob/master/examples/preload_atlas_image.md

There are too many ways to load things in pixi-spine and we moved it to examples folder. When someone adds one more good way I accept his changes and add his example to the folder.

I can't seem to produce the result I'm looking for, the console still said it tried loading from the filename provided inside the atlas file.

Perhaps I should've mentioned this before. I'm trying to load an atlas, which provided with two placeholder images, and I want to link it to the correct texture URLs, outside the base URL.

Thanks in advance.

ivanpopelyshev commented 5 years ago

It should work.

Try again, debug the loader.

Sorry, my telepathy doesn't work in this case anymore :( Reproduce your bug in the demo so I can help you.

gunbaranai commented 5 years ago

I tried it as such

let atlasLoaderOptions = { metadata: {
    images: {
        "page_0": PIXI.BaseTexture.fromImage(textureLinkOne),
        "page_1": PIXI.BaseTexture.fromImage(textureLinkTwo)
    }
}};

PIXI.loader
    .add('skeleton', spriteJsonLink)
    .add('atlas', spriteAtlasLink, atlasLoaderOptions)
    .load(onAssetsLoaded);

After that, in the renderer, I tried adding an atlas loader via spine.core.AtlasAttachmentLoader and SkeletonJson as in this example. Am I getting the flow wrong? I'm also unsure how to pass the image inside the TextureAtlas function since there are two images that I want to include inside the atlas (not to mention the atlas already has placeholder image names so I'm afraid it looked up into that as priority as well).

ivanpopelyshev commented 5 years ago

You didnt reproduce the example, you've added one more resource into loader,

ivanpopelyshev commented 5 years ago

There's line parameter in the function you pass to TextureAtlas. Can you please try to place breakpoint there and see what value has that parameter? ;)

gunbaranai commented 5 years ago

Yes, because it's what I originally wanted to do since all four files (.json, .atlas, and two .pngs) are scattered around folders (specifically in my case, URLs). That's why I load them separately, and I think adding line parameter inside the renderer doesn't work? The error happens right after I loaded the JSON file. Which is why I thought the order of my code is wrong. Here's the rest of the code.

let atlasLoaderOptions = { metadata: {
    images: {
        "page_0": PIXI.BaseTexture.fromImage(spriteTextureLink),
        "page_1": PIXI.BaseTexture.fromImage(spriteWeaponLink)
    }
}};

// load spine data
PIXI.loader
    .add('skeleton', spriteJsonLink)
    .add('atlas', spriteAtlasLink, atlasLoaderOptions)
    .load(onAssetsLoaded);

let sprite = null;

function onAssetsLoaded(loader, res) {
    let rawSkeletonData = JSON.parse('skeleton');
    let rawAtlasData = 'atlas';
    let spineAtlas = new PIXI.spine.core.TextureAtlas(rawAtlasData, function(line, callback) {
        // pass the image here.
        callback(PIXI.BaseTexture.fromImage(line));
    }); // specify path, image.png will be added automatically

    let spineAtlasLoader = new PIXI.spine.core.AtlasAttachmentLoader(spineAtlas)
    let spineJsonParser = new PIXI.spine.core.SkeletonJson(spineAtlasLoader);

    // in case if you want everything scaled up two times
    spineJsonParser.scale = 2.0; 

    let spineData = spineJsonParser.readSkeletonData(rawSkeletonData);
    console.log(spineData);
    // now we can create spine instance
    sprite = new PIXI.spine.Spine(res.skeleton.spineData);

   app.start();
}

Again, I'm sorry because my brain is failing me, I'm quite new to this.

ivanpopelyshev commented 5 years ago

another atlas will be loaded from skeleton.json file because you didnt specify anything that helps the code to connect first json file to that atlas you load.

Also

let rawSkeletonData = JSON.parse('skeleton');
let rawAtlasData = 'atlas';

You should put raw text here, but that means you dont hae to load json files.

It looks like you are just copy-pasting the code without trying to debug it. You have long way ahead before you make it work.

PixiJS has low threshold for newbies, but to get something non-standard you have to know how to debug async functions, its not a gamemaker.

If you really put JSON.parse('skeleton') in your code then you don't know javascript, is that the case?

What's your background? Maybe I can use terms from things that you used before :)

ivanpopelyshev commented 5 years ago

OK, so, right now there are 10 things that can go wrong when you try to combine pixi-spine hacks. Usually users solve 8-9 on their own using devtools debugger and i fill up 1-2 with my telepathy skills :)

In case you cant solve it, there's a general list of how to unstuck something that you dont understand:

  1. Make a separate project based on https://pixijs.io/examples-v4/#/plugin-spine/dragon.js
  2. Add your files in the directory, but do not add any hacks there.
  3. Zip everything
  4. Write down what you want in separate pixi-spine issue, include zip file or codepen/pixi-playground/whatever link.
  5. Wait when someone who knows pixi-spine has free time to help you

While you wait, you can work on other things for your projects, things that I hope are less difficult than pixi-spine-hacky-community-collection.

If you think that you require more help from PixiJS community, I can invite you to PixiJS slack where many people can help you. Of course, every time you feel like you dont understand what are they saying you'll have to use the "how to unstuck" list I provided you :)

Also necroing threads is usually fine if it ends in several comments, so dont worry about it.

gunbaranai commented 5 years ago

This is my actual code (I tried JSON.parse before but seems like there was a synchronizing conflict of sort), I don't know if I'm putting the res.atlas correctly, because I'm not sure how to parse it. The error says it can't split the text.

$.ajax({
        url: spriteJsonLink,
        async: false,
        dataType: 'json',
        success: function(data){
            let rawSkeletonData = data;
        }
    })
    let rawAtlasData = res.atlas;
    var spineAtlas = new PIXI.spine.core.TextureAtlas(rawAtlasData, function(line, callback) {
        // pass the image here.
        callback(PIXI.BaseTexture.fromImage(line));
    });

And yes, I'm still learning about JS too because I've only worked with easier game engines so far (such as Phaser) and english isn't my native tounge.

Also it would be my pleasure to get an invite to a PixiJS slack.

ivanpopelyshev commented 5 years ago

Using jquery to load raw data for both atlas and skeleton, then passing it to https://github.com/pixijs/pixi-spine/blob/master/examples/preloaded_json.md can work too. owever your code isn't clear for me, I dont understand whether you wanted to make rawSkeletonData and where do you get res.atlas. Please make a demo in separate issue, otherwise we'll spend 10 more posts ping-ponging like that.

And yes, I'm still learning about JS too because I've only worked with easier game engines so far (such as Phaser) and english isn't my native tounge.

Invite sent to your email.