我想我需要让我的代码说明问题。我正在创建一个地图以绘制GPS坐标,并决定将其绘制到3D地球仪上。我决定尝试使用javafx并使用javafx-sdk-18.0.2。
我目前无法解决的问题是如何极端缩放
我编写了一个简化的示例来展示我的问题。我在地球仪上添加了一些点来提供大致参考。用户可以使用四个箭头键旋转到地球上的位置,并且我允许使用加号和减号键进行缩放。我尝试了各种缩放方法:测量相机与表面之间的距离、移动相机视角、调整“比例”因素和调整“视场”角度。但是没有一个结果足够好,我怀疑我没有正确使用此API。我遇到的问题有:
我目前无法解决的问题是如何极端缩放
PerspectiveCamera
。我想从太空缩放到10米级别,以显示记录的GPS数据轨迹。我编写了一个简化的示例来展示我的问题。我在地球仪上添加了一些点来提供大致参考。用户可以使用四个箭头键旋转到地球上的位置,并且我允许使用加号和减号键进行缩放。我尝试了各种缩放方法:测量相机与表面之间的距离、移动相机视角、调整“比例”因素和调整“视场”角度。但是没有一个结果足够好,我怀疑我没有正确使用此API。我遇到的问题有:
- 接近表面时,移动过于粗糙;
- 观察者意外地穿过物体,看到了背面的东西;
- 对于非常小的
Camera.nearClip
值,所有形状都会缺少一些部分。
package ui.javafx;
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.transform.*;
import javafx.scene.input.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import javafx.stage.*;
/** Simplified working javafx example for Stackoverflow question */
public class OthographicGlobeMapStackOverflow extends Application {
/**
* An oblate spheroid coordinate system approximating the layout of the Earth.
*/
class Earth {
/*
* Earth size constants from WGS-84 as expressed on
* https://en.wikipedia.org/wiki/Earth_ellipsoid#Historical_Earth_ellipsoids
*/
final static double RADIUS_EQUITORIAL_METERS = 6378137d;
final static double RADIUS_POLAR_METERS = 6356752d;
/**
* Size of the scaled globe in pixels. Radius in X coordinate.
*/
final static double globeRadiusX = 300d;
/**
* Size of the globe in pixels. Radius in Y coordinate.
*/
final static double globeRadiusY = RADIUS_POLAR_METERS / RADIUS_EQUITORIAL_METERS * globeRadiusX;
/**
* Produce a Point3D with the location in the xyz universe, corresponding with
* the location on the globe with the provided coordinates in degrees and
* meters.
* Algorithm adapted from https://dev59.com/y-o6XIcBkEYKwwoYTSou#5983282
*
* @returns a Point3D at the specified location in relation to the globe.
* @param degreesLatitude the Latitude in degrees.
* @param degreesLongitude the longitude in degrees.
* @param metersAltitude the altitude from AMSL in metres.
*/
public static Point3D getWithDegrees( double degreesLatitude, double degreesLongitude, float metersAltitude ) {
double Re = globeRadiusX;
double Rp = globeRadiusY;
// the algorithm produced a globe with longitude -90 facing us
degreesLongitude = ( degreesLongitude - 90d ) % 360d;
double lat = Math.toRadians( degreesLatitude );
double lon = Math.toRadians( degreesLongitude );
double coslat = Math.cos( lat );
double sinlat = Math.sin( lat );
double coslon = Math.cos( lon );
double sinlon = Math.sin( lon );
double term1 = Math.sqrt( Re * Re * coslat * coslat + Rp * Rp * sinlat * sinlat );
double term2 = metersAltitude * coslat + ( Re * Re * coslat ) / term1;
double x = coslon * term2;
double y = sinlon * term2;
double z = metersAltitude * sinlat + ( Rp * Rp * sinlat ) / term1;
// the x,y,z directions were not congruent with the JavaFX layout axes
return new Point3D( x, -z, y );
}
public static Point3D getNorthPole() {
return getWithDegrees( 90, 0, 0 );
}
}
/**
* Angle of globe view, in longitude degrees which effects a rotation of the X
* axis around the Y axis.
*/
private double spinAngle = 0d;
/**
* Angle of globe view, in latitude degrees
*/
private double tiltAngle = 0d;
@Override
public void start( Stage primaryStage ) {
// Universe stays fixed. Contains lighting, camera and the axis of the "tilt" function.
Group universe = new Group();
addSunlight( universe );
// Globe is able to rotate in its own axis. Child nodes that decorate the globe remain in position.
Group globe = new Group();
universe.getChildren().add( globe );
// add a nice looking surface to the globe
drawGlobe( globe );
// paint few dotted lines on the globe surface for orientation
drawLatitude( globe, 60 );
drawLatitude( globe, 30 );
drawLatitude( globe, 0 );
drawLatitude( globe, -30 );
drawLatitude( globe, -60 );
drawLongitude( globe, 0 ); // prime meridian great circle
// decorate the globe with a few positional balls
plotGoldBall( globe, 48.85829501324163, 2.294502751853257, "Tour Eiffel" );
plotGoldBall( globe, 40.68937198546735, -74.04451898086933, "Statue of Liberty" );
plotGoldBall( globe, -22.952395566439044, -43.21046847195321, "Cristo Redentor" );
plotGoldBall( globe, 35.65873215542844, 139.74547513704502, "東京タワー" ); // Tokyo Tower
plotGoldBall( globe, 29.97918805575227, 31.134206635494273, "هرم خوفو" ); // pyramid of Cheops
plotGoldBall( globe, -27.116667, -109.366667, "" ); // Parque nacional Rapa Nui, Easter Island
plotGoldBall( globe, -33.85617854877629, 151.21533961498702, "Sydney Opera House" );
// translate the globe away from the origin in the corner
globe.setTranslateX( Earth.globeRadiusX * 1d );
globe.setTranslateY( Earth.globeRadiusX * 1d );
globe.setTranslateZ( 0d );
// Establish spinning axis for the globe
Rotate globeSpin = new Rotate( spinAngle, Earth.getNorthPole() );
globe.getTransforms().addAll( globeSpin );
// Establish tilting on the universe (or camera view which is how user perceives it)
Rotate globeTilt = new Rotate( tiltAngle, Rotate.X_AXIS );
globeTilt.setPivotX( Earth.globeRadiusX * 1d );
globeTilt.setPivotY( Earth.globeRadiusX * 1d );
globeTilt.setPivotZ( 0 );
universe.getTransforms().add( globeTilt );
// establish the size of the window and display it
Scene scene = new Scene( universe, Earth.globeRadiusX * 2, Earth.globeRadiusX * 2, true );
PerspectiveCamera eye = new PerspectiveCamera();
eye.setNearClip( 0.001d );
scene.setCamera( eye );
primaryStage.setScene( scene );
// add point-to-identify mouse handler
primaryStage.addEventHandler( MouseEvent.MOUSE_PRESSED, event -> {
PickResult clicked = event.getPickResult();
System.out.println( "Clicked on: " + clicked.getIntersectedNode() );
} );
// add ← ↑ → ↓ and +/- controls
primaryStage.addEventHandler( KeyEvent.KEY_PRESSED, event -> {
if ( event.getCode().equals( KeyCode.UP ) ) {
globeTilt.setAngle( --tiltAngle );
}
if ( event.getCode().equals( KeyCode.DOWN ) ) {
globeTilt.setAngle( ++tiltAngle );
}
if ( event.getCode().equals( KeyCode.LEFT ) ) {
globeSpin.setAngle( --spinAngle );
}
if ( event.getCode().equals( KeyCode.RIGHT ) ) {
globeSpin.setAngle( ++spinAngle );
}
if ( event.getCode().equals( KeyCode.EQUALS ) ) {
zoomIn( eye );
}
if ( event.getCode().equals( KeyCode.MINUS ) ) {
zoomOut( eye );
}
} );
primaryStage.show();
}
/**
* Draw a pretty blue spheroid. This is a visual backdrop to the positional elements placed on the globe.
* It also functions as a visual solid, hiding elements that are "behind".
* */
private void drawGlobe( Group globe ) {
Sphere earth = new Sphere( Earth.globeRadiusX );
earth.setScaleY( Earth.globeRadiusY / Earth.globeRadiusX ); // squash into oblate a little
earth.setId( "Earth" );
PhongMaterial surface = new PhongMaterial();
surface.setDiffuseColor( Color.AZURE.deriveColor( 0.0, 1.0, 1.0, 1.0 ) );
earth.setMaterial( surface );
globe.getChildren().add( earth );
}
private void addSunlight( Group universe ) {
PointLight sol = new PointLight( Color.WHITE.deriveColor( 0.0, 0.5, 0.5, 0.5 ) );
sol.setTranslateZ( -3000 );
sol.setTranslateY( -1000 );
sol.setTranslateX( -1000 );
universe.getChildren().add( sol );
AmbientLight starlight = new AmbientLight( Color.ANTIQUEWHITE.deriveColor( 0.0, 0.5, 0.5, 0.5 ) );
universe.getChildren().add( starlight );
}
/**
* Place a gold-looking ball marker on the surface of the globe
* @param labelText
*/
private void plotGoldBall( Group globe, double latitude, double longitude, String labelText ) {
Sphere marker = plotBall( globe, latitude, longitude, labelText, 10d, Color.BLANCHEDALMOND );
Label label = new Label();
label.setText( labelText );
if ( longitude % 180d > 0 ) {
label.setTranslateX( marker.getTranslateX() + 50 );
}
else {
label.setTranslateX( marker.getTranslateX() - ( label.getWidth() + 50 ) );
}
label.setTranslateY( marker.getTranslateY() );
label.setTranslateZ( marker.getTranslateZ() );
globe.getChildren().add( label );
}
/**
* Place a series of small black dots to denote circle of latitude
* @param lat the latitude in degrees.
* */
private void drawLatitude( Group globe, double lat ) {
int step = 1;
if ( Math.abs( lat ) > 45 )
step = 2;
for (double deg = 0; deg < 360; deg += step) {
plotBlackDot( globe, lat, deg );
}
}
/**
* Place a series of small black dots to denote a great circle of longitude
* @param the longitude to start the great circle.
* */
private void drawLongitude( Group globe, double lon ) {
for (double deg = 0; deg < 360; deg++) {
plotBlackDot( globe, deg, lon );
}
}
private void plotBlackDot( Group globe, double lat, double lon ) {
plotBall( globe, lat, lon, null, 1d, Color.DARKSLATEBLUE );
}
private Sphere plotBall( Group globe, double latitude, double longitude, String label, double radius, Color color ) {
Point3D location = Earth.getWithDegrees( latitude, longitude, 0 );
Sphere mapPoint = new Sphere( radius );
mapPoint.setId( label );
mapPoint.setTranslateX( location.getX() );
mapPoint.setTranslateY( location.getY() );
mapPoint.setTranslateZ( location.getZ() );
mapPoint.setMaterial( new PhongMaterial( color ) );
globe.getChildren().add( mapPoint );
return mapPoint;
}
/* WTF */
private void zoomIn( PerspectiveCamera eye ) {
eye.setFieldOfView( eye.getFieldOfView() * 1.1d );
eye.setScaleZ( eye.getScaleZ() / 1.1d );
}
/* WTF */
private void zoomOut( PerspectiveCamera eye ) {
eye.setFieldOfView( eye.getFieldOfView() / 1.1d );
eye.setScaleZ( eye.getScaleZ() * 1.1d );
}
}
新信息
我最初尝试将相机沿 Z 轴平移。但是,如何测量相机与给定点之间的距离?地球仪(Group)处于其自己的坐标系中,并已经进行了旋转变换。我无法理解我所测量的 Z 值。
我的结论是,我应该停止试图找出物体的位置,而是研究相机的能力,这让我了解到视场和缩放。
class ShowJavaSyntaxHighlightingForCodeFragment {
/* WTF */
private void zoomIn( PerspectiveCamera eye ) {
System.out.println( "\nZooming in." );
// distance remaining between eye and nearest globe surface point
Point3D zoomPoint = Earth.getWithDegrees( tiltAngle, -1d * spinAngle, 0 );
System.out.println( "Surface point: " + zoomPoint.getZ() );
System.out.println( "View point: " + eye.getTranslateZ() );
double distance = Math.abs( eye.getTranslateZ() - zoomPoint.getZ() );
System.out.println( "Zoom distance: " + distance );
// close the remaining distance by half
eye.setTranslateZ( ( eye.getTranslateZ() + ( distance / 2d ) ) );
// report the new distance
distance = Math.abs( eye.getTranslateZ() - zoomPoint.getZ() );
System.out.println( "New view point: " + eye.getTranslateZ() );
System.out.println( "New zoom distance: " + distance );
}
/* WTF */
private void zoomOut( PerspectiveCamera eye ) {
System.out.println( "\nZooming out." );
// distance remaining between eye and nearest globe surface point
Point3D zoomPoint = Earth.getWithDegrees( tiltAngle, -1d * spinAngle, 0 );
System.out.println( "Surface point: " + zoomPoint.getZ() );
System.out.println( "View point: " + eye.getTranslateZ() );
double distance = Math.abs( eye.getTranslateZ() - zoomPoint.getZ() );
System.out.println( "Zoom distance: " + distance );
// attempt to double the closing distance
eye.setTranslateZ( ( eye.getTranslateZ() + distance ) ) );
// report the new distance
distance = Math.abs( eye.getTranslateZ() - zoomPoint.getZ() );
System.out.println( "New view point: " + eye.getTranslateZ() );
System.out.println( "New zoom distance: " + distance );
}}
根据这种逻辑,我得到了这个输出。
Zooming in.
Surface point: -300.0
View point: 0.0 // I wasn't expecting 0 in Z-axis here
Zoom distance: 300.0
New view point: 150.0 // OK, plausible
New zoom distance: 450.0 // Nonsense. I was expecting a smaller value.
Zooming in.
Surface point: -300.0
View point: 150.0
Zoom distance: 450.0
New view point: 375.0
New zoom distance: 675.0 // Nonsense. The image is bigger, but the distance is greater.
Zooming in.
Surface point: -300.0
View point: 375.0
Zoom distance: 675.0
New view point: 712.5 // I have no idea what is happening, but the view is definitely zoomed
New zoom distance: 1012.5
Zooming out.
Surface point: -300.0
View point: 712.5
Zoom distance: 1012.5
New view point: -1312.5 // This is nonsense again, and the view is far more zoomed out than I intended.
New zoom distance: 1012.5
subScene.setOnScroll((ScrollEvent event) -> { double modifier = 50.0; double modifierFactor = 0.1; if (event.isControlDown()) { modifier = 1; } if (event.isShiftDown()) { modifier = 100.0; } double z = camera.getTranslateZ(); double newZ = z + event.getDeltaY() * modifierFactor * modifier; camera.setTranslateZ(newZ); });
- Birdasaur