An OGC 3D Tiles engine and server in Rust — and, built on it, a globe of terrain draped with imagery that can be flown interactively or rendered into a film, frame-exact, on a laptop, in a browser, or on a farm of GPUs.
The 3D Tiles world has tilers that produce static files and clients tied to their own ecosystems. What it lacks is the part in the middle: a small, dynamic server with a cache it does not own, and a rendering engine that is not married to one renderer. Tuile is that part.
Everything is one Cargo workspace. The core knows nothing of GPUs, sockets or runtimes; everything that draws, fetches or stores plugs into it through traits.
Status: pre-1.0. The interfaces move. Version numbers are cut when a consumer needs one, not when an API is declared stable.
A render-agnostic core. tuile-core parses tilesets, traverses them by
screen-space error, schedules and decodes content, and hands geometry out as a
logical geometry server: a stream of decoded tiles for whoever draws them. It
compiles to wasm32-unknown-unknown and depends on no backend.
A globe. Quantized-mesh terrain (tuile-terrain) crossed with raster
imagery (tuile-planetary), with all geospatial arithmetic in f64 until the
switch to a local frame — raw Earth-centred coordinates never reach f32.
Renderers. tuile-wgpu is the reference backend, native and WebGPU, and
the engine also draws through OpenUSD/Hydra. Both are described below.
Films, packs, a tile store and its server. A flight is baked once into packs, the source tiles are kept once in a store of PMTiles archives, and both are served by the same small API. Described below.
The primary mode of the engine is streaming: a camera goes in every frame,
decoded tiles come out as they are ready, and the picture refines as you orbit
and zoom. The trait that says so, GeometryStream, lives in the core; a viewer
is whatever holds the other end of it.
- Native.
tuile-wgpu-vieweris a window on the geometry server running in-process: the server traverses, fetches and decodes on a background runtime, the window sends its camera and pumps tiles to the GPU as they arrive. A coarser tile is drawn until its replacement is resident, so the ground refines and never blinks out. - In a browser. The same application:
tuile-viewer's web shell draws on a page's canvas with the sametuile-wgpurenderer — WebGPU, or WebGL2 where there is none — while the same geometry server, traversal and planetary loader run in a Web Worker, its messages crossingpostMessagethroughtuile-web. It ships as an ES module package (packages/tuile-viewer);examples/web-vieweris a page that imports it. - The camera is its own crate.
tuile-camerais a render-agnostic globe camera and input controller — drag anchored on the ellipsoid, zoom by altitude, tilt — that produces the view states the core consumes;tuile-uiadds the on-screen compass, tilt and zoom controls;tuile-atmospheregives the sun, the sky and the aerial perspective. - A flight is repeatable.
tuile-taperecords the camera path of an interactive session and flies it again exactly — the bridge from the interactive side to films: what you flew is what gets baked and rendered.
tuile-orbit-probe flies a headless orbit and measures what loads, what is
dropped and what comes back; tuile-warm-cache fills the tile store with the
coarse pyramid before a flight.
Tuile meets OpenUSD in two separate ways, and keeps them separate.
Writing USD, in pure Rust. tuile-usd writes a manifest stage as plain
.usda text, with no OpenUSD binding: a recorded camera path as an animated
camera, plus one Globe prim that stands for the whole planet. The stage is a
few kilobytes and carries no geometry — it says where the camera is and that
there is a globe; the tiles are streamed when it is rendered.
Rendering inside OpenUSD. tuile-hydra is a C ABI over the geometry
server, one frame at a time, with buffers laid out the way Hydra reads them;
integrations/hydra is the C++ plugin that consumes it.
The plugin is a Hydra 2.0 scene index behind a generative procedural: drop the
Globe prim into any stage and it resolves the stage's camera, asks the
geometry server for that view's tiles, and hands terrain and imagery to
whichever Hydra renderer is drawing — a rasteriser or a path tracer, with
nothing in the design depending on any one of them. That is how a flight
recorded interactively is rendered on a farm with a path tracer's light.
The work itself — choosing a frame's tiles — is tuile-bake's, which knows
nothing of OpenUSD: the same selection feeds the packs of the wgpu film
renderer and the Hydra plugin, so the two pictures are of the same ground.
The images and job scripts that run the plugin on a farm are under
integrations/; the design and its trajectory are in
docs/13-crate-usd.md,
docs/14-crate-hydra.md and
docs/15-usd-scene-index.md.
Rendering a flight from live sources puts a network, a token and a
convergence race on the critical path of every frame, and makes each renderer
pay the globe from cold. So the work moves: a camera path is recorded once
(tuile-tape), baked once (tuile-bake) on a machine that has the time,
and rendered as often as wanted from what the bake wrote.
What it writes is a pack (.tuilepack, crate tuile-pack):
[ "TUILEPK\0" ][ u64 table length ][ FlatBuffers table ][ blob region ]
- The table is read in place — mapped, or handed over as a slice. Nothing is deserialised at open time, whatever the number of tiles.
- Payloads follow it, each an independent LZ4 block: the structure is walked for free and a payload costs a copy only when something asks for it.
- A pack records, per frame, exactly which tiles were selected and the camera they were selected for. A render does not traverse: with no network and no selection to converge, it is reproducible by construction.
- A pack may carry its tiles, or refer to them in the tile store — then it is a few hundred kilobytes for a film, and the pixels are read from the store's archives.
A long film is baked and rendered by chunks, one pack each, in parallel;
tuile-farm reads and writes them on object storage and assembles the
segments into one mp4, in Rust. tuile-film reads a baked film frame by frame
with no GPU and no I/O; tuile-film-gpu draws it (one visibility raster, the
rest in compute); tuile-film-native renders it to an mp4 through the
machine's own encoder.
The tiles of an imagery pyramid do not agree in colour. A level is a mosaic of
captures — another sensor, another season, another exposure — and the level
below it is another mosaic; where the source changes between two levels the
same ground comes in two colours, and a globe that draws both draws the
boundary between them. tuile-radiometry makes the tiles agree.
- Tone is corrected, detail is kept. A picture of ground is a tone — what is left when it is blurred — and detail, everything the blur removes. Two captures of one place differ almost only in tone. So a tile is given the tone of a reference and keeps its own detail: a smooth field of gains, a few numbers a tile, multiplied into its texels in linear light.
- The reference is the pyramid. A tile and its parent cover the same ground entirely — the best overlap there is. A tile's field is the gain that brings its tone to its parent's, plus the parent's own field, chained down from an anchor level that is left as it is. Where two levels are the same source resampled the gain is exactly nothing, so the chain does not drift across the many levels where nothing changes.
- Neighbours agree along the edge they share, and every gain is bounded: a calibration may fail to help, it may not repaint.
- What it is not: not histogram matching, which repaints detail with the reference's content; not a solve over a whole mosaic; and not a look — it makes tiles agree and says nothing of how the agreed picture is graded. The grade towards a look's target (exposure, contrast, saturation) is a separate, explicit step of the film.
It is measured on the renderer itself, not beside it. The instruments are
observers of tuile-film-native, watching the light that goes in and the
light that comes out:
# Measure: per-tile and per-frame tables, the tone found, a report.
tuile-film-render <packs prefix> --meter <dir>
# Fit the film's own grade on its imagery and write it where it is kept.
tuile-film-render <packs prefix> --calibrate <dir>
# Render with it, with part of it, with another, or with none.
tuile-film-render <packs prefix> --out film.mp4
tuile-film-render <packs prefix> --out film.mp4 --tone 0.5
tuile-film-render <packs prefix> --out film.mp4 --tone-table <file>
tuile-film-render <packs prefix> --out film.mp4 --no-toneA place's table of grades is kept in the tile store, beside the layer it
corrects (<layer>/tone/…): calibrated once, read by every film that flies
over it. The functions of tuile-radiometry are pure — texels and parameters
in, gains out — and are the reference a GPU implementation is checked against.
tuile-tile-server is a tile server's job minus the HTTP: terrain and imagery
tiles as their source delivered them, kept once, on any object store.
- Layout. One set of PMTiles
archives per layer and per zone (a tile of a coarse level and
everything under it), a
manifest.jsonper zone listing its archives, and acatalog.jsonlisting the layers — all in the bucket itself. - Writing. Fresh tiles gather in memory and are published as small delta archives; a manifest is replaced by a conditional write, so any number of writers is safe. Every archive gets a name of its own.
- Compaction merges a zone's deltas in a stream — similar sizes, contiguous — and cleans up what a manifest no longer names.
- Absences are tiles too: what a source said it does not have is remembered, so it is not asked again.
- In front of a source,
TileServicefetches a tile once, serves it, keeps the hot ones in memory, and hands each fresh tile to a sink — its own buffer, or a stream other processes store from, so that servers only serve. - For a bake, a scene's slice of the store is projected to local archives: a zone's second tile is a local read.
There is no HTTP server in the library. tuile-repository defines the API as
a function from a request to a reply, and a host is an adapter of a few
lines: tuile-pack-api (a native process) serves these routes, and any
other host can serve the same ones from the same code.
GET /api/projects the projects and the tile store
GET /api/p/<project>/films[/<id>] the films a bucket holds, their packs in order
GET /api/p/<project>/b8/<n>/<key> block n of a pack
GET /api/tiles/<layer>/<z>/<x>/<y> one source tile, as stored
GET /api/store/live/<key> the store's catalog, or a zone's manifest
POST /api/store/live many of those, in one reply
GET /api/store/b8/<n>/<key> block n of one of the store's archives
- Blocks, not ranges. An object is cut into fixed 8 MiB blocks, each a whole reply at its own address — the one shape a browser cache and an edge cache both keep without argument, and the same for every reader of a film.
- Validators throughout. A block carries its object's validator from the
bucket, a manifest its own: what a reader holds is asked about, and an
unchanged object costs a
304and no bytes. A reader keeps each block and downloads it once. - One request for many manifests. A film touches hundreds of zones; their manifests are asked about together, each with the validator held, the bucket read through a sliding window whose width the host chooses.
- Readers on both sides. The same readers (
RemoteLive,RemoteBlocks,ArchivedTiles) look tiles up in the archives from a native process or from wasm, through a store served by someone else — named in configuration by its address — as through a bucket read directly.
Routing, authentication and limits belong to whoever deploys it.
| Area | Crate | What it is |
|---|---|---|
| Core | tuile-core |
3D Tiles parsing, SSE traversal, content decoding, geometry streaming |
tuile-b3dm |
The legacy Batched 3D Model container, parsed and encoded | |
tuile-terrain |
Quantized-mesh decoding and geographic tiling | |
tuile-planetary |
Terrain × imagery, assembled as a tile tree and a loader | |
tuile-camera |
A globe camera and its input controller | |
tuile-atmosphere |
Sun position, aerial perspective and sky | |
tuile-ui |
On-screen controls for viewers | |
| Sources | tuile-cesium-ion |
Connector: asset endpoints, tokens, 3D Tiles, terrain and imagery |
tuile-bing |
Connector: imagery metadata, quadkeys, tiles | |
tuile-native-fetchers |
Pooled HTTP client with a browser-style cache | |
tuile-storage-foyer |
A memory-over-disk content store | |
tuile-web |
Browser transport: fetch, and the streaming protocol over postMessage |
|
| Rendering | tuile-wgpu |
The reference wgpu backend, native and wasm |
tuile-hydra |
C ABI over the geometry server, for a Hydra plugin | |
tuile-usd |
A recorded camera path and a globe, as a USD stage | |
| Films | tuile-tape |
Record a camera path and fly it again, exactly |
tuile-bake |
Bake a frame range into a pack | |
tuile-pack |
The pack container | |
tuile-film |
A baked film read frame by frame — no GPU, no I/O | |
tuile-film-gpu |
A film on wgpu: one visibility raster, the rest in compute | |
tuile-film-native |
A film rendered natively to an mp4 | |
tuile-radiometry |
Imagery tiles brought to one another's light | |
tuile-mp4 |
H.264 samples into an mp4, in memory | |
| Storage and serving | tuile-tile-server |
The tile store: archives per zone and layer, compacted in stream |
tuile-repository |
Repositories over the buckets that hold packs, tapes and tiles | |
tuile-pack-api |
A read-only HTTP API over those buckets | |
tuile-farm |
A render farm's storage: ranged reads, multipart writes, film assembly | |
tuile-metrics |
The core's counters on a metrics endpoint |
Programs carry the tuile- prefix: tuile-bake, tuile-film-render,
tuile-tape (border, merge, orbit, pyrenees, zoom), tuile-farm,
tuile-launch-job, tuile-scan-black, tuile-trace-dump, and the examples
under examples/ (tuile-wgpu-viewer, tuile-orbit-probe,
tuile-warm-cache, …).
You need a recent stable Rust (see rust-version in Cargo.toml).
cargo check --workspace
cargo test --workspace
cargo clippy --workspace -- -D warnings
# The core must keep compiling for the browser.
cargo check --target wasm32-unknown-unknown -p tuile-coreFly the globe in a window — terrain and imagery come from the connectors, with a token you supply at run time and never commit:
CESIUM_ION_TOKEN=... cargo run -p tuile-wgpu-viewerDrag to orbit, right-drag to pan, wheel to zoom. TUILE_RECORD=path.jsonl
writes the camera path; TUILE_REPLAY=path.jsonl flies it again exactly.
It opens straight down on France from 2 000 km. To open somewhere else, say
where the eye is and where it looks — degrees and metres, --help for the
bounds and the keys:
CESIUM_ION_TOKEN=... cargo run -p tuile-wgpu-viewer -- \
--lon 6.86 --lat 45.83 --altitude 6000 --heading 120 --pitch 25N, or a click on the compass ring, turns the view north-up about the point at
its centre. --here at start, or L in the window, centres on the current
location: the operating system's location service is asked (macOS; it asks you
first), the view moves when it answers and stays put if it does not, and the
coordinates are never logged or written anywhere.
On macOS the viewer also packages as an application — Tuile.app, with an
icon — that other programs can steer:
cargo run --release -p tuile-viewer-bundle -- app --out ~/Applications
open "tuile://goto?lon=6.86&lat=45.83&altitude=6000&heading=120&pitch=25"See crates/tuile-viewer/macos/README.md for the token file, the URL
vocabulary and what the signature is worth.
The viewer is a library, tuile-viewer; tuile-wgpu-viewer is that library
over the public connectors. A program with its own tile service reuses the whole
application — window, controls, URLs, scripting, bundle — by implementing one
trait: see docs/16-embedding-the-viewer.md.
Render a baked film to an mp4:
cargo run --release -p tuile-film-native --bin tuile-film-render -- <packs prefix> --out film.mp4The film renderer reads its packs and the tile store from the buckets named in
its environment. Its options and variables are listed at the head of
tuile-film-render.rs;
film-bench.example.toml shows
how buckets are named.
tuile-corenever depends on wgpu, tokio, axum or any backend. I/O and rendering go through traits. A heavy dependency tempting its way into the core means an abstraction is in the wrong place.- The core compiles to wasm, and the wasm path leans on stable, widely adopted crates only.
- Precision is
f64until rebasing.f32begins at the local frame. - Ground is never black. A soft, blurry or stale tile is always preferred to an absent one: a replacement is created and activated before what it replaces is switched off.
- The project is defined by the standard, not by other products. Only connector crates name the service they connect to.
The design documents live in docs/:
| Document | Contents |
|---|---|
00-vision.md |
Goals, non-goals, constraints |
01-architecture.md |
The workspace, the foundational traits, the dependency rules |
02-3d-tiles-primer.md |
The standard: what is implemented and how |
03-roadmap.md |
Milestones and acceptance criteria |
20-conventions.md |
Style, tests, CI, licences, commits |
Each crate has its own specification beside them (10-crate-core.md,
11-crate-wgpu.md, …), and cargo doc --no-deps --workspace builds the API
reference. The project's site, with that reference under /docs, is built
from site/.
Read docs/20-conventions.md first. In short: every
public trait has tests, every parser is tested on real data, and a test is
written with the behaviour it covers — then the behaviour is reverted to watch
the test fail. A green suite is not proof.
A render is a picture: when one is produced, it is opened and looked at.
Dual-licensed under MIT or Apache-2.0, at
your option. Copyright lapoule.dev. Third-party attributions are in
NOTICE.