Skip to content

Render text to node faces client-side.#17052

Open
sofar wants to merge 6 commits intoluanti-org:masterfrom
sofar:sofar/node_text
Open

Render text to node faces client-side.#17052
sofar wants to merge 6 commits intoluanti-org:masterfrom
sofar:sofar/node_text

Conversation

@sofar
Copy link
Copy Markdown
Contributor

@sofar sofar commented Mar 21, 2026

Adds native text rendering on node tile surfaces via a new text_face node definition field and [text| texture modifier.

How it works: Nodes with text_face define which tile gets text (index, UV rect, resolution). Text content is stored in node metadata under the "text" key. During mesh generation, the engine constructs a [text| texture modifier string, which renders styled text onto the base tile texture using a render target and the engine's TrueType font system. The composited texture uses a non-array-texture shader fallback since array textures can't be dynamically modified.

Text styling via \x1b escape sequences in the text content: font size (s@), face (f@mono), weight (w@bold), color
(c@), background (b@), alignment (a@center). Word wrapping, newlines, and text rotation (0/90/180/270) are supported. Textures are cached by TextureSource — identical text produces identical cache keys. Mesh rebuilds are triggered automatically when text metadata changes on the client.

The [text| modifier is not specific to nodes — it works on any texture string (entities, HUD, formspec), making it a general-purpose text-to-texture primitive. For entities, mods construct the modifier string directly using core.encode_base64().

Supported drawtypes: normal, nodebox (including connected), signlike, mesh. Excluded: glasslike, glasslike_framed, allfaces (incompatible tile mapping).

Closes #1367

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 21, 2026

Some screenshots:

image image

@sofar sofar force-pushed the sofar/node_text branch 3 times, most recently from aa5a969 to fa7b3e0 Compare March 21, 2026 22:59
@Zughy Zughy added Feature ✨ PRs that add or enhance a feature Roadmap: supported by core dev PR not adhering to the roadmap, yet some core dev decided to take care of it @ Script API @ Client / Audiovisuals labels Mar 21, 2026
@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 21, 2026

Alternative design, not entirely different in many ways as this: #9999

Also: #15182

@sfan5 sfan5 closed this Mar 22, 2026
@sfan5 sfan5 reopened this Mar 22, 2026
@sfan5
Copy link
Copy Markdown
Member

sfan5 commented Mar 22, 2026

Text styling via \x1b escape sequences in the text content: font size (s@), face (f@mono), weight (w@bold), color
(c@), background (b@), alignment (a@center). Word wrapping, newlines, and text rotation (0/90/180/270) are supported. Textures are cached by TextureSource — identical text produces identical cache keys. Mesh rebuilds are triggered automatically when text metadata changes on the client.

I get the impression that LLMs enabling us to do more in less time is not necessarily better.
For a far-reaching feature like this I would strongly prefer if the initial version we merge is closer to the MVP and then additional features can be added incrementally.
That way more attention can be paid to having a solid foundation.
(An exception would be if we already had a fitting "text rendering thingy" that just needed to be hooked up. But we don't.)

Not talking about (c@...) of course since we already have that.

Also: #15182

IMO we should decide to have either this or the other feature, but not both.

Anyway I'll leave some design- and implementation-related review comments.

@lhofhansl
Copy link
Copy Markdown
Contributor

I get the impression that LLMs enabling us to do more in less time is not necessarily better.

I second this. I use LLMs at work, but any non-test, production code I consider the generated code a PoC at best and always implement it myself (I still get a great speedup from the prototyping and the test code.)

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 27, 2026

I'll definitely do another revision of this PR in a bit to address the feedback, but I've been focusing my time on some other PRs in the mean time. So, this will come back maybe next week.

sofar and others added 6 commits March 28, 2026 15:39
The FrameSpec vectors for animated tiles were allocated with new in
fillTileAttribs and owned by NodeVisuals via raw pointers in TileLayer.
Multiple copies of TileLayer (in AnimationInfo, MapBlockMesh, etc.)
shared the same raw pointer, relying on NodeVisuals to outlive all
consumers.

Replace the raw pointer with std::shared_ptr so ownership is shared
safely. This removes manual delete calls from ~NodeVisuals and makes
the lifetime management automatic.

This is preparatory work for texture reference counting, where
FrameSpec will hold RAII texture handles with non-trivial destructors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add grabTexture/putTexture methods to ITextureSource for reference
counting texture usage. TextureSource now tracks a per-texture refcount
and prunes textures with zero references each frame in processQueue().

This is infrastructure for garbage collecting generated textures
(composites with modifiers like ^[combine, ^[text, etc.) that currently
accumulate without bound. No callers use grab/put yet, so this is a
no-op change — all textures retain refcount 0 and pruning has no
effect until consumers are wired up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MapBlockMesh now grabs texture references for dynamically generated
textures (e.g. [text: composites) and puts them when the mesh is
destroyed. ItemVisuals grabs/puts animation frame textures similarly.

Actual pruning is now enabled with a 10-frame grace period to ensure
Irrlicht has finished drawing before the GPU resource is freed.

Destruction order in ~Game is adjusted so that the scene manager,
ItemVisualsManager, and NodeDefManager are cleaned up before
TextureSource is deleted.

Only consumers that generate dynamic/composite textures need grab/put
wiring. The remaining consumers (Sky, Minimap, HUD, Particles,
GenericCAO) only reference base media textures by name. These are
never grabbed, so their refcounted flag stays false and they are
never pruned. If a future feature adds dynamic texture generation to
any of these systems, it will need grab/put wiring at that point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds native text rendering on node tile surfaces, driven by node
metadata. The full pipeline:

Node definition: new text_face field in ContentFeatures with tile
index, UV rect, and resolution. Serialized over the network.
Read/written from Lua node definitions.

Metadata: text content stored under the "text" metadata key, with
escape sequences for font size, face, weight, color, background,
and alignment.

Texture modifier: [text: in ImageSource renders styled text onto a
transparent overlay via a render target. Base64-encoded text content
avoids parser conflicts. Uses FontEngine for proper TrueType
rendering with the game's configured font.

Text overlay: text is rendered as a separate layer 1 overlay with
alpha blending (TILE_MATERIAL_ALPHA), keeping the base tile texture
on layer 0 at its original resolution. The text texture is cached
independently of the base tile.

Texture pre-generation: text metadata is collected on the main
thread in collectNodeText() and texture IDs are pre-generated there
to avoid blocking roundtrips from the meshgen thread.

Mesh rebuild: handleCommand_NodemetaChanged() triggers mesh updates
for nodes with text_face when metadata changes.

Supported drawtypes: normal, nodebox, signlike, mesh.
The modifier also works on entity textures and anywhere else
textures are used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add documentation for:

- [text: texture modifier: syntax, parameters, escape sequences for
  font size/face/weight/color/bgcolor/alignment, and usage examples
- text_face node definition field: tile, rect (vector2 pair),
  resolution_x/y, supported drawtypes, metadata key
- Escape sequence helper functions: get_font_size_escape_sequence,
  get_font_escape_sequence, get_font_weight_escape_sequence,
  get_alignment_escape_sequence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test nodes covering supported configurations:

- Normal node (top face, side face)
- Nodebox slab
- Signlike (wall-mounted, full-face)
- Mesh cube, mesh slanted display
- Wide sign (3 nodes wide, 1024x341 resolution)
- Mono font, bold font
- Color test (rainbow text)
- Entity with text (cube entity, spawner item)

All nodes have right-click text editing with formspec controls for
font size, face, weight, alignment, text color, and background color.

Includes testtextface.make_text_texture() Lua helper for constructing
[text: modifier strings for use on entities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sofar sofar force-pushed the sofar/node_text branch from fa7b3e0 to b6f7382 Compare March 28, 2026 22:42
@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 28, 2026

Summary of changes (not exhaustive):

  • dynamic textures are now GC'd using refcounting and a grace period of a few frames to avoid removing textures still being rendered.
  • fixed the data race in nodemeta updates
  • fixed the blocking of meshgen on texture requests
  • no longer wordwrapping for simplicity
  • no longer rotating text for simplicity

The rest is detailed in each feedback comment per item.

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 29, 2026

I don't think the test failure is related to this change. Someone might want to re-run them and see.

@ryvnf
Copy link
Copy Markdown
Contributor

ryvnf commented Mar 29, 2026

It is pretty simple to render the text as an entity on top of the node. That is how all current sign mods work. Adding engine support for drawing text as part of the node mesh seems pretty redundant.

The node with entity pattern is also very common and used for other stuff like decorated pot faces, banners, item frames etc. All such use cases are very similar to sign text and should be addressed together, not as separate engine features.

So I think it is better to close this and add #15182. I do not think having a dedicated API for drawing text on nodes is warranted when just having a texture modifier will do.

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 29, 2026

It is pretty simple to render the text as an entity on top of the node. That is how all current sign mods work. Adding engine support for drawing text as part of the node mesh seems pretty redundant.

The node with entity pattern is also very common and used for other stuff like decorated pot faces, banners, item frames etc. All such use cases are very similar to sign text and should be addressed together, not as separate engine features.

This argument is moot because the core issue (#1367) already is concept approved, and everyone agrees that this should be added. Can we not rehash the entire discussion in this PR thread?

@ryvnf
Copy link
Copy Markdown
Contributor

ryvnf commented Mar 29, 2026

This argument is moot because the core issue (#1367) already is concept approved, and everyone agrees that this should be added. Can we not rehash the entire discussion in this PR thread?

It does not feel fair that I cannot voice opinions because I did not participate in a discussion that took place years ago. I do not think the fact that an issue has concept approval means that it cannot be questioned years later. I also think it is worth bringing up when there is a competing PR #15182 that is also concept approved. For all use cases I see, that PR covers it.

If support for rendering text on top of node tiles is to be added, then I think it is better to do so in a way that can be used for dynamic decals useful for other mods as well (paintings, decorated pots, etc). Otherwise the API risks becoming bloated in the future with many features that could have been supported in a unified way. Maybe such related use cases were not fully considered when the issue was initially discussed?

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 29, 2026

It does not feel fair that I cannot voice opinions because I did not participate in a discussion that took place years ago. I do not think the fact that an issue has concept approval means that it cannot be questioned years later. I also think it is worth bringing up when there is a competing PR #15182 that is also concept approved. For all use cases I see, that PR covers it.

That PR misses a key part. Without modifications, each text variation would require a node registration. You wouldn't be able to to place a sign and then modify the text on it.

FWIW I certainly don't think of this PR as "the holy grail" either. I'm just putting my time into driving the feature from an experimental perspective because it's been over 10 years since this feature was wanted, and it's about time some of these long-wanted features get addressed, instead of forgotten. The other PR has been stale for more than a year now, as well.

The GC work itself might warrant reviewing, because surely #15182 also would need it. The nodemeta approach can also be applied to that PR. All something that can be done with time and effort.

@ryvnf
Copy link
Copy Markdown
Contributor

ryvnf commented Mar 29, 2026

That PR misses a key part. Without modifications, each text variation would require a node registration. You wouldn't be able to to place a sign and then modify the text on it.

Replied in #1367 (comment) as it seems reasonable to continue the discussion there. I posted my initial comment here and not in the 12 year old issue because I doubt it would have gotten attention over there. I just want the point I am making to be understood before the core devs decide how to proceed.

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 30, 2026

If support for rendering text on top of node tiles is to be added, then I think it is better to do so in a way that can be used for dynamic decals useful for other mods as well (paintings, decorated pots, etc). Otherwise the API risks becoming bloated in the future with many features that could have been supported in a unified way. Maybe such related use cases were not fully considered when the issue was initially discussed?

This is one of the ways that these features fail - the scope gets too big, and people burn out.

@ryvnf
Copy link
Copy Markdown
Contributor

ryvnf commented Mar 30, 2026

This is one of the ways that these features fail - the scope gets too big, and people burn out.

I am not arguing for increasing the scope of the PR. This is what I propose:

  1. Make the PR just introduce the texture modifier (reducing its scope)
  2. Generalize the dynamic texture overlay logic from text_face for an upcoming PR (if desired)

Just introducing the texture modifier gets you 95% there. If you check the code for sign_lib and mcl_signs, you will see that the code for managing the text entity is very small compared to the text rendering. In Mineclonia it is just 41 lines and then 15 lines for the entity registration.

The text_face API can be discussed separately. Looking at the code, it appears like what it does is create the texture modifier and then rendering that as an overlay in the mapblock mesh. That seems very close to supporting the broader use case of dynamic texture overlays. Doing so requires discussing how such an API should look, but that would be a small price to pay to avoid having to maintain overlapping APIs.

If this would not be implemented by a texture modifier then I could see the argument for a dedicated text API, but that is not its current design.

@sofar
Copy link
Copy Markdown
Contributor Author

sofar commented Mar 30, 2026

I'm wondering now if #14263 shouldn't be the precursor to all this and be the generic layout engine for all things... UI dialogs, entities, nodes. It would allow for a consistent layout for all the things, and possibly expose functionality like having forms on nodes? In any case it would make layout consistent across all uses, and long-term obsolete both formspec and hypertext.

Maybe that's just nonsense, too, correct me if it is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

@ Client / Audiovisuals Feature ✨ PRs that add or enhance a feature Roadmap: supported by core dev PR not adhering to the roadmap, yet some core dev decided to take care of it @ Script API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proper display of text on the surface of a node(box)

6 participants