[SOLVED] Problem with head bone rotation

Hello dear community,

for my game I want to rotate the players head so that the head always faces the view direction. For that I wrote a HeadRotatingControl which extends AbstractControl and is added to the player model. This control rotates the head bone of the player to the according viewDirection (which is a Vector3f) . Another system sets the current viewDirection for that player.

I use an EntitySystem (Zay-Es) for my game. There is a component which stores the current viewDirection for a player. When it is changed another system (app state) sets this new viewDirection for the corresponding HeadRotatingControl.

For Singleplayer this works fine because the updates are made immediately.

Heres the problem: In Multiplayer updates are sent only 10 times a second which doesn’t really lead to fluent movements of the head bone BUT the head rotation was still correct.

Due to the fact that I wanted fluent movements for the head I decided to interpolate (slerp) between the view direction updates. But “slerping” between the updates causes very weird artifacts… The head starts to “jump” away from his body…

For better understanding I made a short video: First you see the Singleplayer mode which works fine and after that you see two connected clients where you can see the issue I described above. (Sorry for laggy video)

Here is the code of the HeadRotatingControl class

import com.jme3.animation.Bone;
import com.jme3.animation.SkeletonControl;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;

/**
 * A control which sets the head bone rotation according to the set view direction.
 *
 * Created by Domenic on 09.06.2017.
 */
public class HeadRotatingControl extends AbstractControl {

    private Vector3f oldViewDir = new Vector3f();
    private Vector3f viewDirection = new Vector3f(Vector3f.UNIT_X);
    private Bone headBone;
    private Quaternion initRotation = new Quaternion();
    private float[] initAngles = new float[3];
    private float angle;
    private Quaternion headRotation = new Quaternion();
    private Quaternion finalHeadRotation = new Quaternion();

    public void setViewDirection(Vector3f viewDirection) {
        this.viewDirection.set(viewDirection).normalizeLocal();
    }

    @Override
    public void setSpatial(Spatial spatial) {
        super.setSpatial(spatial);
        if (spatial != null) {
            // setup
            SkeletonControl skeletonControl = spatial.getControl(SkeletonControl.class);
            this.headBone = skeletonControl.getSkeleton().getBone("Neck1");
            this.headBone.setUserControl(true);
            this.initRotation = headBone.getLocalRotation().clone();
            this.initRotation.toAngles(initAngles); // save init angles

            System.out.println(initAngles[0]*FastMath.RAD_TO_DEG);
        } else {
            // cleanup
            if (headBone != null) {
                headBone.setUserControl(false);
            }
        }
    }

    @Override
    protected void controlUpdate(float tpf) {

        // if true --> viewDirection has been updated, so we need to set a new rotation for the head
        if (!oldViewDir.equals(viewDirection)) {

            // get angle between character rotation and view direction
            angle = spatial.getLocalRotation().getRotationColumn(2).normalize().angleBetween(viewDirection);

            Vector3f vec = viewDirection.subtract(spatial.getLocalRotation().getRotationColumn(2));
            if (vec.getY() > 0) {
                angle *= -1; // if the character is looking up we negate the angle
            }

            headRotation.set(headBone.getLocalRotation());

            finalHeadRotation.fromAngles(initAngles[0] + angle, 0, 0);
            oldViewDir.set(viewDirection);
        }

        // interpolate to new head rotation
        headRotation.slerp(finalHeadRotation, tpf/0.05f);

        // apply new user transforms
        headBone.setUserTransforms(Vector3f.ZERO, headRotation, Vector3f.UNIT_XYZ);
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
    }
    
}

Again: For Singleplayer this works, even with interpolation. But on Multiplayer it doesn’t anymore.

I really hope somebody can help me.

Best regards
Domenic

1 Like

The direction of the vector will be redundant information. Because you just use the rotation of the bone of the head. Use rotation for bone, it will be easier.

1 Like

Isn’t that what I am doing? (see headRotation and finalHeadRotation)

1 Like

But then why is this … if (!oldViewDir.equals(viewDirection))
It is easier to follow the axis of the bone.
Because the view can change horizontally.

1 Like

This is just to set the new final head rotation because the view direction has changed.

Sorry, but what do you mean by that?

1 Like

At you the bone on logic rotates only on one axis. Why watch out for everything?

1 Like

Aha… Well …


I solved the “jumping” of the head. There was just one line of code missing…

headRotation.normalizeLocal(); 

I need to call this after I slerped the rotation and before I apply the user transform.

2 Likes

I’m doing something similar to your work. However, I will do it differently. :slight_smile:

1 Like

Oh cool :grinning:

It would be cool then to see how you did it.

Grettings
Domenic

1 Like

For things like camera or head rotation, it’s almost always better just to keep yaw and pitch and then derive rotation from that.

…and in math like this, any time you are asking for “angle between” and it’s not to display it to the user then there was a much better way. I started to tease it out but there was too much stuff to understand that completely goes away if you just keep yaw and pitch and derive rotation from them.

1 Like

Yes, this probably would be better. But if I change my EntityComponent just for this a lot of other code would “break”.

1 Like

Try using nlerp instead of slerp or, normalize the slerp output

1 Like

Well, if this is a component for head rotation then I would say that other code may already be “broken” and just not realize it yet. Hard to say.

1 Like

@nehon
That’s what solved this issue (posted it above) :slight_smile:

@pspeed
No, the head does not have its own component. The viewDirection is in a component which is handled by a physics state to compute new physics locations for players. I just use this viewDirection in another state to make this head rotation. So this is just something visual and doesn’t affect the game logic.

EDIT:
@pspeed is this wrong?

1 Like

ho I missed the post…

1 Like

It’s just weird to use “angle between” for anything other than a display to the user.

When you already start with a quaternion and end with a quaternion it’s strange to have to involve vectors and this odd method.

I mean this code:

Makes a lot of assumptions that may not hold. If there is ever any roll involved then this up down check gets really bonkers.

It’s probably better just to transform viewDirection into local model space and then things start to make more sense. I think then you can get away with lookAt() to find your target rotation of the head.

2 Likes

Okay, thank you very much for making all this clear! I’ll try to do it like you described tomorrow or so. This actually sounds much better!

Greetings
Domenic

1 Like

Hello @pspeed , it also works like you described but I didn’t transform viewDirection into model space for that (that seems to make things unnecessarily complicated) :slight_smile:

This is how I do it now:

if (!oldViewDir.equals(viewDirection)) {

	finalHeadRotation.lookAt(viewDirection, Vector3f.UNIT_Y);

	finalHeadRotation.toAngles(headAngles);
	headAngles[1] = 0; // we don't want to have any y-rotation
	headAngles[2] = 0; // wo don't want to have any z-rotation
	finalHeadRotation.fromAngles(headAngles);

	// we update our oldViewDirection vector to avoid that all this
	// is computed more than necessary
	oldViewDir.set(viewDirection);
}

But I really would appreciate a little feedback if possible. ( I am always willing to learn :blush: )

1 Like

Not it easier to track the rotation of the bone?

After all, your head rotates up and down.

Why react to a horizontal change …

1 Like

Well, I could check if the y-value has changed but that would feel “dirty” again.

How would I do it then? Could show me some code because currently I don’t know how to check that in a clean way when I just have a vector3f as viewDirection.

1 Like