• Runtimes
  • [Pixi-spine][2.1.14] Problem Mixing animations

Hi guys we had a problem when tried to mix 2 animations that use path constraints.

Animations work perfectly when they played initially only after switching from one to another bones that constrained to the path reset their position at 0 of the path. We have tried to set bones length and change rotation mode from Tangent to Chain did not help.

  • mixing works fine in preview view.

  • we use 3.8 spine version and 2.1.14 pixi-spine runtime

here is a 4 second video showcasing how it gets broken on frontend
https://drive.google.com/file/d/1E7BPeklBPsqoUnZEVQUDEGp97Om1iuQG/view

Related Discussions
...
  • 編集済み

I'll need assets + code to reproduce the issue. Maybe you can provide a minimal pixi example that shows the issue?

hi Mario. I am working with warmanw on this project.
We were able to create a much smaller version of the project and reproduce the issue.
It's deployed here for your convenience.
http://cupid-demo.s3-website-us-west-2.amazonaws.com/

We are building a spine player of sorts and need to be able to jump to any frame in the animation sequence, i.e. fast forward to a given time mark.
At a high level, we stop the pixi app ticker and jump to a given time by manually updating the spine asset instance by small increments, until it reaches the desired time mark.

Here is the code. Pls let us know if you need anything else. Thank you for your input!

import 'pixi.js';
import 'pixi-spine';

function fastForwardSpineInstance({
   pixiSpineInstance,
   // fast forward amount in seconds
   amount,
   deltaStep = 0.01,
}) {
   for (
      let currFastForwardAmount = 0;
      currFastForwardAmount < amount;
      currFastForwardAmount += Math.min(amount - currFastForwardAmount, deltaStep)
   ) {
      pixiSpineInstance.update(deltaStep);
   }
}

const mixDuration = 0.2;
const animationEnd = 0.3;
const timeScale = 2;
const tracks = [
   [{ anim: 'body/idle' }, { anim: 'body/shake' }],
   [
      { anim: 'head/face/talk/graphemes/a', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/b', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/l', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/u', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/w', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/a', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/b', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/l', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/u', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/w', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/a', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/b', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/l', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/u', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/w', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/a', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/b', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/l', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/u', animationEnd, timeScale },
      { anim: 'head/face/talk/graphemes/w', animationEnd, timeScale },
   ],
];

function jumpToTime({ time, pixiSpineInstance, ticker }) {
   pixiSpineInstance.state.setEmptyAnimations(0);
   pixiSpineInstance.lastTime = Date.now();
   ticker.update();

   tracks.forEach((track, trackIndex) => {
      pixiSpineInstance.state.setEmptyAnimation(trackIndex, 0);
      track.forEach((clip) => {
         const trackEntry = pixiSpineInstance.state.addAnimation(
            trackIndex,
            clip.anim,
            false,
            0,
         );
         trackEntry.mixDuration = mixDuration;
         if (clip.animationEnd) {
            trackEntry.animationEnd = clip.animationEnd;
         }
         if (clip.timeScale) {
            trackEntry.timeScale = clip.timeScale;
         }
      });
   });

   fastForwardSpineInstance({
      pixiSpineInstance,
      amount: time,
   });
   ticker.update();
}

export default function loadPixiApp(app, isPlayBroken) {
   function onAssetsLoaded(loader, resources) {
      const pixiSpineInstance = new PIXI.spine.Spine(
         resources.resource.spineData,
      );

  pixiSpineInstance.x = 400;
  pixiSpineInstance.y = 575;

  pixiSpineInstance.scale.set(0.4);
  app.stage.addChild(pixiSpineInstance);

  if (isPlayBroken) {
     // We need to be able to jump to any point in our sequence.
     // When we want to immitate a controlled playback, we stop the ticker
     // then jump to the next frame one by one.
     // That's when it breaks.
     // Without the code below, it doesn't break.

     let currentTime = 0;
     const targetTime = 3;
     const step = 1 / 30;
     const waitTimeInMs = 10;
     app.ticker.stop();
     setTimeout(function scheduleNextStep() {
        if (currentTime < targetTime) {
           jumpToTime({
              time: currentTime,
              pixiSpineInstance,
              ticker: app.ticker,
           });
           currentTime += step;
           setTimeout(scheduleNextStep, waitTimeInMs);
        }
     }, waitTimeInMs);
  } else {
     jumpToTime({ time: 0, pixiSpineInstance, ticker: app.ticker });
  }
   }

   app.loader.add('resource', 'cupid/Cupid.json').load(onAssetsLoaded);
}
4日 後

Sorry, I haven't gotten to this yet, I'll try to find time this week. My first guess is that your use of setTimeout interferes with the requestAnimationFrame() loop that pixi uses. Try to perform the forwarding inside the pixi rendering loop instead.

Hi Mario,

setTimeout is used only to demonstrate what happens when we pause the pixi ticker and then sequentially call the spineInstance.update function. We don't use it in out app. Various events can request the playback to jump to a given time.
Also, fast forwarding cannot happen inside pixi rendering loop because it's ticker is stopped.
Whatever causes this bug, we suspect has to do how path constraints are implemented in the pixi runtime. Our work around right now is to avoid path constraints. In that case the runtime functions as expected. Path constraints is a nice feature though, would be a shame if we can't use it.

I had a look and your code is correct. It seems the problem is indeed related to PathConstraint (and a handful of other classes).

The pixi-spine maintainers take our spine-ts core sources then heavily modify them and do not keep up to date. Things that we fixed in our official spine-ts runtime are sometimes not being ported to pixi-spine. For example:

This is the latest PathConstraint from our 3.8 branch, with an important fix in line 116:
https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/core/src/PathConstraint.ts#L115

This is the corresponding file in the latest pixi-js commit for their fork of the 3.8 runtime:
https://github.com/pixijs/spine/blob/master/packages/runtime-3.8/src/core/PathConstraint.ts#L70

It's missing the fix. Same here:
https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/core/src/PathConstraint.ts#L314
https://github.com/pixijs/spine/blob/master/packages/runtime-3.8/src/core/PathConstraint.ts#L266

Same issue here:
https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/core/src/PathConstraint.ts#L428
https://github.com/pixijs/spine/blob/master/packages/runtime-3.8/src/core/PathConstraint.ts#L377

There are a few other places I could find that differ as well, but that was all manual diffing, which is made hard because the pixi-spine authors stripped our license headers and changed whitespace and things like switching from our transform encoding to pixi's 2D affine transform class.

The gist is: pixi-spine 3.8 seems to lack a lot of fixes from the official runtime. It's impossible for me to say what else is missing, or if they made additional errors in "porting" our official runtime to their format. We do not have access to fix it. The only thing you can do is to approach the pixi-spine authors and ask them to help fix these issues.

Thanks Mario! We appreciate you going through the tedious trouble of finding the implementation bugs. At least now we know where the issue is coming from.

In the future, I may create an official pixi-spine runtime. No promises, and it will likely not have all the fancy things the 3rd party runtime has. But it will be kept up-to-date. Won't backport to 3.8 tho. I'm sorry this is causing you pain :/

??