旋转后如何正确获取3D形状的屏幕坐标

时间:2018-06-11 04:05:13

标签: java javafx rotation javafx-8 javafx-3d

我需要通过绘制一个矩形区域在我的三维模型中选择多个形状,并选择位于该区域的所有形状。

如果只有x或y旋转,我可以绘制区域并选择节点。但是大多数x和y的组合给出了不正确的结果。

我认为在屏幕坐标中获取鼠标和节点位置并比较它们是一件简单的事情,但这不能按预期工作。

在下面的应用程序中,您可以使用鼠标右键绘制一个区域(您必须单击一个球体才能启动,我不知道为什么,只有您单击一个,才会触发它们在子场景上的鼠标事件球?)。另一次右键单击(再次在球体上)清除选择。

您可以左键单击拖动以旋转模型(再次,您必须从球体开始)。在围绕x轴旋转任意数量后,您可以成功选择一个区域。同样地,绕y轴旋转。但是,x和y旋转的组合会产生错误的结果。对于例如对角拖动节点,您将得到如下所示的结果。

Result of selection after x and y rotation

关于出了什么问题的任何想法?或建议其他方法来解决这个问题?提前致谢

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.Slider;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape3D;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

public class ScreenSelection extends Application {


    private final PerspectiveCamera camera = new PerspectiveCamera(true);

    private final Group root = new Group();
    private final Group world = new Group();
    private final XFormWorld camPiv = new XFormWorld();

    private final Slider zoom = new Slider(-100, 0, -50);
    private final Button reset = new Button("Reset");

    private final Pane pane = new Pane();
    private final BorderPane main = new BorderPane();

    double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
    double mouseFactorX, mouseFactorY;


public void start(Stage stage) throws Exception {

    camera.setTranslateZ(zoom.getValue());
    reset.setOnAction(eh -> {
        camPiv.reset();
        zoom.setValue(-50);
    });
    camera.setFieldOfView(60);

    camPiv.getChildren().add(camera);
    Collection<Shape3D> world = createWorld();
    RectangleSelect rs = new RectangleSelect(main, world);

    this.world.getChildren().addAll(world);
    root.getChildren().addAll(camPiv, this.world);

    SubScene subScene = new SubScene(root, -1, -1, true, SceneAntialiasing.BALANCED);
    subScene.setDepthTest(DepthTest.ENABLE);
    subScene.setCamera(camera);

    subScene.heightProperty().bind(pane.heightProperty());
    subScene.widthProperty().bind(pane.widthProperty());

    zoom.valueProperty().addListener((o, oldA, newA) -> camera.setTranslateZ(newA.doubleValue()));


    HBox controls = new HBox();
    controls.getChildren().addAll(new HBox(new Label("Zoom: "), zoom), new HBox(reset));

    pane.getChildren().addAll(controls, subScene);

    MenuBar menu = new MenuBar(new Menu("File"));
    main.setTop(menu);

    main.setCenter(pane);

    Scene scene = new Scene(main);

    subScene.setOnMousePressed((MouseEvent me) -> {
        mousePosX = me.getSceneX();
        mousePosY = me.getSceneY();
    });

    subScene.setOnMouseDragged((MouseEvent me) -> {
        if (me.isSecondaryButtonDown()) {
            rs.onMouseDragged(me);
        } else if (me.isPrimaryButtonDown()) {
            mouseOldX = mousePosX;
            mouseOldY = mousePosY;
            mousePosX = me.getSceneX();
            mousePosY = me.getSceneY();
            mouseDeltaX = (mousePosX - mouseOldX);
            mouseDeltaY = (mousePosY - mouseOldY);
            camPiv.ry(mouseDeltaX * 180.0 / subScene.getWidth());
            camPiv.rx(-mouseDeltaY * 180.0 / subScene.getHeight());


        }
    });
    subScene.setOnMouseReleased((MouseEvent me) -> {
        rs.omMouseDragReleased(me);
    });
    subScene.setOnMouseClicked((MouseEvent me) -> {
        if (me.getButton() == MouseButton.SECONDARY) {
            rs.clearSelection();
        }
    });
    stage.setScene(scene);
    stage.setWidth(800);
    stage.setHeight(800);
    stage.show();

}

private Collection<Shape3D> createWorld() {

    List<Shape3D> shapes = new ArrayList<Shape3D>();

    Random random = new Random(System.currentTimeMillis());
    for (int i=0; i<4000; i++) {
        double x = (random.nextDouble() - 0.5) * 30;
        double y = (random.nextDouble() - 0.5) * 30 ;
        double z = (random.nextDouble() - 0.5) * 30 ;

        Sphere point = new Sphere(0.2);
        point.setMaterial(new PhongMaterial(Color.SKYBLUE));
        point.setPickOnBounds(false);
        point.getTransforms().add(new Translate(x, y, z));
        shapes.add(point);
    }

    return shapes;
}


public static void main(String[] args) {
    launch(args);
}

public class XFormWorld extends Group {
    Transform rotation = new Rotate();
    Translate translate = new Translate();

