I hit two massive problems a few months back. First, my simple text menus were absolutely destroying performance in VR, especially when adding nodes to the scene tree. Second, the font quality looked like shite. This is the story of that rabbit hole and the custom solution, FastText
, that came out of it.
TL;DR: If you're using Label3D
in Godot for anything more than a couple of labels in a performance-critical context like VR, stop. It has terrible performance characteristics. My advice use TextMesh
and flatten it. Or, if you really care about quality, create your text in Blender, run a decimate modifier on it, and export it as an optimized mesh. Honestly, if I'd thought of the TextMesh
approach a few months ago, I might have just done that instead of getting angry enough at the performance to write my own solution.
Note: Also the quality issue has recently been fixed in mainline thanks again to bruvzg and fire recent fixes. See this example of the quality fix. I had a similar local fix but was able to rip it out now that this is in main.
Disclaimer: Use at Your Own Risk ☢️
This code is provided "as is," without any warranties. By using this, you assume full responsibility for any risks. It's not for beginners and is a stripped-down version of what's in my own branch. It's shared because a few other devs were curious. You've been warned.
How the Journey Started
It all began with the classic gamedev mystery: "Why is my frame rate dying?" My project's performance was falling through the floor, and after the usual profiling shenanigans—adding one-frame delayers, staring at graphs—I started to suspect the UI. I fired up RenderDoc to see what was happening on the GPU and got lost pulling apart the text server and rendering nodes.
What I found was horrifying. The default Label3D
was spamming materials like there was no tomorrow when being added to the scene. In the worst-case scenario, it could generate a unique material per glyph. The design relies on the engine's material cache to sort it all out, which is a clever, generalized solution but absolutely brutal for my use case where I needed maximum performance.
The pain came from first finding the issue took way longer than you would expect. Trying to fully understand the text system, being misled by trimesh. Initially thinking it was multiple draw calls, until looking in renderdoc and seeing it was batching, finding out about the whole standard material batch stuff in godot and going down a real rabbit hole. I really wanted there to be a minimal change but ultimately I decided by making high level design changes was how to get max perf.
I realized the scale of the changes needed to fix Label3D
for my needs was just too big. The whole point of my solution would be to throw away most of its bells and whistles for raw speed. So, I took a hatchet to the code. I started building a new node, FastText
, which forced me to create a shared resource type, an editor gizmo, and a bunch of other things I'd never had to build in Godot before. It was a long, painful learning curve full of gotchas.
The Assumptions That Make It Fast
FastText
only works because, like all of game development, it's about trade-offs. Label3D
is a jack-of-all-trades; FastText
is a specialist tool built on a few core assumptions:
- Shared Font Resource: All font parameters (size, settings, etc.) are stored in a
FontResource
which is explicitly shared betweenFastText
nodes. This is the biggest win, as it avoids massive resource duplication and allows for batching. Esp when adding nodes to a scene. - One Draw Call: Each glyph is just a single quad with UVs. The entire
FastText
node renders in a single draw call and surface (unless your font is so large it spreads across multiple texture sheets, then it's one surface per sheet but still one drawcall). - Instance Params: Customization like color is handled through instance parameters fed to a custom shader, keeping the core material shared. Because you don't need per glyph colours.
Quality, MSDF, and a Community Win
After sorting the performance, I still had the quality issue. The text looked blurry and artifacted in VR. It turned out the Multi-channel Signed Distance Field (MSDF) implementation in the engine had some bugs. At this point, I was deep in my own branch and trying to keep my changes isolated, so I patched the MSDF code locally just for FastText
.
But here’s the brilliant part of open source: just as I was getting ready to bring in some recent performance fixes, I saw that bruvzg and others had landed a PR that fixed the very same MSDF issues in Godot's main branch! Huge thanks to him for that work. I was overjoyed to be able to rip out all my ugly local fixes. With the text server now working correctly, FastText
could just focus on rendering.
With outlines, I decided to assume a correct MSDF implementation could handle a small, clean outline, and it worked great. But for our game's art style, outlines didn't quite fit. Instead, I added a fake normal calculation to the shader to create a "bubble rim" effect on the text. It's horribly limited by the MSDF resolution, and the trade-off is a few more samples in the pixel shader, but it picks up the scene lighting beautifully in VR and really helps with visual depth. Note the text in the example is huge on purpose to show the artifacts.
The Future: Slug Rendering
Overall, this has worked incredibly well for me. Long term, though, I'd love to move to a Slug-style font rendering system. It is, by far, the best I've seen for VR text. I stumbled across this MIT-licensed library on GitHub called gpu-font-rendering that I think is the direction I want to go. It's not as good as Slug but is MIT licensed. But I've put it off because my current solution is "good enough for now," and we are so close to that Early Access release.
Why I'm Sharing This Now
I wanted to post these learnings after a great chat on Discord today with Bastiaan Olij, Zi, and Daniel Snd on this exact topic. Daniel mentioned he was working on his own module and was interested in seeing the code, and I know others in the dev chat have asked.
So, if you've made it this far, know that the code is rough, ripped straight out of my project branch (with some bits hastily removed), and is not a plug-and-play solution. But if it's useful to you, here it is:
- place fast_text in modules
- place FastText.svg in editor\icons
Hopefully, by the power of open source, someone can take these ideas and build something much, much better.
And yes, I promise, we are SUPER close to releasing our game in Early Access. People are back from Gamescom, and now with my old friend joining as a business partner, it's full steam ahead. More on that soon.