Distance fields for font rendering (Part 2)

To properly test the font rendering from distance fields,  I adapted chunks of code from Here and Here, to make a Python signed distance field creator:

from PIL import Image
import os
from subprocess import call
from time import time
from math import sqrt


def check(limit, ox, oy, px, py, current):
    lp = getpixel(px, py)
    if lp == -1:
        return current
    if lp != limit:
        nw = float(ox - px)
        nh = float(oy - py)
        new = (nw * nw) + (nh * nh)
        if new < current:
            current = new
    return current


def getField(limit, searchLen):
    print('Calculating field {0}...'.format(str(limit + 1)))
    if limit == 1:
        limit = 255
    distances = []
    for w in range(oim.size[0]):
        distances.append([])
        for h in range(oim.size[1]):
            current = limit if limit > 0 else (searchLen * searchLen)
            for x in range(w - searchLen, w + searchLen):
                for y in range(h - searchLen, h + searchLen):
                    current = check(limit, w, h, x, y, current)
            current = int(sqrt(current))
            current *= int(g)
            distances[w].append(current)
    return distances


def getpixel(x, y):
    try:
        p = inpixels[x, y][0]
        return p
    except IndexError:
        return -1


inPath = os.path.dirname(__file__)
outPath = os.path.join(inPath, 'out.png')
inPath = os.path.join(inPath, 'in.png')
oim = Image.open(inPath).convert('RGBA')
img = Image.new('RGBA', oim.size, (0, 0, 0, 0))

ct = time()

inpixels = oim.load()
outpixels = img.load()

search = 8
last = 0.0
g = 255 / search

field1 = getField(0, search)
ct = time() - ct
print('Took: {0}'.format(str(round(ct, 1))))
ct = time()
field2 = getField(1, search)
ct = time() - ct
print('Took: {0}'.format(str(round(ct, 1))))
ct = time()
print('Mixing fields...')

for cw in range(oim.size[0]):
    for ch in range(oim.size[1]):
        fc1 = 255 - field1[cw][ch]
        fc2 = field2[cw][ch]
        fc = int((fc1 + fc2)/2)
        outpixels[cw, ch] = (fc, fc, fc, 255)

ct = time() - ct
print('Took: {0}'.format(str(round(ct, 1))))
ct = time()

print('Resizing and saving output image (128 width)...')
rf = float(oim.size[0]) / 128
img = img.resize((128, int(oim.size[1] / rf)), Image.BILINEAR)

img.save(outPath)

ct = time() - ct
print('Took: {0}'.format(str(round(ct, 1))))

if os.name.startswith('darwin'):
    call(('open', outPath))
elif os.name == 'nt':
    os.startfile(outPath)
elif os.name == 'posix':
    call(('xdg-open', outPath))

(Since this is only a test, the input and output file paths must be changed manually as needed).

The resulting image is certainly better suited for the font rendering:

SDF:

Signed Distance Field Test

Result (same shader shown in previous part):

SDF font render

Zoomed in (no weird artifacts like with the faked DF):

SDF font render zoomed

Unfortunately, the creation time for the whole DF is around 2 minutes for small images (256*256), and that is too much for the usage that I’m thinking. Now,  that usage involves the GPU so, could be faster to do this with the graphics card? Isn’t the purpose of the GPU to do per-pixel work anyway?

 

In the next part I will publish a complete tool to calculate the distance fields in the GPU, hopefully, in real time .

Edit:

For Python 3 compatibility, data passed to ‘outpixels’ needs to be send as int. Code updated to reflect it (and improved file paths).

Distance fields for font rendering (Part 1)

Pushing myself forward again, I will publish some entries about implementing font rendering into a texture atlas, encoded with distance fields and with support for Unicode chars (the possibility to reduce the texture size will probably allow large amounts of glyphs in one single texture) to finally show proper labels and text boxes in Engendro3D.


 

By checking info about how to render fonts in OpenGL, I found this question, which lead me to the Valve’s paper (.pdf).

The results, along with the need to have huge amounts of chars pre-rendered to a font atlas for certain languages, got my interest.

Since the Distance fields looks slightly like blurred images, I made a quick png texture in Gimp containing 6 letters (with Eufm10 font) and I used the Gaussian Blur filter (10 radius) to produce this faked distance Field:

wtx_blured

Then, with a very simple GLSL fragment shader, this is the result:

dfblured

No Bilinear interpolation:

gblured_noint

Zoomed into the ‘B’:

zoomblur

No Bilinear interpolation:

zoomblur_noint

Has outline and glow and, while the result is not the best, the logic works.

The shader I wrote can probably be improved a lot, but I will use the same for all the tests I’ll do:

#version 110

uniform float time;
uniform sampler2D tex;

void main()
{
float color = texture2D(tex,gl_TexCoord[0].st).r;
	if (color <= 0.5)
		gl_FragColor = vec4(0,0,1,1);
	else
		if (color <= 0.6){
			gl_FragColor = vec4(1,1,1,1);}
		else
		{
			if (color < 1.0)
				gl_FragColor = max(0.0,1.0 - color) * 
				vec4(.9,.4,.2,1) * 2.0 * max(sin(-time),sin(time));
			else
				gl_FragColor = vec4(0.0);
		}
}

 

In the next part I will show a Distance Field generator in Python and Pillow (PIL).