    public XFormWorld() {
        getTransforms().addAll(rotation, translate);
    }

    public void reset() {
        rotation = new Rotate();
        getTransforms().set(0, rotation);

    }

    public void rx(double angle) {
        rotation = rotation.createConcatenation(new Rotate(angle, Rotate.X_AXIS));
        getTransforms().set(0, rotation);
    }

    public void ry(double angle) {
        rotation = rotation.createConcatenation(new Rotate(angle, Rotate.Y_AXIS));
        getTransforms().set(0, rotation); 
    }

    public void tx(double amount) {
        translate.setX(translate.getX() + amount);
    }

}

public class RectangleSelect  {

    private static final int START_X = 0;
    private static final int START_Y = 1;
    private static final int END_X = 2;
    private static final int END_Y = 3;

    private double[] sceneCoords = new double[2]; //mouse drag x, y in scene coords 
    private double[] screenCoords = new double[2]; //mouse drag current x, y in screen coords
    private double[] boundsInScreenCoords = new double[4]; //top left x, y, bottom right x,y in screen coords
    private Collection<Shape3D> world;

    private PhongMaterial selected = new PhongMaterial(Color.YELLOW);
    private Rectangle rectangle;

    public RectangleSelect(Pane pane, Collection<Shape3D> world) {
        sceneCoords[START_X] = Double.MIN_VALUE;
        sceneCoords[START_Y] = Double.MIN_VALUE;
        rectangle = new Rectangle();
        rectangle.setStroke(Color.RED);
        rectangle.setOpacity(0.0);
        rectangle.setMouseTransparent(true);
        rectangle.setFill(null);

        this.world = world;
        pane.getChildren().add(rectangle);
    }


    public void onMouseDragged(MouseEvent me) {
        clearSelection();
        if (sceneCoords[START_X] == Double.MIN_VALUE) {
            sceneCoords[START_X] = me.getSceneX();
            sceneCoords[START_Y] = me.getSceneY();
            screenCoords[START_X] = me.getScreenX();
            screenCoords[START_Y] = me.getScreenY();
        }
        double sceneX = me.getSceneX();
        double sceneY = me.getSceneY();
        double screenX = me.getScreenX();
        double screenY = me.getScreenY();

        double topX = Math.min(sceneCoords[START_X], sceneX);
        double bottomX = Math.max(sceneCoords[START_X], sceneX);
        double leftY = Math.min(sceneCoords[START_Y], sceneY);
        double rightY = Math.max(sceneCoords[START_Y], sceneY);

        boundsInScreenCoords[START_X] = Math.min(screenCoords[START_X], screenX);
        boundsInScreenCoords[END_X]= Math.max(screenCoords[START_X], screenX);
        boundsInScreenCoords[START_Y] = Math.min(screenCoords[START_Y], screenY);
        boundsInScreenCoords[END_Y] = Math.max(screenCoords[START_Y], screenY);

        world.forEach(this::selectIfInBounds);

        rectangle.setX(topX);
        rectangle.setY(leftY);
        rectangle.setWidth(bottomX - topX);
        rectangle.setHeight(rightY - leftY);
        rectangle.setOpacity(1.0);
    }


    private void selectIfInBounds(Shape3D node) {
        Point2D screenCoods = node.localToScreen(0.0, 0.0, 0.0);
        if (screenCoods.getX() > boundsInScreenCoords[START_X] &&
            screenCoods.getY() > boundsInScreenCoords[START_Y] &&
            screenCoods.getX() < boundsInScreenCoords[END_X] &&
            screenCoods.getY() < boundsInScreenCoords[END_Y]) {
            Material m = node.getMaterial();
            node.getProperties().put("material", m);
            node.setMaterial(selected);
        }
    }

    private void unselect(Shape3D node) {
        Material m = (Material) node.getProperties().get("material");
        if (m != null) {
            node.setMaterial(m);
        }
    }

    public void omMouseDragReleased(MouseEvent me) {
        rectangle.setOpacity(0.0);
        sceneCoords[START_X]  = Double.MIN_VALUE;
        sceneCoords[START_Y] = Double.MIN_VALUE;
    }

