世界风视线功能

20

我找到了一个关于如何在WorldWind中渲染视线的例子:http://patmurris.blogspot.com/2008/04/ray-casting-and-line-of-sight-for-wwj.html(有点老,但似乎仍然可以工作)。这是用于该示例的(稍微修改了以下代码以使其与WorldWind 2.0配合使用)。看起来该代码还使用RayCastingSupport (JavadocCode) 来实现其功能。

我想弄清楚这个代码/示例是否在其逻辑中使用了地球的曲率和/或地平线距离。仅从代码上看,我不确定我完全理解它在做什么。

例如,如果我想弄清楚一个高出地面200米的人能够“看到”什么地形,它是否会考虑到地平线距离?如何修改代码才能考虑地平线距离/地球曲率(如果还没有)?

package gov.nasa.worldwindx.examples;

import gov.nasa.worldwind.util.RayCastingSupport;
import gov.nasa.worldwind.view.orbit.OrbitView;
import gov.nasa.worldwind.geom.Angle;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.geom.Sector;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.layers.CrosshairLayer;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.render.*;

import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;


public class LineOfSight extends ApplicationTemplate
{
    public static class AppFrame extends ApplicationTemplate.AppFrame
    {
        private double samplingLength = 30; // Ray casting sample length
        private int centerOffset = 100; // meters above ground for center
        private int pointOffset = 10;   // meters above ground for sampled points
        private Vec4 light = new Vec4(1, 1, -1).normalize3();   // Light direction (from South-East)
        private double ambiant = .4;                            // Minimum lighting (0 - 1)

        private RenderableLayer renderableLayer;
        private SurfaceImage surfaceImage;
        private ScreenAnnotation screenAnnotation;
        private JComboBox radiusCombo;
        private JComboBox samplesCombo;
        private JCheckBox shadingCheck;
        private JButton computeButton;

        public AppFrame()
        {
            super(true, true, false);

            // Add USGS Topo Maps
//            insertBeforePlacenames(getWwd(), new USGSTopographicMaps());

            // Add our renderable layer for result display
            this.renderableLayer = new RenderableLayer();
            this.renderableLayer.setName("Line of sight");
            this.renderableLayer.setPickEnabled(false);
            insertBeforePlacenames(getWwd(), this.renderableLayer);

            // Add crosshair layer
            insertBeforePlacenames(getWwd(), new CrosshairLayer());

            // Update layer panel
            this.getLayerPanel().update(getWwd());

            // Add control panel
            this.getLayerPanel().add(makeControlPanel(),  BorderLayout.SOUTH);
        }

        private JPanel makeControlPanel()
        {
            JPanel controlPanel = new JPanel(new GridLayout(0, 1, 0, 0));
            controlPanel.setBorder(
                new CompoundBorder(BorderFactory.createEmptyBorder(9, 9, 9, 9),
                new TitledBorder("Line Of Sight")));

            // Radius combo
            JPanel radiusPanel = new JPanel(new GridLayout(0, 2, 0, 0));
            radiusPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            radiusPanel.add(new JLabel("Max radius:"));
            radiusCombo = new JComboBox(new String[] {"5km", "10km",
                    "20km", "30km", "50km", "100km", "200km"});
            radiusCombo.setSelectedItem("10km");
            radiusPanel.add(radiusCombo);

            // Samples combo
            JPanel samplesPanel = new JPanel(new GridLayout(0, 2, 0, 0));
            samplesPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            samplesPanel.add(new JLabel("Samples:"));
            samplesCombo = new JComboBox(new String[] {"128", "256", "512"});
            samplesCombo.setSelectedItem("128");
            samplesPanel.add(samplesCombo);

            // Shading checkbox
            JPanel shadingPanel = new JPanel(new GridLayout(0, 2, 0, 0));
            shadingPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            shadingPanel.add(new JLabel("Light:"));
            shadingCheck = new JCheckBox("Add shading");
            shadingCheck.setSelected(false);
            shadingPanel.add(shadingCheck);

            // Compute button
            JPanel buttonPanel = new JPanel(new GridLayout(0, 1, 0, 0));
            buttonPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            computeButton = new JButton("Compute");
            computeButton.addActionListener(new ActionListener()
            {
                public void actionPerformed(ActionEvent actionEvent)
                {
                    update();
                }
            });
            buttonPanel.add(computeButton);

            // Help text
            JPanel helpPanel = new JPanel(new GridLayout(0, 1, 0, 0));
            buttonPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            helpPanel.add(new JLabel("Place view center on an elevated"));
            helpPanel.add(new JLabel("location and click \"Compute\""));

            // Panel assembly
            controlPanel.add(radiusPanel);
            controlPanel.add(samplesPanel);
            controlPanel.add(shadingPanel);
            controlPanel.add(buttonPanel);
            controlPanel.add(helpPanel);

            return controlPanel;
        }

        // Update line of sight computation
        private void update()
        {
            new Thread(new Runnable() {
                public void run()
                {
                    computeLineOfSight();
                }
            }, "LOS thread").start();
        }

