Rocky
3D Terrain Renderer (C++17)
Install / Use
/learn @pelicanmapping/RockyREADME
Rocky is a C++ SDK for rendering maps and globes. <img align="right" width="200" alt="Screenshot 2023-02-22 124318" src="https://user-images.githubusercontent.com/326618/220712284-8a17d87a-431f-4966-a425-0f2628b23b40.png">
Rocky will render an accurate 3D or 2D map with real geospatial imagery and elevation data. It supports thousands of map projections and many popular geodata sources including GeoTIFF, TMS, OpenStreetMap, WMTS, WMS, and Azure Maps. Rocky's data model is inspired by the osgEarth SDK, a 3D GIS toolkit created in 2008 and still in wide use today.
<br/><br/>
Setup
Build the SDK
Rocky uses CMake, and we maintain the build on Windows and Linux.
Rocky comes with a handy Windows batch file to automatically configure the project using vcpkg:
bootstrap-vcpkg.bat
That will download and build all the dependencies (takes a while) and generate your CMake project and Visual Studio solution file.
If you would rather not use vcpkg, you can build and install the dependencies yourself, or use your favorite package manager (like apt on Linux). We also provide a bootstrap-vcpkg.sh script for Linux.
Note: Rocky requires Vulkan SDK 1.3.268 or newer.
Note: Rocky requires ImGui version 1.92 or newer to get full dynamic font support.
Run the Demo
Rocky is pretty good at finding its data files, but if you run into trouble, you might need to set a couple environment variables to help:
set ROCKY_FILE_PATH=%rocky_install_dir%/share/rocky
set PROJ_DATA=%proj_install_dir%/share/proj
If you built with vcpkg you will also need to add the dependencies folder to your path; this will normally be found in vcpkg_installed/x64-windows (or whatever platform you are using).
Now we're ready:
rocky_demo
<img width="500" alt="Screenshot 2023-02-22 124318" src="https://user-images.githubusercontent.com/326618/236200807-73567789-a5a3-46d5-a98d-e9c1f24a0f62.png">
There are some JSON map files in the data folder. Load one with the --map option:
rocky_demo --map data\openstreetmap.map.json
<img width="500" alt="Screenshot 2023-02-22 124318" src="https://github.com/user-attachments/assets/9590cca6-a170-4418-8588-1ee1d2b72924">
<br/><br/>
Hello World
The easiest way to write a turnkey Rocky app is to use the rocky::Application object. It will create a viewer, a default map, and a scene graph to store everything you want to visualize.
main.cpp
#include <rocky/rocky.h>
int main(int argc, char** argv)
{
rocky::Application app(argc, argv);
auto imagery = rocky::TMSImageLayer::create();
imagery->uri = "https://readymap.org/readymap/tiles/1.0.0/7/";
app.mapNode->map->add(imagery);
return app.run();
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(myApp VERSION 0.1.0 LANGUAGES CXX C)
find_package(rocky CONFIG REQUIRED)
add_executable(myApp main.cpp)
target_link_libraries(myApp PRIVATE rocky::rocky)
install(TARGETS myApp RUNTIME DESTINATION bin)
<br/><br/>
Maps
To render a map the first thing we need is map data. Map data can be huge and usually will not fit into the application's memory all at once. To solve that problem the standard approach is to process the source data into a hierarchy of map tiles called a tile pyramid.
Rocky supports a number of different map data layers that will read either tile pyramids from the network, or straight raster data from your local disk.
Imagery
Image layers display the visible colors of the map. It might be satellite or aerial imagery, or it might be a rasterized cartographic map. Here are some ways to load image layers into Rocky.
Loading Imagery from the Network
Let's start with a simple TMS (OSGeo Tile Map Service) layer:
#include <rocky/rocky.h>
using namespace rocky;
...
// The map data model lives in our Application object.
Application app;
auto& map = app.mapNode->map;
...
// A TMS layer can use the popular TMS specification:
auto tms = TMSImageLayer::create();
tms->uri = "https://readymap.org/readymap/tiles/1.0.0/7";
map->add(tms);
We can also use the TMSImageLayer to load generic "XYZ" tile pyramids from the network. In this example, we are loading OpenStreetMap data.
// This will load the rasterizered OpenStreetMap data.
// We comply with the TOS by including attribution too.
auto osm = TMSImageLayer::create();
osm->uri = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
osm->attribution = rocky::Hyperlink{ "\u00a9 OpenStreetMap contributors", "https://openstreetmap.org/copyright" };
// This data source doesn't report any metadata, so we need to tell Rocky
// about its tiling profile. Most online data sources use "spherical-meractor".
osm->profile = rocky::Profile("spherical-mercator");
map->add(osm);
Loading a Local File
You can load imagery datasets from your local disk as well. To do so we will use the GDALImageLayer. This layer is based on the GDAL toolkit which supports a vast array of raster formats.
auto local = GDALImageLayer::create();
local->uri = "data/world.tif"; // a local GeoTIFF file
map->add(local);
You can add as many image layers as you want - Rocky will composite them together at runtime.
Terrain Elevation
Terrain elevation adds 3D heightmap data to your map.
Rocky supports elevation grid data in three formats:
- Single-channel TIFF (32-bit float)
- Mapbox-encoded PNG
- Terrarium-encoded PNG
auto elevation = TMSElevationLayer::create();
elevation->uri = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
elevation->encoding = ElevationLayer::Encoding::TerrariumRGB;
map->add(elevation);
Elevation Queries
It is often useful to query the elevation at a given location. There are two ways to do it.
Intersect the scene graph
This method is fast, but it limited to whatever terrain triangles are currently in memory. Thus the result will depend on where your camera is and how much data is loaded.
GeoPoint point(SRS::WGS84, longitude, latitude);
auto clampedPoint = mapNode->terrain->intersect(point);
if (clampedPoint.ok())
float elevation = clampedPoint->transform(SRS::WGS84).z;
Query the source data
This method is the most accurate, and does not depend on what data is currently loaded in the scene since it goes straight to the source layer. But it's usually slower.
ElevationSampler sampler;
sampler.layer = elevationLayerFromMap;
Result<ElevationSample> result = sampler.sample(point, app.io());
if (result.ok())
Log()->info("Result = {}m", result.value());
// or, return a clamped GeoPoint:
Result<GeoPoint> clampedPoint = sampler.clamp(point, app.io());
You can also batch queries to the ElevationSampler, which speeds things up when querying a localized area. A batched query session modifies the input data directly. It expects the SRS of the points you plan to sample. You also get optional control over the sampling resolution ... lower resolution is often faster.
ElevationSampler sampler;
sampler.layer = ...;
auto session = sampler.session(app.io());
session.srs = pointsSRS; // required!
session.resolution = Distance(100, Units::METERS);
session.clampRange(points.begin(), points.end());
We use this technique in the Demo_MVTFeatures.h example to clamp GIS features to the terrain.
Vector Features
Rocky include some facilities for loading GIS Feature data through GDAL. GDAL has many drivers to load different types of feature data.
Open an ESRI Shapefile and iterate over all its features:
#include <rocky/GDALFeatureSource.h>
...
auto fs = rocky::GDALFeatureSource::create();
fs->uri = "my_vectors.shp";
auto status = fs->open();
if (status.ok())
{
fs.each(app.io(), [](Feature&& feature)
{
// ... each Feature object contains geometry and fields
});
}
FeatureView will help turn features into visible geometry:
FeatureView feature_view;
// add features to our FeatureView helper:
fs.each(app.io(), [&](Feature&& feature)
{
feature_view.features.emplace_back(std::move(feature));
});
// Generate primitives (lines and meshes) in the world coordinate system:
auto prims = feature_view.generate(app.mapNode->worldSRS());
// Put our new primitives into the ECS registry:
if (!prims.empty())
{
app.registry.write([&](entt::registry& registry)
{
auto e = prims.createEntity(registry);
entities.emplace_back(e);
});
}
Worldwide data? Check out the Mapbox Vector Tiles demo.
<br/><br/>
