Text-rendering and forward-compatibility

Started by
9 comments, last by Cathbadh 14 years, 2 months ago
I am creating my own game engine, and I am trying to make it as forward-compatible with opengl as possible. This means not using depreciated features of opengl like the modelview matrix and display lists and the like. While I'm ok with using vertex attribute arrays for everything, I'm at a loss of how to reproduce the functionality that display lists give me. For example, text rendering. The way I currently render text is to store the font faces as a bunch of textures (or perhaps one big one) and map that texture on to a quad, one quad for each character. I draw each character at a 'cursor position' and then advance the cursor position by the width of the character. I currently store these texture quad calls and cursor advancement in a display list, one for each glyph in the font. This makes it convenient to draw text because all I have to do now is use glCallLists() with the text string itself. But now display lists are depreciated, and there is no way to store uniform matrix multiplication operations (i.e. how I advance the 'cursor' every character) into a sort of buffer you can call repeatedly anymore. What am I to do to keep this forward-compatible, yet fast? Perhaps I could use instanced draw calls and put every glyph dimensions and texture offsets into a uniform buffer, and use the text string as a uniform array to use gl_InstanceID for the character being rendered, and use the string to determine which character we are rendering in that instance, but the font would have to be monospace. Something tells me I'm overthinking this and perhaps I'm trying too hard to avoid legacy coding. [Edited by - Cathbadh on February 8, 2010 1:28:16 PM]
Advertisement
You could create a vertex buffer and index buffer. Load the index buffer such that every 6 indices draws two triangles. Before drawing, load the vertex buffer so that every 4 vertices (indexed by 6 indices) will draw a character quad. Loop the vertex buffer adjust the positions for each set of 4 vertices to be where you want the character drawn, the next 4 vertices, will be character 2, so on, also adjust the texture coordinates to pull the correct character coordinates from one large texture. The index buffer wouldnt need to be updated. You could load the vertex buffer with enough space to draw, a single 64 or 128 character line at a time, If you need more less or in a different position, adjust your draw calls and call counts. Once the first 4 vertices are in the correct place, the second character position and size can be adjusted based on the one before it.
This won't be particularly easy, but I would recommend something along the following lines:

  1. Upload your font texture.
  2. Upload your string of text as a 1D texture.
  3. Use the instancing extension to render a single quad N times, where N is the number of characters.
  4. Use the instance ID to calculate the position the quad should be rendered (in the vertex shader).
  5. Use the instance ID to find the character in the string texture (in the fragment shader).
  6. Use the character to render the correct portion of the font texture.

I am pretty sure that this is the fastest possible approach to text rendering, although variations are possible, for instance: using a geometry shader or histopyramid expansion, rather than the instancing extension.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

That's just it-- I know how to use element arrays and vertex buffers, I just miss the ability to just be able to (after some lengthy setup with generating font bitmaps and display lists) give opengl my text string and nothing more, and have it draw the text to the screen correctly. I'd like to not have the cpu involved in anything more during runtime. Is this a pipe dream? I know I can get monospace fonts to work like I stated above because I know that characters are all the same width, so I can figure out where to place a quad simply by gl_InstanceID * glyph_width, but the moment glyph_width is not constant across all characters, all bets are off.
Quote:Original post by Cathbadh
That's just it-- I know how to use element arrays and vertex buffers, I just miss the ability to just be able to (after some lengthy setup with generating font bitmaps and display lists) give opengl my text string and nothing more, and have it draw the text to the screen correctly.
My method will do that. Upload the string as a 1D texture, bind the shader and a single call to drawInstanced
Quote:I'd like to not have the cpu involved in anything more during runtime. Is this a pipe dream?
Nope, just a fair amount of work to implement.
Quote:I know I can get monospace fonts to work like I stated above because I know that characters are all the same width, so I can figure out where to place a quad simply by gl_InstanceID * glyph_width, but the moment glyph_width is not constant across all characters, all bets are off.
Use a second texture containing widths for each glyph.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

I think I am already saving a lot of cycles by rendering the text to an offscreen framebuffer and just plastering that framebuffer on top of the view as just a single big quad. On-screen text doesn't change very often, so I figure it would be redundant to have it draw that string of text over and over again. The text is processed and rendered to the FBO only when the text changes.

It is just bugging me that I can get legacy opengl to do variable-width fonts, and I can't figure out a way to get the core spec to do it too with the same runtime complexity.
Quote:Original post by swiftcoder
Quote:Original post by Cathbadh
That's just it-- I know how to use element arrays and vertex buffers, I just miss the ability to just be able to (after some lengthy setup with generating font bitmaps and display lists) give opengl my text string and nothing more, and have it draw the text to the screen correctly.
My method will do that. Upload the string as a 1D texture, bind the shader and a single call to drawInstanced
Quote:I'd like to not have the cpu involved in anything more during runtime. Is this a pipe dream?
Nope, just a fair amount of work to implement.
Quote:I know I can get monospace fonts to work like I stated above because I know that characters are all the same width, so I can figure out where to place a quad simply by gl_InstanceID * glyph_width, but the moment glyph_width is not constant across all characters, all bets are off.
Use a second texture containing widths for each glyph.


Your method is essentially what I was thinking about in the OP, but using uniform buffers and uniform blocks instead of 1D textures. I can specify character width in a uniform block just fine, but the problem is that I cannot sum up the widths of all the previous characters to determine where the shader should place the instanced quad. It'd be great if I could somehow store the progress of the cursor across the screen in a uniform during shader execution, but as we all know uniforms are read-only in shaderland.
Quote:Original post by CathbadhI can specify character width in a uniform block just fine, but the problem is that I cannot sum up the widths of all the previous characters to determine where the shader should place the instanced quad. It'd be great if I could somehow store the progress of the cursor across the screen in a uniform during shader execution, but as we all know uniforms are read-only in shaderland.
Uniforms aren't the only way to pass data around. Either geometry shaders or a variation on histopyramid expansion would allow you to sum up the variable distances for each character.

If you want an even simpler solution, and you know the length of the string on the CPU, use a multi-pass approach. Attach an empty 1D texture to a framebuffer object, and in the first pass, render the width of each character into its location. Then in consecutive passes, shift the texture containing the string one character to the right, and add to the existing value. For a string of length N, after N-1 passes you will have the correct offset of each character.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Since the text change rarely, I think it's better to calculate those offsets on the cpu. It's indeed simpler to implement and easier to understand than the above gpu-based methods. Moreover, you only have to do it when the text change and only once, so it will never be your bottleneck.

EDIT: It's also a lot simpler to implement multi-line layouts or features like kerning doing it in the cpu.
Quote:Original post by apatriarca
Since the text change rarely, I think it's better to calculate those offsets on the cpu.

Not only the offsets, but the entire text rendering. Rendering bitmap fonts on the CPU is dirt cheap, and vector fonts aren't slow either. Both are highly multithreadable. And since text usually doesn't change multiple times per frame, it's a rather low frequency operation. It's a waste to use GPU geometry shaders or similar to recomposite something 60 times per second that changes once every ten minutes or so.

Just render all text on the CPU (if possible in parallel to the GPU doing something else) into a cache texture, and upload the new data on-demand to the GPU. The latter would then render entire words or rows using a single quad.

BTW, just to clear up a misconception here: the 'old style' one glyph per display list call thing was maybe convenient for the developer, but it was anything but fast. It was (and is) one of the least efficient ways to render text short of plotting glyphs using large amounts of GL_POINTS... (which ironically could even be faster than the display list thing on modern GPUs !)

This topic is closed to new replies.

Advertisement