Загрузка реальных ландшафтов в Unity 3D

  • Tutorial


Введение


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

По этой теме тоже довольно много разного рода публикаций в сети. Однако, многие из них опираются на использование платных приложений или расширений для Unity. Существуют описания и «дешевых» методов, но основная масса их ориентирована на получение так называемых heightmap — черно-белых квадратных изображений местности, где градациями серого определяется относительных уровень высот в данной точке. Существует ряд способов генерации подобных карт высот с использованием например инструментария GDAL. И такой подход не лишен недостатков, связанных с достаточной громоздкостью процедуры создания карты высот и последующей привязкой к полученной местности. Поэтому, в данной статье будет изложен некий альтернативный подход и интересующиеся приглашаются под кат.

1. Добыча геоданных


Тут всё уже давно изобретено до нас, за что следует благодарить NASA, выполнивших в свое время программу SRTM (Shuttle Radar Topography Mission) по радиолокационному картонированию земной поверхности. Данные, полученные в ходе миссии находятся в открытом доступе вот здесь. Идем по этой ссылке и наблюдаем наш родной «шарик» поделенный на зоны.



Для демонстрации описываемой методики выберем район «крыши мира», гору Эверест. Для этого щелкаем по нужному куску карты и жмем кнопку «Download».



Распаковываем скачанный архив, и находим там среди прочего файл srtm_54_07.tif в формате GeoTIFF. GeoTIFF — открытый стандарт хранения картографической информации. Анализируя данный файл можно получить данные по высотам точек земной поверхности. Так как формат открытый, организовать непосредственную работу с ним, скажем из Unity будет несложно, но мы, пока, для упрощения, пойдем более коротким путем и воспользуемся маленькой но полезной программкой 3DEM. Это софтинка бесплатна и легко качается с официального сайта. Качаем её и открываем в ней добытый нами GeoTIFF района Эвереста. При открытии нас попросят выбрать формат геоданных, выбираем GeoTIFF:


После чего выбираем вышеупомянутый файл и видим такую картину:



Показанный район довольно велик. Прямоугольником по центру показан интересующий нас район горы Эверест. Чтобы вырезать нужный кусок жмем F8, выбираем интересующую нас область и жмем Enter. Нам предложат уточнить границы участка по широте и долготе.


Уточняем если нужно и нажав ОК получим интересующий нас район на экране:



Если надо, повторяем эту процедуру. В конце концов мы выберем нужный район, а затем идем в главное меню по пути File → Save Terrain Matrix. Нам предложат выбрать формат сохраняемых данных:


Выбираем бинарный формат float, так как с ним будет меньше возни. Выбираем далее путь и имя сохраняемых файлов. В качестве выхлопа мы получим два файла

  • everest.hdr — текстовый файл с описанием сохраненного участка и формата хранения данных
  • everest.bin — сами данные, которые есть просто двухмерный массив высот в формате с плавающей запятой одинарной точности

3. Описание формата хранения геоданных

Его нетрудно получить из pdf-документа, идущего вместе с 3DEM. Главная информация находится в *.hdr-файле и выглядит она так

file_title             = everest
data_format            = float32
map_projection         = Lat/Lon
left_map_x             = 86.859001
lower_map_y            = 27.899000
right_map_x            = 87.014999
upper_map_y            = 28.046000
number_of_rows         = 177
number_of_columns      = 188
elev_m_unit            = meters
elev_m_minimum         = 4574
elev_m_maximum         = 8794
elev_m_missing_flag    = -9999

Опишу самые важные для нашей задачи поля

  • file_title — имя *.bin-файла с данными
  • data_format — формат представления данных
  • left_map_x, lower_map_y — долгота и широта левого нижнего угла участка (считаем что верх этого условного «прямоугольника» смотрит на север), в градусах
  • right_map_x, upper_map_y — долгота и широта правого верхнего угла участка, в градусах
  • number_of_rows — число строк в матрице данных. Каждая строка описывает высоты на фиксированной широте из указанного координатами углов диапазона с равным шагом.
  • number_of_columns — число столбцов в матрице данных
  • elev_m_unit — единицы измерения высот
  • elev_m_minimum — минимальная высота над уровнем референсного эллипсоида (для простоты — уровнем моря) на данном участке
  • elev_m_maximum — максимальная высота над уровнем референсного эллипсоида

Таким образом, становится понятно как читать бинарный файл — он просто поток float'ов, записанных в файл построчно. Эта матрица, в нашем случае хранит высоты в 177 х 188 = 33276 точках на выбранном участке, ограничения по долготе и широте для которого нам известны. Так же нам известен и диапазон высот на данном участке.

3. Хранение данных о высотах в Unity


