diff --git a/punyverse/assets/textures/font.png b/punyverse/assets/textures/font.png
new file mode 100644
index 0000000..589c370
Binary files /dev/null and b/punyverse/assets/textures/font.png differ
diff --git a/punyverse/entity.py b/punyverse/entity.py
index 19cecc3..b06afc1 100644
--- a/punyverse/entity.py
+++ b/punyverse/entity.py
@@ -5,10 +5,10 @@ from pyglet.gl import *
 # noinspection PyUnresolvedReferences
 from six.moves import range
 
-from punyverse.glgeom import compile, glRestore, belt, Disk, OrbitVBO, Matrix4f, SimpleSphere, TangentSphere, Cube
+from punyverse.glgeom import *
 from punyverse.model import load_model, WavefrontVBO
 from punyverse.orbit import KeplerOrbit
-from punyverse.texture import get_best_texture, load_clouds, get_cube_map, load_texture_1d
+from punyverse.texture import get_best_texture, load_alpha_mask, get_cube_map, load_texture_1d
 from punyverse.utils import cached_property
 
 G = 6.67384e-11  # Gravitation Constant
@@ -325,7 +325,7 @@ class SphericalBody(Body):
             atm_texture = atmosphere_data.get('diffuse_texture', None)
             cloud_texture = atmosphere_data.get('cloud_texture', None)
             if cloud_texture is not None:
-                self.cloud_transparency = get_best_texture(cloud_texture, loader=load_clouds)
+                self.cloud_transparency = get_best_texture(cloud_texture, loader=load_alpha_mask)
                 self.cloud_radius = self.radius + 2
                 self.clouds = self._get_sphere(division, tangent=False)
 
diff --git a/punyverse/glgeom.py b/punyverse/glgeom.py
index 37964c7..c6c968a 100644
--- a/punyverse/glgeom.py
+++ b/punyverse/glgeom.py
@@ -1,3 +1,5 @@
+from __future__ import division
+
 from array import array
 from ctypes import c_int, c_float, byref, cast, POINTER, c_uint, c_short, c_ushort
 from math import *
@@ -10,7 +12,8 @@ from six.moves import range
 TWOPI = pi * 2
 
 __all__ = ['compile', 'ortho', 'frustrum', 'crosshair', 'circle', 'belt',
-           'glSection', 'glRestore', 'progress_bar']
+           'glSection', 'glRestore', 'glContext', 'progress_bar',
+           'FontEngine', 'Matrix4f', 'Disk', 'OrbitVBO', 'SimpleSphere', 'TangentSphere', 'Cube']
 
 
 class glContext(object):
@@ -320,6 +323,53 @@ class OrbitVBO(object):
         self.close()
 
 
+class FontEngine(object):
+    type = GL_SHORT
+    stride = 4 * 2
+    position_offset = 0
+    position_size = 2
+    tex_offset = position_size * 2
+    tex_size = 2
+
+    def __init__(self, max_length=256):
+        self.storage = array('h', max_length * 24 * [0])
+        vbo = GLuint()
+        glGenBuffers(1, byref(vbo))
+        self.vbo = vbo.value
+        self.vertex_count = None
+
+    def draw(self, string):
+        index = 0
+        row = 0
+        col = 0
+        for c in string:
+            if c == '\n':
+                row += 1
+                col = 0
+                continue
+            o = ord(c)
+            if 32 <= o < 128:
+                self.storage[24*index:24*index+24] = array('h', [
+                    row, col, o - 32, 1,
+                    row + 1, col, o - 32, 0,
+                    row + 1, col + 1, o - 31, 0,
+                    row, col, o - 32, 1,
+                    row + 1, col + 1, o - 31, 0,
+                    row, col + 1, o - 31, 1,
+                ])
+                index += 1
+                col += 1
+
+        self.vertex_count = index * 6
+
+        glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
+        glBufferData(GL_ARRAY_BUFFER, self.storage.itemsize * len(self.storage),
+                     array_to_ctypes(self.storage), GL_STREAM_DRAW)
+
+    def end(self):
+        glBindBuffer(GL_ARRAY_BUFFER, 0)
+
+
 def belt(radius, cross, object, count):
     for i in range(count):
         theta = TWOPI * random()
diff --git a/punyverse/loader.py b/punyverse/loader.py
index 958858f..fd31b55 100644
--- a/punyverse/loader.py
+++ b/punyverse/loader.py
@@ -1,5 +1,6 @@
 from __future__ import print_function
 
+import os
 import sys
 import time
 
@@ -26,21 +27,29 @@ def get_context_info(context):
 
 
 class LoaderWindow(pyglet.window.Window):
+    MONOSPACE = ('Consolas', 'Droid Sans Mono', 'Courier', 'Courier New', 'Dejavu Sans Mono')
+
     def __init__(self, *args, **kwargs):
         super(LoaderWindow, self).__init__(*args, **kwargs)
 
