Fast text rendering on the GPU

OnscreenText() is very slow for dynamic texts, so I made a small class which uses shaders and instancing and has almost no cost.

Usage:

text = FastText(font="myFont.ttf", pixelSize=16)
text.setText("Hello world!")
text.setPos(0.5, 0.5)
text.update()

Whenever you change the text (e.g. position, or the text), you will have to call update()

FastText.py

import string
from panda3d.core import DynamicTextFont, Vec4, PTALVecBase4, CardMaker, Vec2
from panda3d.core import Texture, PNMImage

class FastText():

    """ This class is a fast text renderer which is made for onscreen overlays
    to have minimal to no performance impact """

    fontPagePool = {}
    supportedGlyphs = string.ascii_letters + string.digits + string.punctuation + " "

    def __init__(self, font="Data/Font/SourceSansPro-Bold.otf", pixelSize=16):
        """ Creates a new text instance with the given font and pixel size """
        # DebugObject.__init__(self, FastText)
        self.font = font
        self.size = pixelSize
        self.position = Vec2(0, 0)
        self.cacheKey = self.font + "##" + str(self.size)
        self.parent = base.aspect2d
        self.ptaPosition = PTALVecBase4.emptyArray(100)
        self.ptaUV = PTALVecBase4.emptyArray(100)
        self.ptaColor = PTALVecBase4.emptyArray(2)
        self.ptaColor[0] = Vec4(1)
        self.ptaColor[1] = Vec4(0, 0, 0, 1)
        self.currentTextHash = 0
        self.text = ""

        if self.cacheKey in self.fontPagePool:
            print "Found cached font"
            self.fontData = self.fontPagePool[self.cacheKey]
        else:
            print "Create a new font"
            self._extractFontData()
        self._generateCard()

    def _extractFontData(self):
        """ Internal method to extract the font atlas """

        # Create a new font instance to generate a font-texture-page
        fontInstance = DynamicTextFont(self.font)
        fontInstance.setPageSize(1024, 1024)
        fontInstance.setPixelsPerUnit(int(self.size * 1.5))
        fontInstance.setTextureMargin(int(self.size / 4.0 * 1.5))
        
        # Register the glyphs, this automatically creates the font-texture page
        for glyph in self.supportedGlyphs:
            fontInstance.getGlyph(ord(glyph))

        # Extract the page
        page = fontInstance.getPage(0)
        page.setMinfilter(Texture.FTLinear)
        page.setMagfilter(Texture.FTLinear)
        page.setAnisotropicDegree(0)

        blurpnm = PNMImage(1024, 1024, 4, 256)
        page.store(blurpnm)
        blurpnm.gaussianFilter(self.size / 4)

        pageBlurred = Texture("PageBlurred")
        pageBlurred.setup2dTexture(1024, 1024, Texture.TUnsignedByte, Texture.FRgba8)
        pageBlurred.load(blurpnm)

        # Extract glyph data
        glyphData = []

        for glyph in self.supportedGlyphs:
            glyphInstance = fontInstance.getGlyph(ord(glyph))
            uvBegin = glyphInstance.getUvLeft(), glyphInstance.getUvBottom()
            uvSize = glyphInstance.getUvRight() - uvBegin[0], glyphInstance.getUvTop() - uvBegin[1]

            posBegin = glyphInstance.getLeft(), glyphInstance.getBottom()
            posSize = glyphInstance.getRight() - posBegin[0], glyphInstance.getTop() - posBegin[1]

            advance = glyphInstance.getAdvance()

            glyphData.append( (uvBegin, uvSize, posBegin, posSize, advance) )

        self.fontData = [fontInstance, page, glyphData, pageBlurred]
        self.fontPagePool[self.cacheKey] = self.fontData

    def _generateCard(self):
        """ Generates the card used for text rendering """
        c = CardMaker("TextCard")
        c.setFrame(0, 1, 0, 1)
        self.card = NodePath(c.generate())
        self.card.setShaderInput("fontPageTex", self.fontData[1])
        self.card.setShaderInput("fontPageBlurredTex", self.fontData[3])
        self.card.setShaderInput("positionData", self.ptaPosition)
        self.card.setShaderInput("color", self.ptaColor)
        self.card.setShaderInput("uvData", self.ptaUV)
        self.card.setShader(self._makeFontShader(), 1000)
        self.card.setAttrib(
            TransparencyAttrib.make(TransparencyAttrib.MAlpha), 1000)
        self.card.reparentTo(self.parent)


    def _makeFontShader(self):
        """ Generates the shader used for font rendering """
        return Shader.make(Shader.SLGLSL, """
            #version 150
            uniform mat4 p3d_ModelViewProjectionMatrix;
            in vec4 p3d_Vertex;
            in vec2 p3d_MultiTexCoord0;
            uniform vec4 positionData[100];
            uniform vec4 uvData[100];
            out vec2 texcoord;

            void main() {
                int offset = int(gl_InstanceID);
                vec4 pos = positionData[offset];
                vec4 uv = uvData[offset];
                texcoord = uv.xy + p3d_MultiTexCoord0 * uv.zw;
                vec4 finalPos = p3d_Vertex * vec4(pos.z, 0, pos.w, 1.0) + vec4(pos.x, 0, pos.y, 0);
                gl_Position = p3d_ModelViewProjectionMatrix * finalPos;
            }
            """, """
            #version 150
            in vec2 texcoord;
            uniform sampler2D fontPageTex;
            uniform sampler2D fontPageBlurredTex;
            uniform vec4 color[2];
            out vec4 result;
            void main() {
                result = texture(fontPageTex, texcoord);
                float outlineResult = texture(fontPageBlurredTex, texcoord).x * 4.0 * (1.0 - result.w);
                result.xyz = (1.0 - result.xyz ) * color[0].xyz;
                result = mix(result, color[1] * outlineResult, 1.0 - result.w);
            }
        """)

    def setColor(self, r, g, b):
        """ Sets the text color """
        self.ptaColor[0] = Vec4(r, g, b, 1)

    def setOutlineColor(self, r=0.0, g=0.0, b=0.0, a=1.0):
        """ Sets the text outline color """
        if isinstance(r, Vec4):
            self.ptaColor[1] = r
        else:
            self.ptaColor[1] = Vec4(r, g, b, a)

    def setPos(self, x, y):
        """ Sets the position of the text """
        self.position = Vec2(x, y)

    def setText(self, text):
        """ Sets the text, up to a number of 100 chars """
        text = text[:100]
        if hash(text) == self.currentTextHash:
            return
        self.text = text

    def update(self):
        """ Updates the text """

        self.currentTextHash = hash(self.text)
        advanceX = 0.0
        textScaleX = self.size * 2.0 / float(base.win.getYSize())
        textScaleY = textScaleX
        
        for charPos, char in enumerate(self.text):
            idx = self.supportedGlyphs.index(char)
            uvBegin, uvSize, posBegin, posSize, advance = self.fontData[2][idx]

            self.ptaUV[charPos] = Vec4(uvBegin[0], uvBegin[1], uvSize[0], uvSize[1])
            self.ptaPosition[charPos] = Vec4( 
                self.position.x + (advanceX + posBegin[0])*textScaleX,
                self.position.y + posBegin[1] * textScaleY,
                posSize[0] * textScaleX,
                posSize[1] * textScaleY)
            advanceX += advance

        self.card.setInstanceCount(len(self.text))

if __name__ == "__main__":

    from panda3d.core import *
    loadPrcFileData("", "win-size 1920 1080")
    loadPrcFileData("", "show-frame-rate-meter #t")
    loadPrcFileData("", "sync-video #f")
    import direct.directbase.DirectStart
    import time

    posY = 0.9

    for i in xrange(8, 64, 3):
        f = FastText(font="../../Data/Font/DebugFont.ttf", pixelSize=i)
        f.setText("Hello World!")
        f.setPos(-1.0, posY)
        f.update()
        posY -= (i*2+10.0) / float(base.win.getYSize())

    run()

Hello Tobspr

I have tested the fasttext.py script (copy/paste/execute) I got the error message below.

kind regards

PhilippeC


Hi, what type of font did you load? You will probably see this error when it fails to load the font. Make sure you are loading a TTF font

Hello Tobspr

The error come front the font : I do not know where this font are located => : “…/…/Data/Font/DebugFont.ttf”

kind regards

Philippe

You will have to load your custom TTF file, i didn’t include the font in the snippet