Класс Terrain в Unity имеет публичное поле terrainData, через которое можно получить доступ к следующим полям

  • heightmapResolution — разрешение карты высот. Оно кратно степени двойки, выбирается из ряда 128, 256, 512… с добавлением единицы. По умолчанию оно равно 513 х 513
  • size — реальный размер террейна в условных единицах Unity. Это поле имеет тип Vector3, и задает размер террейна сразу по всем осям.
  • SetHeights(int xBase, int yBase, float[,] heights) — метод для загрузки данных карты высот. xBase, yBase — индексы, начиная с которых карта высот читается движком из массива heights.

Карта высот — квадратный массив вполне определенного размера. Матрица геоданных — массив прямоугольный. Вычитать её из файла дело техники, и подобный код настолько прост, что специально приводить его не имеет смысла. Главная сложность, преобразовать данные GeoTIFF к карте высот, удовлетворяющей следующим требованиям

  • квадратная матрица, размером кратным степени двойки + 1
  • высоты, хранимые в матрице, нормированы в диапазоне от 0.0 до 1.0

4. Преобразование геоданных в карту высот


Нормировать высоты достаточно просто:

level_{\,i,j} = \frac{h_{\,i,j} - h_{\min}}{h_{\max} - h_{\min}}


где level[i,j] — нормированная высота; h[i,j] — высота в точке (i,j); h_min, h_max — минимальная и максимальная высота на участке. Для приведения размера матрицы к выставленным требованиям нужна аппроксимация высоты в зависимости от координаты. Воспользуемся простейшей линейной аппроксимацией. Пусть высота есть функция координат точки на участке h = h(x,\, y). Тогда, выберем базовую точку (x_i,\, y_j), совпадающую с одним из узлом заданной координатной сетки и разложим эту функцию в ряд Тейлора

h(x, \, y) \approx h(x_i,\, y_j) + \frac{\partial h}{\partial x}\,(x - x_i) +  \frac{\partial h}{\partial y}\,(y - y_j)


При кажущейся сложности, формула легко реализуется кодом на C#

 float getHeight(float x, float z)
{
    float height = 0.0f;

    // Вычисляем шаг по координатам
    float dx = terrain_data.x_range / (terrain_data.numder_of_rows - 1);
    float dz = terrain_data.z_range / (terrain_data.number_of_columns - 1);

    // Определяем базовую точку
    int i = (int) (x / dx);
    int j = (int) (z / dz);

    // Проверяем индексы на предмет выхода за размер массива данных
    if ((i >= terrain_data.numder_of_rows) || (j >= terrain_data.number_of_columns))
        return 0.0f;

    // Вычисляем частные производные в точке (i, j)
    float dydx = (terrain_data.normalize_data[i + 1, j] - terrain_data.normalize_data[i, j]) / dx;
    float dydz = (terrain_data.normalize_data[i, j + 1] - terrain_data.normalize_data[i, j]) / dz;

    // Вычисляем высоту
    height = terrain_data.normalize_data[i, j] + dydx * (x - i * dx) + dydz * (z - j * dz);

    return height;
}

Пользуясь этой функцией легко формируем матрицу для карты высот

terrain_data.height_map = new float[resolution, resolution];

float dx = terrain_data.x_range / resolution - 1;
float dz = terrain_data.z_range / resolution - 1;
                
for (int i = 0; i < resolution; i++)
{
       for (int j = 0; j < resolution; j++)
       {
             terrain_data.height_map[i, j] = getHeight(i * dx, j * dz);
       }
}

5. Плагин для редактора Unity


Чтобы облегчить жизнь мной был написан небольшой плагин для редактора движка UnityGeoDataLoader, позволяющий решать поставленную задачу. Этот продукт и его исходный код распространяются по лицензии GNU GPL v2.0. По указанной ссылке можно получить и то и другое. Там же есть и инструкция по использованию данного инструмента.

Плагин встраивается в редактор и добавляем в главное меню пункт «Tools -> Load GeoData». Выбрав террейн в инспекторе, жмем этот пункт меню, указываем путь к файлу *.hdr (рядом с которым лежит и файл *.bin с данными) и вуаля — лицезреем гору Эверест в редакторе Unity

image



Дальше с этим террейном делаем всё что душе угодно — текстурируем, применяем шейдеры, расставляем другие объекты. Это уж на ваше усмотрение.

Заключение


Покритикую сам себя. Нехорошо, при открытости стандарта GeoTIFF использовать сторонний инструмент для добычи геоданных. По-хорошему надо весь код поместить в плагин, добавить возможности выбора региона по координатам и много чего ещё. Но предела совершенству нет, главное, что задача загрузки в движок реального ландшафта имеет вполне приемлемое и простое решение.

Благодарю за внимание!
Поделиться публикацией

Комментарии 20

    0

    Эх, главная проблема открытых данных по ландшафтам – их низкое разрешение...

      0
      А зачем нужно разрешение, если нужна только карта высот, которая как я понял — вполне достоверна?
        0

        Я говорю про разрешение карты высот. Она достоверна, но у неё низкое разрешение.

          0
          Это смотря о какой местности идет речь.
          Данные близкого к глобальному покрытия — SRTM (доступна, в том числе, версия с разрешением 1 угловая секунда, т.е. около 30 метров на точку), ASTER GDEM — тоже 30 метров на точку (эта модель получена обработкой стереосъемки), AW3D30 — набор данных от космического агентства Японии, тоже 30 метров, но эта модель получена огрублением более подробной (коммерческой) модели.
          А национальные наборы данных бывают куда подробнее, скажем — NED на територию США имеет разрешение от 10 до 1 метра на пиксель.
        0
        Вторая не менее проблема — они до 60-й широты. У нас полстраны севернее, если не 2/3…
          +1
          Модель ASTER GDEM этого недостатка не имеет. https://asterweb.jpl.nasa.gov/gdem.asp
          +1
          В Outerra эту проблему решили процедурной генерацией деталей ландшафта поверх реальных данных. Реальных данных, это, конечно, не заменит, но выглядит неплохо.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Эх, опередили меня, тоже хотел про рельеф из SRTM написать, только с реализацией на WebGL.
              0
              тоже хотел про рельеф из SRTM написать, только с реализацией на WebGL

              я думаю, что польза от такой статьи будет независимо от того, когда она будет опубликована)
              0
              Существуют описания и «дешевых» методов, но основная масса их ориентирована на получение так называемых heightmap — черно-белых квадратных изображений местности, где градациями серого определяется относительных уровень высот в данной точке.

              Пардон, разве не про это была статья?
                0
                не про это

                Я имел ввиду реализацию цепочки
                geotiff -> png -> raw -> terrain
                которую можно провернуть используя gdal + photoshop/gimp

                В статье же цепочка
                geotiff -> bin -> terrain
                без преобразования в промежуточные растровые форматы и без потери информации из geotiff

                P.S.: И GeoTIFF совсем не карта высот в том смысле, в котором она применяется в юнити при использовании генерации из raw
                0
                Надо на это накладывать текстурки со спутников. Как отправную точку для текстурки, шейдеров и т.п. Или это плохая идея?
                  0
                  Определенная проблема с таким подходом — то, что с данными вы работаете в так называемой географической проекции, а в ней форма объектов достаточно сильно искажена — они тем сильнее растянуты вдоль параллелей (линий широты), чем дальше место от экватора.

                  Это решается либо перепроецированием данных в какую-либо прямоугольную систему координат, либо, если площадь сильно ограничена (сотня километров с юга на север), введением простого множителя для сжатия карты вдоль параллелей — косинуса широты, центральной для рассматриваемого фрагмента.
                    0
                    Интересная статья, сам много работал с данными SRTM, правда в целях картографии. Сам пробовал делать визуализацию на UnrealEngine, но все не хватает времени разобраться как GeoTIFF нормально в сцену добавить.
                    Ещё было бы интересно почитать что-то подобное про использование карты высот полученной с аэрофото )

                    SRTM хорош для рендеринга обзорных сцен больших территорий, когда требуется «крупный» план нужно уже искать более детальные данные, для каких-то участков можно найти данные по аэрофото, иногда даже карты высот по данным лидаров.
                    Еще отдельная не тривиальная задача обеспечить плавный переход от SRTM к более точным данным — низкое разрешение SRTM может привести к тому, что перепад высот на границах может достигать 80-90 метров. Самое простое решение максимально подогнать высоту более точных данных к менее точным и не допускать появления перехода в одном кадре, или маскировать его какими-то объектами.
                      0
                      В растровых ГИС вопрос перехода решается множеством разных способов, от интерполяции в кольцевом буфере вдоль границ, до всякой достаточно примитивной растровой арифметики.
                        0
                        Не уверен, что такие подходы без проблем транслируются в 3d моделирование. В том плане, что у нас получится один большой mesh с большой площадью в низком разрешении и маленькой в большом, что вероятно негативно скажется на скорости отрисовки. Сугубо ИМХО, опыт моей работы с игровыми движками стремится к нулю )
                        Хотя думаю, что ничего не мешает сначала обработать растр а потом вырезать из него фрагмент уже подогнанный по высоте
                          0
                          А зачем это туда транслировать, если действительно, можно обработать растр сначала? Но это, конечно, зависит от конкретной задачи.
                      0
                      На terrain.party можно сразу взять карту высот выделенной области. Останется только сконвертировать PNG > RAW и импортировать в Terrain.
                        0
                        terrain.party

                        Да, довольно удобно. Но есть недостатки:

                        1. Нельзя точно указать границы выбранного участка, а значит к загруженному рельефу не привязаться (что мне требуется) — на выхлопе только png heightmap
                        2. На равнинных участках получается абсолютно черный png, а метод предлагаемый в статье для тех же участков дает вполне точные перепады высот

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое