Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 07158574-5…

Text3D

Renders text in 3D world space. The quad auto-sizes to fit the text content and never clips. Positioning works like TextMeshPro: `offsetX/Y/Z` is a local offset from the entity, `pivotX/pivotY` is the anchor within the quad, and `billboard` makes it face the camera. With `billboa…

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

Text3D

Renders text in 3D world space. The quad auto-sizes to fit the text content and never clips. Positioning works like TextMeshPro: offsetX/Y/Z is a local offset from the entity, pivotX/pivotY is the anchor within the quad, and billboard makes it face the camera. With billboard = false the text reads from the entity's own forward, the local -Z that transform.forward reports and that entity:lookAt aims.

Sizing — three fields, two jobs:

  • worldHeight — the quad's height in world units (default 1.0). This is the one field that resizes a label, and it holds that height under every transform between the label and the world: a label hung off a shrunken detail box on a model comes out the size it asked for, and so does one whose own entity carries a localScale. Width follows the rasterised text's aspect. Measure the result with getWorldSize(), which reports the extent the quad is drawn at.
  • fontSize — the raster resolution in texels (default 32). Higher values sharpen the texture and change wrapping against maxWidth; the world-space size stays worldHeight.
  • scale — rasterisation scale multiplier applied at raster time. Resolution only, like fontSize.

maxWidth is the width the text wraps at, in pixels of the raster fontSize states; 0 keeps it on one line. A label already drawn is laid out again when the field is written, so a caller re-wraps one label to line after line rather than making a label per line.

alphaCutoff decides whether the letters are a surface. At 0 (the default) the text is pure alpha blending: it draws over what is behind it and leaves the depth buffer alone, so a screen-space effect that reads scene depth — volumetric fog, screen-space shadows — integrates the whole distance behind the letters and the text sits inside it. Above 0, coverage at or over the threshold is drawn opaque and written to depth, so those effects stop at the glyph shape instead. 0.5 reads well for most fonts; higher thins the letters, lower keeps more of the antialiased edge.

Public fields: content, fontSize, color, alignment, richText, maxWidth, outline, outlineColor, background, scale, worldHeight, alphaCutoff, shadowX/Y, fontFamily, weight, slant, offsetX/Y/Z, pivotX/Y, billboard. weight and slant are strings ("regular" / "bold", "normal" / "italic").

Methods: setText(content), setStyle(options), getText(), getSize(), getWorldSize(), refresh().

entity(id).component.add("Text3D", { content = "Hello World" })
entity(id).component.add("Text3D", { content = "HP: 100", worldHeight = 0.5, fontSize = 96, color = "red", offsetY = 2.0, pivotY = 0 })
-- A title that keeps its letters crisp through volumetric fog.
entity(id).component.add("Text3D", { content = "RAISING", worldHeight = 3.0, alphaCutoff = 0.5 })

A label that is not showing, or came out in a face you did not ask for, reads back out of the text system: text.observe() lists every live text object with the entity that owns it and the texture its raster is in, and text.face(h) names the font face the shaper actually used against the fontFamily that was requested. topics/text walks both.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

Text3D Component Rasterises text into a texture and draws it on a quad that this component renders directly on its OWN entity — a GPU quad mesh (`renderer.mesh.create`) plus an alpha-blended GPU material (`renderer.material.create`) whose base-color texture is the rasterised text. The mesh + material are attached as ECS components (`ecs.Mesh` / `ecs.Material`) on this entity; there is no child entity and no legacy `Model` component. With `billboard` (the default) the material is the camera-facing `billboard` shader: the quad's four corners collapse to the anchor and each carries its in-plane offset in its vertex colour, so the shader offsets the corner in world space to face whichever camera renders the pass — the main viewport, an offscreen `capture`, and a render-texture camera alike — with no per-frame CPU work. The world-space offset keeps the quad drawn when its anchor leaves the frustum, and the mesh declares a spherical render AABB (the quad faces any direction) so frustum culling keeps it. With `billboard = false` it is the flat `unlit` shader on a quad that faces the entity's authored orientation: the text reads from the entity's own forward, the local -Z that `transform.forward` reports and that `entity:lookAt` aims, so a board aimed at a viewer reads to that viewer. `worldHeight` is a height in the world, and stays one under every transform between the label and the world, a scaled ancestor and the label's own `localScale` alike: the quad's spans are laid out in the local units that draw that height once the object transform has carried them, and the label is laid out again when any of those transforms rescales. `worldHeight` is therefore the field that resizes a label, and `getWorldSize` reports what it draws at. Reactive: the quad + material are rebuilt when the content or style changes. Positioning works like TextMeshPro: - Entity transform = world position of the text - offsetX/Y/Z = local offset of the quad relative to the entity - pivotX/pivotY = anchor point within the quad (0-1, default 0.5 = centered) Usage: entity.find("label").component.add("Text3D", { content = "Hello World" })

build_style( ) → void

ensure_handle( ) → void

destroy_mesh( ) → void

Free the current GPU mesh handle.

world_basis( ) → void

The world length of one unit of the entity's local X, Y and Z, read off the entity's composed object-to-world matrix. A quad corner placed `n` local units from the anchor lands `n * basis` world units from it, so these are what a world-unit extent is divided by to come out of the object transform at the size it names. The matrix is read rather than `lossyScale` because the decomposition folds a non-uniform ancestor scale's skew into the rotation.

local_extent(extent: number, basis: number) → number

The local extent that draws `extent` world units along an axis whose unit measures `basis` world units. An axis the hierarchy has flattened carries the extent through unchanged, and what it then draws is the zero it draws.

argtypedescription
extentnumber
basisnumber

build_mesh( ) → void

Rebuild the quad mesh from the last rasterised size. In billboard mode the four corners collapse to the offset anchor and the `billboard` shader expands them camera-facing (size + pivot come from `sync_billboard_params`). Otherwise the offset + pivot are baked into the vertex positions so the entity origin sits at the pivot anchor and the quad faces the entity's authored orientation. Both modes lay the quad out in the reader's frame — `lx` runs from the reader's left to their right, `ly` from the bottom up — and differ only in what that frame is pinned to. A billboard pins it to the pass camera. A static quad pins it to the entity's own forward, its local -Z, so the reader's right is the entity's local -X and the text reads to whatever the entity is aimed at.

ensure_gpu( ) → void

GPU wiring: the component owns ONE runtime texture (the raster target every rasterise re-uploads into) and ONE alpha-blended, double-sided material bound to that texture's stable guid. A billboard uses the camera-facing `billboard` shader (faces whichever camera renders the pass — viewport, capture, or render-texture — with no per-frame CPU work); otherwise the flat `unlit` shader. The texture and the material live for the component's lifetime and go with it in `onDestroy`; the material is rebuilt in place, under its key, when the `billboard` mode or the cutoff changes.

rasterize( ) → void

Rasterise the current content + style into the component's texture and rebuild the quad geometry when the text's aspect, or its sub-rect of the texture, changed.

awake( ) → void

Lifecycle

onPropertyChanged(key: ?, value: ?, oldValue: ?) → void

argtypedescription
key?
value?
oldValue?

follow_world_basis( ) → void

The quad's local extents are divided by the world length of the local axes it spans, so a hierarchy that rescales under the label leaves the mesh laid out against lengths that no longer hold. The matrix is the only signal for that, so it is read each frame and the quad is laid out again when it has moved.

update( ) → void

editorUpdate( ) → void

onDestroy( ) → void

setText(content: string) → void

Replace the displayed text content.

argtypedescription
contentstringNew text string.

examples

t:setText("Hello World")

setStyle(options: table) → void

Update one or more style fields in a single call. Unknown keys are ignored. Each changed field fires the reactive rebuild (position-only for offset/pivot keys, a re-rasterise for styling keys).

argtypedescription
optionstableTable of style overrides keyed by public-field name.

examples

t:setStyle({fontSize = 64, color = "yellow"})

getText( ) →

Read the current text content.

getSize( ) →

Measure the rasterised text in pixels.

getWorldSize( ) →

Read the world-space size the text quad is drawn at. This is `worldHeight` and the width its aspect gives wherever the hierarchy leaves the label's local axes their own length, and the smaller extent an axis a scaled ancestor has flattened draws where it does not.

refresh( ) → void

Force the text to re-rasterise now.

examples

t:refresh()

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

3items
·
other · born here
▤file
▲ 0↑ born
backing path · components/Text3D.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.