将相机缩放到非常接近3D形状的有效方法是什么?

3
我想我需要让我的代码说明问题。我正在创建一个地图以绘制GPS坐标,并决定将其绘制到3D地球仪上。我决定尝试使用javafx并使用javafx-sdk-18.0.2。
我目前无法解决的问题是如何极端缩放PerspectiveCamera。我想从太空缩放到10米级别,以显示记录的GPS数据轨迹。
我编写了一个简化的示例来展示我的问题。我在地球仪上添加了一些点来提供大致参考。用户可以使用四个箭头键旋转到地球上的位置,并且我允许使用加号和减号键进行缩放。我尝试了各种缩放方法:测量相机与表面之间的距离、移动相机视角、调整“比例”因素和调整“视场”角度。但是没有一个结果足够好,我怀疑我没有正确使用此API。我遇到的问题有:
  1. 接近表面时,移动过于粗糙;
  2. 观察者意外地穿过物体,看到了背面的东西;
  3. 对于非常小的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
2个回答

3
你将FOV和相机Z维度缩放相结合产生的效果创造了一个酷炫的迷幻效果!但这不是缩放的正确方法。
问题:为什么不在缩放时改变相机(视点)的位置?在我的FX 3D场景中,我就是这么做的,并没有遇到任何问题。(Win10/NVidia/OpenJFX18)
已经做到这种程度,这里提供一些建议:
  1. 当在物体表面附近时移动太过粗糙;
不要使用10%的变化,而是使用一个基于地球半径的标量公式。使用10%的变化虽然简单方便,但在接近或远距离时无法按比例缩放。标量应该简单地使用地球半径作为减少缩放速率的方法。这将使它的粒度更细致,更靠近地球时则越明显。专业提示:测试下Ctrl键附加标量来使其更细致(爬行),或Shift键附加标量来使其更快捷(奔跑)。
  1. 观察者意外地“穿过”物体并看到了另一边的东西;
如果你没有约束缩放行为,这是你期待发生的。只需添加逻辑,当相机与中心之间的距离低于一定值时,忽略缩放即可(这也是为什么你应该移动相机的原因)。
  1. 当Camera.nearClip值非常小时,所有形状都变得不完整,有些部分缺失。
如果你正确移动相机,这不应该发生,除非你在大量处理纹理时出现Z-fighting问题。

问:如果您不限制缩放操作,您期望会发生什么?答:我期望视野会变得非常小,接近于0度。但我没有预料到前景元素会消失。 - Douglas Held
相机翻译是我尝试的第一件事。在应用后,相机似乎不与地球共享坐标系,因为它从未出现在我预期的位置上。我将找出我的相机翻译尝试并添加进去。 - Douglas Held
1
我在考虑一些简单的东西。以下是我如何使用鼠标滚轮进行缩放的示例(已更改): 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
2
这是一个使用相机翻译的完整示例。 https://github.com/FXyz/FXyz/blob/master/FXyz-Samples/src/main/java/org/fxyz3d/samples/utilities/FloatingLabels.java 注意:我是该存储库的所有者和该示例的作者。不是在宣传该库。 - Birdasaur
@Birdsaur 非常感谢您。我会仔细研究一下并查看发生了什么。 - Douglas Held

2
一种方法是使用鼠标滚轮沿着Z轴平移(dolly),如 此处 所示。图像被缩放到复活节岛。
scene.setOnScroll((final ScrollEvent e) -> {
    eye.setTranslateZ(eye.getTranslateZ() + e.getDeltaY());
});

“我的相机可以在与赤道相交之前翻译817个像素。”
这个值可能与默认的 PerspectiveCamera 有关。特别地,
“通过调整眼睛位置的 Z 值,使得使用指定的 fieldOfView 生成的投影矩阵会在 Z = 0(投影平面)处产生单位,以设备无关像素为单位,与 ParallelCamera 的匹配。”
将以下内容添加到上面的滚动处理程序中,即可看到当 eye 与地球相交时该值出现:
System.out.println(eye.getTranslateZ());

另请参阅{{link1:JavaFX:使用JavaFX图形:§3相机}}。

Easter Island


这非常简单和有帮助。现在我发现,由于某种原因,我的相机可以在与赤道相交之前翻译817个像素。明天,我可以看看是否有明显的原因导致这个值。 - Douglas Held
我已经详细阐述了上面的内容。 - trashgod
谢谢。当然我读了JavaDoc和教程,但发现它们在阐述功能方面严重不足。 - Douglas Held
1
很高兴能有所帮助;我也想推荐 José Pereda 在这个领域的工作。 - trashgod
1
很明显这里有两个非常好的答案。我选择了你的回答,因为你的一行代码足以帮助我解除障碍并让这个API继续工作。 - Douglas Held

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接