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..a75fc5d 100644 --- a/punyverse/ui.py +++ b/punyverse/ui.py @@ -54,8 +54,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 +125,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 +144,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 +215,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 +263,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 +278,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)