Problem with nodes while rotation

I have a problem with nodes while rotation of model in some cases. When I was rotating a model, I can saw that the side nodes was incorrect rotating in some cases.

Code for test this cases:

import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetManager;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;

import java.util.concurrent.ThreadLocalRandom;

import static java.lang.Math.abs;

public class TestRotation extends SimpleApplication {

    private final Quaternion startRotation;
    private final Quaternion endRotation;
    private final Quaternion resultRotation;
    private final Quaternion bufferRotation;

    private final Quaternion bodyEndRotation;
    private final Quaternion bodyStartRotation;
    private final Quaternion bodyResultRotation;
    private final Quaternion bodyBufferRotation;

    private final Vector3f prevBodyPosition;
    private final Quaternion prevCameraRotation;

    private Node body;

    private int frame;

    private float cameraDone;
    private float cameraStep;
    private float bodyDone;
    private float bodyStep;

    private long lastCamUpdate;
    private long lastBodyUpdate;
    private long lastBodyRotate;

    public TestRotation() {
        this.startRotation = new Quaternion();
        this.endRotation = new Quaternion();
        this.resultRotation = new Quaternion();
        this.bufferRotation = new Quaternion();
        this.prevBodyPosition = new Vector3f();
        this.prevCameraRotation = new Quaternion();
        this.bodyBufferRotation = new Quaternion();
        this.bodyEndRotation = new Quaternion();
        this.bodyStartRotation = new Quaternion();
        this.bodyResultRotation = new Quaternion();
    }

    public static void main(String[] args) {

        final AppSettings settings = new AppSettings(true);
        settings.setResolution(1024, 768);

        TestRotation app = new TestRotation();
        app.setSettings(settings);
        app.setShowSettings(false);
        app.start();
    }

    @Override
    public void simpleInitApp() {
        viewPort.setBackgroundColor(ColorRGBA.Gray);
        cam.setFrustumPerspective(55F, (float) cam.getWidth() / cam.getHeight(), 1f, 1000F);

        final Geometry geometry1 = new Geometry("Box", new Box(1, 1, 1));
        geometry1.setMaterial(createMaterial(assetManager));
        geometry1.setLocalScale(1, 0.2F, 3.8F);

        final Geometry geometry2 = new Geometry("Box", new Box(1, 1, 1));
        geometry2.setMaterial(createMaterial(assetManager));
        geometry2.setLocalScale(3.8F, 0.2F, 1F);
        geometry2.setLocalTranslation(new Vector3f(0F, 0.43930158F, -2.800108F));

        final Geometry geometry3 = new Geometry("Box", new Box(1, 1, 1));
        geometry3.setMaterial(createMaterial(assetManager));
        geometry3.setLocalScale(0.6F, 0.6F, 0.6F);

        final Geometry geometry4 = new Geometry("Box", new Box(1, 1, 1));
        geometry4.setMaterial(createMaterial(assetManager));
        geometry4.setLocalScale(0.6F, 0.6F, 0.6F);

        final Node node2 = new Node();
        node2.attachChild(geometry3);
        node2.setLocalTranslation(new Vector3f(4.421318F, -0.35402215F, -3.193687F));

        final Node node3 = new Node();
        node3.attachChild(geometry4);
        node3.setLocalTranslation(new Vector3f(-4.421318F, -0.35402215F, -3.193687F));

        body = new Node();
        body.attachChild(geometry1);
        body.attachChild(geometry2);
        body.attachChild(node2);
        body.attachChild(node3);
        rootNode.attachChild(body);

        final DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(1, 0, 1).normalizeLocal());
        sun.setColor(ColorRGBA.White);

        rootNode.addLight(sun);

        getFlyByCamera().setDragToRotate(true);


        this.bodyDone = 2;
    }

    private Material createMaterial(final AssetManager assetManager) {
        return new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
    }

    @Override
    public void simpleUpdate(final float tpf) {
        frame++;

        if(bodyDone >= 1F && System.currentTimeMillis() - lastBodyRotate > 1000) {

            final ThreadLocalRandom random = ThreadLocalRandom.current();
            final Quaternion target = new Quaternion();
            target.set(random.nextFloat(), random.nextFloat(), random.nextFloat(), random.nextFloat());
            target.normalizeLocal();

            rotateBodyTo(target);

            lastBodyRotate = System.currentTimeMillis();
        }

        updateBodyRotation();

        if(System.currentTimeMillis() - lastCamUpdate > 15) {
            updateCamRotation();
            lastCamUpdate = System.currentTimeMillis();
        }

        updateCamPosition();
    }

    public void rotateBodyTo(final Quaternion target) {

        final Quaternion start = body.getLocalRotation();
        final float dot = Math.abs(start.dot(target));

        if (dot >= 1F) {
            this.bodyEndRotation.set(target);
            this.bodyDone = 1F;
            return;
        }

        final float radians = (float) Math.acos(dot);
        final float speed = 0.5F;
        final float count = radians / speed;
        final float step = 1 / count;

        this.bodyStartRotation.set(start);
        this.bodyEndRotation.set(target);
        this.lastBodyUpdate = System.currentTimeMillis();
        this.bodyStep = step;
        this.bodyDone = 0F;
    }

    public void updateBodyRotation() {

        float done = this.bodyDone;

        try {

            if (done >= 1F) return;

            final long diff = System.currentTimeMillis() - lastBodyUpdate;
            if (diff < 1) return;

            done += this.bodyStep * diff / 1000F;

            Quaternion result = bodyResultRotation;

            if (done >= 1F) {
                result = bodyEndRotation;
            }

            if (result != bodyEndRotation) {
                bodyBufferRotation.set(bodyEndRotation);
                result = bodyResultRotation.slerp(bodyStartRotation, bodyBufferRotation, done);
            }

            body.setLocalRotation(result);

        } finally {
            lastBodyUpdate = System.currentTimeMillis();
            this.bodyDone = done;
        }
    }

    private void updateCamRotation() {

        final Camera camera = getCamera();

        final Quaternion bodyRotation = body.getLocalRotation();
        final Quaternion cameraRotation = camera.getRotation();

        if (!isNeedRotation(bodyRotation, cameraRotation)) return;

        if (!endRotation.equals(bodyRotation)) {
            startRotation.set(cameraRotation);
            endRotation.set(bodyRotation);

            final float dot = abs(cameraRotation.dot(bodyRotation));
            final float step = (float) Math.pow(1F / (1 * dot), 10);

            this.cameraStep = step;
            this.cameraDone = 0;
        }

        float done = this.cameraDone + 0.03F;

        try {

            if (done >= 1D) {
                camera.setRotation(endRotation);
                endRotation.set(0, 0, 0, 1);
                done = 0;
                return;
            }

            bufferRotation.set(endRotation);
            resultRotation.slerp(startRotation, bufferRotation, done);

            camera.setRotation(resultRotation);

            if (bodyRotation.equals(resultRotation)) {
                camera.setRotation(bodyRotation);
                endRotation.set(0, 0, 0, 1);
                done = 0;
            }

        } finally {
            this.cameraDone = done;
        }
    }

    private void updateCamPosition() {

        final Camera camera = getCamera();

        final Vector3f currentBodyPosition = body.getLocalTranslation();
        final Quaternion currentCamRotation = camera.getRotation();

        if (frame > 2 && prevBodyPosition.equals(currentBodyPosition) && currentCamRotation.equals(prevCameraRotation)) {
            return;
        }

        try {

            float offset = 15;

            Vector3f pos = camera.getDirection();
            pos.multLocal(-offset);
            pos.addLocal(currentBodyPosition);

            final float firstX = pos.getX();
            final float firstY = pos.getY();
            final float firstZ = pos.getZ();

            pos = camera.getUp(pos);
            pos.multLocal(3);
            pos.addLocal(firstX, firstY, firstZ);

            camera.setLocation(pos);

        } finally {
            prevBodyPosition.set(currentBodyPosition);
            prevCameraRotation.set(currentCamRotation);
        }
    }

    protected boolean isNeedRotation(final Quaternion shipRotation, final Quaternion cameraRotation) {
        return !shipRotation.equals(cameraRotation);
    }
}

place three backticks before and after your code on a separated line and it would look wonderful :slight_smile:

This is not necessarily a valid rotation. If random rotation is what you want then this is not the way to do it.

How do I check validity of rotation?

Not sure… but you could try just creating valid rotations in the first place.

Anyway, if you are having issues then it would be better to create a simpler test case. Normal parent/child relationships work just fine for rotation so the problem must be in the extra stuff you have.

I removed the code which is not required for reproducing.

import com.jme3.asset.AssetManager;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;

import java.util.concurrent.ThreadLocalRandom;

public class TestRotation extends SimpleApplication {

    private final Quaternion bodyEndRotation;
    private final Quaternion bodyStartRotation;
    private final Quaternion bodyResultRotation;
    private final Quaternion bodyBufferRotation;

    private Node body;

    private float bodyDone;
    private float bodyStep;

    private long lastBodyUpdate;
    private long lastBodyRotate;

    public TestRotation() {
        this.bodyBufferRotation = new Quaternion();
        this.bodyEndRotation = new Quaternion();
        this.bodyStartRotation = new Quaternion();
        this.bodyResultRotation = new Quaternion();
    }

    public static void main(String[] args) {

        final AppSettings settings = new AppSettings(true);
        settings.setResolution(1024, 768);

        TestRotation app = new TestRotation();
        app.setSettings(settings);
        app.setShowSettings(false);
        app.start();
    }

    @Override
    public void simpleInitApp() {
        viewPort.setBackgroundColor(ColorRGBA.Gray);
        cam.setFrustumPerspective(55F, (float) cam.getWidth() / cam.getHeight(), 1f, 1000F);

        final Geometry geometry1 = new Geometry("Box", new Box(1, 1, 1));
        geometry1.setMaterial(createMaterial(assetManager));
        geometry1.setLocalScale(1, 0.2F, 3.8F);

        final Geometry geometry2 = new Geometry("Box", new Box(1, 1, 1));
        geometry2.setMaterial(createMaterial(assetManager));
        geometry2.setLocalScale(3.8F, 0.2F, 1F);
        geometry2.setLocalTranslation(new Vector3f(0F, 0.43930158F, -2.800108F));

        final Geometry geometry3 = new Geometry("Box", new Box(1, 1, 1));
        geometry3.setMaterial(createMaterial(assetManager));
        geometry3.setLocalScale(0.6F, 0.6F, 0.6F);

        final Geometry geometry4 = new Geometry("Box", new Box(1, 1, 1));
        geometry4.setMaterial(createMaterial(assetManager));
        geometry4.setLocalScale(0.6F, 0.6F, 0.6F);

        final Node node2 = new Node();
        node2.attachChild(geometry3);
        node2.setLocalTranslation(new Vector3f(4.421318F, -0.35402215F, -3.193687F));

        final Node node3 = new Node();
        node3.attachChild(geometry4);
        node3.setLocalTranslation(new Vector3f(-4.421318F, -0.35402215F, -3.193687F));

        body = new Node();
        body.attachChild(geometry1);
        body.attachChild(geometry2);
        body.attachChild(node2);
        body.attachChild(node3);
        rootNode.attachChild(body);

        final DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(1, 0, 1).normalizeLocal());
        sun.setColor(ColorRGBA.White);

        rootNode.addLight(sun);

        this.bodyDone = 2;
    }

    private Material createMaterial(final AssetManager assetManager) {
        return new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
    }

    @Override
    public void simpleUpdate(final float tpf) {

        if(bodyDone >= 1F && System.currentTimeMillis() - lastBodyRotate > 1000) {

            final ThreadLocalRandom random = ThreadLocalRandom.current();
            final Quaternion target = new Quaternion();
            target.set(random.nextFloat(), random.nextFloat(), random.nextFloat(), random.nextFloat());
            target.normalizeLocal();

            rotateBodyTo(target);

            lastBodyRotate = System.currentTimeMillis();
        }

        updateBodyRotation();

        float offset = 15;

        final Camera camera = getCamera();
        camera.setRotation(body.getLocalRotation());

        Vector3f pos = camera.getDirection();
        pos.multLocal(-offset);
        pos.addLocal(body.getLocalTranslation());

        final float firstX = pos.getX();
        final float firstY = pos.getY();
        final float firstZ = pos.getZ();

        pos = camera.getUp(pos);
        pos.multLocal(3);
        pos.addLocal(firstX, firstY, firstZ);

        camera.setLocation(pos);
    }

    public void rotateBodyTo(final Quaternion target) {

        final Quaternion start = body.getLocalRotation();
        final float dot = Math.abs(start.dot(target));

        if (dot >= 1F) {
            this.bodyEndRotation.set(target);
            this.bodyDone = 1F;
            return;
        }

        final float radians = (float) Math.acos(dot);
        final float speed = 0.5F;
        final float count = radians / speed;
        final float step = 1 / count;

        this.bodyStartRotation.set(start);
        this.bodyEndRotation.set(target);
        this.lastBodyUpdate = System.currentTimeMillis();
        this.bodyStep = step;
        this.bodyDone = 0F;
    }

    public void updateBodyRotation() {

        float done = this.bodyDone;

        try {

            if (done >= 1F) return;

            final long diff = System.currentTimeMillis() - lastBodyUpdate;
            if (diff < 1) return;

            done += this.bodyStep * diff / 1000F;

            Quaternion result = bodyResultRotation;

            if (done >= 1F) {
                result = bodyEndRotation;
            }

            if (result != bodyEndRotation) {
                bodyBufferRotation.set(bodyEndRotation);
                result = bodyResultRotation.slerp(bodyStartRotation, bodyBufferRotation, done);
            }

            body.setLocalRotation(result);

        } finally {
            lastBodyUpdate = System.currentTimeMillis();
            this.bodyDone = done;
        }
    }
}

I think we have a problem with this method:
result = bodyResultRotation.slerp(bodyStartRotation, bodyBufferRotation, done);
In any cases the random rotation after normalize is correct, but in some cases the new rotation, which we get from slerp() leads to this problem.

Just because if visually looks correct, does not mean that it is correct.

I’ve never had a problem with slerp.

Note: if slerp has a problem then you should be able to isolate that to specific real rotations that cause the issue.

Ok, I will try to detect the values of rotation, which leads to it.

There were some issues with slerp and animations… for that there was a nlerp method added
maybe you could try with it?

nlerp works fine, thanks :slight_smile:

then we definitely have an issue with slerp.

If I use slerp with normalize, it resolves my problem.