如何在 javafx 中为 3D 对象获取 window 上的 2D 坐标

How to get 2D coordinates on window for 3D object in javafx

在 javafx 中,如果我们有 2D HUD(由 Pane 制成,然后我们从中创建 2D Hud 的 SubScene 对象)和 3D SubScene,在 3D 场景中我们有一些坐标为 (x,y,z) 的对象 -如果物体在我们的透视相机的视野中,我们如何在我们的 HUD 中获得物体的 2D 坐标?

我试图获取对象的第一个场景坐标,然后将其转换为 (sceneToScreen) 坐标,并与窗格的点 (0,0) 坐标相同,然后从第二点中减去第一个点,但我没有得到正确的结果。抱歉,因为我的错误 English.Can 有人帮忙吗?

有一种方法可以将子场景中的对象的3D坐标转换为2D场景坐标,但不幸的是它使用私有API,所以建议不要依赖它。

这个想法基于相机投影的工作原理,它基于通常用于来自 PickResult 的输入事件的 com.sun.javafx.scene.input.InputEventUtils.recomputeCoordinates() 方法。

假设您在子场景中有一个节点。对于该节点的给定点,您可以获得其坐标,如:

Point3D coordinates = node.localToScene(Point3D.ZERO);

并且可以了解节点的子场景:

SubScene subScene = NodeHelper.getSubScene(node);

现在您可以使用 SceneUtils::subSceneToScene 方法

Translates point from inner subScene coordinates to scene coordinates.

获取一组新坐标,参考场景:

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 library has another similar sample.

编辑

这个答案的后期编辑,但值得一提的是不需要私有 API。 Node::localToScene方法中有publicAPI允许遍历subScene。

所以这有效(注意true参数):

Point3D p2 = box.localToScene(dot, true);

根据 Node::localToScene 的 JavaDoc:

Transforms a point from the local coordinate space of this Node into the coordinate space of its scene. If the Node does not have any SubScene or rootScene is set to true, the result point is in Scene coordinates of the Node returned by getScene(). Otherwise, the subscene coordinates are used, which is equivalent to calling localToScene(Point3D).

没有 true 转换是在子场景内进行的,但是有了它,转换从当前子场景到场景。在这种情况下,这个方法调用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);
            });
}