Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Документация jMonkeyEngine
        jMonkeyEngine Уроки и Документация
            Пользовательские Инструменты
                ME3 Урок - Визуализация 3D-карт

ME3 Урок — Визуализация 3D-карт

Опубликованно: 04.07.2017, 20:27
Последняя редакция, Andry: 05.07.2017 13:20

Предыдущий урок: Навигация

В этом уроке мы рассмотрим создание простого приложения для 3D-картографирования, которое позволяет отображать 3D-карты с различными уровнями масштабирования.

В этом руководстве предполагается, что вы знаете:

mercator_grid_3d_small

Вы узнаете, как учитывать искажения, возникающие при переводе одной системы координат в другую (например, при преобразовании долготы/широты в мировые единицы JME), о том, как построить дерево плиток и как создать динамическую сетку Меркатора.

Эта статья спонсировалась компанией PlanetMayo Ltd

Отображение вашей первой карты

Давайте подумаем о том, как мы собираемся заставить JME отображать нашу местность. Самый простой способ — использовать JME»ImageBasedHeightMap». Вспомните из урока Hello Terrain, эти изображения представляющие из себя градации серого, которые JME использует для создания quad. Итак, чтобы отобразить карту, нам нужно изображение (двумерной) проекции меркатора (например, изображенное ниже), которое мы затем загружаем, используя:

Texture heightMapImage = assetManager.loadTexture(DEFAULT_HEIGHTMAP);
heightmap = new ImageBasedHeightMap(heightMapImage.getImage());
heightmap.load();
terrain = new TerrainQuad("terrain", 257, TERRAIN_SIZE, heightmap.getHeightMap());
applyDefaultTexture();

globe

В сущности, визуализация 3D-карт достигается путем преобразования многоугольников, составляющих сушу планеты Земля, в float матрицы, посредством чего каждое значение в матрице представляет определенную высоту ландшафта. Например, для ландшафта 100х100 мировых единиц мы строим карту высот, создавая матрицу 100х100. Каждая ячейка внутри матрицы соответствует координате местности; Значение каждой ячейки это высота этой координаты. Но вы уже это знали, так где же сложная часть? Ну, при визуализации диаграммы точная проекция требует перевода координат широты/долготы в эквивалентные им мировые единицы (x, y, z). Однако этот перевод не является прямым преобразованием одной системы координат в другую из-за искажений, возникающих при проецировании сплющенного сфероида на плоскую поверхность (см. Мою предыдущую статью в вики). Это означает, что если бы кто-то придерживался точного масштаба, проекция Меркатора искажала бы размер и форму объектов, поскольку пришлось бы масштабировать объект которые были бы все дальше от экватора, что в конечном итоге приводит к бесконечному масштабированию по мере достижения полюса. Итак, первая задача — построить точную 2D проекции планеты Земля, которую затем мы можем использовать в качестве карт высот. Мы можем добиться этого, используя пакет jme3.tools.navigation и координатные комплекты, доступные на noaa.gov.

Как обсуждалось ранее, искажение широты происходит с использованием разности меридиональных частей между центром диаграммы и текущей позицией в качестве базовой линии; Путем преобразования разницы в мировые единицы путем деления его на количество мировых единиц, содержащихся в течение одной минуты, можно получить координату y широты. Вычисление x-координаты позиции несколько проще, поскольку искажение применяется только к широте, а не по долготе. Следовательно, x просто равен сумме или разности между собой и центральной координатой окна просмотра, в зависимости от относительного местоположения самой позиции и центра диаграммы. Несмотря на возможность преобразования между двумя системами координат, небольшие проблемы точности остаются после того, как проекция графика уменьшилась ниже уровня 6 метров. Это вызвано тем, что система отображения пикселов современных дисплеев является целочисленной; Как только отношение минут к пикселям превышает вышеупомянутый порог, на дисплей вводятся незначительные неточности. Однако это не имеет большого значения для большинства ГИС (таких как «Разбор»): а) неточности — это счетчики (или даже сантиметры), и б) невозможно заметить этот вариант, поскольку GPS предоставляет гораздо более высокую неточность (между 10 — 100 метров).

Чтобы уменьшить вычислительную нагрузку, преобразование системы координат должно происходить только в том случае, если либо карта перецентрирована, либо требуется изменение масштаба / разрешения. После преобразования наборы координат отображаются в буфер, содержимое которого затем нарисовано для каждого цикла обновления пользовательского интерфейса.

Создание карт высот

Существует два способа создания карт высот (также называемых «плитки», поскольку каждая карта высот — это плитка, которая составляет нашу карту мира). Один из них — использование стороннего программного обеспечения, такого как GeoTools. Другой — использовать пакет jme3.tools.navigation для написания генератора плитки:

public class TileGenerator {
    private int lineCount;

    /* List of polygons representing the countries that are to be drawn. */
    private List<PositionContainer> polygons;

    /* The map projection used to generate the chart image. */
    private MapModel2D map;

    /* The chart's resolution in minutes of longitude per pixel. */
    private double mpp;

    /* The chart's centre. */
    private Position centre;

    /**
     * Constructs a new instance of TileGenerator.
     *
     * @param worldSize         The width of the chart for which tiles are to be
     *                          generated.
     * @since 1.0
     */
    public TileGenerator(int worldSize, double mpp, Position centre) {
        File dataDirectory = new File("data");
        map = new MapModel2D(worldSize);
        lineCount = 0;
        File[] files = dataDirectory.listFiles(new FileFilter() {

            public boolean accept(File pathname) {
                if (pathname.toString().endsWith(".out")) {
                    return true;
                }
                return false;
            }
        });
        loadChartData(files);
        this.mpp = mpp;
        this.centre = centre;
    }
    public void createImageMap(int worldSize) {
        map.setCentre(centre);
        map.calculateMinutesPerPixel(mpp);
        System.out.println("Generating chart with world width (in pixels): " + worldSize);
        System.out.println("Generating chart with meters per pixel: " + map.getMetersPerPixel());
        BufferedImage img = new BufferedImage(worldSize,
                worldSize, BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g = img.createGraphics();
        Point point1, point2;
        GeneralPath polygonPath;
        g.setColor(Color.WHITE);
        int containerSize;

        for (PositionContainer container : polygons) {
            polygonPath = new GeneralPath();
            containerSize = container.getPositions().size();
            for (int i = 1; i < containerSize; i++) {
                point1 = map.toPixel(container.getPositions().get(i));
                point2 = map.toPixel(container.getPositions().get(i - 1));
                polygonPath.moveTo((double) point1.getX(), (double) point1.getY());
                polygonPath.lineTo((double) point1.getX(), (double) point1.getY());
                polygonPath.lineTo((double) point2.getX(), (double) point2.getY());
            }
            g.draw(polygonPath);
        }

        // Write resulting image to file
        try {
            ImageIO.write(img, "png", new File("map.png"));
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }


    /**
     * Draws depth contours.
     *
     * @param img           The image to draw to.
     * @param worldSize     The size of the chart.
     * @since 1.0
     */
    private void drawContours(BufferedImage img, int worldSize) {
        map.setCentre(centre);
        map.calculateMinutesPerPixel(mpp);
        BufferedImage img2 = new BufferedImage(worldSize,
                worldSize, BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g = img2.createGraphics();
        g.drawImage(img, null, null);
        Point point1, point2;
        GeneralPath polygonPath;
//        g.setColor(new Color(21, 21, 21));
        g.setColor(Color.WHITE);
        int containerSize;

        for (PositionContainer container : polygons) {
            polygonPath = new GeneralPath();
            containerSize = container.getPositions().size();
            for (int i = 1; i < containerSize; i++) {
                point1 = map.toPixel(container.getPositions().get(i));
                point2 = map.toPixel(container.getPositions().get(i - 1));
                polygonPath.moveTo((double) point1.getX(), (double) point1.getY());
                polygonPath.lineTo((double) point1.getX(), (double) point1.getY());
                polygonPath.lineTo((double) point2.getX(), (double) point2.getY());
            }
            g.draw(polygonPath);
        }

        // Write resulting image to file
        try {
            ImageIO.write(img2, "png", new File("map.png"));
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    /**
     * Loads country border information from .out files, parses the information
     * and stores it as a PositionContainer which is later used to
     * produce the .png chart image.
     *
     * @param files             A List of files that contain
     *                          country border data.
     * @since 1.0
     */
    private void loadChartData(File[] files) {
        Scanner scan;
        PositionContainer countryBorderPosition;
        polygons = new ArrayList<PositionContainer>(300);
        String tmp = "";
        String tmpLat;
        String tmpLong;
        StringTokenizer stk;
        Position pos;
        for (File file : files) {
            try {
                scan = new Scanner(file);
                countryBorderPosition = new PositionContainer();
                while (scan.hasNext()) {
                    tmp = scan.nextLine();
                    if (tmp.startsWith("{") || tmp.startsWith("$") || tmp.startsWith(";")) {
                        continue;
                    }
                    if (tmp.equals("-1")) {
                        polygons.add(countryBorderPosition);
                        countryBorderPosition = new PositionContainer();
                        continue;
                    }
                    stk = new StringTokenizer(tmp, " +");
                    while (stk.hasMoreTokens()) {
                        tmpLat = stk.nextToken().trim();
                        if (tmpLat.equals("-1")) {
                            polygons.add(countryBorderPosition);
                            countryBorderPosition = new PositionContainer();
                            continue;
                        }
                        tmpLong = stk.nextToken().trim();
                        pos = new Position(Double.parseDouble(tmpLat), Double.parseDouble(tmpLong));
                        countryBorderPosition.add(pos);
                        lineCount++;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.err.println(tmp);
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("Debug 3D Tile Generator");
        System.out.println("===========================");
        args = new String[3];
        args[0] = "1.2";
        args[1] = "51.8";
        args[2] = "-8.3";
        if (args.length < 3 || args.length > 3) {
            System.err.println("Incorrect argument usage. Should be mpp latitude longitude");
            System.err.println("Exiting");
            return;
        }
        String mppStr = args[0];
        String latitudeStr = args[1];
        String longitudeStr = args[2];
        double lon, lat, mpp;
        Position centre;
        try {
            mpp = Double.parseDouble(mppStr);
        } catch (Exception e) {
            System.err.println("MPP must be of type Double or Integer.");
            System.err.println("Exiting");
            return;
        }
        try {
            lat = Double.parseDouble(latitudeStr);
        } catch (Exception e) {
            System.err.println("Latitude must be of type Double or Integer.");
            System.err.println("Exiting");
            return;
        }
        try {
            lon = Double.parseDouble(longitudeStr);
        } catch (Exception e) {
            System.err.println("Longitude must be of type Double or Integer.");
            System.err.println("Exiting");
            return;
        }
        try {
            centre = new Position(lat, lon);
        } catch (InvalidPositionException ipe) {
            System.err.println("Invalid latitude or longitude coordinates.");
            System.err.println("Exiting");
            return;
        }
        System.out.println("Generating chart...Please wait...");
        TileGenerator generator = new TileGenerator(TerrainViewer.TERRAIN_SIZE - 1, mpp, centre);
        File chart = new File("map.png");
        if (!chart.exists()) {
            generator.createImageMap(TerrainViewer.TERRAIN_SIZE - 1);
        }
        try {
            BufferedImage img = ImageIO.read(chart);
            generator.drawContours(img, TerrainViewer.TERRAIN_SIZE - 1);

        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Chart generated. Placed in file 'chart.png'. Exiting.");
    }
}

… где .out файл содержит координатные пары долготы / широты, определяющие контуры ландшафта. Здесь выдержка:

51.79188150756147+-8.25435369629442
51.79184641740534+-8.254357553715453
51.79182071886024+-8.254353833180712
51.79181370477922+-8.254312317813477
51.79181369284153+-8.254267011113086
51.79182535405747+-8.254221642581026
51.79184870922772+-8.254183732747943
51.79188146269924+-8.254183530764353
51.79190724220316+-8.254221208836046
51.79190960635914+-8.254296874457655
51.79188150756147+-8.25435369629442
-1
51.79165344300885+-8.255042583168985
51.79161872648091+-8.255072177259352
51.79158175153456+-8.255082912194254
51.79156558301037+-8.255041382314799
51.79156556852833+-8.254985072910559
51.79158171385971+-8.254936452917438
51.79159555664058+-8.25487274689492
51.79161403682817+-8.254824070938184
51.79164411466118+-8.254798004805433
51.79168584436759+-8.254817161260844
51.79170675060084+-8.25487006519348
51.79169051462138+-8.254930145346941
51.79167197282713+-8.254993914789209
51.79165344300885+-8.255042583168985

(-1 действует как разделитель, обозначающий конец одного многоугольника и начало другого).

Так что здесь происходит? Ну, мы в основном читаем содержимое всех указанных файлов, в результате чего каждая строка разбивается на пары долготы / широты, преобразуется в координаты пикселя (x, y) и используется для построения многоугольника, который добавляется в контейнер многоугольника, когда многоугольник Встречается. Когда вызывается метод рисования объекта, этот многоугольный контейнер повторяется, и любые полигоны, попадающие в рамки холста (aka viewport), окрашиваются в графический контекст. По существу, этот алгоритм можно суммировать следующим образом:

Constructor ( files ):
   for each file in files
      for each line in file
         if line == -1
            polygonList.add(polygon)
            new polygon
         else
            polygon.add(parse(line))

Paint ( graphics context ):
   for each polygon in polygonList
      if polygon inside view bounds
         graphics context.paint(polygon)

heightmap_modelling

Выше: суммирование процесса визуализации диаграммы. Слева направо: мы рисуем координаты, загруженные с noaa.gov. В идеале, каждый многоугольник должен быть заполнен светлым цветом, в то время как окружающий океан остается темным. JME использует эти изображения для создания внутреннего представления рельефа (матрицы с плавающей точкой).

Макеты высот, создаваемые TileGenerator, по существу представляют собой массивы, содержащие значения float от 0 до 255. Для удобства и эффективности JME обрабатывает эти массивы как изображения с графическим интерфейсом (PNG) (опять же, см. Учебник Hello Terrain). Это позволяет хранить каждую плиту в качестве изображения, а это означает, что каждая плитка должна быть построена только один раз. По существу, что делает генератор плитки, это рисовать изображение в оттенках серого каждой плитки, в результате чего темные цвета (т. Е. Низкие значения от 0 до 50) являются долинами, а высокие значения (200 — 255) становятся горами или холмами. Для поддержания масштаба эти значения масштабируются путем деления максимальной высоты (в метрах) на морском дне на метры на пиксель текущей диаграммы. Имея только несколько указанных точек, JME интерполирует остальные, делая построение ландшафта с использованием карт высот более эффективным, чем определение отдельных вершин для каждого пикселя на диаграмме. Текстура плитки определяется ее «Альфамапом». Это копия его карты высот, но вместо определения значений высоты поплавки, составляющие альфа-изображение, определяют текстуры. Для этой цели используется метод, известный как «текстурирование splatting», в результате чего данные текстуры кодируются цветом. То есть, предполагая, что пространственный объект имеет два слоя текстуры (назовем их Tex1 и Tex2), каждый слой ассоциируется с цветом: в случае Debrief 3D синий относится к текстуре песка, а красный относится к текстурам грязи / травы. Хотя такой подход к текстурированию может показаться запутанным вначале, он имеет то преимущество, что как карты высот, так и альфа-карты могут быть созданы за один раз и, поскольку они основаны на том же принципе, могут быть легко изменены в пакетном режиме, а не отдельно.

Что все это говорит о плитки?

Дерево плитки — наш способ отслеживать отдельные плитки. Все, что это такое, представляет собой набор вложенных подкаталогов, в которых хранятся диаграммы. Каталог верхнего уровня (наш корень) содержит диаграмму всего мира, а каждый подкаталог — увеличенную область нашей планеты. Например, внутри корня, у нас могут быть папки, содержащие диаграмму только для Ирландии, Великобритании и Франции. По мере дальнейшего продвижения дерева мы получаем отдельные округа или провинции для каждой страны. Один из способов представления этого беспорядка каталогов — через Дерево поиска.

Создание дерева плитки

try {
            File resourceDirectory = new File(worldResourcesDirectory);
            if (!resourceDirectory.isDirectory()) {
                System.out.println("Resource path must be a directory");
                System.exit(1);
            }
            worldStructure = new TileTree(resourceDirectory);
        } catch (Exception e) {
            e.printStackTrace();
        }

После инициализации эти фрагменты считываются в память объектом TileTree, который рассматривает, как следует из названия, плитки, составляющие диаграмму, в виде дерева, посредством которого корневой узел ссылается на весь земной шар. Его дети относятся к подразделам земного шара и своим детям, в свою очередь, к подразделам этого подраздела. Например, узел «Ирландия» является прямым потомком корневого узла. Узел «Корк-Харбор», в свою очередь, является прямым ребенком узла «Ирландии» и представляет собой увеличенную версию подрайона ирландского побережья. Каждый такой узел состоит из уникального идентификатора (используется для идентификации узла), списка дочерних узлов, пути к карте высот (плитки), которую он представляет, уровня масштабирования (называемого уровнем долготы, поскольку масштаб определяется Минуты долготы на пиксель) и пара широты / долготы, обозначающая центр плитки.

Каждая карта высот отображается в зависимости от того, какой идентификатор выбирается пользователем (где каждый узел в дереве указан по его уникальному идентификатору). Когда идентификатор выбран, дерево перемещается, чтобы найти узел, соответствующий данному ID. Путь к его карте высот извлекается, а heightmap отображается путем извлечения массива float из загруженного изображения (т. Е. Создается объект текстуры и загружается с помощью карты высот. Затем объект ImageBasedHeightMap используется для преобразования карты высот и альфа-карты в соответствующий Высота и текстурные массивы). → снова, см. Учебник JME на местности.

Содержимое Tile Tree хранится в активах / Heightmaps, и каждый уровень каталога состоит из одного файла дескриптора, одной карты высот (в виде PNG-изображения) и одного альфа-файла (также в виде PNG-изображения). Файлы дескрипторов заканчиваются расширением имени файла a.desc и содержат центр геокоординирования плитки, а также разрешение узла, который они представляют (как всегда, разрешение представлено в минутах на пиксель (mpp)). Единственная цель файла дескриптора — разрешить повторное построение дерева плитки при инициализации приложения. В частности, это достигается с помощью объекта ChartModel, который создает экземпляр TileTree, передавая ссылку на активы / карты высот, которые TileTree затем рекурсивно сканирует, конструируя дерево, интерпретируя файлы дескриптора. Стоит отметить, что все файлы на уровне должны быть названы в соответствии с отображением высотной плитки, которую он представляет. То есть, если ваш уровень представляет собой график Ирландии, а ваша карта роста называется Ireland.png, то ваш файл дескриптора должен быть назван Ireland.desc, а ваш альфа-файл должен быть назван Ireland.png.Alphamap.png.

slide1

slide2

узел. Мы можем видеть карту высот, файл дескриптора и альфа-карту.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package util.datastructure;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import jme3tools.navigation.Position;

/**
 * The TileTree handles the storage and retrieval of individual
 * charts. Each Node corresponds to one chart (a node's value being
 * the chart's absolute path. It's ID being the ID under which it is displayed).
 *
 * The tree reflects the application's chart directory structure, with the world
 * as its root and individual countries as its children. Sub-children of these nodes
 * represent "close up" version of each country / geographic sub areas of those countries.
 *
 * @author Benjamin Jakobus
 * @since 1.0
 * @version 1.0
 */
public class TileTree {

    /* The tree's root node. */
    private Node root;

    /**
     * Creates a new instance of TileTree. Nodes are generated
     * depending on the contents of the resource directory.
     *
     * @param resourceDirectory         The root of the application's resource
     *                                  directory (the resource directory being the
     *                                  directory in which all charts (aka Heightmaps)
     *                                  are being stored).
     * @since 1.0
     */
    public TileTree(File resourceDirectory) {
        File directory = null;
        for (File f : resourceDirectory.listFiles()) {
            if (f.isDirectory()) {
                directory = f;
                continue;
            }
            if (f.getName().endsWith(".desc")) {
                root = initNode(f);

            }
        }
        initTileTree(directory, root);
    }

    /**
     * Initializes the tree's children. The root node should be initialized
     * prior to calling this method.
     *
     * @param resourceDirectory         The root of the application's resource
     *                                  directory (the resource directory being the
     *                                  directory in which all charts (aka Heightmaps)
     *                                  are being stored).
     * @param parentNode                The Node to which all
     *                                  subsequent nodes should be attached.
     * @since 1.0
     */
    private void initTileTree(File resourceDirectory, Node parentNode) {
        File directory = null;
        Node node = null;
        if (parentNode == null || resourceDirectory == null) {
            return;
        }
        for (File f : resourceDirectory.listFiles()) {
            if (f.isDirectory()) {
                directory = f;
                continue;
            }
            if (!f.getName().endsWith(".desc")) {
                continue;
            } else {
                node = initNode(f);
            }
        }
        parentNode.attachChild(node);
        node = parentNode;
        initTileTree(directory, parentNode);
    }

    /**
     * Initializes an individual node depending on the contents of the descriptor
     * file (for information on descriptor files, refer to the software documentation).
     *
     * @param file                          The descriptor File with
     *                                      which to initialize the node's contents.
     * @return                              A new Node.
     * @since 1.0
     */
    private Node initNode(File file) {
        Node node = null;
        Scanner scan;
        String resourcePath = null;
        String nodeID = null;
        String longitudeLevel = null;
        Position centre = null;
        int currentLine = 0;
        if (file == null) {
            return node;
        }
        try {
            scan = new Scanner(file);
            resourcePath = file.getAbsolutePath().replace(".desc", ".png");
            resourcePath = resourcePath.substring(resourcePath.indexOf("assets"));
            nodeID =  file.getName().replace(".desc", "") + "_" + file.getParentFile().getName();
            while (scan.hasNextLine()) {
                if (currentLine == 0) {
                    String tmp = scan.nextLine();
                    String[] array = tmp.split("\\+");
                    centre = new Position(Double.parseDouble(array[0]),
                            Double.parseDouble(array[1]));
                    currentLine++;
                } else {
                    longitudeLevel = scan.nextLine().trim();
                }
            }
            node = new Node(nodeID, resourcePath, longitudeLevel, centre);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return node;
    }

    /**
     * Returns the Node matching the given ID.
     *
     * @param nodeID                The ID of the Node that you
     *                              wish to retrieve.
     * @return                      The Node matching the given ID.
     * @since 1.0
     */
    public Node find(String nodeID) {
        return find(root, nodeID);
    }

    /**
     * Returns the Node matching the given ID. This method is similar
     * to find() with the exception that it only begins searching from
     * a certain node downwards.
     *
     * @param nodeToSearch          The Node from which to start searching.
     * @param nodeID                The ID of the Node that you
     *                              wish to retrieve.
     * @return                      The Node matching the given ID.
     * @since 1.0
     */
    private Node find(Node nodeToSearch, String nodeID) {
        Node newNode = null;
        if (nodeToSearch == null) {
            return newNode;
        }
        if (nodeToSearch.getNodeID().trim().compareTo(nodeID.trim()) == 0) {
            return nodeToSearch;
        } else {
            for (Node n : nodeToSearch.getChildren()) {
                newNode = find(n, nodeID);
                if (newNode != null) {
                    return newNode;
                }
            }
        }
        return newNode;
    }

    /**
     * Retrieves all nodes within the tree.
     *
     * @return                      A List of all nodes within the
     *                              tree.
     * @since 1.0
     */
    public List<Node> getNodes() {
        List<Node> nodes = new ArrayList<Node>();
        getNodes(root, nodes);
        return nodes;
    }

    /**
     * Returns all the children of a specific Node.
     *
     * @param node                  The Node whose children you want.
     * @param nodes                 The List to which to add these children.
     * @since 1.0
     */
    private void getNodes(Node node, List<Node> nodes) {
        if (node == null) {
            return;
        }
        for (Node n : node.getChildren()) {
            getNodes(n, nodes);
        }
        nodes.add(node);
    }
}

.. и Узел:

package util.datastructure;

import java.util.ArrayList;
import java.util.List;
import jme3tools.navigation.Position;

/**
 * An individual node within the TileTree. Each Node
 * represents an individual tile (i.e. heightmap + alphamap).
 *
 * @author Benjamin Jakobus
 * @since 1.0
 * @version 1.0
 */
public class Node {
    /* The node's unique identifier. */
    private String nodeID;

    /* Path to the resource that the node represents (aka the node's value). */
    private String resource;

    /* The node's children. */
    private List<Node> children;

    /* The resolution (width in degrees of longitude) represented by this node.
     * i.e. the resolution of the chart that the node represents.
     */
    private double longitudeLevel;

    /* The centre of the chart (aka tile) that the node represents. */
    private Position centre;

    /**
     * Constructor.
     *
     * @param nodeID                The node's unique identifier.
     * @param resource              Path to the resource that the node represents
     *                              (aka the node's value).
     * @param longitudeLevel        The resolution (width in degrees of longitude)
     *                              represented by this node.
     * @param centre                The centre of the chart (aka tile) that the
     *                              node represents.
     * @since 1.0
     */
    public Node(String nodeID, String resource, String longitudeLevel, Position centre) {
        this.nodeID = nodeID;
        this.resource = resource;
        this.longitudeLevel = Double.parseDouble(longitudeLevel);
        this.centre = centre;
        children = new ArrayList<Node>();
    }

    /**
     * Returns all of the node's children.
     *
     * @return          A List containing all of the node's children.
     * @since 1.0
     */
    public List<Node> getChildren() {
        return children;
    }

    /**
     * Returns the node's ID.
     *
     * @return          The node's unique identifier.
     * @since 1.0
     */
    public String getNodeID() {
        return nodeID;
    }

    /**
     * Returns the path to the tile that the node represents.
     *
     * @return          The path to the tile that the node represents.
     * @since 1.0
     */
    public String getResource() {
        return resource;
    }

    /**
     * Attaches a child to this node.
     *
     * @param child     The Node to attach.
     * @since 1.0
     */
    public void attachChild(Node child) {
        children.add(child);
    }

    /**
     * Returns the width in degrees of longitude of the chart / resource that this
     * node represents.
     *
     * @return          the width in degrees of longitude of the chart / resource
     *                  that this node represents.
     * @since 1.0
     */
    public double getLongitudeLevel() {
        return longitudeLevel;
    }

    /**
     * The centre coordinate of the tile / chart that this node represents.
     *
     * @return          The chart's centre in terms of latitude / longitude.
     * @since 1.0
     */
    public Position getCentre() {
        return centre;
    }
}

Загрузка новой диаграммы проста: все, что нам нужно сделать, это получить TileTree, чтобы найти диаграмму для нас (она будет обрабатывать загрузку файла дескриптора и просто вернуть узел):

Node node = tileTree.find(chartID);

Затем мы используем возвращаемый узел, чтобы отрегулировать наш уровень масштабирования:

mapModel.calculateMinutesPerWorldUnit(node.getLongitudeLevel());
mapModel.setCentre(node.getCentre());

Рисование сетки Меркатора

Меркаторная сетка отражает «растяжение» плоской карты Меркатора за счет расширения линий долготы / широты. Используя класс Mesh JME, трехмерное представление сетки может быть проиграно процедурно (в отличие от предварительного определения вершин сетки с помощью инструмента 3D-моделирования, такого как Blender). Это достигается путем определения вектора позиций, при котором каждая запись i внутри вектора обозначает начальную точку линии сетки и i + 1, определяющую конечную точку линии. Затем определяется порядок, в котором сетка построена из этих координат. Это в основном пары индексов, поскольку сетка состоит из набора строк, каждый из которых имеет начальную и конечную точку. Наконец, вектор координат и индексы добавляются в ячейку, которая, в свою очередь, используется для определения геометрии, которая добавляется к графу сцены:

public void createGrid(double longitudeLevel, float increment) {
        granularity = increment;
        Mesh m = new Mesh();
        m.setMode(Mesh.Mode.Lines);
        m.setLineWidth(1f);

        float max = (longitudeLevel < 8 ? 2 : 85);
        float max2 = (longitudeLevel < 8 ? 180 / 8 : 180);
        Vector3f[] positions = new Vector3f[(int) (Math.ceil(max / increment) * 4) + (int) (Math.ceil(max2 / increment) * 4)];
        Position pos;

        int i = 0;
        try {
            // Calculate initial spacings for the meridians and
            // parallels


            // Approach the grid construction from north to south and
            // east to west.
            positions[0] = new Vector3f(0, 0, 0);

            // Latitude lines for northern hemisphere
            for (float lat = 0; lat < max; lat += increment, i += 2) {
                pos = new Position(lat, 180);
                positions[i] = TerrainViewer.mapModel.toWorldUnit(new Position(lat, -180));
                positions[i + 1] = TerrainViewer.mapModel.toWorldUnit(pos);
            }

            // Latitude lines for southern hemisphere
            for (float lat = 0; lat < max; lat += increment, i += 2) {
                pos = new Position(lat * -1, 180);
                positions[i] = TerrainViewer.mapModel.toWorldUnit(new Position(lat * -1, -180));
                positions[i + 1] = TerrainViewer.mapModel.toWorldUnit(pos);
            }

            max = (longitudeLevel < 8 ? 180 / 8 : 180);
            // Longitude lines for northern hemisphere
            for (float lon = 0; lon < max; lon += increment, i += 2) {
                pos = new Position(85, lon);
                positions[i] = TerrainViewer.mapModel.toWorldUnit(new Position(-85, lon));
                positions[i + 1] = TerrainViewer.mapModel.toWorldUnit(pos);
            }

            // Longitude lines for southern hemisphere
            for (float lon = 0; lon < max; lon += increment, i += 2) {
                pos = new Position(85, lon * -1);
                positions[i] = TerrainViewer.mapModel.toWorldUnit(new Position(-85, lon * -1));
                positions[i + 1] = TerrainViewer.mapModel.toWorldUnit(pos);
            }
        } catch (Exception ipe) {

        }

        int[] indices = new int[i];
        int v;
        for (i = 0, v = 0; i < indices.length; i += 2, v++) {
            indices[i] = i;
            indices[i + 1] = i + 1;
        }

        m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(positions));

        m.setBuffer(Type.Index, 1, indices);
        m.updateBound();
        m.updateCounts();
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("m_Color", ColorRGBA.Gray);
        gridGeometry = new Geometry("Grid", m);
        gridGeometry.setMaterial(mat);
        Vector3f worldCentre = TerrainViewer.mapModel.getCentreWu();
        gridGeometry.setLocalTranslation(new Vector3f(worldCentre.getX(),
                gridHeight, worldCentre.getZ()));
    }

screen_shot_2011-12-18_at_13.12.01


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добавить комментарий

jMonkeyEngine.ru © 2017. Все права сохранены.