ESTIMATED READ: 8 MIN.

Inside ShapeDiver: Efficiently Rendering the Text Tag 3D Component via Instanced Meshes

January 30th, 2025 by Michael Oppitz

A technical deep dive into how the ShapeDiver viewer now efficiently handles large amounts of 3D text. Learn more about the new approach, which is already live and ready to use!

Cover image for Inside ShapeDiver: Efficiently Rendering the Text Tag 3D Component via Instanced Meshes

Introduction

The performance of rendering the Text Tag 3D component has been something we’ve wanted to improve for a long time. While we knew our solution wasn’t optimal in terms of performance, it worked well enough that we pushed the issue into our backlog. However, with large amounts of text, the performance, both in creating the text and rendering it, became noticeably slow, particularly on lower-end devices. When one of our clients brought up this issue while trying to render a large number of text tags, we knew it was time to find a better solution. We developed an approach that’s relatively simple and works really well, and we’re excited to share it with you!


The Problem: Creating 3D Text (fast)

To understand why our new approach was necessary, let me briefly explain the kind of data we transfer, how our old approach worked, and where the bottlenecks were. This will help make our new approach clearer.

Screenshot of Grasshopper canvas showing the Text Tag 3D component.Screenshot of Grasshopper canvas showing the Text Tag 3D component.

When the Text Tag 3D component is used in Grasshopper, the ShapeDiver Plugin doesn't do much with the data provided as input. It gathers all that data into an output, and the content of that output is then interpreted by our viewer. This means that the text provided to the Text Tag 3D component is transferred simply as a string, without any geometry created in the Plugin. The advantage of this approach is that the data sent can be kept to a minimum, just the text itself and metadata for positioning, color, etc.

While this approach is efficient for transferring data, the 3D geometry of these tags must be created within the viewer. In our old approach, we used three.js functionality to generate this 3D geometry. Three.js includes a TextGeometry class that can create 3D text from a previously loaded font and some provided options. While this approach works well for many applications, we encountered a few issues that affected performance.

One issue was the creation of the geometry itself. Characters in a font are defined as vector paths, which mathematically describe curves and lines. In three.js, these vector paths are combined into a single shape per character and then extruded. This process can take some time, as some characters are defined by many vector paths. An additional problem arises because curves must be approximated. WebGL only supports primitives like points, lines, or triangles, so any round or curved shape must be approximated using these primitives. Curves are approximated through tessellation, which uses line segments to simulate a curve. These issues impacted the initial loading performance of models.

In addition to the creation of these 3D objects being performance-intensive during loading, the rendering of them can also be affected. As most characters have rounded shapes, these characters, when turned into 3D geometry, become meshes with a high number of triangles. With a lot of text geometry, this can result in a large number of triangles in the scene, which negatively affects rendering performance. This is why some models experienced low FPS while rendering, even though the models themselves, without the 3D text tags, were well-optimized.

To summarize, there are two main problems with the approach we were using:

- The creation of 3D text can be slow, as generating 3D characters takes time.

- The rendering of 3D text can be slow, as the created geometry may contain a high number of triangles.


The Solution - Part 1: Pre-Computing 3D Characters

Now that I’ve explained the two issues we faced with our previous approach, let me explain how we solved the first one: the slow initial loading of 3D geometry. The solution was fairly simple, we pre-computed the 3D geometry and loaded the characters as needed.

We accomplished this by creating a small internal program that allows us to generate multiple GLB files for all characters defined in a font. This process takes some time, but we only had to do it once, and now those GLB files are uploaded to our servers. In addition to the 3D geometry for each character, we also stored some metadata (such as height, width, horizontal advance, etc.) within the GLBs, which we can later read to construct words and sentences based on the rules of the font.

Screenshot of the GLB with all ASCII characters when loading them all at once. All characters are positioned at the origin to allow for seamless transformations.Screenshot of the GLB with all ASCII characters when loading them all at once. All characters are positioned at the origin to allow for seamless transformations.

One thing we noticed right away was that a single GLB for all characters could become quite large. So, we decided to split the characters into two separate GLBs: one containing all printable ASCII characters, and the other containing the rest of the characters defined in the font. We only load the larger GLB on demand, when a character is defined in the original font but not part of the printable ASCII set.

With this change, the loading of the 3D geometry became much faster. We only need to download the GLB once, search for the required characters, and combine them into the specified text. This solves the first problem, the slow initial loading, and also sets the stage for solving the second problem, which is the rendering performance.


The Solution - Part 2: Instancing of Characters

Now that we’ve loaded all the characters we need in the first step, rendering each character separately results in the same performance as before. Once again, for large amounts of text, the rendering performance slowed down significantly.

This is where the concept of instancing comes in. Instancing allows us to use the geometry of a unique character just once and then instance it as many times as needed. The key is that you can supply separate transformation matrices and colors for each instance. This means you can adjust the position, scale, rotation, and color of each instance individually. Instancing works by loading the geometry onto the GPU only once, and then re-using it with the corresponding instance transformation and color. This means that even though the same geometry appears multiple times in the scene, the GPU only needs to store it once, which drastically reduces memory usage and improves rendering efficiency.

Screenshot of a model that uses large amounts of text tags without any performance issues.Screenshot of a model that uses large amounts of text tags without any performance issues.

Let’s consider an example. Suppose we want to render the sentence, "Hello, this is Michael from ShapeDiver!" If we were to render each character separately, we’d have 35 characters. However, with instancing, we can reduce this to 19 characters, as, for instance, the character “e” appears 4 times in the sentence.

What you can already see from this small example is that the more characters have been processed, the higher the chance that a character has already been used. This is particularly effective when rendering model data that includes numbers. For example, the 10 possible digits are each used once and then instanced multiple times.



Limitations and Future Work


While this approach works very well in general, there is one simple limitation. The best performance is achieved when a single Text Tag 3D component is used in Grasshopper, which contains all the data that needs to be displayed. Using trees in Grasshopper makes it easy to achieve this. Aside from that, there are no downsides to using 3D text.

For instancing, there are more limitations that don’t affect 3D text tags. Since instancing only allows for changes in transformation (position, scale, rotation) and color, its use is quite restricted. While we are considering performance improvements with instancing in other areas of our system, we must keep these limitations in mind.

A potential future improvement could be the implementation of different fonts. Currently, we use the font in Grasshopper to ensure an identical representation in our viewer. However, for other projects, you might want to use a different font. If that’s the case, feel free to reach out, we might be able to find a solution for you in the form of a small project.


Conclusion

To sum up, we’ve implemented a new solution for rendering 3D text tags. We pre-compute the text tags and only load the pre-computed geometry on demand. Once loaded, we use instancing to reduce the number of meshes to an absolute minimum. While none of these approaches are new or groundbreaking, they significantly improve the performance of loading and rendering 3D text tags.

Explore More Interviews & Case Studies