Skip to content

Configuration

Ziv Perry edited this page May 18, 2026 · 10 revisions

raytiles is configured through four plain structs that you pass to the streamer at construction:

All four structs ship with sensible defaults; you only need to override the fields that matter to your scene.


World Configuration

World topology and geometry parameters. Everything in this struct is effectively immutable once a streamer exists: changing any field requires rebuilding meshes, re-uploading textures, or re-anchoring the world. Set it once at construction.

Anchor

World-space anchor expressed in tile coordinates at base_zoom. The streamer translates tile XY to world XZ relative to this anchor, so the world origin ends up wherever you want it (e.g. at your runway).

raytiles uses raylib's coordinate system, where the XZ plane is the world surface.

int anchor_x_tile = 306;
int anchor_z_tile = 207;

The following services can help you pick an anchor. Select an area at the right zoom level (9 is the default) and copy the values into the configuration as shown above:

The (X, Y, Z) format reported by those tools maps to:

$zoom = Z,\ x = X,\ z = Y$

// default zoom is 9 (Z)
int anchor_x_tile = X;
int anchor_z_tile = Y;

Base zoom

Lowest level-of-detail zoom that will ever be loaded. Tiles outside the camera's near radius are kept at this zoom to bound the working set.

int base_zoom = 9;

Changing this value also requires updating streaming_config::thresholds and base_zoom_tile_size.

Note: the library has never been tested with a base_zoom lower than 9.

Max zoom

Highest level-of-detail zoom available. Tiles directly under the camera are subdivided up to this zoom.

int max_zoom = 15;

Changing this value also requires updating streaming_config::thresholds. 15 is the maximum zoom currently supported.

Base tile size

World size, in meters, of one tile at base_zoom. Tiles at higher zooms are scaled by 1 / (1 << (zoom - base_zoom)).

float base_zoom_tile_size = 66400.0f;

Why this value is latitude-dependent

raytiles consumes XYZ (slippy-map) tiles, which are produced in the Web Mercator projection. In Web Mercator, the ground distance covered by a single tile changes with latitude: tiles near the equator cover much more ground than tiles near the poles, even at the same zoom level. There is no single "correct" tile size for the whole planet — you pick the size that matches the latitude band your scene lives in.

The width (and height) of one tile, in meters on the ground, at zoom $z$ and latitude $\varphi$ is:

$$Tile_{size}(z, \varphi) = \frac{2\pi \cdot R \cdot \cos(\varphi)}{2^{z}}$$

where:

  • $R = 6{,}378{,}137\ \text{m}$ is the Earth's equatorial radius (WGS-84).
  • $\varphi$ is the latitude of your anchor, in radians.
  • $z$ is the zoom level.

The default 66400.0f corresponds to zoom 9 at roughly 30°–32° latitude (e.g. North Africa, the southern US, parts of the Middle East). If your scene sits at a very different latitude, you should recompute this value or your terrain will be horizontally stretched or squashed.

Examples at zoom 9:

Latitude base_zoom_tile_size
~78,272 m
30° ~67,786 m
45° ~55,346 m
60° ~39,136 m

Note: this is also why changing base_zoom requires updating base_zoom_tile_size — the meters-per-tile term scales by $2^{\Delta z}$.

Skirt overlap

Per-zoom skirt overlap factors used to hide cracks between neighboring tiles at different LODs. The factor scales the geometric skirt baked into the tile mesh at that zoom — a value of 1.0 keeps the default overlap, larger values widen the skirt (more reliable crack-hiding, more fill rate), and 0.0 disables it for that zoom. Must cover every zoom in [base_zoom, max_zoom]; missing entries cause the streamer to throw at construction. Baked into generated meshes.

std::unordered_map<Zoom, float> skirt_overlap = {
    {9,  1.0f},
    {10, 1.0f},
    {11, 1.0f},
    {12, 1.0f},
    {13, 1.0f},
    {14, 1.0f},
    {15, 1.0f},
};

Note: the vertical drop of the skirt geometry is configured separately via rendering_config::skirt_drop — it's a shader uniform, not baked into the mesh.

Mipmaps

Generate trilinear / anisotropic mipmaps for the albedo texture on upload. Strongly recommended — it avoids shimmering at distance.

bool use_mipmap = true;

Logger

Whether to emit log lines from the streamer. Logs from the main thread / process are routed through raylib's TraceLog.

bool use_logger = false;

Streaming Configuration

Tile-streaming parameters. Governs which tiles are kept resident and how aggressively the working set is updated. Safe to tweak at runtime (no mesh / texture rebuild needed), but most users set it once.

Rendering radius

Radius, expressed in world_config::base_zoom tiles, of the disc of tiles loaded around the camera. Larger values mean more tiles in flight, and therefore more memory and bandwidth.

int rendering_radius = 6;

To translate to meters on the ground, multiply by base_zoom_tile_size. With the defaults (6 tiles × 66400 m), the streamer keeps tiles resident out to roughly 400 km from the camera.

Zoom thresholds

Per-zoom distance thresholds covering world_config::base_zoom through world_config::max_zoom. For each zoom level, the map gives the maximum camera distance (in meters) at which tiles of that zoom may be used. The streamer picks the highest zoom whose threshold still includes the tile, so closer tiles get more detail and far tiles fall back to base_zoom.

std::unordered_map<Zoom, Meters> thresholds = {
    {9,  100000.0f},
    {10,  80000.0f},
    {11,  40000.0f},
    {12,  20000.0f},
    {13,  10000.0f},
    {14,   5000.0f},
    {15,   2500.0f},
};