    public void clearSelection() {
        world.forEach(this::unselect);
    }
}   

}

2 个答案:

答案 0 :(得分:1)

感谢user1803551获取这些链接,第一个是用于跟踪错误的一个很好的用例,它似乎是GeneralTransform3D.transform(Vec3d)中的错误

GeneralTransform3D.transform(Vec3d)(在计算鼠标位置的过程中由Camera.project调用)的实现调用具有相同点对象的两个arg变换方法。即。

public Vec3d transform(Vec3d point) {
    return transform(point, point);
}

通过使用相同的对象调用它,计算结果是borked。你可能会看到如果pointOut和point是同一个对象,那么pointOut.y的计算将是不正确的(这是来自GeneralTransform3D.transform)

    pointOut.x = (float) (mat[0] * point.x + mat[1] * point.y
            + mat[2] * point.z + mat[3]);
    pointOut.y = (float) (mat[4] * point.x + mat[5] * point.y
            + mat[6] * point.z + mat[7]); 

一切都很好,不知道如何解决这个问题

答案 1 :(得分:1)

我已经确认这是comments中列出的错误。对于您的情况,我认为最简单的解决方案是旋转world而不是相机。由于这些是相对于彼此移动的仅有的两个对象,因此移动哪个对象并不重要。如果你想统一变换,你也可以将变焦应用于世界而不是相机,但这并不重要。

旋转世界

让世界可以变为XFormWorld,并完全删除camPiv。请注意,没有理由将camPiv添加到场景中,因为它是一个空组;只能通过setCamera添加摄像头,然后您可以绑定其变换(见下文)。

