【问题标题】:How to get 2D scene coordinates of a 3D object in JavaFX如何在 JavaFX 中获取 3D 对象的 2D 场景坐标
【发布时间】:2022-02-02 06:08:14
【问题描述】:

我正在尝试创建一个 3D 子场景,其中使用 2D 叠加层中的 Label 对象标记对象。我在这个主题上看到过类似的问题,它们都指向在要在 3D 空间中标记的节点上使用 Node.localToScene 方法。但这似乎不适用于我的情况。我在这里取了 FXyz FloatingLabels 示例中的示例代码:

FloatingLabels.java

标签对象需要随着 3D 场景的修改而更新它们的位置,我已经这样做了,但是当我打印出 Node.localToScene 方法返回的坐标时,它们太大而无法在应用程序中场景,因此标签在场景中永远不可见。我编写了一个示例程序来说明该问题,其设置与 FXyz 示例代码非常相似,但我创建了一个额外的 SubScene 对象来保存 2D 和 3D SubScene 对象,以便将它们植入带有滑块的更大的应用程序窗口中控制。 3D 场景使用透视相机,显示一个大球体,沿 x/y/z 轴有彩色球体,表面上还有一些额外的小块供参考:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SubScene;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.AmbientLight;
import javafx.scene.PointLight;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.Priority;
import javafx.scene.shape.Sphere;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.geometry.Point3D;

import java.util.Map;
import java.util.HashMap;

public class LabelTest extends Application {

  private Map<Node, Label> nodeToLabelMap;

  @Override
  public void start (Stage stage) {

    // Create main scene graph

    var objects3d = new Group();

    Rotate xRotate = new Rotate (0, 0, 0, 0, Rotate.X_AXIS);
    Rotate yRotate = new Rotate (0, 0, 0, 0, Rotate.Y_AXIS);
    objects3d.getTransforms().addAll (
      xRotate,
      yRotate
    );

    var root3d = new Group();
    root3d.getChildren().add (objects3d);

    var camera = new PerspectiveCamera (true);
    camera.setTranslateZ (-25);
    
    var scene3d = new SubScene (root3d, 500, 500, true, SceneAntialiasing.BALANCED);
    scene3d.setFill (Color.rgb (20, 20, 80));
    scene3d.setCamera (camera);

    var sceneRoot = new Group (scene3d);
    var objects2d = new Group();
    sceneRoot.getChildren().add (objects2d);
    var viewScene = new SubScene (sceneRoot, 500, 500);

    scene3d.widthProperty().bind (viewScene.widthProperty());
    scene3d.heightProperty().bind (viewScene.heightProperty());

    // Add lights and objects

    var ambient = new AmbientLight (Color.color (0.7, 0.7, 0.7));
    var point = new PointLight (Color.color (0.3, 0.3, 0.3));
    point.setTranslateX (-25);
    point.setTranslateY (-25);
    point.setTranslateZ (-50);
    
    root3d.getChildren().addAll (ambient, point);

    var globe = new Sphere (5);
    globe.setMaterial (new PhongMaterial (Color.color (0.3, 0.3, 0.3)));

    var xSphere = new Sphere (0.5);
    xSphere.setMaterial (new PhongMaterial (Color.RED));
    xSphere.setTranslateX (5);

    var ySphere = new Sphere (0.5);
    ySphere.setMaterial (new PhongMaterial (Color.GREEN));
    ySphere.setTranslateY (5);

    var zSphere = new Sphere (0.5);
    zSphere.setMaterial (new PhongMaterial (Color.BLUE));
    zSphere.setTranslateZ (5);
    
    objects3d.getChildren().addAll (globe, xSphere, ySphere, zSphere);

    var nubMaterial = new PhongMaterial (Color.color (0.2, 0.2, 0.2));
    for (int i = 0; i < 200; i++) {
      var nub = new Sphere (0.125);
      nub.setMaterial (nubMaterial);
      var phi = 2*Math.PI*Math.random();
      var theta = Math.acos (2*Math.random() - 1);
      var z = -5 * Math.sin (theta) * Math.cos (phi);
      var x = 5 * Math.sin (theta) * Math.sin (phi);
      var y = -5 * Math.cos (theta);
      nub.setTranslateX (x);
      nub.setTranslateY (y);
      nub.setTranslateZ (z);
      objects3d.getChildren().add (nub);
    } // for

    // Add labels

    var xLabel = new Label ("X axis");
    xLabel.setTextFill (Color.RED);

    var yLabel = new Label ("Y axis");
    yLabel.setTextFill (Color.GREEN);

    var zLabel = new Label ("Z axis");
    zLabel.setTextFill (Color.BLUE);

    objects2d.getChildren().addAll (xLabel, yLabel, zLabel);

    nodeToLabelMap = new HashMap<>();
    nodeToLabelMap.put (xSphere, xLabel);
    nodeToLabelMap.put (ySphere, yLabel);
    nodeToLabelMap.put (zSphere, zLabel);

    xRotate.angleProperty().addListener ((obs, oldVal, newVal) -> updateLabels());
    yRotate.angleProperty().addListener ((obs, oldVal, newVal) -> updateLabels());
    camera.translateZProperty().addListener ((obs, oldVal, newVal) -> updateLabels());
    Platform.runLater (() -> updateLabels());

    // Create main pane

    var gridPane = new GridPane();
    
    var stackPane = new StackPane (viewScene);
    viewScene.heightProperty().bind (stackPane.heightProperty());
    viewScene.widthProperty().bind (stackPane.widthProperty());
    viewScene.setManaged (false);

    gridPane.add (stackPane, 0, 0);
    gridPane.setVgrow (stackPane, Priority.ALWAYS);
    gridPane.setHgrow (stackPane, Priority.ALWAYS);

    // Add controls

    var xSlider = new Slider (-90, 90, 0);
    xRotate.angleProperty().bind (xSlider.valueProperty());
    var ySlider = new Slider (-180, 180, 0);
    yRotate.angleProperty().bind (ySlider.valueProperty());
    var zSlider = new Slider (-60, -25, -25);
    camera.translateZProperty().bind (zSlider.valueProperty());

    ToolBar toolbar = new ToolBar (
      new Label ("X rotation:"),
      xSlider,
      new Label ("Y rotation:"),
      ySlider,
      new Label ("Z position:"),
      zSlider
    );
    gridPane.add (toolbar, 0, 1);

    // Start the show

    stage.setTitle ("Label Test");
    stage.setScene (new Scene (gridPane, 800, 600));
    stage.show();

  } // start
  
  private void updateLabels () {
    
    nodeToLabelMap.forEach ((node, label) -> {
      var coord = node.localToScene (Point3D.ZERO, true);
      System.out.println ("label = " + label.getText() + ", coord = " + coord);
      label.getTransforms().setAll (new Translate(coord.getX(), coord.getY()));
    });

  } // updateLabels
  
  public static void main (String[] args) {

    launch (args);

  } // main

} // LabelTest class

您可以使用此脚本编译和运行 LabelTest.java 程序(我在 Mac 上使用 JavaFX 14 和 JDK 14.0.2):

#!/bin/sh

set -x

export PATH_TO_FX=javafx-sdk-14/lib

javac --module-path $PATH_TO_FX --add-modules javafx.controls LabelTest.java
if [ $? -ne 0 ] ; then
  exit 1
fi

java -cp . --module-path $PATH_TO_FX --add-modules javafx.controls LabelTest

我的测试程序输出包含非常大的标签坐标,不代表彩色轴球体的位置,例如:

label = Y axis, coord = Point3D [x = 17448.00808897467, y = 21535.846392310217, z = 0.0]
label = X axis, coord = Point3D [x = 26530.33870777918, y = 12453.515773505665, z = 0.0]
label = Z axis, coord = Point3D [x = 17448.008088974653, y = 12453.515773505665, z = 0.0]

我的显示如下:

它应该看起来更像 FXyz 的示例,在轴球旁边带有标签:

【问题讨论】:

    标签: java javafx 3d coordinates fxyz3d


    【解决方案1】:

    如果您遵循您已发布的link 中所做的操作,您就会成功。

    对于初学者来说,只有一个子场景,而不是两个。

    所以我删除了这些行:

    -        var viewScene = new SubScene (new Group (scene3d), 500, 500);
    
    -        scene3d.widthProperty().bind (viewScene.widthProperty());
    -        scene3d.heightProperty().bind (viewScene.heightProperty());
    

    并替换了这些:

    -    var stackPane = new StackPane (viewScene);
    -    viewScene.heightProperty().bind (stackPane.heightProperty());
    -    viewScene.widthProperty().bind (stackPane.widthProperty());
    -    viewScene.setManaged (false);
    
    +    var stackPane = new StackPane (sceneRoot);
    +    scene3d.heightProperty().bind (stackPane.heightProperty());
    +    scene3d.widthProperty().bind (stackPane.widthProperty());
    

    现在对我来说效果很好:

    label = Z axis, coord = Point3D [x = 613.2085772621016, y = 286.33580935946725, z = 0.0]
    label = X axis, coord = Point3D [x = 401.67010722785966, y = 219.90328164976754, z = 0.0]
    label = Y axis, coord = Point3D [x = 400.0, y = 503.57735384935296, z = 0.0]
    

    【讨论】:

    • 谢谢!我只是希望我能更好地理解为什么原始代码不起作用以及子场景是如何工作的。我查看了 JavaFX API,但无论出于何种原因,我似乎并没有真正掌握场景图分支坐标转换方面的幕后情况。
    • 这是一个复杂的过程,但最后都是一堆矩阵变换......但问题是找出如何去做(相机,子场景,每个节点的变换, ... 参与)。不久前我发布了这个solution。编辑之前使用的内容运行良好,并且可以更好地解释内部工作的完成方式。
    • 令我误解的是 localToScene​(Point3D localPoint, boolean rootScene) 方法的文档。由于我们指定 rootScene=true,文档说它返回一个相对于场景的坐标。我们用来更新标签位置的这些坐标是相对于包含 3D 场景的面板的左上角(即:整个场景中的子场景),而不是返回的整个场景的左上角获取场景()。我确实阅读了您之前发布的解决方案并进行了编辑。无论如何,非常感谢您的帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-14
    • 2011-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-21
    相关资源
    最近更新 更多