OneText

Every language,
one draw call.

Thai stacks. Arabic joins. Emoji stay whole.
A text engine for Unity uGUI that gets every script right, in a single draw call. Free and open source.
Because correct text shouldn't cost extra.

See the demo Install Star on GitHub free means free · no pro tier · no paid modules

Your engine speaks English.
Your players don't.

Arabic, Hindi, Thai, Korean and an emoji ZWJ sequence rendered by TextMeshPro and by OneText, side by side
Same string. Same font. Same box. Only the engine differs. Rendered in Unity, headless, from this repository, each column clipped to the box it was given. Korean needs no rescue, and the table says so. Neither does Arabic. The difference is everything in between.

performance

Fast where it hurts.

Anyone can win the benchmark they chose. So here are all of them: medians, tails, draw calls, and the runs we lose.5 The medians are mostly a tie. But a player never feels the median. A player feels the frame that arrives late.

11–51×
the worst frame, when the atlas has work to do1 A language pack lands mid-session. TextMeshPro stops for 351.8 ms. OneText takes 6.9 ms.
global ui · 60 labels, language switchOneTextTMP dynamic 
median0.446 ms0.454 mstie
p991.385 ms14.438 ms10×
worst frame6.875 ms351.800 ms51×
draw calls155×

1 The range across the four scenarios: 11× (unseen glyphs) to 51× (a global UI switching language). The scenario with no atlas work, short ASCII labels over a warm atlas, has nothing to win and is a tie on the median, a few per cent behind at the tail. It is published here with the rest.

2 A static atlas, frozen at build time, does no atlas work at all, and cannot draw two characters in five of this corpus. A smaller job, not a faster engine.

3 The scenario built to suit the other engine: nothing to shape, nothing to bake, nothing a correct engine can be correct at. It used to lose by 16 %; it is now a tie on the median.

4 TMP's 62 % median win is bought with thirty-two atlas pages, 20 MB of texture, one character in five undrawn, and 191 ms frames while the atlas grows. OneText holds 4 MB and one draw call; its worst frame is 16.7 ms.

5 Apple M4 Pro, Unity 6000.0.77, Burst compiled ahead of the run, null graphics device, identical scenes and frame loops; median of 3 repetitions per cell, chosen by p99. Harness, scenarios and raw per-frame CSVs in Docs/BENCHMARKS.md, losses included, because a comparison that lists only its wins is an advertisement.

memory

Memory that stops growing.

The 4 MB in the table above is not a starting point. It is the whole budget, and it is the same at the end of the session as at the start. Three things make that true.

4 MB
the texture budget, fixed for the session 1024 × 1024 × 4 layers, one byte a texel. A session that keeps meeting characters it has never drawn re-bakes inside that budget. It does not allocate a thirty-second atlas page.
−67 %
what packing takes off a font on the way into the asset Pretendard, 6.4 MB → 2.09 MB. A CJK face is already dense and gives up less: Noto Sans CJK KR, 15.7 MB → 11.1 MB, or 29 %.

* A label with precise on draws through a second, multi-channel atlas at four bytes a texel. That one is a fixed budget too, and it is per-label opt-in for exactly this reason. Atlas size and layer count are project settings; the Hub reports demand rather than occupancy, because an atlas under pressure recycles and reads 30 % full for ever.

install

Just two steps. No baking.

Add the package. Put a label on a canvas. Type in any language on Earth: no atlas to bake, no charset to declare, nothing to configure first.

1 · add the package
// Packages/manifest.json
{
  "dependencies": {
    "com.onetext.core": "https://github.com/CodeOneLabs/OneText.git"
  }
}
2 · put a label on a canvas
// or: Add Component > OneText > Label
var label = go.AddComponent<OneTextLabel>();
label.SetFont(File.ReadAllBytes(path));
label.FontSize = 38;
label.Text = "สวัสดีชาวโลก";
fallback fonts, in one call
label.SetFont(latin, arabic, thai, emoji);
label.Language = "ja";   // locl: Han unification
label.CjkLatinSpacing = true;
label.PunctuationCompression = true;
typewriter, without a coroutine
label.CharactersPerSecond = 28;
label.RevealGranularity = Syllable;
label.SkipToEnd();      // the mash-to-skip button
label.RestartReveal();
The Hub overview: stat tiles, a first-steps checklist that ticks itself off, and a map of every section
Everything after those two steps lives on one page, the Hub (Project Settings > OneText). First steps that tick themselves off, and every number a link to the screen that owns it.
The dictionaries screen after a one-click install: four word lists registered, coverage at 100%
Thai, Lao, Khmer and Burmese word lists ship inside the package, and one click installs them and re-measures word-break coverage against your own strings, not somebody else's corpus.
The Hub's string gallery: every localised string in its box, overflow framed in red
The string gallery, rendered headlessly by the package itself: every localised string in the box it has to live in, overflow framed in red. The pass this replaces is opening every screen in every language, by hand.
Doctor: renderability findings with rule badges, and a passing verdict
Doctor, the renderability lint: a character no font in the chain can draw, a locale whose line breaking needs a dictionary nobody installed. All of it decidable before the build, and it exits 1, so CI can fail the merge.

