Search code examples
3dparentjavafx-3d

javafx 3D Box, translation displayed is not what is expected


I'm currently working on a project and I should display 3D boxes on a Pane and I'm using javafx 3D for that. First I draw a big box, called container in my project and create a camera. Then with buttons added to the scene I draw other smaller boxes in the big container. However for some reason the small boxes affect each other and change their coordinates. More explanation with pictures:

This is box1, which I draw with key "X":

This is box1, which I draw with key "X"

This is box2, which I draw with key "C":

This is box2, which I draw with key "C"

If I only add box1 and restart my application and then draw box2, the output is image 1 and image 2, accordingly. However, if I don't restart the application each time and I wish to draw another box, I get the following output.

Output of box2, if before it box1 is drawn:

Output of box2, if before it box1 is drawn

Output of box1, if before it box2 is drawn:

Output of box1, if before it box2 is drawn

If I first draw box1, box1 looks okay, but image 3 is the output of box2. If I first draw box2, box1 output will change to image 4.

import javafx.scene.Camera;   
import javafx.scene.Group;     
import javafx.scene.Parent; 
import javafx.scene.PerspectiveCamera;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;

public class ContainerPane extends Parent {
//size of big container
private final double CONTAINER_DEPTH = 16.5;
private final double CONTAINER_WIDTH = 2.5;
private final double CONTAINER_HEIGHT = 4.0;
//group in which the box, container and camera are added
private Group root;
//coordiantes of box 1, row one is the actual coordinates and row two is the width, height, depth
private double[][] box1 = {{0, 4, 30},
                            {1, 2, 1.5}};
//coordiantes of box 2, row one is the actual coordinates and row two is the width, height, depth                           
private double[][] box2 = {{0, 6, 28},
                           {1.5, 1, 2}};

public ContainerPane(int Scene_Width, int Scene_Length){
    //create the group
    root = new Group();
    root.setAutoSizeChildren(false);

    //creating container
    Box container = new Box(CONTAINER_WIDTH , CONTAINER_HEIGHT, CONTAINER_DEPTH);
    container.setCullFace(CullFace.NONE);
    //drawing the container with only lines
    container.setDrawMode(DrawMode.LINE);
    //setting the color of the container
    PhongMaterial material = new PhongMaterial(Color.ORANGE);
    container.setMaterial(material);        
    root.getChildren().add(container);

    //create a camera
    PerspectiveCamera camera = new PerspectiveCamera(true);
    //add possible rotations and position of camera
    camera.getTransforms().addAll(new Translate(0, 0, -35));
    root.getChildren().add(camera);

    //create a Scene from the group
    SubScene subScene = new SubScene(root, Scene_Width, Scene_Length, true, SceneAntialiasing.BALANCED);
    //set a camera for the scene
    subScene.setCamera(camera);
    getChildren().add(subScene);
}
public void drawBox1(){
    //clear everything from root except container and camera
    try{
        root.getChildren().remove(2);
    }
    catch(Exception exception){

    }
    //create box1
    Box box = new Box(box1[1][0], box1[1][1], box1[1][2]);
    box.setDrawMode(DrawMode.FILL);
    box.setMaterial(new PhongMaterial(Color.BLUE));
    box.setTranslateX(-CONTAINER_WIDTH/2 + box.getWidth()/2 + 0.5*box1[0][0]);
    box.setTranslateY(-CONTAINER_HEIGHT/2 + box.getHeight()/2 + 0.5*box1[0][1]);
    box.setTranslateZ(CONTAINER_DEPTH/2 - box.getDepth()/2 - 0.5*box1[0][2]);
    //add it to the group
    root.getChildren().add(box);
}

public void drawBox2(){
    try{
        root.getChildren().remove(2);
    }
    catch(Exception exception){

    }
    Box box = new Box(box2[1][0], box2[1][1], box2[1][2]);
    box.setDrawMode(DrawMode.FILL);
    box.setMaterial(new PhongMaterial(Color.BLUE));
    box.setTranslateX(-CONTAINER_WIDTH/2 + box.getWidth()/2 + 0.5*box2[0][0]);
    box.setTranslateY(-CONTAINER_HEIGHT/2 + box.getHeight()/2 + 0.5*box2[0][1]);
    box.setTranslateZ(CONTAINER_DEPTH/2 - box.getDepth()/2 - 0.5*box2[0][2]);
    root.getChildren().add(box);
}
}

