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

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

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

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

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

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

mercator_grid_3d_small

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

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

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

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

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 проекции планеты Земля, которую затем мы можем использовать в качестве карт высот(heightmap). Мы можем добиться этого, используя пакет jme3.tools.navigation и наборы координат, доступные на noaa.gov.

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

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

Создание ваших карт высот(Creating your heightmaps)

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

public class TileGenerator {
    private int lineCount;

    /* Список полигонов, представляющих страны, которые должны быть нарисованы. */
    private List<PositionContainer> polygons;

    /* Проекция карты, используемая для генерации изображения карты. */
    private MapModel2D map;

    /* Разрешение карты в минутах долготы на пиксель. */
    private double mpp;

    /* Центр карты. */
    private Position centre;

    /**
     * Создает новый экземпляр TileGenerator.
     *
     * @param worldSize         Ширина карты, для которой должны быть сгенерированы
     *                          плитки.
     * @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);
        }

        // Запись полученного изображения в файл
        try {
            ImageIO.write(img, "png", new File("map.png"));
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }


    /**
     * Рисует контуры глубины.
     *
     * @param img           Изображение для рисования.
     * @param worldSize     Размер карты.
     * @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);
        }

        // Запись полученного изображения в файл
        try {
            ImageIO.write(img2, "png", new File("map.png"));
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    /**
     * Загружает информацию о границах страны из .out файлов, анализирует информацию
     * и сохраняет ее как PositionContainer, который позже используется 
     * для создания изображения карты в формате .png.
     *
     * @param files             Список файлов, содержащих
     *                          данные о границах стран.
     * @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("Отладка 3D Генератора Плиток");
        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("Неправильное использование аргументов. Должна быть mpp широта долгота");
            System.err.println("Выход");
            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 должен иметь тип Double или Integer.");
            System.err.println("Exiting");
            return;
        }
        try {
            lat = Double.parseDouble(latitudeStr);
        } catch (Exception e) {
            System.err.println("Широта должна иметь тип Double или Integer.");
            System.err.println("Выход");
            return;
        }
        try {
            lon = Double.parseDouble(longitudeStr);
        } catch (Exception e) {
            System.err.println("Долгота должна иметь тип Double или Integer.");
            System.err.println("Выход");
            return;
        }
        try {
            centre = new Position(lat, lon);
        } catch (InvalidPositionException ipe) {
            System.err.println("Недопустимые координаты широты или долготы.");
            System.err.println("Выход");
            return;
        }
        System.out.println("Генерирование карты... Пожалуйста подождите...");
        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.png'. Выход.");
    }
}

… где .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) и используем для построения полигонов, которые добавляется в контейнер полигонов, когда встречаются отдельные полигоны. Когда вызывается метод рисования объекта, этот контейнер полигонов выполняет итерации, и любые полигоны, попадающие в рамки холста (также называемый viewport или окно просмотра), границы окрашиваются в графическом контекст. По существу, этот алгоритм можно подытожить следующим образом:

Конструктор (файлы):                              Constructor ( files ):
   для каждого файла в файлах                        for each file in files
      для каждой строки в файле                         for each line in file
         если строка == -1                                 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 использует эти изображения для создания внутреннего представления ландшафта (float матрицы).

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

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

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

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

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 (используемый для идентификации узла), списка узлов потомков, пути к карте высот (плитки), которую он представляет, уровня масштабирования (называемого уровнем долготы, поскольку масштаб определяется в минутах долготы на пиксель) и пары широты/долготы, указывающий центр плитки.

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

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

slide1

slide2

Выше: Мы заглянули внутрь узла «Ирландия». Мы можем увидеть здесь карту высот, файл дескриптор и альфа-карту.

/*
 * Чтобы изменить этот шаблон, выберите Сервис | Шаблоны
 * и откройте шаблон в редакторе.
 */
package util.datastructure;

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

/**
 * TileTree(Древо Плиток) обрабатывает хранение и извлечение отдельных
 * карт. Каждому Узлу соответствует одна карта (значение узла является
 * абсолютным путем карты. Этот ID является ID под которым он отображается).
 *
 * Дерево отражает структуру папки приложения карты, причем мир
 * является его корнем(root) а отдельной страной это его потомки. Под-потомки этих узлов 
 * представляют собой «близкую» версию каждой страны / географических подрайонов этих стран.
 *
 * @author Benjamin Jakobus
 * @since 1.0
 * @version 1.0
 */
public class TileTree {

    /* Корневой(root) узел дерева. */
    private Node root;

