pythonfontssfmlvertex-arraybitmap-fonts

Bitmap fonts in SFML (OpenGL)


I'm writting a simple bitmap font renderer in pySFML and wanted to ask is there a better and faster way to approach this problem.

I'm using VertexArray and create a quad for each character in a string. Each quad has appropriate texture coordinates applied.

Example font (PNG file): Bitmap font example

Font rendering code:

import sfml


class BitmapFont(object):
    '''
    Loads a bitmap font. 
    `chars` is string with all characters available in the font file, example: '+0123456789x'.
    `widths` is mapping between characters and character width in pixels.
    '''
    def __init__(self, path, chars, widths, colors=1, kerning=0):
        self.texture = sfml.Texture.from_file(path)
        self.colors = colors
        self.height = self.texture.height / self.colors
        self.chars = chars
        self.kerning = kerning
        self.widths = widths

        self.glyphs = []
        y = 0
        for color in range(self.colors):
            x = 0
            self.glyphs.append({})
            for char in self.chars:
                glyph_pos = x, y
                glyph_size = self.widths[char], self.height
                glyph = sfml.Rectangle(glyph_pos, glyph_size)
                self.glyphs[color][char] = glyph
                x += glyph.width
            y += self.height


class BitmapText(sfml.TransformableDrawable):
    '''Used to render text with `BitmapFonts`.'''

    def __init__(self, string='', font=None, color=0, align='left', position=(0, 0)):
        super().__init__()
        self.vertices = sfml.VertexArray(sfml.PrimitiveType.QUADS, 4)
        self.font = font
        self.color = color
        self._string = ''
        self.string = string
        self.position = position

    @property
    def string(self):
        return self._string

    @string.setter
    def string(self, value):
        '''Calculates new vertices each time string has changed.'''
        # This function is slowest and probably can be optimized.

        if value == self._string:
            return
        if len(value) != len(self._string):
            self.vertices.resize(4 * len(value))
        self._string = value
        x = 0
        y = 0
        vertices = self.vertices
        glyphs = self.font.glyphs[self.color]
        for i, char in enumerate(self._string):
            glyph = glyphs[char]
            p = i * 4
            vertices[p + 0].position = x, y
            vertices[p + 1].position = x + glyph.width, y
            vertices[p + 2].position = x + glyph.width, y + glyph.height
            vertices[p + 3].position = x, y + glyph.height
            vertices[p + 0].tex_coords = glyph.left, glyph.top
            vertices[p + 1].tex_coords = glyph.right, glyph.top
            vertices[p + 2].tex_coords = glyph.right, glyph.bottom
            vertices[p + 3].tex_coords = glyph.left, glyph.bottom
            x += glyph.width + self.font.kerning

    def draw(self, target, states):
        '''Draws whole string using texture from a font.'''
        states.texture = self.font.texture
        states.transform = self.transform
        target.draw(self.vertices, states)

Simple benchmark with FPS counter:

from random import random, randint

import sfml

from font import BitmapFont, BitmapText


font = sfml.Font.from_file('arial.ttf')

bitmap_font = BitmapFont('font.png', chars='-x+0123456789 ', kerning=-3,
                         widths={'x': 21, '+': 18, '0': 18, '1': 14, '2': 18, '3': 18, '4': 19, '5': 18, '6': 18,
                                 '7': 17, '8': 18, '9': 18, '-': 17, ' ': 8})

window = sfml.RenderWindow(sfml.VideoMode(960, 640), 'Font test')

fps_text = sfml.Text('', font, 18)
fps_text.position = 10, 10
fps_text.color = sfml.Color.WHITE

fps_text_shadow = sfml.Text('', font, 18)
fps_text_shadow.position = 12, 12
fps_text_shadow.color = sfml.Color.BLACK

frame = fps = frame_time = 0

clock = sfml.Clock()
texts = [BitmapText('x01234 56789', font=bitmap_font, color=randint(0, bitmap_font.colors - 1)) for i in range(1000)]

while window.is_open:
    for event in window.events:
        if type(event) is sfml.CloseEvent:
            window.close()

    time_delta = clock.restart().seconds
    if time_delta > .2:
        continue

    frame_time += time_delta
    if frame_time >= 1:
        fps = frame
        frame_time = frame = 0
        fps_text_shadow.string = fps_text.string = 'FPS: {fps}'.format(fps=fps)
    else:
        frame += 1

    window.clear(sfml.Color(63, 63, 63))

    for t in texts:
        t.position = random() * 960, random() * 640
        t.string = str(randint(0, 10000000))
        window.draw(t)

    window.draw(fps_text_shadow)
    window.draw(fps_text)
    window.display()

I'm using Python 3.3, pySFML 1.3, SFML 2.0 and Windows.


Solution

  • Laurent Gomila (author of SFML) confirmed in other forum, that my approach to bitmap fonts is same as vector fonts implementation in SFML (namely VertexArray and quad for each character).