Как правильно получить координаты экрана трехмерной фигуры после поворота - PullRequest
0 голосов
/ 11 июня 2018

Мне нужно иметь возможность выбрать количество фигур в моей 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);
    }
}   

}

Ответы [ 2 ]

0 голосов
/ 15 июня 2018

Я подтвердил, что это ошибка (и), перечисленные в комментариях .Я полагаю, что для вашего случая самое простое решение - повернуть world вместо камеры.Поскольку это только 2 объекта, которые движутся относительно друг друга, не имеет значения, какой из них перемещается.Вы также можете применить масштабирование к миру вместо камеры, если хотите объединить преобразования, но это не имеет значения.

Вращение мира

Сделайте мир вращающимся, имеяэто будет XFormWorld, и вообще удалите camPiv.Обратите внимание, что не было никакой причины добавлять camPiv к сцене, потому что это пустая группа;камера добавляется только через setCamera, а затем вы можете связать ее преобразования (см. ниже).

Вам нужно изменить математику двумя способами:

  1. Отразить вращениезначения rx и ry, потому что вращение мира в +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 s, значение которого равно значению, которое вы меняете с помощью ввода.

События мыши и выбор границ

Ваш subSceneworld) содержитмного узлов с пустыми пространствами между ними.По умолчанию, когда вы нажимаете на subScene, событие будет доставлено на него, только если вы щелкнете (не прозрачный для мыши) узел внутри него.Это потому, что pickOnBounds равно false, что означает, что щелчок «проходит» до тех пор, пока не достигнет чего-либо.Если вы добавите

subScene.setPickOnBounds(true);

, то контейнер (subScene) будет получать любые события в пределах своих рамок, независимо от того, есть ли там узел или нет.

После того, как это исправлено,Вы столкнетесь с новой проблемой: если вы отпустите мышку после рисования прямоугольника, она исчезнет через clearSelection().Это потому, что вы вызываете этот метод в onMouseClicked, но в конце перетаскивания генерируется событие щелчка, потому что было нажатие и выпуск.То, что вы хотите, это очистить выделение, если это щелчок без перетаскивания .Это делается с помощью isStillSincePress():

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

Причина, по которой вы не столкнулись с этим, заключается в том, что subScene не получил событие освобождения, если оно произошло в пустом пространстве.Подведем итог:

  • Нажатие на пустое место: событие не зарегистрировано - ничего не произошло.
  • Нажатие на сферу: событие зарегистрировано - началось рисование треугольника.
    • Освобождение в пустом пространстве: событие не зарегистрировано - прямоугольник не очищен.
    • Освобождение в сфере: событие зарегистрировано - прямоугольник очищен.

Layout

Не используйте 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);
        }
    }
}
0 голосов
/ 12 июня 2018

Спасибо 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]); 

Все хорошо, не знаю, как работатьвокруг этого, хотя

...