    /**
     * Создает новый экземпляр TileTree(Древа Плиток). Узлы генерируются
     * в зависимости от содержимого папки с ресурсами.
     *
     * @param resourceDirectory         Корень папки ресурсов приложения
     *                                  (папка ресурсов - это папка, в котором хранятся
     *                                  все карты (Так же известная как
     *                                  Heightmaps)).
     * @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);
    }

    /**
     * Инициализирует потомков дерева. Корневой(root) узел должен быть инициализирован
     * до вызова этого метода.
     *
     * @param resourceDirectory         Корень папки ресурсов приложения
     *                                  (папка ресурсов - это папка, в котором хранятся
     *                                  все карты (Так же известная как
     *                                  Heightmaps)).
     * @param parentNode                Node, узел к которому должны быть присоединены
     *                                  все последующие узлы.
     * @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);
    }

    /**
     * Инициализирует отдельный узел в зависимости от содержимого файла дескриптора (для получения 
     * информации о файлах дескриптора см. документацию по программному обеспечению).
     *
     * @param file                          Файл дескриптор, с помощью которого
     *                                      можно инициализировать содержимое узла.
     * @return                              Новый 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;
    }

    /**
     * Возвращает Node соответствующий данному ID.
     *
     * @param nodeID                ID узла Node который вы
     *                              хотите получить.
     * @return                      Node соответствующий данному ID.
     * @since 1.0
     */
    public Node find(String nodeID) {
        return find(root, nodeID);
    }

    /**
     * Возвращает Node соответствующий данному ID. Этот метод похож
     * на find() за исключением лишь того, что он только начинает
     *  поиск с определенного узла вниз.
     *
     * @param nodeToSearch          Node из которого можно начать поиск.
     * @param nodeID                ID узла Node который вы
     *                              хотите получить.
     * @return                      Node соответствующий данному 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;
    }

    /**
     * Извлекает все узлы в дереве.
     *
     * @return                      Список всех узлов в 
     *                              дереве.
     * @since 1.0
     */
    public List<Node> getNodes() {
        List<Node> nodes = new ArrayList<Node>();
        getNodes(root, nodes);
        return nodes;
    }

    /**
     * Возвращает всех потомков определённого Node.
     *
     * @param node                  Узел чьи потомки вам нужны
     * @param nodes                 Список, в который нужно добавить этих потомков.
     * @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;

/**
 * Отдельный узел в TileTree(Древе Плиток). Каждый Узел
 * представляет собой отдельную плиту(tile) (то есть heightmap + alphamap).
 *
 * @author Benjamin Jakobus
 * @since 1.0
 * @version 1.0
 */
public class Node {
    /* Уникальный идентификатор узлов. */
    private String nodeID;

    /* Путь к ресурсу, который представляет узел (так называемое значение узлов). */
    private String resource;

    /* Потомки узлов. */
    private List<Node> children;

    /* Разрешение (ширина в градусах долготы), представленное этим узлом.
     * то есть разрешение карты, которую представляет узел.
     */
    private double longitudeLevel;

    /* Центр карты (называемой так же плитка(tile)), который представляет узел. */
    private Position centre;

    /**
     * Конструктор.
     *
     * @param nodeID                Уникальный идентификатор узла.
     * @param resource              Путь к ресурсу, который представляет узел
     *                              (также называемый значение узлов).
     * @param longitudeLevel        Разрешение (ширина в градусах долготы),
     *                              представленное этим узлом.
     * @param centre                Центр карты (называемой так же плитка(tile)), который
     *                              представляет узел.
     * @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>();
    }

    /**
     * Возвращает все узлы потомки.
     *
     * @return          Список, содержащий все узлы потомки.
     * @since 1.0
     */
    public List<Node> getChildren() {
        return children;
    }

    /**
     * Возвращает ID узлов.
     *
     * @return          Уникальный идентификатор узла.
     * @since 1.0
     */
    public String getNodeID() {
        return nodeID;
    }

    /**
     * Возвращает путь к плитке(tile), который представляет узел.
     *
     * @return          Путь к плитке(tile), который представляет узел.
     * @since 1.0
     */
    public String getResource() {
        return resource;
    }

    /**
     * Прикрепляет потомка к этому узлу.
     *
     * @param child     Узел для присоединения.
     * @since 1.0
     */
    public void attachChild(Node child) {
        children.add(child);
    }

    /**
     * Возвращает ширину в градусах долготы карты / ресурса, который
     * представляет этот узел.
     *
     * @return          ширину в градусах долготы карты / ресурса,
     *                  которую представляет этот узел.
     * @since 1.0
     */
    public double getLongitudeLevel() {
        return longitudeLevel;
    }

    /**
     * Центральная координата плитки / карты, которую представляет этот узел.
     *
     * @return          Центр карты по широте / долготе.
     * @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 представление координатных линий может быть нарисовано процедурно (в отличие от определения заданных заранее вершин сетки с помощью инструментов 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 {
            // Рассчитать начальные расстояния для меридианов и 
            // параллелей


            // Подходите к построению клеток с севера на юг и с 
            // востока на запад.
            positions[0] = new Vector3f(0, 0, 0);

            // Линии широты для северного полушария
            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);
            }

            // Линии широты для южного полушария
            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);
            // Линии долготы для северного полушария
            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);
            }

            // Линии долготы для южного полушария
            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. Все права сохранены.