Shenmue Animation Debug Thread

One small improvement: added some axis helpers to the viewport to make it easier to track forward, back, left, right, to act as a reference for working with the lookup between motn and mt5 bone numbers.

quality_of_life.JPG

And then we run into a problem with interpolation.

Screenshot_2020-11-07 Shenmoo.png

I started to fill in the table with linear interpolation to be able to fill in the table and not worry too much about the easing functions while we're working with debugging. The problem is that I'm an idiot and the table is missing some values that should be there, so I need to figure out what's causing that inside my function.

But a little bit of a bigger problem is that not every value has a first frame and end frame value, which kind of leaves what goes in between there up to interpretation. I tried to look at a few other animations, only to find that my animation parsing function breaks on a lot of animation, specifically around block 4. So to be able to contrast with other animations, I should probably take some more animation samples, and trace through them. That should give some other animations to compare with to see how the data is laid out in the table.
 
Added some more debug tools to the Shenpoo repository. At this point LemonHaze's plugin might make a lot of this obsolete, but hopefully tools like this make it easier to analyze and break down the animations.

Screenshot_2020-11-12 Shenpoo.png

For the viewer I added a small feature that let's you select which frame of an animation you want to look at. Right now all that gets moved is root bone position, base y position, and the right foot and left foot targets. We're going to need to implement the solver for the feet, hands and torso. The idea behind this approach is that it allows for fine tune control to be able to take a given frame, solve for all of the rotations in the bones. And then be able to play that back as a normal animation.

Screenshot_2020-11-12 Shenpoo(1).png

For the block 4 thing i think it's "fixed". And the reason ended up being that the uh, the animation format is kind of stupid. Block 2 gives the number of in-between keyframes. And then block 3 specifically defines the key frame numbers. And then block 4 provides a list of bit flags that define how to read the block 5 values for each one of the frames.

The weird part is that the way they do this you'd think every frame declared would have a value. For instance if you have an animation with 37 frames, then you have an implicit frame for frame 0 and 36. You then declare 3 in-between frames for something like 10, 20, 30. Which gives you [ 0, 10, 20, 30, 36 ]. Block 4 then defines bit flags to give a value or easing property for each one of these values. The problem is that it doesn't. Sometimes in block 4 there are no bits defined for a frame, which means no values. Which means the frame might as well not be defined at all.

I have no idea why that is, or what's going on. All I can say is that the values that are exported to the table look correct. And that the script I wrote to isolate different parts of the animation in the game works based on this interpretation. So right now all I can do is assume that I we have all of the data that we're going to get out of the motn file.

The concerning part is that it's not enough for an animation. For axis that don't have anything defined, I think the correct approach is to use the default bone value, and just plug it in there, and it doesn't have an effect (highlighted in yellow). The part that I'm concerned about is the red parts. Where the game often doesn't define any start or end frames, but defined middle frames.

And the reason I think that is, is probably the game has cross-fading built into it's animation system. And what I mean by that is that the game's programmer / director probably wanted the movement to flow. So that one action leads to another. And probably specifically when using IK, you're not moving the character directly but a target. For somethings like the hands, if you have a target for the beginning and end frame on all of the animations, it would probably cause some jankiness.

I think what this means is the developers decides to take the option to have one animation transition into the next. So for the walking animation, the devs probably repeated the flipped version of the walk animation. Which might be where the missing values for the beginning and end are, if they calculate from the last keyframe of the previous animation, into the first keyframe of the next animation.

If this is really the case, it would imply a lot of suck on the aspect of making the tool. As I might need to provide a "previous animation", or "next animation" option to be able to calculate the missing frame values. What these means for the moment is that I will cheat and hard code in values leading into the next walk cycle to fill in each of the x,y,z values needed to calculate a position for a frame.
 
I have two main concerns at the moment. The first is the spline/bezier interpolation. Not because it's important, but mostly because it's vexing. In terms of pragmatic application, linear interpolation is going to be fine, it's mostly the prevalence of the easing values in the animation. This combined with the fact that I'm stupid to interpret these values causes my brain to wonder back to this topic pretty often. Right now, it's not a high priority so I will continue to put it off.

The second issue is the confidence in the animation values themselves. Right now what we can say about the values being read from the animation is that they seem to match up with the animation format. Meaning that when we read through the values, we end up of at the end of each respective block, and things seem to "fit". If we have the wrong interpretation things would be way off. The some what concerning part about the animation values, is they don't seem to be as "full" as I would like. And it could be that's an intentional move by the developers for built-in blending.

