繁体   English   中英

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

[英]How to correctly obtain screen coordinates of 3D shape after rotation

我需要能够通过绘制矩形区域来选择3d模型中的多种形状,然后选择该区域中的所有形状。

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

我以为获取鼠标和节点在屏幕坐标中的位置并进行比较是一个简单的问题,但这并没有按预期进行。

在下面的应用程序中,您可以使用鼠标右键来绘制区域(您必须单击一个球体才能开始,我不确定为什么,它们的子事件仅在您单击球体时才触发。) 。 再次右键单击(再次在一个球上)清除选择。

您可以单击鼠标左键拖动以旋转模型(再次必须从球体开始)。 在绕x轴旋转任何角度后,您都可以成功选择一个区域。 同样,绕y轴旋转。 但是,x和y旋转的组合给出了错误的结果。 例如,沿对角线拖动节点,您将得到如下所示的结果。

x和y旋转后的选择结果

对出什么问题有任何想法吗? 或其他解决方法的建议? 提前致谢

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);
    }
}   

}

感谢user1803551的那些链接,第一个是跟踪该错误的好用例,它的确是GeneralTransform3D.transform(Vec3d)中的一个错误。

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

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

通过使用同一对象调用它,计算将变得很乏味。 您可能会看到,如果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]); 

一切都很好,虽然不确定如何解决

我已验证这是注释中列出的错误。 对于您的情况,我认为最简单的解决方案是旋转world而不是摄影机。 由于这些是相对移动的仅有2个对象,因此移动哪一个无关紧要。 如果要统一转换,也可以使缩放适用于整个世界而不是摄影机,但这并不重要。

旋转世界

通过使其成为XFormWorld来使世界旋转,并完全删除camPiv 请注意,没有必要将camPiv添加到场景,因为它是一个空组。 仅通过setCamera添加摄像机,然后可以绑定其变换(请参见下文)。

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

  1. 翻转rxry的旋转值,因为在+x旋转世界就像在-x旋转相机(与y相同)。
  2. 校正旋转枢轴。 如果你在旋转x轴,然后在y轴, y轴旋转实际上它旋转围绕z (因为旋转矩阵规则)。 这意味着新旋转的枢轴取决于当前旋转。 如果在x旋转,则现在需要在z上旋转以获得y旋转。 数学很简单,但是您需要知道自己在做什么。

直接转换相机

即使变换相机,也不需要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 ,这意味着单击“通过”,直到点击到某物为止。 如果添加

subScene.setPickOnBounds(true);

容器( subScene )将在其框范围内接收任何事件,无论那里是否有节点。

解决此问题后,您将遇到一个新问题:在绘制矩形后释放鼠标将导致它通过clearSelection()消失。 这是因为您在onMouseClicked调用了该方法,但是在拖动的末尾会产生click事件,因为有新闻和发行了。 您想要的是清除选​​择,如果它是单击而没有拖动 这是通过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);
        }
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM