How to create real-world terrains


First, we need to decide where we get the data (height maps and satellite maps) from. This part took most of the time I worked on this feature. After some extensive searching I chose:

Now, we need to:

  1. let user select the place he wants in GUI
  2. download the data
  3. save them as images

I will not talk about the 3rd point since image saving is an old feature from the editor.

Let user select the place he wants in GUI

I implemented simple Slippy Map in imgui so user can navigate to place he wants to export.

Downloading the data

Terrain Tiles on AWS

To download a heith map from Amazon, we just need to construct the correct web address like this:

  1. start with http://s3.amazonaws.com/elevation-tiles-prod/terrarium/
  2. add zoom level http://s3.amazonaws.com/elevation-tiles-prod/terrarium/4/
  3. add x coordinate http://s3.amazonaws.com/elevation-tiles-prod/terrarium/4/1/
  4. add y coordinate and extension http://s3.amazonaws.com/elevation-tiles-prod/terrarium/4/1/2.png

There is a png image at this address. It contains elevation data in meters, split into RGB value:

height_in_meters = (red * 256 + green + blue / 256) - 32768

XYZ coordinates

Zoom parameter is an integer between 0 and 18. Although higher zoom levels might not be available.
Zoom level N contains 1 << N x 1 << N tiles.
X and Y go from 0 to (1 << N) - 1.
Left edge (X == 0) is 180 degrees west.
Top edge (Y == 0) is 85.0511 degrees north.

Google Statis Maps

Similar to Amazon, we just need to construct the correct web address to get an image from Google:

  1. start with http://maps.googleapis.com/maps/api/staticmap?center=
  2. add latitude and longitude http://maps.googleapis.com/maps/api/staticmap?center=0,0
  3. add zoom http://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=4
  4. add some other params http://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=4&size=256x256&maptype=satellite
  5. finally add key http://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=4&size=256x256&maptype=satellite&key=YOUR_API_KEY

Coordinates

Since Google uses different coordinate system than Amazon, we need to convert from one to another to be able to download matching data. We already know how Amazon’s XYZ data are related to latitude and longitude, so it should not be a problem. Note that latitude and longitude parameters mark center of the tile.

double tilex2long(float x, int z)
{
    return x / pow(2.0, z) * 360.0 - 180;
}
double tiley2lat(float y, int z)
{
    double n = M_PI - 2.0 * M_PI * y / pow(2.0, z);
    return 180.0 / M_PI * atan(0.5 * (exp(n) - exp(-n)));
}

Winsock

Since there is no simple library to do this I decided to use Winsock to download the images.

1. Initialize Winsock

Before we can use socket, we have to initialize Winsock.

#include <WinSock2.h>
#include <Windows.h>
#include <cmath>
#include <cstdlib>
#pragma comment(lib, "wininet.lib")
#pragma comment(lib, "Ws2_32.lib")
    WORD sockVer;
    WSADATA wsaData;
    sockVer = 2 | (2 << 8);
    if (WSAStartup(sockVer, &wsaData) != 0) return error("failed to initialize Winsock");

2. Create socket

    SOCKET socket = ::socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (socket == INVALID_SOCKET) return error("failed to create socket");

3. transform hostname to ip address

    const char* host = "s3.amazonaws.com"
    SOCKADDR_IN sin;
    setMemory(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80);
    hostent* hostname = gethostbyname(host);
    if (!hostname) return false;
    const char* ip = inet_ntoa(**(in_addr**)hostname->h_addr_list);
    sin.sin_addr.s_addr = ip ? ::inet_addr(ip) : INADDR_ANY;

4. connect

    if (connect(socket, (LPSOCKADDR)&sin, sizeof(sin)) != 0)
    {
        closesocket(socket);
        return error("failed to connect");
    }

5. Cleanup

When we are done with the socket, clean up

    closesocket(socket);

and when we are done with the whole application

    WSACleanup();

HTTP

The servers communicate in HTTP. First, we have to send HTTP GET request to the server and then we can receive an answer.

    static bool writeRawString(SOCKET socket, const char* str)
    {
        int len = (int)strlen(str);
        int send = ::send(socket, str, len, 0);
        return send == len;
    }
    writeRawString(socket, "GET ");
    writeRawString(socket, path);
    writeRawString(socket, " HTTP/1.1\r\nHost: ");
    writeRawString(socket, host);
    writeRawString(socket, "\r\nConnection: close\r\n\r\n");

So the request looks like this:

GET /elevation-tiles-prod/terrarium/4/1/2.png HTTP/1.1\r\n
Host: s3.amazonaws.com\r\n
Connection: close\r\n\r\n

After that server should send us its response and we can read it

    Array<u8> data(allocator);
    data.reserve(16 * 1024);
    u8 buf[1024];
    while (int r = ::recv(socket, (char*)buf, sizeof(buf), 0))
    {
        if (r > 0)
        {
            data.resize(data.size() + r);
            copyMemory(&data[data.size() - r], buf, r);
        }
    }

Note: we should handle errors here, but I am not doing it because I am lazy.

Response from the server should contain HTTP header and png image right after the header.

Get Lumix Engine

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.