What we can say about the values is that right now they definitely seem "good-enough". As they seem to fit in with the expected parameters of the model. Meaning the values seem to have the correct side of the body, seem to be about the right height, and generally seem to be realistic values for what you would expect. So right now I want to set these two issues aside and jump into interpretation and start replication the IK functions in a clean environment where we can control what's happening at each frame. That way we can also fine-tune and control the rotations for each frame.

Basically jumping ahead and doing this gives us some really easy debug tools for viewing the animations. And then we can use these to improve the two issues mentioned above, as it would give us something pretty easy to check against.

Screenshot_2020-11-14 Screenshot.png

As luck would have it there is an example program that is really close to what suites our needs. The live version is here: https://yosipy.github.io/Threejs_IK_Sample/ and the Github Repository is here: https://github.com/yosipy/Threejs_IK_Sample. Weirdly enough it's by a Japanese developer, so I don't know if they have some kind unhealthy relationship with IK or not, but that's kind of beside the point. This example program has targets that can be dragged and then are solved by a function for each frame rather than being baked into the animation function like with the MMD loader.

After playing around with the example, the movement seems to look pretty natural. So I'm going to see about trying to copy and paste incorporate as much of this example as possible into the Shenpoo repository to solve for the rotations at any given frame. And right now I'm also working on the opposite, to try and take the animation values from Shenpoo and play them on this rig directly to see how well they work.
 
After a few quick tests I am not getting the same results between the sample rig and the Shenmue rig. In the case of the sample rig, the girl model bends her knee behind her for the first frame, which look correct. While the Shenmue rig seems to just have Ryo's leg straight and pulled back.

Right now there are two main differences between these two rigs. The first is scale. The original size of the sample rig is probably about the same size as the Shenmue rig, but then it's scaled up by 400. So probably adjusting the scale would be a good idea. And the skeleton helper looks nice, but if it's difficult to adjust the scale on the that, then it might be a good idea to take that away in favor of using a normal Skeleton Helper.

The other difference is the pre-rotation values on the Shenmue rig. What I think I might do on this is create a clone of the Ryo mesh and have two copies. One with the pre-rotation values as originally defined by the mt5 file, and then add the option to toggle visibility between them.

 
Last edited:
Last night I managed to apply these changes. I scaled down the sample rig to 1:1 from 400:1 to be able to compare apples to apples if needed. After doing that I checked the values for the solver once the scale was adjusted, and i still got the same different result. From this I decided to then go ahead and remove pre-rotation from the Ryo Mt5 rig. And that seems to have been the issue and I'm now getting reasonable results from the (CCD) IK solver.

ik_solver_fixed.JPG

I think what the issue is with pre-rotations, the IK solver has a hard time figuring out the min and max rotation limits for different joints. This is similar to the issue that I was running into for the MMD loader, in that IK worked with removing pre-rotations, but got really terrible results when trying to add in FK rotation.

In this case I think it's possible to actually take the opposite approach. With the MMD solver, that was kind of out of my hands in terms of creating data and passing it into the animation parser. In this case I'm still in the general context of the editor, which provides more flexibility for how to interpret and display the animation state.

And I think what I can do in this case is the opposite of the MMD loader. In that case I removed the model's pre-rotation and then attempted to solve FK rotations for the rig with the removed rotations. In this case I think I can make two models, one with pre-rotation and one without pre-rotation. I can use the non-rotation model to solve for IK and then get the rotation for the bones and apply that to the original rig. We'll see how that goes.
 
shenoo_screenshot.JPG

I'm not sure if this went better than expected or not. What's going on here is that we have the Ryo model with removed pre-rotation on the bottom, and then the vanilla mt5 model above it with 50% opacity. What's going on here is that we're using the rig with the removed pre-rotation to solve for the IK position, and then we take that solved rotation and apply it to the original model.

The weird part about this is that the the solved IK rotation is bending the knee in the X-direction. How ever if we apply this rotation to the original model as-is, it actually causes the knee to rotation inwards as opposed to bending the knee back. In order to be able to replicate the position I actually had to apply the X-rotation from the solved model, to the Z-axis on the original model.

And this is the quirky part about working with pre-rotation, as I think there is effectively a 90 degree rotation somewhere in there which causes the orientation of the bone to get swapped. Which is probably why the MMD IK solver and this IK solver have problems solving for the original model, expecting the T-pose to have zero initial rotation.

In this case we at lease got a proof of concept, and we have confirmed that we can apply the solved rotations to the original model if needed. The main question I have right now is what the best approach to do that is. Right now I'm simply swapping the axis for the knee from a small amount of trial and error. But I think the correct approach is to multiply the solved angle by the rotation of either the parent bone, or all of the parent bones up to the root. We'll have to do a little more testing to see how that works out.

For now I'm going to jump ahead a little bit. I'm going to manually fill in some of the missing values that are not included in the current table and then see if we can record each on of the rotations at each from and then export the leg part of the animation as a normal FK animation. Once we have confirmed that mechanic works, we can go back and start fine tuning more of the details.
 
Finally some good news. For a while I've been hypothesizing about the possibility of solving the IK for each frame and converting that into an FK animation. The problem is that talk is easy, action takes time. And with that I've managed to talk the walk animation, solve the rotation for the knees and thighs at each frame, and then export those to a file as a normal animation.


In doing this, I skipped over a lot of details to focus on the proof of concept and specifically getting these animations outside of the preview editor and into a file. In terms of things to touch up, we still have the hands, look at target, and the torso IK. We also have the spline interpolation, and the confidence in the animation values and the foot planting.

The main defining aspect about this approach to the animation is probably going to be the built-in blending. You can do blending with FK, but the approach is generally pretty different. Normally FK animations like to have a first and last frame, and then when switching between animations you cross-fade from one to the next. In terms of the IK values, a lot of the start and end values for the animation simply don't exist in favor or manually cross-fading from state to the next by taking advantage of the IK target system.

Right now I think I'll continue to test the walk animation by manually filling in any missing values to test that way. After that we can take another look at the spline interpolation, and confidence for the values.
 
Firstly, in my old GIF below, you can see that the hands definitely do have a full range of keyframes. The reason you're not seeing them is likely due to a parsing issue, there is a block of keyframe data which should be for the hands.

Shown below is one of those keyframe blocks for the hands, with another with a static position.

ryo_walk_lhand_rhand.gif


The main defining aspect about this approach to the animation is probably going to be the built-in blending.

This part I'm not so sure will represent a big problem. The blending that happens in-game is primarily for interpolating the changes of animation states, for example the animation/blending between a walk and a run and from slightly jogging to full speed running/sprinting. Some animations which are used in-game will not look the same, like the 'medium' jogging animation. In this case with the walk animation, it is a standalone animation. Meaning that the whole sequence described in the animation we're looking at now is the only thing that's being played out on the rig in-game. So no blending here.

Now that you've done the part where you've split the FK and IK rigs, it might be worth checking out those rotation keyframes and confirming you can replicate the result there.
 
Making some notes about the issues that are on the table at the moment. This is a stub, so I'll come back and try to add more information and notes about each point.


1. Parsing Confidence [ ]


Quick summary: For parsing confidence. Basically we can take the approach for reading the animation binary, and isolate different bones to be moved. We can then take that and make a trimmed down version of the animation to pass back into the game. If the game moves only the bones that we filtered out and in the correct motion, then we can say with pretty high confidence that we have the correct values, and the rest is up to interpretation.

As for this specific test with the arms. The motion definition for the arms comes really late in the binary for the walk animation. Which means that for only the arms to work we need to skip over the animation values for the legs for only the arms to work. We can use this as a condition that by skipping over the legs and isolating only the arms, which means that we satisfy for the in-between values as well. As we'll not simply truncating values off the end, but splicing values out of the animation binary, and then have the game play the result.

2. Spline Interpolation [ ]

I was going to make this a series, but I haven't had a chance to get on and record recently. So I'll write this with text. For spline interpretation we are referring the to "pair values". With Shenmue animations, for a given frame the game will read a pair value when a high bit is set (0x80, 0x20, 0x08, 0x02) and a single value when a low bit is set (0x40, 0x10, 0x04, 0x01). The single value is pretty clearly the keyframe value for either the rotation or position at that specific frame.

What's unclear is what the pair values are for. For a moment I though these could potentially be alternate keyframe values where the game will use one of the two pair values as the keyframe value depending on specific conditions. But the more I look over the pair values, the more I'm convinced this isn't the case. Right now my image for this is something like the pen tool.

pen_tool.JPG