+        # work around pyglet bug: decoding font names as utf-8 instead of mbcs when using EnumFontsA.
+        stderr = sys.stderr
+        sys.stderr = open(os.devnull, 'w')
+        pyglet.font.have_font(self.MONOSPACE[0])
+        sys.stderr = stderr
+
         self.loading_phase = pyglet.text.Label(
-            font_name='Consolas', font_size=20, x=10, y=self.height - 50,
+            font_name=self.MONOSPACE, font_size=20, x=10, y=self.height - 50,
             color=(255, 255, 255, 255), width=self.width - 20, align='center',
             multiline=True, text='Punyverse is starting...'
         )
         self.loading_label = pyglet.text.Label(
-            font_name='Consolas', font_size=16, x=10, y=self.height - 120,
+            font_name=self.MONOSPACE, font_size=16, x=10, y=self.height - 120,
             color=(255, 255, 255, 255), width=self.width - 20, align='center',
             multiline=True
         )
         self.info_label = pyglet.text.Label(
-            font_name='Consolas', font_size=13, x=10, y=self.height - 220,
+            font_name=self.MONOSPACE, font_size=13, x=10, y=self.height - 220,
             color=(255, 255, 255, 255), width=self.width - 20,
             multiline=True
         )
diff --git a/punyverse/shader.py b/punyverse/shader.py
index c1ef974..835571f 100644
--- a/punyverse/shader.py
+++ b/punyverse/shader.py
@@ -103,6 +103,9 @@ class Program(object):
     def uniform_bool(self, name, value):
         glUniform1i(self.uniforms[name], bool(value))
 
+    def uniform_vec2(self, name, a, b):
+        glUniform2f(self.uniforms[name], a, b)
+
     def uniform_vec3(self, name, a, b, c):
         glUniform3f(self.uniforms[name], a, b, c)
 
diff --git a/punyverse/shaders/text.fragment.glsl b/punyverse/shaders/text.fragment.glsl
new file mode 100644
index 0000000..3b28333
--- /dev/null
+++ b/punyverse/shaders/text.fragment.glsl
@@ -0,0 +1,12 @@
+#version 130
+
+in vec2 v_uv;
+
+out vec4 o_fragColor;
+
+uniform sampler2D u_alpha;
+uniform vec3 u_color;
+
+void main() {
+    o_fragColor = vec4(u_color, texture(u_alpha, v_uv).r);
+}
diff --git a/punyverse/shaders/text.vertex.glsl b/punyverse/shaders/text.vertex.glsl
new file mode 100644
index 0000000..89a9621
--- /dev/null
+++ b/punyverse/shaders/text.vertex.glsl
@@ -0,0 +1,14 @@
+#version 130
+
+in vec2 a_rc;
+in vec2 a_tex;
+
+out vec2 v_uv;
+
+uniform mat4 u_projMatrix;
+uniform vec2 u_start;
+
+void main() {
+    gl_Position = u_projMatrix * vec4(a_rc.y * 8 + u_start.x, a_rc.x * 16 + u_start.y, 0, 1);
+    v_uv = vec2(a_tex.x * 8 / 1024, a_tex.y);
+}
diff --git a/punyverse/texture.py b/punyverse/texture.py
index 36c3f7b..8bb1680 100644
--- a/punyverse/texture.py
+++ b/punyverse/texture.py
@@ -42,7 +42,8 @@ except ImportError:
             result[y1 * row:y1 * row + row] = source[y2 * row:y2 * row + row]
         return six.binary_type(result)
 
-__all__ = ['load_texture', 'load_clouds', 'load_image', 'get_best_texture', 'max_texture_size', 'get_cube_map']
+__all__ = ['load_texture', 'load_alpha_mask', 'load_image', 'get_best_texture', 'max_texture_size',
+           'get_cube_map', 'load_texture_1d']
 
 id = 0
 cache = {}
@@ -259,11 +260,8 @@ def load_texture_1d(file, clamp=False):
     return id
 
 
-def load_clouds(file):
+def load_alpha_mask(file, clamp=False):
     path, file = get_file_path(file)
-    if path in cache:
-        return cache[path]
-
     path, width, height, depth, mode, texture = load_image(file, path)
 
     if depth != 1:
@@ -284,10 +282,13 @@ def load_clouds(file):
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
 
+    if clamp:
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+
     if gl_info.have_extension('GL_EXT_texture_filter_anisotropic'):
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, glGetInteger(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT))
 
-    cache[path] = id
     return id
 
 
diff --git a/punyverse/ui.py b/punyverse/ui.py
index 4b35521..70cc18a 100644
--- a/punyverse/ui.py
+++ b/punyverse/ui.py
@@ -1,4 +1,5 @@
 #!/usr/bin/python
+from __future__ import division
 import os
 import time
 from math import hypot
@@ -54,8 +55,6 @@ class Punyverse(pyglet.window.Window):
         self.key_handler = {}
         self.mouse_press_handler = {}
 
-        self.label = pyglet.text.Label('', font_name='Consolas', font_size=12, x=10, y=self.height - 20,
-                                       color=(255,) * 4, multiline=True, width=600)
         self.exclusive = False
         self.modifiers = 0
 
