FX Experience Has Gone Read-Only

I've been maintaining FX Experience for a really long time now, and I love hearing from people who enjoy my weekly links roundup. One thing I've noticed recently is that maintaining two sites (FX Experience and JonathanGiles.net) takes more time than ideal, and splits the audience up. Therefore, FX Experience will become read-only for new blog posts, but weekly posts will continue to be published on JonathanGiles.net. If you follow @FXExperience on Twitter, I suggest you also follow @JonathanGiles. This is not the end - just a consolidation of my online presence to make my life a little easier!

tl;dr: Follow me on Twitter and check for the latest news on JonathanGiles.net.

Here is a little sample to show you how to create 3D content in JavaFX 2.0. More complex and fun examples will be coming in the future.


The main thing to learn here is any node in the JavaFX scene can be translated and rotated in 3D space. Basic transforms are provided for convenance on the Node class itself with translateX/Y/Z and rotate/rotateAxis properties. For more complex transforms you can get the Transforms list from a node with getTransforms() and add javafx.scene.transform.* classes. In this case I am adding javafx.scene.transform.Rotate transforms so that I can set the pivot point around which the node is rotated. By default if you use the rotate property on node it translates around the center of the node, which is the most common case for 2D but may not be what you want for 3D.

To setup a scene for doing 3D there are two things you need to set: stage.initDepthBuffer(true) and scene.setCamera(new PerspectiveCamera()). The first enables 3D Z-Ordering or Depth Testing this means that what ever is nearest the camera is drawn on top, with the furthest away stuff behind just as you would expect in a 3D world. The second means use a camera with perspective which means that two parallel lines will look closer together the further they are away from the camera. These two properties make the scene resemble the everyday 3D world we live in.

Complete Code

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.builders.RectangleBuilder;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
 * @author Jasper Potts
 */
public class Cube3D extends Application{

    @Override public void start(Stage stage) throws Exception {
        stage.setTitle("Cube 3D");
        stage.initDepthBuffer(true);

        Cube c = new Cube(50,Color.RED,1);
        c.rx.setAngle(45);
        c.ry.setAngle(45);
        Cube c2 = new Cube(50,Color.GREEN,1);
        c2.setTranslateX(100);
        c2.rx.setAngle(45);
        c2.ry.setAngle(45);
        Cube c3 = new Cube(50,Color.ORANGE,1);
        c3.setTranslateX(-100);
        c3.rx.setAngle(45);
        c3.ry.setAngle(45);

        Timeline animation = new Timeline();
        animation.getKeyFrames().addAll(
                new KeyFrame(Duration.ZERO,
                        new KeyValue(c.ry.angleProperty(), 0d),
                        new KeyValue(c2.rx.angleProperty(), 0d),
                        new KeyValue(c3.rz.angleProperty(), 0d)
                ),
                new KeyFrame(Duration.valueOf(1000),
                        new KeyValue(c.ry.angleProperty(), 360d),
                        new KeyValue(c2.rx.angleProperty(), 360d),
                        new KeyValue(c3.rz.angleProperty(), 360d)
                ));
        animation.setCycleCount(Animation.INDEFINITE);
        // create root group
        Group root = new Group(c,c2,c3);
        // translate and rotate group so that origin is center and +Y is up
        root.setTranslateX(400/2);
        root.setTranslateY(150/2);
        root.getTransforms().add(new Rotate(180,Rotate.X_AXIS));
        // create scene
        Scene scene = new Scene(root, 400,150);
        scene.setCamera(new PerspectiveCamera());
        stage.setScene(scene);
        stage.setVisible(true);
        // start spining animation
        animation.play();
    }

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

    public class Cube extends Group {
        final Rotate rx = new Rotate(0,Rotate.X_AXIS);
        final Rotate ry = new Rotate(0,Rotate.Y_AXIS);
        final Rotate rz = new Rotate(0,Rotate.Z_AXIS);
        public Cube(double size, Color color, double shade) {
            getTransforms().addAll(rz, ry, rx);
            getChildren().addAll(
                new RectangleBuilder() // back face
                    .width(size).height(size)
                    .fill(color.deriveColor(0.0, 1.0, (1 - 0.5*shade), 1.0))
                    .translateX(-0.5*size)
                    .translateY(-0.5*size)
                    .translateZ(0.5*size)
                    .build(),
                new RectangleBuilder() // bottom face
                    .width(size).height(size)
                    .fill(color.deriveColor(0.0, 1.0, (1 - 0.4*shade), 1.0))
                    .translateX(-0.5*size)
                    .translateY(0)
                    .rotationAxis(Rotate.X_AXIS)
                    .rotate(90)
                    .build(),
                new RectangleBuilder() // right face
                    .width(size).height(size)
                    .fill(color.deriveColor(0.0, 1.0, (1 - 0.3*shade), 1.0))
                    .translateX(-1*size)
                    .translateY(-0.5*size)
                    .rotationAxis(Rotate.Y_AXIS)
                    .rotate(90)
                    .build(),
                new RectangleBuilder() // left face
                    .width(size).height(size)
                    .fill(color.deriveColor(0.0, 1.0, (1 - 0.2*shade), 1.0))
                    .translateX(0)
                    .translateY(-0.5*size)
                    .rotationAxis(Rotate.Y_AXIS)
                    .rotate(90)
                    .build(),
                new RectangleBuilder() // top face
                    .width(size).height(size)
                    .fill(color.deriveColor(0.0, 1.0, (1 - 0.1*shade), 1.0))
                    .translateX(-0.5*size)
                    .translateY(-1*size)
                    .rotationAxis(Rotate.X_AXIS)
                    .rotate(90)
                    .build(),
                new RectangleBuilder() // top face
                    .width(size).height(size)
                    .fill(color)
                    .translateX(-0.5*size)
                    .translateY(-0.5*size)
                    .translateZ(-0.5*size)
                    .build()
            );
        }
    }
}