        private void computeLineOfSight()
        {
            computeButton.setEnabled(false);
            computeButton.setText("Computing...");

            try
            {
                Globe globe = getWwd().getModel().getGlobe();
                OrbitView view = (OrbitView)getWwd().getView();
                Position centerPosition = view.getCenterPosition();

                // Compute sector
                String radiusString = ((String)radiusCombo.getSelectedItem());
                double radius = 1000 * Double.parseDouble(radiusString.substring(0, radiusString.length() - 2));
                double deltaLatRadians = radius / globe.getEquatorialRadius();
                double deltaLonRadians = deltaLatRadians / Math.cos(centerPosition.getLatitude().radians);
                Sector sector = new Sector(centerPosition.getLatitude().subtractRadians(deltaLatRadians),
                        centerPosition.getLatitude().addRadians(deltaLatRadians),
                        centerPosition.getLongitude().subtractRadians(deltaLonRadians),
                        centerPosition.getLongitude().addRadians(deltaLonRadians));

                // Compute center point
                double centerElevation = globe.getElevation(centerPosition.getLatitude(),
                        centerPosition.getLongitude());
                Vec4 center = globe.computePointFromPosition(
                        new Position(centerPosition, centerElevation + centerOffset));

                // Compute image
                float hueScaleFactor = .7f;
                int samples = Integer.parseInt((String)samplesCombo.getSelectedItem());
                BufferedImage image = new BufferedImage(samples, samples, BufferedImage.TYPE_4BYTE_ABGR);
                double latStepRadians = sector.getDeltaLatRadians() / image.getHeight();
                double lonStepRadians = sector.getDeltaLonRadians() / image.getWidth();
                for (int x = 0; x < image.getWidth(); x++)
                {
                    Angle lon = sector.getMinLongitude().addRadians(lonStepRadians * x + lonStepRadians / 2);
                    for (int y = 0; y < image.getHeight(); y++)
                    {
                        Angle lat = sector.getMaxLatitude().subtractRadians(latStepRadians * y + latStepRadians / 2);
                        double el = globe.getElevation(lat, lon);
                        // Test line of sight from point to center
                        Vec4 point = globe.computePointFromPosition(lat, lon, el + pointOffset);
                        double distance = point.distanceTo3(center);
                        if (distance <= radius)
                        {
                            if (RayCastingSupport.intersectSegmentWithTerrain(
                                    globe, point, center, samplingLength, samplingLength) == null)
                            {
                                // Center visible from point: set pixel color and shade
                                float hue = (float)Math.min(distance / radius, 1) * hueScaleFactor;
                                float shade = shadingCheck.isSelected() ?
                                        (float)computeShading(globe, lat, lon, light, ambiant) : 0f;
                                image.setRGB(x, y, Color.HSBtoRGB(hue, 1f, 1f - shade));
                            }
                            else if (shadingCheck.isSelected())
                            {
                                // Center not visible: apply shading nonetheless if selected
                                float shade = (float)computeShading(globe, lat, lon, light, ambiant);
                                image.setRGB(x, y, new Color(0f, 0f, 0f, shade).getRGB());
                            }
                        }
                    }
                }
                // Blur image
                PatternFactory.blur(PatternFactory.blur(PatternFactory.blur(PatternFactory.blur(image))));

                // Update surface image
                if (this.surfaceImage != null)
                    this.renderableLayer.removeRenderable(this.surfaceImage);
                this.surfaceImage = new SurfaceImage(image, sector);
                this.surfaceImage.setOpacity(.5);
                this.renderableLayer.addRenderable(this.surfaceImage);

                // Compute distance scale image
                BufferedImage scaleImage = new BufferedImage(64, 256, BufferedImage.TYPE_4BYTE_ABGR);
                Graphics g2 = scaleImage.getGraphics();
                int divisions = 10;
                int labelStep = scaleImage.getHeight() / divisions;
                for (int y = 0; y < scaleImage.getHeight(); y++)
                {
                    int x1 = scaleImage.getWidth() / 5;
                    if (y % labelStep == 0 && y != 0)
                    {
                        double d = radius / divisions * y / labelStep / 1000;
                        String label = Double.toString(d) + "km";
                        g2.setColor(Color.BLACK);
                        g2.drawString(label, x1 + 6, y + 6);
                        g2.setColor(Color.WHITE);
                        g2.drawLine(x1, y, x1 + 4 , y);
                        g2.drawString(label, x1 + 5, y + 5);
                    }
                    float hue = (float)y / (scaleImage.getHeight() - 1) * hueScaleFactor;
                    g2.setColor(Color.getHSBColor(hue, 1f, 1f));
                    g2.drawLine(0, y, x1, y);
                }

                // Update distance scale screen annotation
                if (this.screenAnnotation != null)
                    this.renderableLayer.removeRenderable(this.screenAnnotation);
                this.screenAnnotation = new ScreenAnnotation("", new Point(20, 20));
                this.screenAnnotation.getAttributes().setImageSource(scaleImage);
                this.screenAnnotation.getAttributes().setSize(
                        new Dimension(scaleImage.getWidth(), scaleImage.getHeight()));
                this.screenAnnotation.getAttributes().setAdjustWidthToText(Annotation.SIZE_FIXED);
                this.screenAnnotation.getAttributes().setDrawOffset(new Point(scaleImage.getWidth() / 2, 0));
                this.screenAnnotation.getAttributes().setBorderWidth(0);
                this.screenAnnotation.getAttributes().setCornerRadius(0);
                this.screenAnnotation.getAttributes().setBackgroundColor(new Color(0f, 0f, 0f, 0f));
                this.renderableLayer.addRenderable(this.screenAnnotation);

                // Redraw
                this.getWwd().redraw();
            }
            finally
            {
                computeButton.setEnabled(true);
                computeButton.setText("Compute");
            }
        }