@@ -127,6 +126,8 @@ class Punyverse(pyglet.window.Window):
         glEnable(GL_DEPTH_TEST)
         glShadeModel(GL_SMOOTH)
 
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+
         glMatrixMode(GL_MODELVIEW)
 
         glEnable(GL_LIGHTING)
@@ -144,6 +145,8 @@ class Punyverse(pyglet.window.Window):
         glLightfv(GL_LIGHT1, GL_DIFFUSE, fv4(.5, .5, .5, 1))
         glLightfv(GL_LIGHT1, GL_SPECULAR, fv4(1, 1, 1, 1))
 
+        self.info_engine = FontEngine()
+
         pyglet.clock.schedule(self.update)
         self.on_resize(self.width, self.height)  # On resize handler does nothing unless it's loaded
 
@@ -213,9 +216,8 @@ class Punyverse(pyglet.window.Window):
         if not width or not height:
             # Sometimes this happen for no reason?
             return
-        self.label.y = height - 20
-        glViewport(0, 0, width, height)
 
+        glViewport(0, 0, width, height)
         glMatrixMode(GL_PROJECTION)
         self.world.resize(width, height)
         glLoadMatrixf(self.world.projection_matrix())
@@ -262,7 +264,13 @@ class Punyverse(pyglet.window.Window):
         width, height = self.get_size()
 
         if self.info:
-            ortho(width, height)
+            projection = Matrix4f([
+                2 / width, 0, 0, 0,
+                0, -2 / height, 0, 0,
+                0, 0, -1, 0,
+                -1, 1, 0, 1,
+            ])
+
             if self.info_precise:
                 info = ('%d FPS @ (x=%.2f, y=%.2f, z=%.2f) @ %s, %s/s\n'
                         'Direction(pitch=%.2f, yaw=%.2f, roll=%.2f)\nTick: %d' %
@@ -271,8 +279,30 @@ class Punyverse(pyglet.window.Window):
             else:
                 info = ('%d FPS @ (x=%.2f, y=%.2f, z=%.2f) @ %s, %s/s\n' %
                         (pyglet.clock.get_fps(), c.x, c.y, c.z, self.world.cam.speed, self.get_time_per_second()))
-            self.label.text = info
-            self.label.draw()
+
+            glEnable(GL_BLEND)
+            shader = self.world.activate_shader('text')
+            shader.uniform_mat4('u_projMatrix', projection)
+            self.info_engine.draw(info)
+
+            glActiveTexture(GL_TEXTURE0)
+            glBindTexture(GL_TEXTURE_2D, self.world.font_tex)
+            shader.uniform_texture('u_alpha', 0)
+            shader.uniform_vec3('u_color', 1, 1, 1)
+            shader.uniform_vec2('u_start', 10, 10)
+
+            shader.vertex_attribute('a_rc', self.info_engine.position_size, self.info_engine.type, GL_FALSE,
+                                    self.info_engine.stride, self.info_engine.position_offset)
+            shader.vertex_attribute('a_tex', self.info_engine.tex_size, self.info_engine.type, GL_FALSE,
+                                    self.info_engine.stride, self.info_engine.tex_offset)
+
+            glDrawArrays(GL_TRIANGLES, 0, self.info_engine.vertex_count)
+
+            self.info_engine.end()
+            self.world.activate_shader(None)
+            glDisable(GL_BLEND)
+
+            ortho(width, height)
             with glRestore(GL_CURRENT_BIT | GL_LINE_BIT):
                 glLineWidth(2)
                 cx, cy = width / 2, height / 2
diff --git a/punyverse/world.json b/punyverse/world.json
index 0fac145..5807862 100644
--- a/punyverse/world.json
+++ b/punyverse/world.json
@@ -312,6 +312,7 @@
     "yaw": -97
   },
   "asteroids": ["asteroids/01.obj", "asteroids/02.obj", "asteroids/03.obj"],
+  "font": "font.png",
   "start": {
     "z": "AU - 400",
     "yaw": 180
diff --git a/punyverse/world.py b/punyverse/world.py
index 0297f86..f48549e 100644
--- a/punyverse/world.py
+++ b/punyverse/world.py
@@ -24,6 +24,7 @@ class World(object):
         'star': ('star.vertex.glsl', 'star.fragment.glsl'),
         'ring': ('ring.vertex.glsl', 'ring.fragment.glsl'),
         'atmosphere': ('atmosphere.vertex.glsl', 'atmosphere.fragment.glsl'),
+        'text': ('text.vertex.glsl', 'text.fragment.glsl'),
     }
 
     def __init__(self, file, callback):
@@ -133,6 +134,8 @@ class World(object):
                 self.callback('Loading asteroids...', 'Loading %s...' % file, i / len(asteroids))
                 self.asteroids.load(file)
 
+        self.font_tex = load_alpha_mask(root['font'], clamp=True)
+
     def _body(self, name, info, parent=None):
         if 'texture' in info:
             body = SphericalBody(name, self, info, parent)