Where the value is the point being set, and the pair values are adjustments for the slope. The main problem with this is that is we're to believe the current values, there are a lot of occasions where the key frame value simply isn't being set as there are often sequences where no low bit is set. Which means there are only pair values. Which kind of completely ruins my pen tool example as I don't know how the game would be adjusting the slope for easing when there is no fixed point to work from.

3. Rotation Key Frames [ ]

For rotation only keyframes, this is mostly an issue that comes from working in Threejs. With the CCD solvers I have available to me, they really don't like having pre-rotation in them. So mixing normal FK rotation keyframes (like the thigh) with the IK position frames of the foot becomes this mess of using two models, where you solve for IK on one, solve for FK on the other and then merge the two rigs together into a final result. This could be mitigated by using different tools such as Unreal, Unity, or other editors with more sophisticated IK tools. The main reason I use Threejs is that it's more open and it allows to you create and manage your own tools around a dataset, and have more control over importing, exporting and debugging. So this ends up being a trade off.
 
Last edited:
Okay, pretty big update with a pretty huge fix. And with this fix I guess I will proudly wear the "dunce" hat.


The video is pretty much non-nonsensical since it was my brain being blown by how the game manages key frames. Which I will attempt to explain here.

As stated multiple times, the game's MOTN format is effectively a binary-encoded dope sheet. And it does this by encoding different pieces of information into different blocks. (1) The bone id's, (2) the number of in between frames, (3) and specifically which frames. The part where I got stuck was the block after that (4) which describes a list of bits to describe how to read the (5) key frame values.

So to explain my folly, I'll start with what I adapted from PhilzYeah. And mentioned several times back since this post.
Code:
node.axis.forEach(d => {

    let index = 0;

    do {

        let keyframeBlockType = this.MEM.view.getUint8(this.MEM.ofs);

        b4++;
        this.MEM.ofs++;

        if (keyframeBlockType & 0x80) {
            node[d][index].high = true;
        }

        if (keyframeBlockType & 0x40) {
            node[d][index].low = true;
        }
        index++;

        if (keyframeBlockType & 0x20) {
            node[d][index].high = true;
        }

        if (keyframeBlockType & 0x10) {
            node[d][index].low = true;
        }
        index++;

        if (keyframeBlockType & 0x08) {
            node[d][index].high = true;
        }

        if (keyframeBlockType & 0x04) {
            node[d][index].low = true;
        }
        index++;

        if (keyframeBlockType & 0x02) {
            node[d][index].high = true;
        }

        if (keyframeBlockType & 0x01) {
            node[d][index].low = true;
        }
        index++;

    } while (index < node[d].length);

});

Basically say we have a list of frames, for each frame we need to figure out what values are going to be read for each frame. The way the game does this is by declaring a list of bits, where a high bit reads a pair value and a low bit reads a single value. So I took that to mean that a high bit means the game reads an easing, left / right slope value, and a low bit reads a key frame value.

And the implementation I use to interpret that was as follows.
Code:
node.axis.forEach(d => {

    node[d].forEach(frame => {

        if (frame.high) {
            let a = this.MEM.view.getUint16(this.MEM.ofs, true);
            a = this.API.decodeFloat16(a);
            this.MEM.ofs += 2;
            let b = this.MEM.view.getUint16(this.MEM.ofs, true);
            b = this.API.decodeFloat16(b);
            this.MEM.ofs += 2;
            frame.easing = [a, b];
        }

        if (frame.low) {
            let a = this.MEM.view.getUint16(this.MEM.ofs, true);
            a = this.API.decodeFloat16(a);
            if (node.type === 'rot') {
                a *= 65536;
                a = a % (Math.PI * 2);
            }
            this.MEM.ofs += 2;
            frame.value = a;

        }
        delete frame.high;
        delete frame.low;

    });

});

I will take a moment for clarification to specify that the game doesn't actually use this implementation. The game actually uses a look up table for each value in the bit field. But the values in the look up table, are simply the number of values being read from the specific bits being set. So those can be derived and the use of the look up table is not actually required. Actually I would say if anything the use of the look up table is actually more work and I'm not sure why it exists to begin with, but I digress.

The problem with this was a pretty common mistake when it comes to reverse engineering in that this wasn't actually the complete story. And I'll explain. Generally with reverse engineering the difference between stuff that works and stuff that doesn't work is pretty extreme. If something doesn't work you will generally get out of bounds issues, or errors, or really weird values. And you will know very quickly when something is broken. On the other side, when something works, it is generally when it is correct. And in this case with this implementation, my parser would read to the end of the blocks exactly as expected, and the values being produced where realistic and reasonable.

