Search code examples
javafx3dlight

Make 3D object illuminate differently over time in JavaFX


Need to make a sphere in JavaFX "blink" over time, like fade in and fade out. Is it possible? Or at least change shades of color. I managed to make it blink by changing SelfIlluminationMap property of the PhongMaterial I am using, with this code

import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.Stage;
import javafx.util.Duration;

public class JavaFXApplication15 extends Application {

    Image im = new Image("bump.jpg");
    PhongMaterial ph = new PhongMaterial(Color.GREEN);
    int nums = 0;

    UpdateTimer timer;

    private class UpdateTimer extends AnimationTimer {

        int counter = 0;

        @Override
        public void handle(long now) {
            if (counter == 20) {
                update();
                counter = 0;
            }
            counter++;

        }
    }

    private Parent createContent() throws Exception {

        timer = new UpdateTimer();
        ph = new PhongMaterial(Color.YELLOW, null, null, null, im);

        Box box = new Box(5, 5, 5);
        box.setMaterial(ph);

        // Create and position camera
        PerspectiveCamera camera = new PerspectiveCamera(true);
        camera.getTransforms().addAll(
                new Rotate(-20, Rotate.X_AXIS),
                new Translate(0, 0, -50)
        );

        // Build the Scene Graph
        Group root = new Group();
        root.getChildren().add(camera);
        root.getChildren().add(box);

        // Use a SubScene
        SubScene subScene = new SubScene(
                root,
                300, 300,
                true,
                SceneAntialiasing.BALANCED
        );
        subScene.setFill(Color.ALICEBLUE);
        subScene.setCamera(camera);
        Group group = new Group();
        group.getChildren().add(subScene);

        return group;
    }

    void update() {

        if (ph.getSelfIlluminationMap() != null) {
            ph.setSelfIlluminationMap(null);
        } else {
            ph.setSelfIlluminationMap(im);
        }

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setResizable(false);
        Scene scene = new Scene(createContent());
        stage.setScene(scene);
        stage.show();
        timer.start();
    }

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

but is it possible to make it fade in and out? With some kind of transition possibly?


Solution

  • Just to see the effect, I tried animating the specularColor of a sphere's PhongMaterial. Starting from this example, I followed the approach shown here to get a color lookup table of equally spaced brightness values of a given hue.

    private final Queue<Color> clut = new LinkedList<>();
    

    The implementation of handle() simply cycles through the table.

    @Override
    public void handle(long now) {
        redMaterial.setSpecularColor(clut.peek());
        clut.add(clut.remove());
    }
    

    If the result is appealing, a more flexible approach might accrue from using a concrete subclass of Transition.

    private final Animation animation = new Transition() {
    
         {
             setCycleDuration(Duration.millis(1000));
             setAutoReverse(true);
             setCycleCount(INDEFINITE);
         }
    
         @Override
         protected void interpolate(double d) {
             redMaterial.setSpecularColor(Color.hsb(LITE.getHue(), 1, d));
             redMaterial.setDiffuseColor(Color.hsb(DARK.getHue(), 1, d / 2));
         }
     };
    

    image

    import java.util.LinkedList;
    import java.util.Queue;
    import javafx.animation.AnimationTimer;
    import javafx.animation.Animation;
    import javafx.animation.Transition;
    import javafx.application.Application;
    import javafx.geometry.Point3D;
    import javafx.scene.Group;
    import javafx.scene.PerspectiveCamera;
    import javafx.scene.Scene;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.input.ScrollEvent;
    import javafx.scene.paint.Color;
    import javafx.scene.paint.PhongMaterial;
    import javafx.scene.shape.Box;
    import javafx.scene.shape.DrawMode;
    import javafx.scene.shape.Sphere;
    import javafx.scene.transform.Rotate;
    import javafx.scene.transform.Transform;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    /**
     * @see https://stackoverflow.com/a/44447913/230513
     * @see https://stackoverflow.com/a/37755149/230513
     * @see https://stackoverflow.com/a/37743539/230513
     * @see https://stackoverflow.com/a/37370840/230513
     */
    public class TriadBox extends Application {
    
        private static final double SIZE = 300;
        private final Content content = Content.create(SIZE);
        private double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
    
        private static final class Content {
    
            private static final double WIDTH = 3;
            private static final Color LITE = Color.RED;
            private static final Color DARK = Color.RED.darker().darker();
            private final Xform group = new Xform();
            private final Group cube = new Group();
            private final Group axes = new Group();
            private final Box xAxis;
            private final Box yAxis;
            private final Box zAxis;
            private final Box box;
            private final Sphere sphere;
            private final PhongMaterial redMaterial = new PhongMaterial();
            private final UpdateTimer timer = new UpdateTimer();
    
            private class UpdateTimer extends AnimationTimer {
                private static final double N = 32d;
                private final Queue<Color> clut = new LinkedList<>();
    
