2

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);
}

}
José Pereda
  • 44,311
  • 7
  • 104
  • 132
J.Peshev
  • 23
  • 3
  • A group adapts its bounds to the nodes it contains. One quick trick you can try is adding a big box (with `DrawMode.LINE`) to the group, in order to define the expected bounding box of such group. Then adding the different boxes shouldn't modify the group's dimensions. Anyway you should clean your code and present something we can try in order to reproduce your issue and try to help you solve it. – José Pereda Jan 19 '18 at 17:01
  • Im not sure, if I explained my problem correct. Image 1 and image 4 have the same coordinates for all boxes. If I however draw image 3 first, then the something happens and the output is image 4(it should be image 1) Image two is the same things added as in image 3 appart from the bottom left blue box(If this is displayed first than I have an output of image 1(correct one). I print the coordinates of all boxes part of the group and on image 1 and 4 they are the same, however the output is diffrent... – J.Peshev Jan 19 '18 at 22:13
  • This problem occurs when a spefic box is added to the Group that is set as root of the SubScene, so I dont think uploading more code would change anything. Also it is quite visible from image 1 and 4 what the problem is, diffuculty might be my english and poor explanation. – J.Peshev Jan 19 '18 at 22:15
  • Can you post how you create the `scene` and the `subScene`? – José Pereda Jan 19 '18 at 22:25
  • [`code`] subScene = new SubScene(root, Scene_Width, Scene_Length, true, SceneAntialiasing.BALANCED); //set a camera for the scene subScene.setCamera(camera); getChildren().add(subScene); I'm sorry, but I don't know how to upload code as a comment. My class extends Parent, so I just add the subScene to it. – J.Peshev Jan 19 '18 at 22:27
  • Later when I create the actual Scene I add a BorderPane, in which an instance of this class with the subScene is added. – J.Peshev Jan 19 '18 at 22:30
  • If not clear enough, the problem is in image 4 with the blue box that looks like is in the other boxes, this blue box has the same coordinates as the one in image 1 that is in different rotation... – J.Peshev Jan 19 '18 at 22:34
  • If it might help, some additional things. If I display image 1 first and then image 3, image 3 looks different, even though nothing else is changed, just the order of displaying. The box that looks different is bottom blue box in image 3, as if it gets the rotation of the blue box from image 1. I'm new to java or any programming language, so the following thing might sound really stupid. I believe that somehow java keeps some storage of coordinates and when I draw new boxes it thinks the one that I'm displaying is similar to a previous one, so it kind of modifies its coordinates. – J.Peshev Jan 19 '18 at 22:42
  • In order to help you, we need to be able to reproduce your issue. You need to *edit* your question and post a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve). Else you can post a link to your project (i.e. GitHub), if you can share it. – José Pereda Jan 20 '18 at 09:19
  • I hope this edit, would make it clearer. – J.Peshev Jan 20 '18 at 13:03
  • Yes, at least anyone can copy your code and try to reproduce the issue. I'll try it later – José Pereda Jan 20 '18 at 14:41
  • The good news is I can reproduce your issue now. The bad news, it seems there is a bug... – José Pereda Jan 20 '18 at 17:46
  • If it will make a difference, the error doesn't occur if I change the dimensions of one of the boxes, but I do not want that. – J.Peshev Jan 20 '18 at 18:11
  • That actually makes a difference... see my answer. – José Pereda Jan 20 '18 at 18:15

1 Answers1

1

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.

José Pereda
  • 44,311
  • 7
  • 104
  • 132