        /**
         * Compute shadow intensity at a globe position.
         * @param globe the <code>Globe</code>.
         * @param lat the location latitude.
         * @param lon the location longitude.
         * @param light the light direction vector. Expected to be normalized.
         * @param ambiant the minimum ambiant light level (0..1).
         * @return  the shadow intensity for the location. No shadow = 0, totaly obscured = 1.
         */
        private static double computeShading(Globe globe, Angle lat, Angle lon, Vec4 light, double ambiant)
        {
            double thirtyMetersRadians = 30 / globe.getEquatorialRadius();
            Vec4 p0 = globe.computePointFromPosition(lat, lon, 0);
            Vec4 px = globe.computePointFromPosition(lat, Angle.fromRadians(lon.radians - thirtyMetersRadians), 0);
            Vec4 py = globe.computePointFromPosition(Angle.fromRadians(lat.radians + thirtyMetersRadians), lon, 0);

            double el0 = globe.getElevation(lat, lon);
            double elx = globe.getElevation(lat, Angle.fromRadians(lon.radians - thirtyMetersRadians));
            double ely = globe.getElevation(Angle.fromRadians(lat.radians + thirtyMetersRadians), lon);

            Vec4 vx = new Vec4(p0.distanceTo3(px), 0, elx - el0).normalize3();
            Vec4 vy = new Vec4(0, p0.distanceTo3(py), ely - el0).normalize3();
            Vec4 normal = vx.cross3(vy).normalize3();

            return 1d - Math.max(-light.dot3(normal), ambiant);
        }
    }

    public static void main(String[] args)
    {
        ApplicationTemplate.start("World Wind Line Of Sight Calculation", AppFrame.class);
    }
}

我想我并不是在问“为什么这段代码不起作用?”的问题,而更多的是一种“你能告诉我这段代码是如何工作的吗?”的问题。我会考虑如何使包含的代码更加“简洁”,但我也希望包括所有内容,以确切地复制博客文章所讨论的内容。 - mainstringargs
我可能会(或可能不会)提供详细的、规范的答案来解决后续的问题,但简短而甜美的是,代码如何工作的“关键”在于调用RayCastingSupport.intersectSegmentWithTerrain()。 - JasonInVegas
3
这个例程使用的方法是射线投射算法,用点乘将地球对象的表面法向量和提供的向量相交。请注意,此调用需要一个地球对象和一个要相交的向量。地球本身可以是投影(平面)或椭球体。因此,这种射线投射方法是否依赖于地球的曲率?答案取决于传递给例程的地球对象。在这个例子中(我从经纬度坐标推断出),使用的是一个椭球体地球,并且“是的”,曲率与地形高程一起被考虑进去了。 - JasonInVegas
谢谢您以简单的方式解释。我理解代码的基本操作,但不一定涉及到的数学知识。如果您将此作为答案,我会给予奖励。 - mainstringargs
3
抱歉,我误说了。在阅读了RayCastingSupport的Java代码后,算法会沿着提供的向量向地球下降...在每一步中,它都会使用一个相交函数来测试测试点是否在地形内。如果在里面,则该算法会反转方向并采取更小的步骤向后查找,以找到向量与地球相遇的点。它是否考虑椭球曲率?“这取决于”提供的是哪个地球,因为“相交”函数测试平坦的地球和椭圆形的地球并返回略有不同的结果。 - JasonInVegas
1个回答

1
你说得对。这段代码没有考虑地球曲率。据我所见,光线追踪是针对光的中心进行的,但光锥被绘制在一张灰度图像上(我不确定,但看起来好像这个例子是在灰度图像上绘制的)。无论如何,这个演示是关于检测是否击中地面以停止光线追踪的。从我理解的情况来看,该算法在设置的距离范围内停止(5公里、10公里...200公里等等)。我不明白光线的方向。只有当你检查来自外太空的光时,才有意义去检查200公里半径。如果你想考虑地平线,你应该先检查光源的俯仰角。这对正俯仰值(在地平线以上)是相关的。在这种情况下,当光的中心高出地面很高时,你应该决定何时停止。高度取决于你将光照向山坡还是相对平坦的地形,或者光源是窄束还是宽束。

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