How to animate a spinning top?
Asked Answered
Z

2

7

It's Hanukkah and I'm trying to animate a spinning top (dreidel):

spinning top

I can get it to spin on its own axis. Here is my code:

import static javafx.scene.paint.Color.*;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point3D;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;

public class DreidelAnim extends Application {

    private double bodyBase = 30;
    private double bodyHeight = bodyBase * 3 / 2;
    private double baseRadius = bodyBase / 2;

    @Override
    public void start(Stage stage) throws Exception {
        DoubleProperty spinAngle = new SimpleDoubleProperty();
        Rotate spin = new Rotate(0, Rotate.Z_AXIS);
        spin.angleProperty().bind(spinAngle);

        Timeline spinAnim = new Timeline(new KeyFrame(Duration.seconds(2), new KeyValue(spinAngle, 360)));
        spinAnim.setCycleCount(Timeline.INDEFINITE);
        spinAnim.play();

        Group dreidel = createDreidel();
        Translate zTrans = new Translate(0, 0, -(bodyHeight/2 + baseRadius));
        dreidel.getTransforms().addAll(spin, zTrans);

        Scene scene = new Scene(dreidel, 200, 200, true, SceneAntialiasing.BALANCED);
        scene.setFill(SKYBLUE);
        scene.setCamera(createCamera());

        stage.setScene(scene);
        stage.show();
    }

    private Group createDreidel() {
        double handleHeight = bodyBase * 3/4;
        Cylinder handle = new Cylinder(bodyBase / 6, handleHeight);
        handle.setTranslateZ(-(bodyHeight + handleHeight) / 2);
        handle.setRotationAxis(Rotate.X_AXIS);
        handle.setRotate(90);
        handle.setMaterial(new PhongMaterial(RED));

        Box body = new Box(bodyBase, bodyBase, bodyHeight);
        body.setMaterial(new PhongMaterial(BLUE));

        Sphere base = new Sphere(baseRadius);
        base.setTranslateZ(bodyHeight / 2);
        base.setMaterial(new PhongMaterial(GREEN));

        return new Group(handle, body, base);
    }

    private Camera createCamera() {
        PerspectiveCamera camera = new PerspectiveCamera(true);
        camera.setFarClip(1000);

        int xy = 150;
        Translate trans = new Translate(-xy, xy, -120);
        Rotate rotXY = new Rotate(70, new Point3D(1, 1, 0));
        Rotate rotZ = new Rotate(45, new Point3D(0, 0, 1));
        camera.getTransforms().addAll(trans, rotXY, rotZ);

        return camera;
    }

    public static void main(String[] args) {
        launch();
    }
}

I created a simple model, spinning it around its axis, and translated it so that its tip is on (0, 0, 0). Here is the result:

enter image description here

How can I achieve something similar to the picture on the top where it's also spinning around a rotating axis?

Zerlina answered 14/12, 2017 at 18:24 Comment(0)
Z
9

The rotation of the axis around which the object spins is called a Precession. The spinning top motion requires 2 rotations:

  1. Rotation of the object around its internal axis (parallel to the red handle).
  2. Rotation one of the internal axis around a static axis (z in this case).

On the face of it, you'd need 2 Animation instances. However, both rotations are actually the same. The pivot point for both is (0, 0, 0) (after the zTrans) and they are both around the z axis, only one of them is tilted at an angle.

Here is the modified code:

import static javafx.scene.paint.Color.*;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point3D;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;

public class FinalDreidelSpin extends Application {

    private double bodyBase = 30;
    private double bodyHeight = bodyBase * 3 / 2;
    private double baseRadius = bodyBase / 2;

    @Override
    public void start(Stage stage) throws Exception {
        double tiltAngle = 40;
        DoubleProperty spinAngle = new SimpleDoubleProperty();

        Rotate spin = new Rotate(0, Rotate.Z_AXIS);
        Rotate tilt = new Rotate(tiltAngle, Rotate.X_AXIS);

        spin.angleProperty().bind(spinAngle);

        Timeline spinAnim = new Timeline();
        spinAnim.getKeyFrames().add(new KeyFrame(Duration.seconds(2), new KeyValue(spinAngle, 360)));
        spinAnim.setCycleCount(Timeline.INDEFINITE);
        spinAnim.play();

        Group dreidel = createDreidel();
        Translate zTrans = new Translate(0, 0, -(bodyHeight/2 + baseRadius));
        dreidel.getTransforms().addAll(spin, tilt, spin, zTrans);

        Scene scene = new Scene(new Group(dreidel, createAxes()), 200, 200, true, SceneAntialiasing.BALANCED);
        scene.setFill(SKYBLUE);
        scene.setCamera(createCamera());

        stage.setScene(scene);
        stage.show();
    }

