
us.ihmc.scs2.sessionVisualizer.jfx.controllers.camera.PerspectiveCameraController Maven / Gradle / Ivy
package us.ihmc.scs2.sessionVisualizer.jfx.controllers.camera;
import java.util.function.DoubleSupplier;
import java.util.function.Function;
import java.util.function.Predicate;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SubScene;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.PickResult;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Transform;
import javafx.util.Duration;
import us.ihmc.commons.Epsilons;
import us.ihmc.commons.MathTools;
import us.ihmc.euclid.tuple2D.Point2D;
import us.ihmc.euclid.tuple2D.Vector2D;
import us.ihmc.euclid.tuple3D.Vector3D;
import us.ihmc.euclid.tuple3D.interfaces.Point3DReadOnly;
import us.ihmc.euclid.tuple3D.interfaces.Vector3DReadOnly;
import us.ihmc.scs2.session.SessionPropertiesHelper;
import us.ihmc.scs2.sessionVisualizer.jfx.controllers.camera.CameraFocalPointHandler.FocalPointKeyEventHandler;
import us.ihmc.scs2.sessionVisualizer.jfx.tools.ObservedAnimationTimer;
import us.ihmc.scs2.sessionVisualizer.jfx.tools.TranslateSCS2;
import us.ihmc.scs2.sessionVisualizer.jfx.yoComposite.Tuple3DProperty;
/**
* This class provides a simple controller for a JavaFX {@link PerspectiveCamera}. The control is
* achieved via event handling by adding this controller as an {@link EventHandler} to the scene or
* sub-scene the camera is attached to.
*
* Behavior of this camera controller:
*
The camera is always pointing toward a focus point.
* The focus point can be translated via keyboard bindings, or instantly moved with a mouse
* shortcut only if {@link #setupRayBasedFocusTranslation(Predicate)} or
* {@link #enableShiftClickFocusTranslation()} has been called.
* The camera zoom can be changed vi the mouse wheel.
* Using the mouse, the camera can be rotated around the focus point.
*
* @author Sylvain Bertrand
*/
public class PerspectiveCameraController extends ObservedAnimationTimer implements EventHandler
{
private static final boolean FOCUS_POINT_SHOW = SessionPropertiesHelper.loadBooleanPropertyOrEnvironment("scs2.session.gui.camera.focuspoint.show",
"SCS2_GUI_CAMERA_FOCUS_SHOW",
true);
private static final double FOCUS_POINT_SIZE = SessionPropertiesHelper.loadDoublePropertyOrEnvironment("scs2.session.gui.camera.focuspoint.size",
"SCS2_GUI_CAMERA_FOCUS_SIZE",
0.0025);
private final Sphere focalPointViz;
private final CameraFocalPointHandler focalPointHandler;
private final CameraOrbitHandler orbitHandler;
/** Minimum value of a translation offset when using the keyboard. */
private final DoubleProperty minTranslationOffset = new SimpleDoubleProperty(this, "minTranslationOffset", 0.1);
/**
* The zoom-to-translation pow is used to define the relation between the current zoom value and the
* translation speed of the camera.
*/
private final DoubleProperty zoomToTranslationPow = new SimpleDoubleProperty(this, "zoomToTranslationPow", 0.75);
private final EventHandler zoomEventHandler;
private final EventHandler orbitalRotationEventHandler;
private final FocalPointKeyEventHandler translationEventHandler;
private final PerspectiveCamera camera;
private final Property cameraPositionCoordinatesToTrack = new SimpleObjectProperty<>(this, "cameraPositionCoordinatesToTrack", null);
private final Property cameraOrbitalCoordinatesToTrack = new SimpleObjectProperty<>(this, "cameraOrbitCoordinatesToTrack", null);
private final Property cameraLevelOrbitalCoordinatesToTrack = new SimpleObjectProperty<>(this,
"cameraOrbit2DCoordinatesToTrack",
null);
public PerspectiveCameraController(ReadOnlyDoubleProperty sceneWidthProperty,
ReadOnlyDoubleProperty sceneHeightProperty,
PerspectiveCamera camera,
Vector3DReadOnly up,
Vector3DReadOnly forward)
{
this.camera = camera;
Vector3D left = new Vector3D();
left.cross(up, forward);
if (!MathTools.epsilonEquals(left.norm(), 1.0, Epsilons.ONE_HUNDRED_THOUSANDTH))
throw new RuntimeException("The vectors up and forward must be orthogonal. Received: up = " + up + ", forward = " + forward);
orbitHandler = new CameraOrbitHandler(up, forward);
orbitHandler.fastModifierPredicateProperty().set(event -> event.isShiftDown());
orbitalRotationEventHandler = orbitHandler.createMouseEventHandler(sceneWidthProperty, sceneHeightProperty);
zoomEventHandler = orbitHandler.createScrollEventHandler();
orbitHandler.minDistanceProperty().set(1.10 * camera.getNearClip());
orbitHandler.maxDistanceProperty().set(0.90 * camera.getFarClip());
focalPointHandler = new CameraFocalPointHandler(up);
focalPointHandler.fastModifierPredicateProperty().set(event -> event.isShiftDown());
focalPointHandler.setCameraOrientation(orbitHandler.getCameraPose());
translationEventHandler = focalPointHandler.createKeyEventHandler();
focalPointHandler.setTranslationRateModifier(translationRate ->
{
return Math.min(translationRate * Math.pow(orbitHandler.distanceProperty().get(), zoomToTranslationPow.get()), minTranslationOffset.get());
});
setCameraPosition(-2.0, 0.7, 1.0);
TranslateSCS2 focalPointTranslation = focalPointHandler.getTranslation();
camera.getTransforms().addAll(focalPointTranslation, orbitHandler.getCameraPose());
orbitHandler.createCameraWorldCoordinates(focalPointTranslation.xProperty(), focalPointTranslation.yProperty(), focalPointTranslation.zProperty());
cameraPositionCoordinatesToTrack.addListener((o, oldValue, newValue) ->
{
CameraBindingsHelper.removeCameraPositionBindings(oldValue, orbitHandler);
if (cameraControlMode().getValue() == CameraControlMode.Position)
CameraBindingsHelper.addCameraPositionBindings(newValue, orbitHandler);
});
cameraOrbitalCoordinatesToTrack.addListener((o, oldValue, newValue) ->
{
CameraBindingsHelper.removeCameraOrbitalBindings(oldValue, orbitHandler);
if (cameraControlMode().getValue() == CameraControlMode.Orbital)
CameraBindingsHelper.addCameraOrbitalBindings(newValue, orbitHandler);
});
cameraLevelOrbitalCoordinatesToTrack.addListener((o, oldValue, newValue) ->
{
CameraBindingsHelper.removeCameraLevelOrbitalBindings(oldValue, orbitHandler);
if (cameraControlMode().getValue() == CameraControlMode.LevelOrbital)
CameraBindingsHelper.addCameraLevelOrbitalBindings(newValue, orbitHandler);
});
cameraControlMode().addListener((o, oldValue, newValue) ->
{
switch (oldValue)
{
case Position:
CameraBindingsHelper.removeCameraPositionBindings(cameraPositionCoordinatesToTrack.getValue(), orbitHandler);
break;
case Orbital:
CameraBindingsHelper.removeCameraOrbitalBindings(cameraOrbitalCoordinatesToTrack.getValue(), orbitHandler);
break;
case LevelOrbital:
CameraBindingsHelper.removeCameraLevelOrbitalBindings(cameraLevelOrbitalCoordinatesToTrack.getValue(), orbitHandler);
break;
default:
throw new IllegalArgumentException("Unexpected value: " + oldValue);
}
switch (newValue)
{
case Position:
CameraBindingsHelper.addCameraPositionBindings(cameraPositionCoordinatesToTrack.getValue(), orbitHandler);
break;
case Orbital:
CameraBindingsHelper.addCameraOrbitalBindings(cameraOrbitalCoordinatesToTrack.getValue(), orbitHandler);
break;
case LevelOrbital:
CameraBindingsHelper.addCameraLevelOrbitalBindings(cameraLevelOrbitalCoordinatesToTrack.getValue(), orbitHandler);
break;
default:
throw new IllegalArgumentException("Unexpected value: " + newValue);
}
});
if (FOCUS_POINT_SHOW)
{
focalPointViz = new Sphere(0.01);
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.DARKRED);
material.setSpecularColor(Color.RED);
focalPointViz.setMaterial(material);
focalPointViz.getTransforms().add(focalPointTranslation);
orbitHandler.distanceProperty().addListener((o, oldValue, newValue) ->
{
focalPointViz.setRadius(FOCUS_POINT_SIZE * newValue.doubleValue());
});
}
else
{
focalPointViz = null;
}
setupCameraRotationHandler();
}
@Override
public void handleImpl(long now)
{
focalPointHandler.update();
if (translationEventHandler.isTranslating())
{
Vector3DReadOnly translation = translationEventHandler.getActiveTranslationWorldFrame();
if (cameraControlMode().getValue() == CameraControlMode.Position)
{
double x = orbitHandler.xProperty().get() + translation.getX();
double y = orbitHandler.yProperty().get() + translation.getY();
double z = orbitHandler.zProperty().get() + translation.getZ();
orbitHandler.setPosition(x, y, z, Double.NaN);
}
else if (cameraControlMode().getValue() == CameraControlMode.LevelOrbital && translation.getZ() != 0.0)
{
double height = orbitHandler.zProperty().get() + translation.getZ();
orbitHandler.setLevelOrbit(Double.NaN, Double.NaN, height, Double.NaN);
}
}
}
public void setCameraPosition(Point3DReadOnly desiredCameraPosition)
{
setCameraPosition(desiredCameraPosition, false);
}
public void setCameraPosition(Point3DReadOnly desiredCameraPosition, boolean translateFocalPoint)
{
setCameraPosition(desiredCameraPosition.getX(), desiredCameraPosition.getY(), desiredCameraPosition.getZ(), translateFocalPoint);
}
public void setCameraPosition(double x, double y, double z)
{
setCameraPosition(x, y, z, false);
}
public void setCameraPosition(double x, double y, double z, boolean translateFocalPoint)
{
if (translateFocalPoint)
{
Transform cameraTransform = camera.getLocalToSceneTransform();
focalPointHandler.translateWorldFrame(x - cameraTransform.getTx(), y - cameraTransform.getTy(), z - cameraTransform.getTz());
}
else
{
TranslateSCS2 focalPoint = focalPointHandler.getTranslation();
orbitHandler.setPosition(x - focalPoint.getX(), y - focalPoint.getY(), z - focalPoint.getZ(), 0.0);
}
}
/**
* Sets the coordinates of the focus point the camera is looking at.
*
* This can be done in 2 different ways controlled by the argument {@code translateCamera}:
*
* - translating the camera: the offset between the focus point and the camera is preserved as
* well as the camera orientation. This will be used when {@code translateCamera = true}.
*
- rotating the camera: the distance between the focus point and the camera changes, the camera
* will pitch and/or yaw as a result of this operation. This will be used when
* {@code translateCamera = false}.
*
*
*
* @param desiredFocalPoint the new focus location.
* @param translateCamera whether to translate or rotate the camera when updating the focus point.
*/
public void setFocalPoint(Point3DReadOnly desiredFocalPoint, boolean translateCamera)
{
setFocalPoint(desiredFocalPoint.getX(), desiredFocalPoint.getY(), desiredFocalPoint.getZ(), translateCamera);
}
/**
* Sets the coordinates of the focus point the camera is looking at.
*
* This can be done in 2 different ways controlled by the argument {@code translateCamera}:
*
* - translating the camera: the offset between the focus point and the camera is preserved as
* well as the camera orientation. This will be used when {@code translateCamera = true}.
*
- rotating the camera: the distance between the focus point and the camera changes, the camera
* will pitch and/or yaw as a result of this operation. This will be used when
* {@code translateCamera = false}.
*
*
*
* @param x the x-coordinate of the new focus location.
* @param y the y-coordinate of the new focus location.
* @param z the z-coordinate of the new focus location.
* @param translateCamera whether to translate or rotate the camera when updating the focus point.
*/
public void setFocalPoint(double x, double y, double z, boolean translateCamera)
{
focalPointHandler.disableTracking();
if (translateCamera)
{
focalPointHandler.setPositionWorldFrame(x, y, z);
}
else
{
// The focus position is used to compute the camera transform, so first want to get the camera position.
Transform cameraTransform = camera.getLocalToSceneTransform();
orbitHandler.setPosition(cameraTransform.getTx() - x, cameraTransform.getTy() - y, cameraTransform.getTz() - z, 0.0);
focalPointHandler.setPositionWorldFrame(x, y, z);
}
}
public void setCameraOrientation(double longitude, double latitude, double roll, boolean translateFocalPoint)
{
if (translateFocalPoint)
{
Vector3DReadOnly focalPointShift = orbitHandler.setRotation(longitude, latitude, roll, true);
focalPointHandler.translateWorldFrame(focalPointShift);
}
else
{
orbitHandler.setRotation(longitude, latitude, roll);
}
}
public void setCameraOrbit(double distance, double longitude, double latitude, double roll, boolean translateFocalPoint)
{
if (translateFocalPoint)
{
Vector3DReadOnly focalPointShift = orbitHandler.setOrbit(distance, longitude, latitude, roll, true);
focalPointHandler.translateWorldFrame(focalPointShift);
}
else
{
orbitHandler.setOrbit(distance, longitude, latitude, roll);
}
}
public void setCameraLevelOrbit(double distance, double longitude, double height, double roll, boolean translateFocalPoint)
{
if (translateFocalPoint)
{
height -= focalPointHandler.getTranslation().getZ();
Vector3DReadOnly focalPointShift = orbitHandler.setLevelOrbit(distance, longitude, height, roll, true);
focalPointHandler.translateWorldFrame(focalPointShift);
}
else
{
orbitHandler.setLevelOrbit(distance, longitude, height, roll);
}
}
public void rotateCamera(double deltaLongitude, double deltaLatitude, double deltaRoll, boolean translateFocalPoint)
{
if (translateFocalPoint)
{
Vector3DReadOnly focalPointShift = switch (cameraControlMode().getValue())
{
case Position:
yield orbitHandler.computeFocalPointShift(0.0, deltaLongitude, deltaLatitude, deltaRoll);
case Orbital:
yield orbitHandler.rotate(deltaLongitude, deltaLatitude, deltaRoll, true);
case LevelOrbital:
{
Vector3D shift = new Vector3D(orbitHandler.setLevelOrbit(Double.NaN,
orbitHandler.longitudeProperty().get() + deltaLongitude,
Double.NaN,
orbitHandler.rollProperty().get() + deltaRoll,
true));
shift.setZ(orbitHandler.computeFocalPointShift(0.0, deltaLongitude, deltaLatitude, deltaRoll).getZ());
yield shift;
}
};
focalPointHandler.translateWorldFrame(focalPointShift);
}
else
{
orbitHandler.rotate(deltaLongitude, deltaLatitude, deltaRoll);
}
}
@Override
public void handle(Event event)
{
if (event instanceof ScrollEvent)
zoomEventHandler.handle((ScrollEvent) event);
if (event instanceof KeyEvent)
translationEventHandler.handle((KeyEvent) event);
if (event instanceof MouseEvent)
{
MouseEvent mouseEvent = (MouseEvent) event;
if (rayBasedFocusTranslation != null)
rayBasedFocusTranslation.handle(mouseEvent);
if (!event.isConsumed())
orbitalRotationEventHandler.handle(mouseEvent);
if (!event.isConsumed())
{
if (cameraRotationEventHandler != null)
cameraRotationEventHandler.handle(mouseEvent);
}
}
}
private EventHandler rayBasedFocusTranslation = null;
public void enableShiftClickFocusTranslation()
{
enableShiftClickFocusTranslation(MouseEvent::getPickResult);
}
public void enableShiftClickFocusTranslation(Function nodePickingFunction)
{
setupRayBasedFocusTranslation(event ->
{
if (!event.isShiftDown())
return false;
if (event.getButton() != MouseButton.PRIMARY)
return false;
if (!event.isStillSincePress())
return false;
if (event.getEventType() != MouseEvent.MOUSE_CLICKED)
return false;
return true;
}, nodePickingFunction);
}
public void setupRayBasedFocusTranslation(Predicate condition)
{
setupRayBasedFocusTranslation(condition, MouseEvent::getPickResult);
}
public void setupRayBasedFocusTranslation(Predicate condition, Function nodePickingFunction)
{
setupRayBasedFocusTranslation(condition, nodePickingFunction, 0.1);
}
public void setupRayBasedFocusTranslation(Predicate condition, double animationDuration)
{
setupRayBasedFocusTranslation(condition, MouseEvent::getPickResult, animationDuration);
}
public void setupRayBasedFocusTranslation(Predicate condition, Function nodePickingFunction, double animationDuration)
{
rayBasedFocusTranslation = new EventHandler()
{
@Override
public void handle(MouseEvent event)
{
if (condition.test(event))
{
PickResult pickResult = nodePickingFunction.apply(event);
if (pickResult == null)
return;
Node intersectedNode = pickResult.getIntersectedNode();
if (intersectedNode == null || intersectedNode instanceof SubScene)
return;
javafx.geometry.Point3D localPoint = pickResult.getIntersectedPoint();
javafx.geometry.Point3D scenePoint = intersectedNode.getLocalToSceneTransform().transform(localPoint);
focalPointHandler.disableTracking();
TranslateSCS2 translate = focalPointHandler.getOffsetTranslation();
if (animationDuration > 0.0)
{
Timeline animation = new Timeline(new KeyFrame(Duration.seconds(animationDuration),
new KeyValue(translate.xProperty(), scenePoint.getX(), Interpolator.EASE_BOTH),
new KeyValue(translate.yProperty(), scenePoint.getY(), Interpolator.EASE_BOTH),
new KeyValue(translate.zProperty(), scenePoint.getZ(), Interpolator.EASE_BOTH)));
animation.playFromStart();
}
else
{
translate.set(scenePoint);
}
event.consume();
}
}
};
}
private EventHandler cameraRotationEventHandler;
public void setupCameraRotationHandler()
{
setupCameraRotationHandler(MouseButton.SECONDARY);
}
public void setupCameraRotationHandler(MouseButton mouseButton)
{
setupCameraRotationHandler(mouseButton, () -> 0.003);
}
public void setupCameraRotationHandler(MouseButton mouseButton, DoubleSupplier modifier)
{
cameraRotationEventHandler = new EventHandler()
{
private final Point2D oldMouseLocation = new Point2D();
@Override
public void handle(MouseEvent event)
{
if (event.getButton() != mouseButton)
return;
// Filters single clicks
if (event.isStillSincePress())
return;
if (event.getEventType() == MouseEvent.MOUSE_PRESSED)
{
oldMouseLocation.set(event.getSceneX(), event.getSceneY());
return;
}
if (event.getEventType() != MouseEvent.MOUSE_DRAGGED)
return;
// Acquire the new mouse coordinates from the recent event
Point2D newMouseLocation = new Point2D(event.getSceneX(), event.getSceneY());
Vector2D drag = new Vector2D();
drag.sub(newMouseLocation, oldMouseLocation);
drag.scale(modifier.getAsDouble());
focalPointHandler.disableTracking(); // Automatically disabling tracking when rotating the camera on itself
rotateCamera(-drag.getX(), drag.getY(), 0.0, true);
oldMouseLocation.set(newMouseLocation);
}
};
}
public Sphere getFocalPointViz()
{
return focalPointViz;
}
public TranslateSCS2 getFocalPointTranslate()
{
return focalPointHandler.getTranslation();
}
public PerspectiveCamera getCamera()
{
return camera;
}
public CameraFocalPointHandler getFocalPointHandler()
{
return focalPointHandler;
}
public CameraOrbitHandler getOrbitHandler()
{
return orbitHandler;
}
public Property cameraControlMode()
{
return orbitHandler.controlMode();
}
public Property cameraPositionCoordinatesToTrackProperty()
{
return cameraPositionCoordinatesToTrack;
}
public Property cameraOrbitalCoordinatesToTrackProperty()
{
return cameraOrbitalCoordinatesToTrack;
}
public Property cameraLevelOrbitalCoordinatesToTrackProperty()
{
return cameraLevelOrbitalCoordinatesToTrack;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy