@luma.gl/engine/text
Install @luma.gl/engine to use these optional subpaths. Text APIs are not exported from the
engine root. Use @luma.gl/engine/text-fonts for font preparation,
@luma.gl/engine/text-3d for extruded geometry, and
@luma.gl/engine/text-experimental for low-level models.
@luma.gl/engine/text provides an intent-level facade for experimental GPU-only 2D text rendering.
Font Atlases​
FontAtlas is the common input format for atlas-backed text. It keeps glyph metrics, baseline and
line-height data, optional kerning, image pages, and fragment sampling settings together so layout
and rendering code do not branch on the source font format.
Import font preparation from the dependency-isolated subpath:
import {
buildBitmapFontAtlas,
buildMsdfFontAtlas,
buildSdfFontAtlas,
loadMsdfFontAtlas,
measureFontAtlasText,
type FontAtlas
} from '@luma.gl/engine/text-fonts';
The font preparation subpath has no GPU runtime or loaders.gl imports. Existing imports from
@luma.gl/engine/text remain supported.
buildBitmapFontAtlas()measures and rasterizes a browser font into a bitmap atlas.buildSdfFontAtlas()uses the same measurement and packing path, but rasterizes glyphs as signed distance fields and records the required threshold and smoothing settings.
SDF atlases request ceil(radius * (1 - cutoff)) pixels of padding (9 pixels with the
defaults). Following deck.gl, this automatic padding is capped at
floor(atlasWidth / 2 - fontSize) to leave room for wide glyphs. Very large font sizes or
distance-field radii can therefore still truncate the field. Larger caller-provided
buffer values are preserved. Glyph frames from buildSdfFontAtlas() include the
rasterized padding while layout advances and baselines remain unchanged.
Both builders cache identical inputs and incrementally add newly requested characters.
@luma.gl/engine/arrow owns Arrow source data, source mapping, upload, and Arrow UTF-8/dictionary preparation. It converts Arrow vectors into GPUVector objects or prepared GPU state, then constructs renderer models that only know about GPU-resident resources.
ArrowTextRenderer accepts Utf8, Utf8View, Dictionary<Utf8>, and
Dictionary<Utf8View> label vectors. Utf8View is detected at runtime and normalized before GPU
upload, so applications can use Apache Arrow 21.2 view columns while the package remains compatible
with Apache Arrow 17 runtimes. Binary and BinaryView columns are not text inputs.
Glyph range fonts​
GlyphRangeLoader reads Mapbox/MapLibre-compatible glyph PBF endpoints without a DOM or GPU.
parseGlyphRange(bytes) is also exported for callers that already own transport. Glyph bitmaps
include a three-pixel border; width, height, left/top bearings and advances use 24 pixels per em.
import {GlyphRangeLoader, buildGlyphFontAtlas} from '@luma.gl/engine/text-fonts';
const glyphs = new GlyphRangeLoader({
url: 'https://fonts.example.com/{fontstack}/{range}.pbf',
maxRanges: 64,
fetchOptions: {credentials: 'omit'}
});
const fontAtlas = await buildGlyphFontAtlas(glyphs, 'Noto Sans Regular', 'Hello');
// Supply fontAtlas to GPUTextResources as for other atlas builders.
Share one loader among consumers of the same endpoint and network policy. Concurrent range loads
share both fetch and decode; the least recently used completed ranges are evicted beyond
maxRanges (64 by default). In-flight ranges remain until they settle. A failed request rejects
all its waiters and is removed, so a later call retries. An absent glyph in a valid response is
omitted. No requests are issued for empty text. Font names are individually URL encoded;
dot-only names are rejected because URL parsers normalize them as path segments.
loadGlyphs(fontStack, texts) returns a snapshot map containing only requested code points.
The map survives cache eviction; treat its shared glyph records and bitmap bytes as read-only.
Call it again whenever the text or font stack changes, even if geometry is unchanged.
createGlyphFontRenderer(glyphs) adapts a loaded snapshot to the existing custom atlas renderer
interface, allowing callers to own packing themselves. Its draw result carries an explicit
layoutOffsetX: glyph bearings move the drawn text while bitmap cells remain separate.
buildGlyphFontAtlas(loader, fontStack, characterSet) waits for loading before packing and needs
a browser 2D canvas (HTMLCanvasElement or OffscreenCanvas). Each call creates an independent
atlas page, preserving server advances and bearings, with SDF threshold 0.75 and smoothing 0.1.
Missing glyphs have zero advance. Loader/decoder-only use works in Node and adds no dependency.
Local ideograph fallback, shaping, style selection, tile readiness, placement and GPU lifetime
remain consumer responsibilities. No MapLibre local-font substitution policy is imposed.
Try glyph loading in Text Space Crawl​
Run yarn workspace luma.gl-examples-text-space-crawl start, then choose Server glyph SDF atlas
in the Rendering control, or append ?textRendering=glyphs to the example URL. It loads Noto Sans Regular
ranges from OpenFreeMap (network access required), packs them with buildGlyphFontAtlas, and draws
them through the same AtlasTextRenderer used by the other atlas modes. Switch away and back to
exercise the shared loader cache. No API key is required; the existing MSDF mode remains the default.
Code provenance and font licenses​
The glyph decoder, loader and renderer adapter are adapted from the MIT-licensed deck.gl-community glyph utilities. Their source headers retain the vis.gl contributor copyright and MIT SPDX identifier. Tests construct synthetic protocol messages; no third-party font binaries or glyph ranges are bundled.
This path decodes precomputed SDF bitmap bytes and metrics from protocol buffers. It does not parse TrueType/OpenType outlines, execute font hinting bytecode, shape text, or generate distance fields. It adds no font parser or rasterizer dependency. This describes the implementation scope, not a patent clearance or an additional patent grant.
Font data has its own license, separate from the MIT implementation. The example requests Noto Sans Regular from OpenFreeMap at runtime; OpenFreeMap's font notice identifies Noto Sans as SIL Open Font License 1.1. The Noto project license provides the font copyright and license terms. Applications that bundle or redistribute fonts or derived glyph assets must preserve the applicable font notices and license; the loader does not relicense downloaded glyph data as MIT.
Public Architecture​
| Responsibility | Public APIs |
|---|---|
CPU font preparation in @luma.gl/engine/text-fonts | FontAtlas, atlas builders/loaders, mapping, kerning, and measurement |
Arrow conversion in @luma.gl/engine/arrow | makeGPUTextDataFromArrow(), makeGPUTextDataFromArrowStream(), ArrowTextRenderer |
Stable rendering in @luma.gl/engine/text | GPUTextResources, GPUTextData, TextRenderer |
| Benchmark internals | @luma.gl/engine/text-experimental specialized models and forced strategies |
FontAtlas contains CPU-side pages and metrics. GPUTextResources owns their device-specific
texture upload and can be shared by any number of prepared batches and renderers. Each
GPUTextData owns one source batch's generated buffers while borrowing the shared resources.
const resources = new GPUTextResources(device, {fontAtlas});
const data = makeGPUTextDataFromArrow(device, {...textProps, resources});
const renderer = new TextRenderer(device, {data});
renderer.draw(renderPass);
const nextData = makeGPUTextDataFromArrow(device, {...nextTextProps, resources});
for (const batch of nextData) renderer.appendData(batch);
renderer.destroy();
for (const batch of data) batch.destroy();
for (const batch of nextData) batch.destroy();
resources.destroy();
appendData() adds a prepared source batch without reconstructing the model or touching existing
batches. setProps() remains available for complete replacement. Neither method destroys
caller-owned data, and destroy() releases only the renderer's render and picking models.
makeGPUTextDataFromArrowStream() yields one GPUTextData per source chunk and preserves global
row and glyph bases. The caller appends each yielded object and retains it until the renderer is
destroyed. ArrowTextRenderer.create() is the convenience path for raw Arrow streams: it creates
and owns shared resources internally and incrementally uploads and appends each arriving batch.
Automatic Strategy Selection​
| Condition | Strategy |
|---|---|
| WebGL, per-character colors, or unsupported WebGPU storage | Attribute |
| Supported WebGPU dictionary input | Dictionary storage |
| Other supported WebGPU text | Storage |
Row-indexed storage remains force-selectable from @luma.gl/engine/text-experimental for benchmarks but
is not selected automatically.
GPUTextData.stats exposes strategy, row and glyph counts, source and render batch counts,
preparation time, retained bytes, and transient compute-input bytes. Strategy-specific buffers,
schemas, shader contracts, and prepared state remain experimental.
Attribute Path​
The automatic strategy uses the attribute path for WebGL and per-character colors.
The attribute path supports row colors and per-character color lists. It expands text rows into generated glyph vertex attributes and renders through a GPU table.
Atlas-backed text requires a normalized fontAtlas. Use @luma.gl/engine/text-fonts to build browser-font
atlases explicitly with buildBitmapFontAtlas() or buildSdfFontAtlas(), or load BMFont JSON MSDF
atlases with buildMsdfFontAtlas() or loadMsdfFontAtlas() before constructing a text renderer or
model.
Storage Path​
Supported WebGPU inputs automatically use storage-backed text.
TextRowIndexedStorageModel stores one extra source-row index per generated glyph. This avoids shader-side row lookup by binary search at the cost of a larger generated glyph vertex record.
Clip Rectangles​
Arrow text clipping is optional. When clipRects is absent, renderers bind a constant disabled
rectangle and do not maintain one rectangle per text row. When supplied, the column is a
FixedSizeList<Float32, 4> (or a GPUVector<'float32x4'>) containing [x, y, width, height].
Negative width disables horizontal clipping and negative height disables vertical clipping.
In ArrowTextLayer, rectangle values are world-space offsets from the text anchor. The layer
projects the rectangle through the active deck viewport, including independent X/Y scale and
flipY. contentAlignHorizontal and contentAlignVertical can keep text aligned to the visible
part of a partially off-screen rectangle; contentCutoffPixels hides content when the visible
rectangle becomes smaller than the requested screen-pixel threshold. Glyph atlas coordinates are
independent of this feature—the rectangle clips positioned text, not atlas sampling.
Dictionary Path​
Supported dictionary-encoded WebGPU input automatically uses compressed dictionary storage.
Resource Ownership​
GPUTextData owns one source batch and its generated buffers. GPUTextResources separately owns
the uploaded atlas texture. TextRenderer and its internal models borrow both. Destroy renderers
first, then every data batch, and finally the shared resources. This split permits multiple
renderers and streams to share one atlas upload without hidden caches or ambiguous ownership.
Direct specialized model construction is intentionally unstable. Import the models and forced
strategy preparation contracts from @luma.gl/engine/text-experimental only in benchmarks and
diagnostic tools.
Custom browser font atlases​
@luma.gl/engine/text-fonts exports buildBrowserFontAtlas, resolveBrowserFontAtlasSettings,
and the BrowserFontRenderer type for integrations that supply their own glyph images.
The renderer measures glyphs and font-wide metrics and returns ImageData for each glyph.
Omitting cacheKey builds an independent atlas; callers that opt into shared caching must
provide a key that uniquely identifies the renderer and all rasterization settings.
import {buildBrowserFontAtlas, resolveBrowserFontAtlasSettings} from '@luma.gl/engine/text-fonts';
const atlas = buildBrowserFontAtlas({
settings: resolveBrowserFontAtlasSettings({characterSet: 'Hello'}),
renderSettings: {mode: 'bitmap', threshold: 0, smoothing: 0},
createRenderer: defaultMeasure => ({
measure: defaultMeasure,
draw: character => drawCustomGlyph(character)
})
});
setFontAtlasCacheLimit(limit) resets the shared browser-font atlas cache and sets its
capacity. The default and minimum capacity are three atlases. Set it once during application
initialization; changing it clears existing cache entries. Uncached custom atlases are unaffected.