The defaults are tuned for performance and to keep the resident tile count under 600. If the zoom range changes, this map must be updated to match.

Update distance

Squared XZ distance the camera must travel before the desired-tile set is recomputed. The value is squared so the streamer can compare against dx*dx + dz*dz without ever calling sqrt. The default of 1000.0f * 1000.0f means recompute after moving 1 km. Keep this large enough that small movements don't churn the working set.

MetersSq update_distance_sq = 1000.0f * 1000.0f;

Update height

Altitude delta, in meters, that triggers a desired-set recomputation independent of update_distance_sq. Lets you stream new LODs as you climb or descend without horizontal motion.

Meters update_height = 500.0f;

Upload budget

Wall-clock budget, in seconds, per frame for promoting downloaded tiles into GPU resources. Caps the cost of a single bursty frame.

double upload_budget_sec = 0.002;

Max uploads per frame

Hard cap on tile promotions per frame, applied on top of upload_budget_sec. Whichever limit is hit first stops the loop.

int max_uploads_per_frame = 8;

Rendering Configuration

Rendering and shader-uniform parameters. Every field here is genuinely runtime-mutable; most have matching streamer::set_* setters that push new values to the shader on the next update().

Near plane

Near clip plane, in meters, used by the displacement shader for fog and depth-precision tuning. Match this to your camera setup.

MetersD near_plane = 1;

Far plane

Far clip plane, in meters, used by the displacement shader for fog and depth-precision tuning. Match this to your camera setup.

MetersD far_plane = 400000;

Fog start

Distance, in meters, at which atmospheric fog starts fading tiles toward fog_color.

Meters fog_start = 100000.0f;

Fog end

Distance, in meters, at which fog reaches full coverage.

Meters fog_end = 150000.0f;

Skirt drop

Vertical drop, in meters, of the skirt geometry below each tile's edge. Larger values hide cracks more reliably between neighboring tiles at different LODs, but cost more fill rate. Applied as a shader uniform, so it can be tweaked at runtime. Set to 0.0f to disable the vertical drop entirely.

Meters skirt_drop = 0.0f;

Fog color

Fog color (RGBA, 0..1). Match this to your sky color for a seamless horizon.

float fog_color[4] = {0.0f, 0.0f, 1.0f, 1.0f};

Ambient light

World ambient color (RGBA, 0..1). Drives day / night / weather lighting changes.

float ambient_light[4] = {1.0f, 1.0f, 1.0f, 1.0f};

Sun direction

Sun direction vector. The shader normalizes it internally, so the magnitude is irrelevant.

float sun_direction[3] = {0.1f, 1.0f, 0.1f};

Sun scale

Sun lighting intensity. Controls the contrast between lit and shaded areas.

float sun_scale = 1.0f;

Height scale

Scales the heightmap to exaggerate or flatten the terrain relief (drama factor).

float height_scale = 1.0f;

Normals scale

Scales the normals to increase or reduce lighting contrast. Higher values make the terrain look bumpier, but can cause lighting artifacts if the normals become too steep.

float normals_scale = 1.0f;

Pool Configuration

Background tile-fetching parameters: download workers, on-disk cache layout, and the tile provider URLs.

Download threads

Number of background download workers. Downloads are I/O-bound, so it is safe to use more threads than CPU cores; 2 is a reasonable starting point for HTTP keep-alive against a single host.

int download_threads = 4;

Insecure TLS

Skip TLS certificate verification for tile downloads. Only useful for local proxies — never enable this against a real server.

bool allow_insecure_tls = false;

Logger

Whether the pool's worker threads emit log lines.

bool use_logger = false;

Cache paths

On-disk cache path templates, formatted with {zoom}/{x}/{z} via std::vformat. Parent directories are created on demand.

std::string texture_cache_path   = "assets/texture/{}/{}/{}.png";
std::string heightmap_cache_path = "assets/heightmap/{}/{}/{}.png";
std::string normals_cache_path   = "assets/normals/{}/{}/{}.png";

Provider URLs

URL templates for the three tile streams: satellite imagery (texture_url), heightmaps (heightmap_url), and surface normals (normals_url). The placeholders :zoom:, :x:, and :y: are substituted with the tile coordinates at request time (plus any optional token baked into the template). Any provider that follows the XYZ (slippy-map) convention works, as long as the heightmap provider returns RGB-encoded heightmaps.

The defaults point at public providers — Esri World Imagery for textures and AWS Terrain Tiles for heightmaps and normals — so an unconfigured pool_config already produces a working world:

// the order zoom/y/x is not a mistake — that's how Esri encodes their URLs
std::string texture_url   = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/:zoom:/:y:/:x:";
std::string heightmap_url = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/:zoom:/:x:/:y:.png";
std::string normals_url   = "https://s3.amazonaws.com/elevation-tiles-prod/normal/:zoom:/:x:/:y:.png";

At compile time the defaults can be overridden via the RAYTILES_TEXTURE_URL, RAYTILES_HEIGHTMAP_URL, and RAYTILES_NORMALS_URL preprocessor macros.

Note: pool_config also exposes texture_host / texture_url_path (and the matching heightmap_* / normals_* pairs). You don't set these yourself — the pool splits each *_url into host and path at construction so the HTTP client can keep one connection per host alive. Set the *_url fields and leave the rest empty.

See Working with Mapbox for a concrete provider setup.

Clone this wiki locally