有一种方法可以将 subScene 中物体的 3D 坐标转换为 2D 场景坐标,但不幸的是它使用了私有 API,因此建议不要依赖它。
该想法基于相机投影的工作原理,并且基于com.sun.javafx.scene.input.InputEventUtils.recomputeCoordinates()
通常用于来自PickResult
.
假设您在子场景中有一个节点。对于该节点的给定点,您可以获得其坐标,例如:
Point3D coordinates = node.localToScene(Point3D.ZERO);
您可以了解节点的子场景:
SubScene subScene = NodeHelper.getSubScene(node);
现在您可以使用以下SceneUtils::subSceneToScene
方法
将点从内部 subScene 坐标转换为场景坐标。
获取一组新的坐标,参考场景:
coordinates = SceneUtils.subSceneToScene(subScene, coordinates);
但这些仍然是 3D 坐标。
将它们转换为 2D 的最后一步是使用CameraHelper::project
:
final Camera effectiveCamera = SceneHelper.getEffectiveCamera(node.getScene());
Point2D p2 = CameraHelper.project(effectiveCamera, coordinates);
以下示例将 2D 标签放置在场景中,与子场景中 3D 框的 8 个顶点完全相同的位置。
private final Rotate rotateX = new Rotate(0, Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(0, Rotate.Y_AXIS);
private double mousePosX;
private double mousePosY;
private double mouseOldX;
private double mouseOldY;
private Group root;
@Override
public void start(Stage primaryStage) {
Box box = new Box(150, 100, 50);
box.setDrawMode(DrawMode.LINE);
box.setCullFace(CullFace.NONE);
Group group = new Group(box);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.setFieldOfView(20);
camera.getTransforms().addAll (rotateX, rotateY, new Translate(0, 0, -500));
SubScene subScene = new SubScene(group, 500, 400, true, SceneAntialiasing.BALANCED);
subScene.setCamera(camera);
root = new Group(subScene);
Scene scene = new Scene(root, 500, 400);
primaryStage.setTitle("HUD: 2D Labels over 3D SubScene");
primaryStage.setScene(scene);
primaryStage.show();
updateLabels(box);
scene.setOnMousePressed(event -> {
mousePosX = event.getSceneX();
mousePosY = event.getSceneY();
});
scene.setOnMouseDragged(event -> {
mousePosX = event.getSceneX();
mousePosY = event.getSceneY();
rotateX.setAngle(rotateX.getAngle() - (mousePosY - mouseOldY));
rotateY.setAngle(rotateY.getAngle() + (mousePosX - mouseOldX));
mouseOldX = mousePosX;
mouseOldY = mousePosY;
updateLabels(box);
});
}
private List<Point3D> generateDots(Node box) {
List<Point3D> vertices = new ArrayList<>();
Bounds bounds = box.getBoundsInLocal();
vertices.add(box.localToScene(new Point3D(bounds.getMinX(), bounds.getMinY(), bounds.getMinZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMinX(), bounds.getMinY(), bounds.getMaxZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMinX(), bounds.getMaxY(), bounds.getMinZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMinX(), bounds.getMaxY(), bounds.getMaxZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMaxX(), bounds.getMinY(), bounds.getMinZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMaxX(), bounds.getMinY(), bounds.getMaxZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMaxX(), bounds.getMaxY(), bounds.getMinZ())));
vertices.add(box.localToScene(new Point3D(bounds.getMaxX(), bounds.getMaxY(), bounds.getMaxZ())));
return vertices;
}
private void updateLabels(Node box) {
root.getChildren().removeIf(Label.class::isInstance);
SubScene oldSubScene = NodeHelper.getSubScene(box);
AtomicInteger counter = new AtomicInteger(1);
generateDots(box).stream()
.forEach(dot -> {
Point3D coordinates = SceneUtils.subSceneToScene(oldSubScene, dot);
Point2D p2 = CameraHelper.project(SceneHelper.getEffectiveCamera(box.getScene()), coordinates);
Label label = new Label("" + counter.getAndIncrement() + String.format(" (%.1f,%.1f)", p2.getX(), p2.getY()));
label.setStyle("-fx-font-size:1.3em; -fx-text-fill: blue;");
label.getTransforms().setAll(new Translate(p2.getX(), p2.getY()));
root.getChildren().add(label);
});
}
FXyz3D库有另一个类似的示例。
编辑
这个答案的后期编辑,但值得一提的是不需要私有 API。Node::localToScene
允许遍历子场景的方法中有公共 API 。
所以这只是工作(注意true
论点):
Point3D p2 = box.localToScene(dot, true);
根据JavaDoc Node::localToScene
:
将一个点从该节点的局部坐标空间变换到其场景的坐标空间。如果 Node 没有任何 SubScene 或 rootScene 设置为 true,则结果点在 getScene() 返回的 Node 的场景坐标中。否则使用子场景坐标,相当于调用localToScene(Point3D)。
没有true
转换是在 subScene 内,但是有了它,转换就会从当前 subScene 到场景。在这种情况下,此方法调用SceneUtils::subSceneToScene
,因此我们不再需要这样做。
有了这个,updateLabels
简化为:
private void updateLabels(Node box) {
root.getChildren().removeIf(Label.class::isInstance);
AtomicInteger counter = new AtomicInteger(1);
generateDots(box).stream()
.forEach(dot -> {
Point3D p2 = box.localToScene(dot, true);
Label label = new Label("" + counter.getAndIncrement() + String.format(" (%.1f,%.1f)", p2.getX(), p2.getY()));
label.setStyle("-fx-font-size:1.3em; -fx-text-fill: blue;");
label.getTransforms().setAll(new Translate(p2.getX(), p2.getY()));
root.getChildren().add(label);
});
}