Main file, where I create an instance of the class.

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

public class Error extends Application {

@Override
public void start(Stage primaryStage) {
    ContainerPane container = new ContainerPane(750, 750);
    Scene scene = new Scene(container);
    scene.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>(){
        @Override
        public void handle(KeyEvent event){
           if(event.getCode() == KeyCode.X){
               container.drawBox1();
           }
           if(event.getCode() == KeyCode.C){
               container.drawBox2();
           }
    }});
    primaryStage.setScene(scene);
    primaryStage.show();
}

/**
 * @param args the command line arguments
 */
public static void main(String[] args) {
    launch(args);
}

}

Solution

  • To sum up the problem presented: drawing two similar 3D boxes should work regardless the order in which those are added to the scene, but the fact is that the order matters and the result is wrong.

    (I've set the box 2 to red for convenience)

    When drawing the taller blue box 1 first, the shorter, red one is drawn with the exact same dimensions as the taller one.

    When the shorter red box 2 is drawn first, the taller blue one is drawn with the exact same dimensions as the shorter red one.

    Explanation

    There is an explanation for this behavior: When you draw a box, cylinder or sphere to a scene/subscene, there is an internal cache in a mesh manager javafx.scene.shape.PredefinedMeshManager class:

    private HashMap<Integer, TriangleMesh> boxCache = null;
    

    When you add the mesh for the first time:

    if (key == 0) {
        key = generateKey(w, h, d);
    }
    mesh = manager.getBoxMesh(w, h, d, key);
    

    the key is null, and the manager creates the box for you:

    if (mesh == null) {
        mesh = Box.createMesh(w, h, d);
        boxCache.put(key, mesh);
    }
    

    and stores a key for this mesh. In case of a Box, this is how this key is generated:

    private static int generateKey(float w, float h, float d) {
        int hash = 3;
        hash = 97 * hash + Float.floatToIntBits(w);
        hash = 97 * hash + Float.floatToIntBits(h);
        hash = 97 * hash + Float.floatToIntBits(d);
        return hash;
    }
    

    Now, for the second mesh, you generate the key, and go and ask the manager for an existing mesh:

    TriangleMesh mesh = boxCache.get(key); 
    

    Given that box1 and box2 are different:

     Box1: {W: 1, H: 2, D: 1.5}
     Box2: {W: 1.5, H: 1, D: 2}
    

    any one would expect that the cache will return null and a new mesh was generated...

    ...but this is not what we are getting. Here we have the bug: the method that generates the key only takes into account the values of height, width and depth, but not the order, and any permutation of w, h or d will have the same key:

    hash = 97 * hash + Float.floatToIntBits(w);
    hash = 97 * hash + Float.floatToIntBits(h);
    hash = 97 * hash + Float.floatToIntBits(d);
    

    While this bug has nothing to do with this question, it happens due to a problem with the mesh manager, and the bug was filed.

    Workaround

    For now I would just use slightly different dimensions, like:

     Box1: {W: 1, H: 2, D: 1.5}
     Box2: {W: 1.500001, H: 1, D: 2}
    

    Or if you can't change those dimension, you can provide your own TriangleMesh implementation of a box, that won't be cached. For instance, you can find one in the FXyz3D library.

    Update

    I've just noticed that this bug is already filed here

    This bug is the result of mistakenly assuming that two boxes with an equal hash key have equal dimensions.