    private Group createDreidel() {
        double handleHeight = bodyBase * 3/4;
        Cylinder handle = new Cylinder(bodyBase / 6, handleHeight);
        handle.setTranslateZ(-(bodyHeight + handleHeight) / 2);
        handle.setRotationAxis(Rotate.X_AXIS);
        handle.setRotate(90);
        handle.setMaterial(new PhongMaterial(RED));

        Box body = new Box(bodyBase, bodyBase, bodyHeight);
        body.setMaterial(new PhongMaterial(BLUE));

        Sphere base = new Sphere(baseRadius);
        base.setTranslateZ(bodyHeight / 2);
        base.setMaterial(new PhongMaterial(GREEN));

        return new Group(handle, body, base);
    }

    private Camera createCamera() {
        PerspectiveCamera camera = new PerspectiveCamera(true);
        camera.setFarClip(1000);

        int xy = 150;
        Translate trans = new Translate(-xy, xy, -100);
        Rotate rotXY = new Rotate(70, new Point3D(1, 1, 0));
        Rotate rotZ = new Rotate(45, new Point3D(0, 0, 1));
        camera.getTransforms().addAll(trans, rotXY, rotZ);

        return camera;
    }

    private Group createAxes() {
        int axisWidth = 1;
        int axisLength = 400;

        Cylinder xAxis = new Cylinder(axisWidth, axisLength);
        xAxis.setMaterial(new PhongMaterial(CYAN));

        Cylinder yAxis = new Cylinder(axisWidth, axisLength);
        yAxis.setRotationAxis(Rotate.Z_AXIS);
        yAxis.setRotate(90);
        yAxis.setMaterial(new PhongMaterial(MAGENTA));

        Cylinder zAxis = new Cylinder(axisWidth, axisLength);
        zAxis.setRotationAxis(Rotate.X_AXIS);
        zAxis.setRotate(90);
        zAxis.setMaterial(new PhongMaterial(YELLOW));

        return new Group(xAxis, yAxis, zAxis);
    }

    public static void main(String[] args) {
        launch();
    }
}

Where I added axes representations for viewing convenience. Note that the getTransforms() list does not require its objects to be unique (unlike getChildren()), which allows us to reuse the same animation. The order of the animations is also important as noted below.

The tilting is a simple rotation around the x or y axis.
If we tilt and then spin, getTransforms().addAll(tilt, spin, zTrans), we would get the internal rotation (listed 1 above), only tilted:

spin

If we spin and then tilt, getTransforms().addAll(spin, tilt, zTrans), we would get the precession (listed 2 above):

precession

Combining the 2 as in the complete code would give the desired result:

full

Zerlina answered 14/12, 2017 at 18:24 Comment(0)
A
3

This is another possible answer, very much based in @user1803551 approach, but using a 3D mesh that can use a texture image, and a different precession period.

This is how it looks like:

dreidel

In order to apply a texture, I'll use the net concept for the body of the dreidel, and this image:

that is based on this image.

Finally I'll add a regular cylinder for the handle.

I won't go into details on how to create the TriangleMesh for the body, but we define 9 vertices (3D coordinates), 16 texture coordinates (2D), and 14 triangle faces including the vertex indices and the texture indices. The cube is defined by its side width, and the pyramid by its height. The net dimensions are L = 4 * width, H = 2 * width + height.

For instance, face 0 has vertices 0 - 2 - 1, and texture indices 8 - 3 - 7, where vertex 0 has coordinates {width / 2, width / 2, width / 2}, and texture index 8 has coordinates {width, 2 * width}, which are normalized between [0, 1]: {width / L, 2 * width / H}.

In this case, and for the sake of the sample, the values are hardcoded:

float width = 375f;
float height = 351f;

This is the 3D shape class:

class DreidelMesh extends Group {

    float width = 375f;
    float height = 351f; 

    public DreidelMesh(){
        MeshView bodyMesh = new MeshView(createBodyMesh());
        PhongMaterial material = new PhongMaterial();
        material.setDiffuseMap(new Image(getClass().getResourceAsStream("3dreidel3d.png")));
        bodyMesh.setMaterial(material);

        Cylinder handle = new Cylinder(45, 260);
        handle.setTranslateY(-(handle.getHeight() + width) / 2);
        material = new PhongMaterial(Color.web("#daaf6d"));
        handle.setMaterial(material);

        getTransforms().add(new Rotate(90, Rotate.X_AXIS));
        getChildren().addAll(bodyMesh, handle);
    }

    private TriangleMesh createBodyMesh() {
        TriangleMesh m = new TriangleMesh();

        float L = 4f * width;
        float H = 2f * width + height;
        float w2 = width / 2f;

        // POINTS
        m.getPoints().addAll(
             w2,  w2,  w2, 
             w2,  w2, -w2, 
             w2, -w2,  w2, 
             w2, -w2, -w2, 
            -w2,  w2,  w2, 
            -w2,  w2, -w2, 
            -w2, -w2,  w2, 
            -w2, -w2, -w2, 
             0f,  w2 + height,  0f
        );

        // TEXTURES
        m.getTexCoords().addAll(
            width / L, 0f, 
            2f * width/ L, 0f, 
            0f, width / H,
            width / L, width / H, 
            2f * width/ L, width / H, 
            3f * width/ L, width / H, 
            1f, width / H, 
            0f, 2f * width / H,
            width / L, 2f * width / H, 
            2f * width/ L, 2f * width / H, 
            3f * width/ L, 2f * width / H, 
            1f, 2f * width / H, 
            width / 2f / L, 1f,
            3f * width / 2f / L, 1f,
            5f * width / 2f / L, 1f,
            7f * width / 2f / L, 1f
        );

        // FACES
        m.getFaces().addAll(
            0,  8, 2,  3, 1,  7,           
            2,  3, 3,  2, 1,  7,           
            4,  9, 5, 10, 6,  4,           
            6,  4, 5, 10, 7,  5,           
            0,  8, 1,  7, 8, 12,        
            4,  9, 0,  8, 8, 13,           
            5, 10, 4,  9, 8, 14,           
            1, 11, 5, 10, 8, 15,            
            2,  3, 6,  4, 3,  0,            
            3,  0, 6,  4, 7,  1,            
            0,  8, 4,  9, 2,  3,          
            2,  3, 4,  9, 6,  4,           
            1, 11, 3,  6, 5, 10,           
            5, 10, 3,  6, 7,  5
        );
        return m;
    }
}

dreidel

Finally, this shape is added to the scene, and I'll provide two animations (instead of one), one for the spin, and one slower for the precession:

@Override
public void start(Stage stage) {
    double tiltAngle = 15;
    DoubleProperty spinAngle = new SimpleDoubleProperty();
    DoubleProperty precessionAngle = new SimpleDoubleProperty();

    Rotate spin = new Rotate(0, Rotate.Z_AXIS);
    Rotate precession = new Rotate(0, Rotate.Z_AXIS);
    Rotate tilt = new Rotate(tiltAngle, Rotate.X_AXIS);

    spin.angleProperty().bind(spinAngle);
    precession.angleProperty().bind(precessionAngle);

    Timeline spinAnim = new Timeline();
    spinAnim.getKeyFrames().add(new KeyFrame(Duration.seconds(1.5), new KeyValue(spinAngle, 360)));
    spinAnim.setCycleCount(Timeline.INDEFINITE);
    spinAnim.play();

    Timeline precessionAnim = new Timeline();
    precessionAnim.getKeyFrames().add(new KeyFrame(Duration.seconds(4), new KeyValue(precessionAngle, 360)));
    precessionAnim.setCycleCount(Timeline.INDEFINITE);
    precessionAnim.play();

    Group dreidel = new Group(new DreidelMesh());
    Translate zTrans = new Translate(0, 0, - dreidel.getBoundsInLocal().getMaxZ());
    dreidel.getTransforms().addAll(precession, tilt, spin, zTrans);

    Scene scene = new Scene(new Group(dreidel), 300, 300, true, SceneAntialiasing.BALANCED);
    scene.setFill(SKYBLUE);
    scene.setCamera(createCamera());

    stage.setScene(scene);
    stage.setTitle("JavaFX 3D - Dreidel");
    stage.show();
}

Running the application will show the animation displayed above.

Agley answered 16/12, 2017 at 21:31 Comment(3)
Yeah well, I think you answered the extended question "How to animate a good-looking spinning top?" :) My intent was to focus on the math/animations. Very nice end result with the DIY!Zerlina
By the way, your animated gif stops for me after 1 cycle and the image disappears. Could be an issue on my end.Zerlina
It works fine here (Mac/Safari and Windows/Firefox).Condemnation

© 2022 - 2024 — McMap. All rights reserved.