您需要以两种方式更改数学:

  1. 翻转rxry的轮值,因为在+x中旋转世界就像在-x中旋转相机一样(y也相同)。
  2. 纠正旋转枢轴。如果您在x轴上旋转,然后在y轴上旋转,y轴旋转实际上会围绕z旋转(因为旋转矩阵规则)。这意味着新旋转的枢轴取决于当前旋转。如果您在x上进行了轮播,则现在需要在z上轮换以获得y轮换。数学很简单,但你需要知道自己在做什么。
  3. 直接转换相机

    即使转换相机,您也不需要camPiv的原因是因为您可以直接绑定到其变换。在你的情况下,你可以做

    camera.translateZProperty().bind(zoom.valueProperty());
    

    而不是恼人的组合

    camera.setTranslateZ(zoom.getValue());
    zoom.valueProperty().addListener((o, oldA, newA) -> camera.setTranslateZ(newA.doubleValue()));
    

    对于任何Transform,将其添加到camera.getTransforms()并将其值(角度,平移...)绑定到DoubleProperty,其值是您通过输入更改的值。

    鼠标事件和边界选择

    您的subScene(和world)包含许多节点,它们之间有空格。默认情况下,当您单击subScene时,仅当您单击其中的(非鼠标透明)节点时,才会将事件传递给它。这是因为pickOnBoundsfalse,这意味着点击&#34;经过&#34;直到碰到什么东西。如果你添加

    subScene.setPickOnBounds(true);
    

    容器(subScene)将在其框边界内接收任何事件,无论是否存在节点。

    修复后,您将遇到一个新问题:绘制矩形后释放鼠标将导致它通过clearSelection()消失。这是因为您在onMouseClicked中调用了该方法,但是在拖动结束时会生成一个单击事件,因为有一个按下和一个版本。你想要的是清除选​​择,如果它点击没有拖动。这是通过isStillSincePress()

    完成的
    subScene.setOnMouseClicked(me -> {
        if (me.getButton() == MouseButton.SECONDARY && me.isStillSincePress()) {
            rs.clearSelection();
        }
    });
    

    您没有遇到此问题的原因是subScene如果发生在空白区域,则没有收到发布事件。总结一下:

    • 按空位:事件未注册 - 没有任何反应。
    • 按在球体上:已注册事件 - 已开始绘制三角形图。
      • 在空白区域发布:未注册的事件 - 未清除矩形。
      • 在球体上发布:已注册事件 - 已清除矩形。

    布局

    除非你需要绝对定位(并且你很少这样做),否则不要使用Pane。选择一个更好地完成工作的子类。 StackPane允许您通过使用图层将控件置于SubScene之上。将setPickOnBounds设置为false可让较低层正常接收事件。另外,我使用AnchorPane将控件放在左上角。

    工作解决方案

    这是您修改后的代码。我在进行重构时做了一些重构,这样我就更容易使用了。我相信整个RectangleSelect也可能会被大量修改,但问题已经足够加载了。

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Random;
    
    import javafx.application.Application;
    import javafx.geometry.Point2D;
    import javafx.geometry.Point3D;
    import javafx.scene.Group;
    import javafx.scene.PerspectiveCamera;
    import javafx.scene.Scene;
    import javafx.scene.SceneAntialiasing;
    import javafx.scene.SubScene;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.Menu;
    import javafx.scene.control.MenuBar;
    import javafx.scene.control.Slider;
    import javafx.scene.input.MouseButton;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.AnchorPane;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.StackPane;
    import javafx.scene.paint.Color;
    import javafx.scene.paint.Material;
    import javafx.scene.paint.PhongMaterial;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.shape.Shape3D;
    import javafx.scene.shape.Sphere;
    import javafx.scene.transform.Rotate;
    import javafx.scene.transform.Transform;
    import javafx.scene.transform.Translate;
    import javafx.stage.Stage;
    
    public class ScreenSelectionNew extends Application {
    
        private final PerspectiveCamera camera = new PerspectiveCamera(true);
    
        private final XFormWorld world = new XFormWorld();
    
        private double mousePosX, mousePosY, mouseOldX, mouseOldY;
    
        @Override
        public void start(Stage stage) throws Exception {
            BorderPane main = new BorderPane();
            StackPane stackPane = new StackPane();
    
            SubScene subScene = setupSubScene(main);
            subScene.heightProperty().bind(stackPane.heightProperty());
            subScene.widthProperty().bind(stackPane.widthProperty());
            stackPane.getChildren().addAll(subScene, setupControls());
    
            MenuBar menu = new MenuBar(new Menu("File"));
    
            main.setTop(menu);
            main.setCenter(stackPane);
            Scene scene = new Scene(main);
    
            stage.setScene(scene);
            stage.setWidth(800);
            stage.setHeight(800);
            stage.show();
        }
    
        private SubScene setupSubScene(Pane parent) {
            Collection<Shape3D> worldContent = createWorld();
            world.getChildren().addAll(worldContent);
    
            SubScene subScene = new SubScene(world, -1, -1, true, SceneAntialiasing.BALANCED);
            subScene.setCamera(camera);
            subScene.setPickOnBounds(true);
            camera.setFieldOfView(60);
    
            RectangleSelect rs = new RectangleSelect(parent, worldContent);
    
            subScene.setOnMousePressed(me -> {
                mousePosX = me.getX();
                mousePosY = me.getY();
            });
    
            subScene.setOnMouseDragged(me -> {
                if (me.isSecondaryButtonDown()) {
                    rs.onMouseDragged(me);
                } else if (me.isPrimaryButtonDown()) {
                    mouseOldX = mousePosX;
                    mouseOldY = mousePosY;
                    mousePosX = me.getX();
                    mousePosY = me.getY();
                    double mouseDeltaX = (mousePosX - mouseOldX);
                    double mouseDeltaY = (mousePosY - mouseOldY);
                    world.rx(mouseDeltaY * 180.0 / subScene.getHeight());
                    world.ry(-mouseDeltaX * 180.0 / subScene.getWidth());
                }
            });
    
            subScene.setOnMouseReleased(me -> rs.onMouseDragReleased(me));
    
            subScene.setOnMouseClicked(me -> {
                if (me.getButton() == MouseButton.SECONDARY && me.isStillSincePress()) {
                    rs.clearSelection();
                }
            });
    
            return subScene;
        }
    
        private Pane setupControls() {
            Slider zoom = new Slider(-100, 0, -50);
            camera.translateZProperty().bind(zoom.valueProperty());
    
            Button reset = new Button("Reset");
            reset.setOnAction(eh -> {
                world.reset();
                zoom.setValue(-50);
            });
    
            HBox controls = new HBox(new Label("Zoom: "), zoom, reset);
            AnchorPane anchorPane = new AnchorPane(controls);
            anchorPane.setPickOnBounds(false);
            return anchorPane;
        }
    
        private Collection<Shape3D> createWorld() {
    
            List<Shape3D> shapes = new ArrayList<>();
    
            Random random = new Random(System.currentTimeMillis());
            for (int i = 0; i < 4000; i++) {
                double x = (random.nextDouble() - 0.5) * 30;
                double y = (random.nextDouble() - 0.5) * 30;
                double z = (random.nextDouble() - 0.5) * 30;
    
                Sphere point = new Sphere(0.2);
                point.setMaterial(new PhongMaterial(Color.SKYBLUE));
                point.getTransforms().add(new Translate(x, y, z));
                shapes.add(point);
            }
    
            return shapes;
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
        public class XFormWorld extends Group {
            Transform rotation = new Rotate();
    
            public XFormWorld() {
                getTransforms().addAll(rotation);
            }
    
            public void reset() {
                rotation = new Rotate();
                getTransforms().set(0, rotation);
            }
    
            public void rx(double angle) {
                Point3D axis = new Point3D(rotation.getMxx(), rotation.getMxy(), rotation.getMxz());
                rotation = rotation.createConcatenation(new Rotate(angle, axis));
                getTransforms().set(0, rotation);
            }
    
            public void ry(double angle) {
                Point3D axis = new Point3D(rotation.getMyx(), rotation.getMyy(), rotation.getMyz());
                rotation = rotation.createConcatenation(new Rotate(angle, axis));
                getTransforms().set(0, rotation);
            }
        }
    
        public class RectangleSelect {
    
            private static final int START_X = 0;
            private static final int START_Y = 1;
            private static final int END_X = 2;
            private static final int END_Y = 3;
    
            private double[] sceneCoords = new double[2]; //mouse drag x, y in scene coords 
            private double[] screenCoords = new double[2]; //mouse drag current x, y in screen coords
            private double[] boundsInScreenCoords = new double[4]; //top left x, y, bottom right x,y in screen coords
            private Collection<Shape3D> world;
    
            private PhongMaterial selected = new PhongMaterial(Color.YELLOW);
            private Rectangle rectangle;
    
            public RectangleSelect(Pane pane, Collection<Shape3D> world) {
                sceneCoords[START_X] = Double.MIN_VALUE;
                sceneCoords[START_Y] = Double.MIN_VALUE;
                rectangle = new Rectangle();
                rectangle.setStroke(Color.RED);
                rectangle.setOpacity(0.0);
                rectangle.setMouseTransparent(true);
                rectangle.setFill(null);
    
                this.world = world;
                pane.getChildren().add(rectangle);
            }
    
            public void onMouseDragged(MouseEvent me) {
                clearSelection();
                if (sceneCoords[START_X] == Double.MIN_VALUE) {
                    sceneCoords[START_X] = me.getSceneX();
                    sceneCoords[START_Y] = me.getSceneY();
                    screenCoords[START_X] = me.getScreenX();
                    screenCoords[START_Y] = me.getScreenY();
                }
                double sceneX = me.getSceneX();
                double sceneY = me.getSceneY();
                double screenX = me.getScreenX();
                double screenY = me.getScreenY();
    
                double topX = Math.min(sceneCoords[START_X], sceneX);
                double bottomX = Math.max(sceneCoords[START_X], sceneX);
                double leftY = Math.min(sceneCoords[START_Y], sceneY);
                double rightY = Math.max(sceneCoords[START_Y], sceneY);
    
                boundsInScreenCoords[START_X] = Math.min(screenCoords[START_X], screenX);
                boundsInScreenCoords[END_X] = Math.max(screenCoords[START_X], screenX);
                boundsInScreenCoords[START_Y] = Math.min(screenCoords[START_Y], screenY);
                boundsInScreenCoords[END_Y] = Math.max(screenCoords[START_Y], screenY);
    
                world.forEach(this::selectIfInBounds);
    
                rectangle.setX(topX);
                rectangle.setY(leftY);
                rectangle.setWidth(bottomX - topX);
                rectangle.setHeight(rightY - leftY);
                rectangle.setOpacity(1.0);
            }
    
            private void selectIfInBounds(Shape3D node) {
                Point2D screenCoods = node.localToScreen(0.0, 0.0, 0.0);
                if (screenCoods.getX() > boundsInScreenCoords[START_X] &&
                    screenCoods.getY() > boundsInScreenCoords[START_Y] &&
                    screenCoods.getX() < boundsInScreenCoords[END_X] &&
                    screenCoods.getY() < boundsInScreenCoords[END_Y]) {
                    Material m = node.getMaterial();
                    node.getProperties().put("material", m);
                    node.setMaterial(selected);
                }
            }
    
            private void unselect(Shape3D node) {
                Material m = (Material) node.getProperties().get("material");
                if (m != null) {
                    node.setMaterial(m);
                }
            }
    
            public void onMouseDragReleased(MouseEvent me) {
                rectangle.setOpacity(0.0);
                sceneCoords[START_X] = Double.MIN_VALUE;
                sceneCoords[START_Y] = Double.MIN_VALUE;
            }
    
            public void clearSelection() {
                world.forEach(this::unselect);
            }
        }
    }