FULL PRODUCT VERSION :
Java version: 1.8.0_121
ADDITIONAL OS VERSION INFORMATION :
Windows 10.0.1.15063 Build 15063
A DESCRIPTION OF THE PROBLEM :
I create a Sphere and put it in a Group and add a Perspective camera. I give the sphere a small Z displacement. I place the Group in a SubScene. I expect the sphere to be in the centre of the view. It is only if integers are used for the SubScene size. If I make the scene 400 x 400 it works OK. If I make the scene 400.5 x 400, the sphere is no longer at the centre. Instead it tracks some fraction of the camera movement.
I calculate the needed scene size so I get non-integer values. My fix is to use Math.floor to force integers..
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The attached test program creates side-by-side subscenes, one is 400 x 400, the other is 400+ some fraction. I use .5.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The attached test program creates side-by-side subscenes, one is 400 x 400, the other is 400+ some fraction. I use .5. I then animate the camera-sphere group rotating in X-Y. There is a stationary box. The correct view uses 400x400.
The view of the stationary box rotates correctly because the camera is rotating. The sphere is stationary because it is the same group as the moving camera.
In the right-hand subScene the red sphere is incorrectly moving. The only difference is the subScene size.
ACTUAL -
See above
ERROR MESSAGES/STACK TRACES THAT OCCUR :
Not applicable
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
import java.util.logging.Logger;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ObservableDoubleValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Point3D;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
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.Scale;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
* TestCameraFovWithSubScene - No problems!
*
* @author Robin
*
*/
public class TestCameraFovWithSubScene extends Application{
static final Material redmat = ofColor(Color.RED);
final static Color BACKGROUND_3D = Color.rgb(200, 200, 250); // 20,20,50 = dark navy
static final String SidePanelStyle = "-fx-background-color: DAE6F3;-fx-padding:5;";
static final String BoxStyle = "-fx-background-color: DAE6F3;-fx-padding:5;";
private final Logger log = Logger.getLogger( getClass().getSimpleName() );
Transformer transformer = new Transformer();
Translate translate = new Translate(0,0,-10); // Camera at z=0
Rotate rotate = new Rotate();
/**
*
* @param args
*/
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage){
VBox sidePanel = new VBox();
Label label = new Label("Side panel");
label.setStyle(BoxStyle);
sidePanel.getChildren().add(label);
sidePanel.setStyle(SidePanelStyle);
HBox sceneRoot = new HBox(2,sidePanel, transformer.createSubScene(0), transformer.createSubScene(.5) );
// Create a Scene with depth buffer enabled
Scene scene = new Scene( sceneRoot, 1000, 1000, true);
// Add the Scene to the Stage
stage.setScene(scene);
// Set the Title of the Stage
stage.setTitle("An Example with Predefined 3D Shapes");
// Display the Stage
stage.show();
}
static public PhongMaterial ofColor(Color c){
PhongMaterial mat = new PhongMaterial(Color.RED );
mat.setDiffuseColor(c);
mat.setSpecularColor(Color.BLACK);
return mat;
}
// ====================================================================
/**
* Create a Y-up camera.
* @author Robin
*/
static public class DynamicCamera extends Group{
static final double D2R = Math.PI/180.;
public final static double FIELD_OF_VIEW = 60; /* Nominal in degrees*/
public final static double NEAR_CLIP = 0.0003;
public final static double FAR_CLIP = 100.;
String tag;
private final Logger log = Logger.getLogger( getClass().getSimpleName() );
PerspectiveCamera rawCamera;
double sc = 1.0;
Scale flipXy = new Scale(-sc, -sc, sc); /* Scale Y by -1 so camera Y is up.
Also need to flip X for right-handed scheme. After, x=right, y=up z=fwd*/
Group yUpCamera = this;
/**
* To get the camera into the right coordinate system, we first build a raw camera,
* then flip x and y to put it in Heliocentric equatorial coordinates. We then
* put it into a group (this this of this class). Then 'this' camera gets the various
* transform applied to it derived from orbits and user controls.
*
* @param id
* @param cameraControls for this camera
*/
public DynamicCamera( double fieldOfViewAngle, boolean isMain){
rawCamera = new PerspectiveCamera(true); /* true = eye position fixed at 0,0,0
in local coord. of the camera node. In non-fixed eye case, the camera position
is (w/2, h/2, h/2/tan) in local coord. of the camera. */
tag = isMain ? "Main" : "Side";
rawCamera.setNearClip(NEAR_CLIP); // Making this too small gives artifacts
rawCamera.setFarClip(FAR_CLIP);
rawCamera.setFieldOfView(fieldOfViewAngle); /* Specifies the field of view angle
of the camera's projection plane, measured in degrees.*/
rawCamera.getTransforms().add(flipXy); // Note flip is added to raw camera
yUpCamera.getChildren().add(rawCamera);
}
//public void setTransforms(ObservableList<Transform> transforms){
// yUpCamera.getTransforms().addAll(transforms );
//}
/**
* @return the raw perspective camera that should be added to the subScene
*/
public Camera getRawCamera(){
return rawCamera;
}
/**
* Bind camera width of field of view to a supplied observable double.
* @param observableFovAngle
*/
public void bindFov(ObservableDoubleValue observableFovAngle){
rawCamera.fieldOfViewProperty().bind(
observableFovAngle ); /* */
}
}
// =========================================
static public class Transformer{
double radius = 3;
Translate translate = new Translate(0,0,-10); // Camera at z=0
Rotate rotate = new Rotate();
Timeline timeline;
KeyFrame keyframe;
Duration duration = Duration.millis( 30 );
private final Logger log = Logger.getLogger( getClass().getSimpleName() );
double angle;
private boolean DEPTH_ON = true;
Group subSceneRoot;
public Transformer(){
}
public SubScene createSubScene(double fraction){
subSceneRoot = new Group();
SubScene subScene = new SubScene(subSceneRoot, 400+fraction, 400 , DEPTH_ON, SceneAntialiasing.BALANCED);
subScene.setFill( BACKGROUND_3D ); // Set BG color os scene
DynamicCamera yUpCamera = new DynamicCamera(60,true);
subScene.setCamera(yUpCamera.getRawCamera());
Group viewPoint = new Group( yUpCamera, makeSphere() ); //, light
viewPoint.getTransforms().addAll(buildTransforms() );
subSceneRoot.getChildren().addAll( viewPoint, makeBox() );
startTimeline();
return subScene;
}
public Box makeBox(){
// Create a Box
double len = 2.;
Box box = new Box(len,len,len);
box.setTranslateX(0);
box.setTranslateY(0);
box.setTranslateZ(10);
return box;
}
public Sphere makeSphere(){
// Create a Sphere
Sphere sphere = new Sphere(.005);
sphere.setMaterial(redmat);
sphere.setDrawMode(DrawMode.LINE);
sphere.setTranslateZ(.1);
return sphere;
}
public ObservableList<Transform> buildTransforms(){
ObservableList<Transform> transforms =
FXCollections.observableArrayList(translate);
return transforms;
}
/**
* startTimeline & run indefinitely
*/
public void startTimeline(){
log.info("Timeline starting ...");
timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
keyframe = new KeyFrame( duration,
eh -> timeEvent() ); // forwards=true, reset=false
timeline.getKeyFrames().clear();
timeline.getKeyFrames().add(keyframe);
log.info("Timeline playing at frame rate " + duration );
timeline.play();
log.info("... Timeline started");
}
private void timeEvent() {
angle += 5.;
if(angle >360)angle = 0;
//log.info("Tick " + x);
update(angle);
}
public void update(double angle){
double rad = angle*Math.PI / 180.;
double x = radius * Math.cos(rad);
double y = radius * Math.sin(rad);
translate.setX(x); // Problem is with x
translate.setY(y);
rotate.setAngle(angle);
rotate.setAxis(new Point3D(0,0,1.) );
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Use only integers for subScene size