It is all included.
Except the paywall.

in the box

Everything in the box.

One package. Every feature. Nothing sold separately. Somewhere else, this list is a price sheet.

The same string drawn plain, outlined, shadowed, glowing and both at once, plus an outlined Arabic line
Outline, shadow, glow: every row is one material, one canvas, one draw call. The Arabic line is the one that matters: the outline follows the joined contour, where a per-glyph outline shows a seam at every join.
Vertical writing: columns right to left, kinsoku at the column ends, rotated Latin, ruby beside the column
Vertical writing (縦書き): columns right to left, the same line-breaking and kinsoku pipeline turned ninety degrees, Latin rotated by UAX #50, ruby beside its column. No Unity text solution, first-party or paid, has this.
Ruby placed by the W3C rules: distribution, padding, overhang, decorations, any script
Ruby (furigana) placed by the published rules: slack distributed under a narrow reading, a wide reading allowed to hang only over a neighbour's blank, base and reading unbreakable across a wrap. Compare the padded row with the overhang row: the punctuation gave up its blank, and the line stayed tight.
Single-channel SDF beside the MSDF precise option at display size
The per-label precise option: multi-channel distance fields keep corners sharp at display sizes, where a single channel rounds them off. Same material, same draw call: a precise heading still batches with the paragraph under it.

migrate

Switching is the boring part.

It is a MaskableGraphic, so everything you already know just works: RectTransform, layout groups, ContentSizeFitter, masks, raycasting. Rename the component, keep your habits. Boring, on purpose.

TextMeshProOneTextnote
TextMeshProUGUIOneTextLabelsame component slot
.text.Textrename only
.fontSize.FontSizerename only
.font (baked asset).SetFont(bytes, …fallbacks)no atlas to bake first
.enableWordWrapping.WrapNoWrap / Wrap
TMP_InputFieldOneTextInputFieldcomposition is handled, not worked around
fallback font list per assetone call, one shared atlasfallbacks do not split the draw

888,289 tests.
Zero failures.

correctness

Correctness as a number.

Unicode publishes a test for every algorithm it defines. OneText runs all of them, on every commit: 888,289 cases, zero failures. Correct isn't a claim here. It's a build step.

suitestandardcasesresult
BidiTestUAX #9 · by class770,241PASS
BidiCharacterTestUAX #9 · by character91,707PASS
LineBreakTestUAX #1419,338PASS
emoji-testUTS #513,781PASS
WordBreakTestUAX #291,944PASS
GraphemeBreakTestUAX #29766PASS
SentenceBreakTestUAX #29512PASS
every official suiteno exceptions888,289PASS

Then once more, against real fonts: every assigned codepoint in Unicode 17.0.0, shaped, outlined, rasterized and placed. All 159,631 of them.

[coverage] shaped 159,631/159,631 codepoints, 177,400 glyphs, 0 threw; 53 s
[coverage] 155,496 of 159,631 have a glyph in 213 fonts (97.41 %)
[coverage] 155,496 outlines read, 66 blank, 0 malformed

status

It only gets stronger.

Early, and built in the open. What isn't done is listed, not implied.

areastate
Shaping, bidi, segmentation, layout, SDF atlas, uGUI componentsdone
Rich text, colour emoji, animation, Asian typography, decorations, Hub, IME inputdone
Windows · macOS · Linux · Android · iOS nativesshipped
Verified end to end on a real devicemacOS only so far
Web native (HarfBuzz on wasm), verified in WebGL2 and WebGPU playersshipped
Playable browser demoplay it
Outline, shadow, glowdone
MSDF, sharp corners at large sizesdone
Ruby (furigana), vertical writingdone
System font fallback, so a missing character is not a boxdone
Tate-chū-yoko, vertical editing, COLRv1unbuilt
UI Toolkit frontend, ECS, screen readersunbuilt
Requirements. Unity 2022.3 LTS or newer. HarfBuzz and FreeType binaries ship with the package. MIT licensed; the native dependencies are permissively licensed too. Full ICU word-break dictionaries for Thai, Lao, Khmer and Burmese import as a package sample, so a project that ships none of those languages does not carry the 4.2 MB.