That being said, Lemonhaze, who actively looks at the game code kept telling me that my interpretation was incomplete, much to my confusion. Until I came up with a way to test out his claims, which is the video above which led me to finally being able to correct this. And the confusion arises with how the game hands bitflags.

In my brain, bit flags are set to specify one thing and one thing only. A single true false statement. So if high bit, read two easing values, if low bit read one key value. But this is not how the game interprets the bitflags. The way the game interprets the bit flags is more like the following.

- If high bit is set, read two values
- If low bit is set, read one value
- If high and low bit are set, read three values

And this leads to why my brain was blown in the youtube video as I realized when the game was reading only the high bit, but was actually reading a key value as well. And this allowed me to go back and discuss with LemonHaze and update the implementation. So the correct way to interpret these bitflags is shown with the following code.

Code:
node.axis.forEach(d => {

    node[d].forEach(frame => {

        if (frame.high && frame.low) {
            let a = this.MEM.view.getUint16(this.MEM.ofs, true);
            a = this.API.decodeFloat16(a);
            this.MEM.ofs += 2;

            let b = this.MEM.view.getUint16(this.MEM.ofs, true);
            b = this.API.decodeFloat16(b);
            this.MEM.ofs += 2;

            let c = this.MEM.view.getUint16(this.MEM.ofs, true);
            c = this.API.decodeFloat16(c);
            this.MEM.ofs += 2;

            frame.easing = [a, b];
            if (node.type === 'rot') {
                c *= 65536;
                c = c % (Math.PI * 2);
            }
            frame.value = c;

        } else if (frame.high) {
            let a = this.MEM.view.getUint16(this.MEM.ofs, true);
            a = this.API.decodeFloat16(a);
            this.MEM.ofs += 2;

            let b = this.MEM.view.getUint16(this.MEM.ofs, true);
            b = this.API.decodeFloat16(b);
            this.MEM.ofs += 2;

            frame.easing = [a];

            if (node.type === 'rot') {
                b *= 65536;
                b = b % (Math.PI * 2);
            }
            frame.value = b;

        } else if (frame.low) {
            let a = this.MEM.view.getUint16(this.MEM.ofs, true);
            a = this.API.decodeFloat16(a);
            this.MEM.ofs += 2;

            if (node.type === 'rot') {
                a *= 65536;
                a = a % (Math.PI * 2);
            }

            frame.value = a;
        }

        delete frame.high;
        delete frame.low;

    });

});

With this, we finally have a realistic view of all of the game's motion keyframe values. When only the low bit is set, we read the keyframe value. When only the high bit is set, we read a slope value and the key frame value. And then when both the high and low bits are set, we read two slope values and then the keyframe data. And effectively fills in the table where there were a massive number of blank slots. Which means now we can have pretty massive confidence in the output from the parser. And solves a lot of mysteries I had with the code previously. Which means that what remains generally boils down to implementation.
 
Last edited:
Now that we have extremely high confidence in the values being produced by the parser, we can shift our attention to implementation. Right now in Threejs, I have the model loaded in, with the hands and feet target connect with an IK solver. Right now I'm using 20 iterations and that seems to be working well for getting the hands and feet in place.

Which means that right now, we have two possibilities for the next step to work on. The first is trying to mix in rotation values into the IK animations, and the second is to try to solve for the bezier interpolations. I'll go ahead and break down the challenges associated with each of these.

FK Rotations

For FK rotations, the main issue is the problems introduced by pre-rotation. This might be hard to explain by text, so I might have to make a video follow up to show why this is an issue. But I'll make an attempt to write this out as text first.

For the Ryo model, which is an MT5 model, the original model uses position and rotation values on the bones to create the T-pose for the model. The problem that this creates is that CCD solvers really don't like having rotation values for the T-pose position. They expect the rotation on all of the bones to be { x : 0, y : 0, z : 0 } because this makes it easier to tell how far the bone has bent. If the rotation of the knee starts off at { x : 0, y : 0, z : 0 } and then we bend the knee slightly, then we get a rotation of { x : 1.1, y : 0, z : 0 }.

So the way we get rid of these pre-rotation values on the model, is that we take the world position of each bone, and then make a second virtual skeleton that has the same parent-child relationship of the original skeleton. And then we don't set any rotation values. This gives us a working T-pose without any rotation that works with the CCD solver. But the problem with this rig is that the FK rotation values are intended to work with the rig that has pre-rotations. And we end up with a conundrum where FK will work, IK doesn't work. And in the conditions where IK works, FK doesn't work.

And in these cases, if something doesn't work we need to figure out how we can make it work. And right now, we have a couple of options.

1. Try to apply FK to the IK rig
2. Try to apply the solved IK to the FK rig
3. Try to apply IK to the FK rig (CCD)
4. Try to apply IK to the FK rig (by trying to use the game's code directly)

1. Try to apply FK to the IK rig

When we zero-out the rotations we kind of really mess up how rotations are applied. The FK rotation values are basically the local rotation for that bone, which are depend on the rotations of all of the parent bones. Since we've removed all of the parent bone values, that basically means that we need to multiply by the inverse of all of the parent bone rotations to remove their effect on the local bone position, or something like that. This gets complicated and often goes wrong.

2. Try to apply the solved IK to the FK rig

With a different approach we can use the two rigs and then try to combine them together. We can take the IK rig, solve for the IK and then try to apply those rotations to the FK rig. But then we run into two issues. First of all the same issue as above applies but in the opposite direction. Because the local rotations depend on the rotation of the parent, just taking the solved IK rotations and applying them directly to the FK rig doesn't exactly work either. We need to do some magic with the parent bones to get the rotation to apply directly. And I'm never sure if I need to multiply by the parent, the inverse of the parent, or the inverse transpose of the parent, or all of the parents.

And the other issue is that the IK solver might use the FK rotations to place the bone in a certain state before solving. Which would make this approach might lead to problem. Or it could be that the FK rotations like the thigh end up being over-writen by the IK solver anyways, and the only thing we really need to apply is the rotation for the hands, feet and body.

3. Try to apply IK to the FK rig (CCD)

Right now I'm trying to use a CCD solver that I copy and pasted from another project. That CCD solver expects the T-pose to not have any rotations on the bones. I might be able to cheat and set a range for the CCD solver based on the original rotation. So rather than expecting the initial rotation to be zero and then check against that, we set the CCD solver to expect plus or minus a certain range and see if that works. This seems like the easiest option to attempt to be lazy. I really don't want to use my brain (if I can help it).

4. Try to apply IK to the FK rig (by trying to use the game's code directly)

The last option would be to try and clone the game's solver directly. And I would like to avoid this option and it's painful and time consuming. The actual code doesn't look to difficult. But then trying to spend the time to debug, create conditions and trace through the code to make sure we're getting the same values wouldn't exactly be kind to the time I have to allocate to working on this in the wee hours of the morning. So if possible I would like to try to find easier ways to approach this before going for this option.
 
Where the value is the point being set, and the pair values are adjustments for the slope. The main problem with this is that is we're to believe the current values, there are a lot of occasions where the key frame value simply isn't being set as there are often sequences where no low bit is set. Which means there are only pair values. Which kind of completely ruins my pen tool example as I don't know how the game would be adjusting the slope for easing when there is no fixed point to work from.
Now that I've finally gotten through to @Kion regarding this, I feel like I should probably explain a little bit here.

The interpolation used here is a mix between linear interpolation and Bezier spline interpolation. Mainly Bezier spline interpolation, though. For this, as Kion eloquently describes, you are given two "handles" or "slopes".

In Shenmue, the interpolation function can only ease between 2 keyframes at a time. This is in line with Bezier curves when used in animation and is the reason why we see the keyframe blocks split up the way they are. The actual logic behind this is pretty simple.

Once the block 4 data pointer has been calculated, the game will read the keyframe block value (the one which is used with the LUT) in order to retrieve the keyframe block size. This, used with some simple math, is then used to identify the type of FCV keyframe is stored in the data. Theoretically, an animation should start off with the more simplistic FCV key type (just a frame index and keyframe value) and then some of the 2nd FCV key type (the one with a frame index, left slope and a value) and then some of the 3rd FCV key type (one with a frame index, left slope and right slope and the value itself).

The actual LUT itself seems to just be a compiler optimization.

4. Try to apply IK to the FK rig (by trying to use the game's code directly)

The last option would be to try and clone the game's solver directly. And I would like to avoid this option and it's painful and time consuming. The actual code doesn't look to difficult. But then trying to spend the time to debug, create conditions and trace through the code to make sure we're getting the same values wouldn't exactly be kind to the time I have to allocate to working on this in the wee hours of the morning. So if possible I would like to try to find easier ways to approach this before going for this option.

In general your approach is exactly what we want, as our preliminary tests confirmed that the IK target positions work as intended with other solvers, we can safely rule out that the actual math behind Shenmue's "IK solver" is probably grounded and not some completely crazy, custom approach or something like that.

With that being said, the actual code itself is pretty straightforward to follow through and a lot of context has already been applied in this area. The code primarily just has a 4x4 matrix pointer, a pointer to any children/parents and the actual keyframe array itself, all of which we can easily recreate outside of the original engine.
 
Last edited:
Just thought I'd share some of my progress. I have implemented root rotation keyframes and I'm not too sure if I'll continue doing this for the rest of the nodes:

(Getting on the bus animation)

(Turning 180 degrees in-place)

1. Try to apply FK to the IK rig
2. Try to apply the solved IK to the FK rig
3. Try to apply IK to the FK rig (CCD)
4. Try to apply IK to the FK rig (by trying to use the game's code directly)

As of now, the logic in the code seems to be something like "read rotation keyframes, apply rotation keyframes, read IK position keyframes, apply to effectors, solve IK, then copy over the pose". So I'd say that creates a fifth option. The rotations sort of act as a pole vector, as Phil perfectly described. In other words, the rotations get the bones into an orientation, usually to control the way a limb will bend, and then IK targets are used to get them all in the proper position.

This is extremely evident in the running animation, where Ryo's arms bend in a very particular manner. If we apply just the IK keyframes from the running animation, we get this:

AKI_AKI_RUN_ST2

Just for reference, here is the same animation, in-game:
 
Last edited:
Just thought I'd share some of my progress. I have implemented root rotation keyframes and I'm not too sure if I'll continue doing this for the rest of the nodes:

Glad someone is making progress. Right now I'm severely limited by the lack of mature ThreeJS IK tools.

Screenshot_2020-12-10 Shenpoo.png

And to quickly demonstrate this. I have the normal skeleton ontop. And I can apply the FK rotations to that model and it will work. And I have the dirty-hack removed rotation skeleton on the bottom. And IK positions will work on this one.

I only found two examples of CCD IK for ThreeJs, and neither of them take into account rotation on the T-pose. So neither of them are a drop-in implementation for what's needed here.

As of now, the logic in the code seems to be something like "read rotation keyframes, apply rotation keyframes, read IK position keyframes, apply to effectors, solve IK, then copy over the pose". So I'd say that creates a fifth option. The rotations sort of act as a pole vector, as Phil perfectly described. In other words, the rotations get the bones into an orientation, usually to control the way a limb will bend, and then IK targets are used to get them all in the proper position.

I agree with this statement. And that's currently how I have my tool set up. To be able to set the body in a certain position, and then start the solver.

Screenshot from 2020-12-10 17-58-29.png

The issue I keep running into is that if I have rotations on the T-pose, then IK doesn't work. And if I remove the rotations and FK doesn't work. Right now the limitation is the IK solvers that other people have implemented. So I'm getting to the point where the only option remaining in threejs is to try and replicate the solver from the game. Which is something that I really wanted to avoid doing, as it would involve using braincells.
 
I only found two examples of CCD IK for ThreeJs, and neither of them take into account rotation on the T-pose. So neither of them are a drop-in implementation for what's needed here.
Would it be feasible to try something like this?

This is a FABRIK solver (Forwards and backwards reaching IK) which is way better than CCD in multiple ways, but I'd assume that the rotation thing should be less of a problem with it. FABRIK is a little more modern though, so it'll need some constraints I think (which Shenmue doesn't seem to have).

It should be worth noting that both CCD and FABRIK and more than likely most other heuristic iterative solvers will not have any problems with rotations because for something like CCD it just builds up rotation vectors for each bone in a chain, whereas FABRIK will mostly do everything with matrix operations (obviously including orientation-related ops). So really both of these solvers are likely fine for the purpose here, but there's some implementation or "syntax sugar" which needs applying to these specific implementations, which is why you're seeing some issues.

I looked over your implementation as you know the other day and mostly everything seems to be ok, but I don't think the actual library doing the solving is working properly right now.
 
Last edited:
Back
Top