Skip to main content

Tile Layer

Type​

Component

Default Usage​

TileLayer projects standard XYZ / slippy-map raster tiles (256 px, Web Mercator, the scheme used by OpenStreetMap, AMap and Tianditu) onto the flat ground plane of the Scene's GeoReference.

  • Pass the tile source as a url template with {z}, {x}, {y} and an optional {s} subdomain placeholder.
  • By default the layer follows the camera: the visible ground rectangle (the camera frustum projected onto the ground plane) is tiled at the given zoom, so the basemap always covers the whole viewport, and tiles are loaded/disposed automatically as you pan, zoom or orbit. A padding ring of extra tiles keeps the map covered while moving.
  • For a fixed basemap instead, pass the geographic corners it must cover as points (with followCamera={false}); every tile intersecting their bounding rectangle is requested once.
  • Tile images load lazily and fade in as they arrive; tiles use a plain MeshBasicMaterial, so the layer works with both the WebGPU and WebGL renderer.
  • A GeoObject anchored at a WGS84 (GPS standard) coordinate drops a pin marker on the map; its onReady callback adds a cone mesh to the internal Group, which is disposed on unmount.

The tile imagery's datum must match the datum prop (and, in static mode, the datum of the coverage points). OSM imagery is indexed in WGS84, so the Scene's default datum is enough.

Every example below can be viewed with either the WebGPU (default) or the WebGL renderer — switch tabs to compare.

import * as THREE from 'three'
import { Scene, TileLayer, GeoObject } from 'react-three-lite'
import type { GeoPoint, SceneComponents } from 'react-three-lite'

// WGS84 (GPS standard) point near Suzhou; used as both the local origin and the pin anchor.
const PIN: GeoPoint = { lng: 120.60682, lat: 31.330898 }
const ORIGIN = PIN

// OpenStreetMap standard raster tiles are indexed in WGS84 coordinates.
const TILE_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
const TILE_ZOOM = 15

// GeoObject creates an internal Group and disposes its meshes on unmount.
const decoratePin = (anchor: THREE.Object3D) => {
const cone = new THREE.Mesh(
new THREE.ConeGeometry(40, 120, 24),
new THREE.MeshBasicMaterial({ color: 0xff3b30 })
)
// Lift the cone so its base sits on the ground.
cone.position.y = 60
anchor.add(cone)
}

function App() {
const handleCreated = (scene: THREE.Scene, components: SceneComponents) => {
const { camera } = components
// A fixed south-side viewpoint keeps the raster map readable like a
// conventional map; looking north from due south keeps it north-up.
scene.setPosition(camera, { x: 0, y: 600, z: 850 })
camera.lookAt(0, 0, 0)
}

return (
<Scene
origin={ORIGIN}
gridHelper={false}
onCreated={handleCreated}
bgColor="rgb(40, 42, 54)"
style={{ marginTop: '10px', width: '100%', height: '300px' }}
>
<TileLayer url={TILE_URL} zoom={TILE_ZOOM} datum="WGS84" />
<GeoObject coordinate={PIN} datum="WGS84" onReady={decoratePin} />
</Scene>
)
}

GCJ02 Imagery​

Chinese map services such as AMap (Gaode) and Tencent publish tiles in GCJ02 coordinates. Set datum="GCJ02" (and, in static points mode, pass coverage points expressed in GCJ02). Tile selection and corner placement then share the same datum, so the non-linear GCJ02 shift is absorbed per tile corner and adjacent tiles stay seamless. The {s} placeholder can be customized with subdomains.

const AMAP_URL =
'https://webrst.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'

<TileLayer url={AMAP_URL} zoom={15} datum="GCJ02" />

Props​

NameTypeDefaultDescription
urlstring—Tile URL template with {z}, {x}, {y} and optional {s}
zoomnumber—Slippy-map zoom level (non-negative integer)
followCamerabooleantrueTile the visible ground rectangle every time the camera moves; when false, points defines a fixed coverage
pointsGeoPoint[]—Static coverage corners in datum, used when followCamera is false
paddingnumber1Extra tile rings loaded beyond the visible rectangle so the map stays covered while panning
datum'WGS84' | 'GCJ02' | 'BD09'Scene datumDatum of the tile imagery (and of points in static mode)
subdomainsstring[]['a','b','c']Values cycling through the {s} placeholder
opacitynumber1Opacity of the tile images
ynumber0.05Height in local metres at which the ground quads are laid
fogbooleantrueDistance fog anchored to the edge of the tiled coverage plus a background-tinted backing plane, so the map dissolves into the background at oblique angles instead of ending in a hard rectangle; the anchor re-fits as the camera zooms or orbits
groundColornumber | stringScene bgColor, else #1a1a2eColour of the distance fog and backing plane; set it to the Scene bgColor when using a background image
maxTilesnumber256Cap on retained tiles; camera-following keeps the nearest ones, static mode throws when exceeded
maxPolarAnglenumberPI/3 (60°)Largest OrbitControls polar angle (radians from +y) allowed while following the camera; the same 60-degree camera-pitch ceiling MapLibre uses on flat maps, past which planar ground tiles stretch into blurred ribbons and the tiled coverage can no longer stay ahead of the horizon. A stricter existing limit is kept and the original value is restored on unmount
onReady(group: THREE.Group) => void—Fired after all tile meshes were created
onTileError(index: TileIndex, url: string) => void—Fired when one tile image fails to download; the tile stays hidden
sceneTHREE.Scenecontext sceneExplicit scene for non-JSX usage

Tile Math Utilities​

The underlying slippy-map functions are exported for custom tile workflows.

NameParametersDescription
TILE_SIZE—Edge length of a standard tile in pixels (256)
lngLatToTile(lng: number, lat: number, zoom: number) => TileIndexTile column/row covering a coordinate; longitude wraps at the antimeridian and latitude is clamped to ±85.05112878
tileToLngLatBounds(index: TileIndex) => TileBoundsWest/north/east/south bounds of a tile in degrees
tileToCorners(index: TileIndex) => TileCornersNW/NE/SE/SW corner coordinates of a tile
selectTiles(points: GeoPoint[], zoom: number) => TileIndex[]Every tile intersecting the bounding rectangle of the points
selectTilesInBounds(west: number, south: number, east: number, north: number, zoom: number, padding?: number) => TileIndex[]Every tile intersecting a west/south/east/north rectangle, expanded by padding rings; antimeridian-safe
tileUrl(template: string, index: TileIndex, subdomains?: string[]) => stringExpands the {s}/{z}/{x}/{y} placeholders

Events​

NameParametersDescription
onReady(group: THREE.Group) => voidFired once after the layer group is added to the scene
onTileError(index: TileIndex, url: string) => voidFired when one tile image fails to download