                public UpdateTimer() {
                    for (int i = 0; i < N; i++) {
                        clut.add(Color.hsb(LITE.getHue(), 1, 1 - (i / N)));
                    }
                    for (int i = 0; i < N; i++) {
                        clut.add(Color.hsb(LITE.getHue(), 1, i / N));
                    }
                } 
    
                @Override
                public void handle(long now) {
                    redMaterial.setSpecularColor(clut.peek());
                    clut.add(clut.remove());
                }
            }
    
            private final Animation animation = new Transition() {
    
                 {
                     setCycleDuration(Duration.millis(1000));
                     setAutoReverse(true);
                     setCycleCount(INDEFINITE);
                 }
    
                 @Override
                 protected void interpolate(double d) {
                     redMaterial.setSpecularColor(Color.hsb(LITE.getHue(), 1, d));
                     redMaterial.setDiffuseColor(Color.hsb(DARK.getHue(), 1, d / 2));
                 }
             };
    
            private static Content create(double size) {
                Content c = new Content(size);
                c.cube.getChildren().addAll(c.box, c.sphere);
                c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
                c.group.getChildren().addAll(c.cube, c.axes);
                return c;
            }
    
            private Content(double size) {
                double edge = 3 * size / 4;
                xAxis = createBox(edge, WIDTH, WIDTH, edge);
                yAxis = createBox(WIDTH, edge / 2, WIDTH, edge);
                zAxis = createBox(WIDTH, WIDTH, edge / 4, edge);
                box = new Box(edge, edge / 2, edge / 4);
                box.setDrawMode(DrawMode.LINE);
                sphere = new Sphere(8);
                redMaterial.setDiffuseColor(DARK);
                redMaterial.setSpecularColor(LITE);
                sphere.setMaterial(redMaterial);
                sphere.setTranslateX(edge / 2);
                sphere.setTranslateY(-edge / 4);
                sphere.setTranslateZ(-edge / 8);
            }
    
            private Box createBox(double w, double h, double d, double edge) {
                Box b = new Box(w, h, d);
                b.setMaterial(new PhongMaterial(Color.AQUA));
                b.setTranslateX(-edge / 2 + w / 2);
                b.setTranslateY(edge / 4 - h / 2);
                b.setTranslateZ(edge / 8 - d / 2);
                return b;
            }
        }
    
        private static class Xform extends Group {
    
            private final Point3D px = new Point3D(1.0, 0.0, 0.0);
            private final Point3D py = new Point3D(0.0, 1.0, 0.0);
            private Rotate r;
            private Transform t = new Rotate();
    
            public void rx(double angle) {
                r = new Rotate(angle, px);
                this.t = t.createConcatenation(r);
                this.getTransforms().clear();
                this.getTransforms().addAll(t);
            }
    
            public void ry(double angle) {
                r = new Rotate(angle, py);
                this.t = t.createConcatenation(r);
                this.getTransforms().clear();
                this.getTransforms().addAll(t);
            }
    
            public void rz(double angle) {
                r = new Rotate(angle);
                this.t = t.createConcatenation(r);
                this.getTransforms().clear();
                this.getTransforms().addAll(t);
            }
        }
    
        @Override
        public void start(Stage primaryStage) throws Exception {
            primaryStage.setTitle("JavaFX 3D");
            Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
            primaryStage.setScene(scene);
            scene.setFill(Color.BLACK);
            PerspectiveCamera camera = new PerspectiveCamera(true);
            camera.setFarClip(SIZE * 6);
            camera.setTranslateZ(-2 * SIZE);
            scene.setCamera(camera);
            scene.setOnMousePressed((MouseEvent e) -> {
                mousePosX = e.getSceneX();
                mousePosY = e.getSceneY();
                mouseOldX = e.getSceneX();
                mouseOldY = e.getSceneY();
            });
            scene.setOnMouseDragged((MouseEvent e) -> {
                mouseOldX = mousePosX;
                mouseOldY = mousePosY;
                mousePosX = e.getSceneX();
                mousePosY = e.getSceneY();
                mouseDeltaX = (mousePosX - mouseOldX);
                mouseDeltaY = (mousePosY - mouseOldY);
                if (e.isShiftDown()) {
                    content.group.rz(-mouseDeltaX * 180.0 / scene.getWidth());
                } else if (e.isPrimaryButtonDown()) {
                    content.group.rx(+mouseDeltaY * 180.0 / scene.getHeight());
                    content.group.ry(-mouseDeltaX * 180.0 / scene.getWidth());
                } else if (e.isSecondaryButtonDown()) {
                    camera.setTranslateX(camera.getTranslateX() - mouseDeltaX * 0.1);
                    camera.setTranslateY(camera.getTranslateY() - mouseDeltaY * 0.1);
                }
            });
            scene.setOnScroll((final ScrollEvent e) -> {
                camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
            });
            primaryStage.show();
            //content.timer.start();
            content.animation.play();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }