changeset 370:74471afbac37

Add a programmable pipeline renderer, and a --fixed-pipeline switch to use the old one.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 27 Jul 2012 18:43:48 +0200
parents f305cdd6f6c5
children 6702bc0215dc
files eosd pytouhou/ui/gamerenderer.pyx pytouhou/ui/gamerunner.py pytouhou/ui/renderer.pxd pytouhou/ui/renderer.pyx pytouhou/ui/shader.py pytouhou/ui/shaders/__init__.py pytouhou/ui/shaders/eosd.py pytouhou/utils/matrix.pyx pytouhou/utils/vector.py
diffstat 10 files changed, 416 insertions(+), 35 deletions(-) [+]
line wrap: on
line diff
--- a/eosd
+++ b/eosd
@@ -61,7 +61,7 @@ class EoSDGameBossRush(EoSDGame):
 
 
 
-def main(path, data, stage_num, rank, character, replay, boss_rush, fps_limit, single_buffer, debug):
+def main(path, data, stage_num, rank, character, replay, boss_rush, fps_limit, single_buffer, debug, fixed_pipeline):
     resource_loader = Loader(path)
 
     try:
@@ -94,7 +94,7 @@ def main(path, data, stage_num, rank, ch
 
     game_class = EoSDGameBossRush if boss_rush else EoSDGame
 
-    runner = GameRunner(resource_loader, fps_limit=fps_limit, double_buffer=(not single_buffer))
+    runner = GameRunner(resource_loader, fps_limit=fps_limit, double_buffer=(not single_buffer), fixed_pipeline=fixed_pipeline)
     while True:
         if replay:
             level = replay.levels[stage_num - 1]
@@ -160,9 +160,10 @@ parser.add_argument('-b', '--boss-rush',
 parser.add_argument('--single-buffer', action='store_true', help='Disable double buffering')
 parser.add_argument('--fps-limit', metavar='FPS', default=60, type=int, help='Set fps limit')
 parser.add_argument('--debug', action='store_true', help='Set unlimited continues, and perhaps other debug features.')
+parser.add_argument('--fixed-pipeline', action='store_true', help='Use the fixed pipeline instead of the new programmable one.')
 
 args = parser.parse_args()
 
 main(args.path, tuple(args.data), args.stage, args.rank, args.character,
      args.replay, args.boss_rush, args.fps_limit, args.single_buffer,
-     args.debug)
+     args.debug, args.fixed_pipeline)
--- a/pytouhou/ui/gamerenderer.pyx
+++ b/pytouhou/ui/gamerenderer.pyx
@@ -17,6 +17,8 @@ from itertools import chain
 
 from pyglet.gl import *
 
+from pytouhou.utils.matrix import Matrix
+
 from .renderer cimport Renderer
 from .background cimport get_background_rendering_data
 
@@ -49,13 +51,25 @@ cdef class GameRenderer(Renderer):
         game = self.game
         texture_manager = self.texture_manager
 
-        if game is not None and game.spellcard_effect is not None:
-            self.setup_camera(0, 0, 1)
+        if self.use_fixed_pipeline:
+            glMatrixMode(GL_PROJECTION)
+            glLoadIdentity()
 
-            glDisable(GL_FOG)
+        if game is not None and game.spellcard_effect is not None:
+            if self.use_fixed_pipeline:
+                glMatrixMode(GL_MODELVIEW)
+                glLoadMatrixf(self.game_mvp.get_c_data())
+                glDisable(GL_FOG)
+            else:
+                self.game_shader.bind()
+                self.game_shader.uniform_matrixf('mvp', self.game_mvp.get_c_data())
+
             self.render_elements([game.spellcard_effect])
-            glEnable(GL_FOG)
         elif back is not None:
+            if self.use_fixed_pipeline:
+                glEnable(GL_FOG)
+            else:
+                self.background_shader.bind()
             fog_b, fog_g, fog_r, fog_start, fog_end = back.fog_interpolator.values
             x, y, z = back.position_interpolator.values
             dx, dy, dz = back.position2_interpolator.values
@@ -65,8 +79,18 @@ cdef class GameRenderer(Renderer):
             glFogf(GL_FOG_END,  fog_end)
             glFogfv(GL_FOG_COLOR, (GLfloat * 4)(fog_r / 255., fog_g / 255., fog_b / 255., 1.))
 
-            self.setup_camera(dx, dy, dz)
-            glTranslatef(-x, -y, -z)
+            model = Matrix()
+            model.data[3] = [-x, -y, -z, 1]
+            view = self.setup_camera(dx, dy, dz)
+            model_view = model * view
+            model_view_projection = model * view * self.proj
+
+            if self.use_fixed_pipeline:
+                glMatrixMode(GL_MODELVIEW)
+                glLoadMatrixf(model_view_projection.get_c_data())
+            else:
+                self.background_shader.uniform_matrixf('model_view', model_view.get_c_data())
+                self.background_shader.uniform_matrixf('projection', self.proj.get_c_data())
 
             glEnable(GL_DEPTH_TEST)
             for (texture_key, blendfunc), (nb_vertices, vertices, uvs, colors) in get_background_rendering_data(back):
@@ -81,9 +105,14 @@ cdef class GameRenderer(Renderer):
             glClear(GL_COLOR_BUFFER_BIT)
 
         if game is not None:
-            self.setup_camera(0, 0, 1)
+            if self.use_fixed_pipeline:
+                glMatrixMode(GL_MODELVIEW)
+                glLoadMatrixf(self.game_mvp.get_c_data())
+                glDisable(GL_FOG)
+            else:
+                self.game_shader.bind()
+                self.game_shader.uniform_matrixf('mvp', self.game_mvp.get_c_data())
 
-            glDisable(GL_FOG)
             self.render_elements(chain(*(enemy.objects() for enemy in game.enemies if enemy.visible)))
             self.render_elements(enemy for enemy in game.enemies if enemy.visible)
             self.render_elements(game.effects)
@@ -96,5 +125,4 @@ cdef class GameRenderer(Renderer):
                                        game.cancelled_bullets, game.items,
                                        (item.indicator for item in game.items if item.indicator),
                                        *(label.objects() for label in game.labels)))
-            glEnable(GL_FOG)
 
--- a/pytouhou/ui/gamerunner.py
+++ b/pytouhou/ui/gamerunner.py
@@ -18,7 +18,7 @@ from itertools import chain
 
 from pyglet.gl import (glMatrixMode, glLoadIdentity, glEnable, glDisable,
                        glHint, glEnableClientState, glViewport, glScissor,
-                       gluPerspective, gluOrtho2D,
+                       glLoadMatrixf,
                        GL_MODELVIEW, GL_PROJECTION,
                        GL_TEXTURE_2D, GL_BLEND, GL_FOG,
                        GL_PERSPECTIVE_CORRECTION_HINT, GL_FOG_HINT, GL_NICEST,
@@ -26,16 +26,18 @@ from pyglet.gl import (glMatrixMode, glL
                        GL_SCISSOR_TEST)
 
 from pytouhou.utils.helpers import get_logger
+from pytouhou.utils.matrix import Matrix
 
 from .gamerenderer import GameRenderer
 from .music import MusicPlayer, SFXPlayer
+from .shaders.eosd import GameShader, BackgroundShader
 
 
 logger = get_logger(__name__)
 
 
 class GameRunner(pyglet.window.Window, GameRenderer):
-    def __init__(self, resource_loader, game=None, background=None, replay=None, double_buffer=True, fps_limit=60):
+    def __init__(self, resource_loader, game=None, background=None, replay=None, double_buffer=True, fps_limit=60, fixed_pipeline=False):
         GameRenderer.__init__(self, resource_loader, game, background)
 
         config = pyglet.gl.Config(double_buffer=double_buffer)
@@ -45,8 +47,14 @@ class GameRunner(pyglet.window.Window, G
                                       config=config)
 
         self.fps_limit = fps_limit
+        self.use_fixed_pipeline = fixed_pipeline
         self.replay_level = None
 
+        if not self.use_fixed_pipeline:
+            self.game_shader = GameShader()
+            self.background_shader = BackgroundShader()
+            self.interface_shader = self.game_shader
+
         if game:
             self.load_game(game, background, replay)
 
@@ -83,13 +91,18 @@ class GameRunner(pyglet.window.Window, G
         # Initialize OpenGL
         glEnable(GL_BLEND)
         glEnable(GL_TEXTURE_2D)
-        glEnable(GL_FOG)
         glHint(GL_FOG_HINT, GL_NICEST)
         glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
         glEnableClientState(GL_COLOR_ARRAY)
         glEnableClientState(GL_VERTEX_ARRAY)
         glEnableClientState(GL_TEXTURE_COORD_ARRAY)
 
+        self.proj = self.perspective(30, float(self.game.width) / float(self.game.height),
+                                     101010101./2010101., 101010101./10101.)
+        game_view = self.setup_camera(0, 0, 1)
+        self.game_mvp = game_view * self.proj
+        self.interface_mvp = self.ortho_2d(0., float(self.width), float(self.height), 0.)
+
         if self.fps_limit > 0:
             pyglet.clock.set_fps_limit(self.fps_limit)
         while not self.has_exit:
@@ -163,10 +176,6 @@ class GameRunner(pyglet.window.Window, G
         glViewport(x, y, self.game.width, self.game.height)
         glScissor(x, y, self.game.width, self.game.height)
         glEnable(GL_SCISSOR_TEST)
-        glMatrixMode(GL_PROJECTION)
-        glLoadIdentity()
-        gluPerspective(30, float(self.game.width) / float(self.game.height),
-                       101010101./2010101., 101010101./10101.)
 
         GameRenderer.render(self)
 
@@ -174,15 +183,16 @@ class GameRunner(pyglet.window.Window, G
 
 
     def render_interface(self):
-        # Interface
         interface = self.game.interface
         interface.labels['framerate'].set_text('%.2ffps' % self.clock.get_fps())
 
-        glMatrixMode(GL_PROJECTION)
-        glLoadIdentity()
-        glMatrixMode(GL_MODELVIEW)
-        glLoadIdentity()
-        gluOrtho2D(0., float(self.width), float(self.height), 0.)
+        if self.use_fixed_pipeline:
+            glMatrixMode(GL_MODELVIEW)
+            glLoadMatrixf(self.interface_mvp.get_c_data())
+            glDisable(GL_FOG)
+        else:
+            self.interface_shader.bind()
+            self.interface_shader.uniform_matrixf('mvp', self.interface_mvp.get_c_data())
         glViewport(0, 0, self.width, self.height)
 
         items = [item for item in interface.items if item.anmrunner and item.anmrunner.running]
--- a/pytouhou/ui/renderer.pxd
+++ b/pytouhou/ui/renderer.pxd
@@ -9,4 +9,7 @@ cdef class Renderer:
     cdef Vertex *vertex_buffer
 
     cpdef render_elements(self, elements)
+    cpdef ortho_2d(self, left, right, bottom, top)
+    cpdef look_at(self, eye, center, up)
+    cpdef perspective(self, fovy, aspect, zNear, zFar)
     cpdef setup_camera(self, dx, dy, dz)
--- a/pytouhou/ui/renderer.pyx
+++ b/pytouhou/ui/renderer.pyx
@@ -13,6 +13,8 @@
 ##
 
 from libc.stdlib cimport malloc, free
+from libc.math cimport tan
+from math import radians
 
 import ctypes
 
@@ -22,6 +24,8 @@ from pyglet.gl import *
 
 from .sprite cimport get_sprite_rendering_data
 from .texture cimport TextureManager
+from pytouhou.utils.matrix cimport Matrix
+from pytouhou.utils.vector import Vector, normalize, cross, dot
 
 
 MAX_ELEMENTS = 640*4*3
@@ -83,14 +87,54 @@ cdef class Renderer:
             glDrawElements(GL_QUADS, nb_indices, GL_UNSIGNED_SHORT, indices)
 
 
+    cpdef ortho_2d(self, left, right, bottom, top):
+        mat = Matrix()
+        mat[0][0] = 2 / (right - left)
+        mat[1][1] = 2 / (top - bottom)
+        mat[2][2] = -1
+        mat[3][0] = -(right + left) / (right - left)
+        mat[3][1] = -(top + bottom) / (top - bottom)
+        return mat
+
+
+    cpdef look_at(self, eye, center, up):
+        eye = Vector(eye)
+        center = Vector(center)
+        up = Vector(up)
+
+        f = normalize(center - eye)
+        u = normalize(up)
+        s = normalize(cross(f, u))
+        u = cross(s, f)
+
+        return Matrix([[s[0], u[0], -f[0], 0],
+                       [s[1], u[1], -f[1], 0],
+                       [s[2], u[2], -f[2], 0],
+                       [-dot(s, eye), -dot(u, eye), dot(f, eye), 1]])
+
+
+    cpdef perspective(self, fovy, aspect, z_near, z_far):
+        top = tan(radians(fovy / 2)) * z_near
+        bottom = -top
+        left = -top * aspect
+        right = top * aspect
+
+        mat = Matrix()
+        mat[0][0] = (2 * z_near) / (right - left)
+        mat[1][1] = (2 * z_near) / (top - bottom)
+        mat[2][2] = -(z_far + z_near) / (z_far - z_near)
+        mat[2][3] = -1
+        mat[3][2] = -(2 * z_far * z_near) / (z_far - z_near)
+        mat[3][3] = 0
+        return mat
+
+
     cpdef setup_camera(self, dx, dy, dz):
-            glMatrixMode(GL_MODELVIEW)
-            glLoadIdentity()
-            # Some explanations on the magic constants:
-            # 192. = 384. / 2. = width / 2.
-            # 224. = 448. / 2. = height / 2.
-            # 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2))
-            # This is so that objects on the (O, x, y) plane use pixel coordinates
-            gluLookAt(192., 224., - 835.979370 * dz,
-                      192. + dx, 224. - dy, 0., 0., -1., 0.)
+        # Some explanations on the magic constants:
+        # 192. = 384. / 2. = width / 2.
+        # 224. = 448. / 2. = height / 2.
+        # 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2))
+        # This is so that objects on the (O, x, y) plane use pixel coordinates
+        return self.look_at((192., 224., - 835.979370 * dz),
+                            (192. + dx, 224. - dy, 0.), (0., -1., 0.))
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/shader.py
@@ -0,0 +1,153 @@
+#
+# Copyright Tristam Macdonald 2008.
+# Copyright Emmanuel Gil Peyrot 2012.
+#
+# Distributed under the Boost Software License, Version 1.0
+# (see http://www.boost.org/LICENSE_1_0.txt)
+#
+# Source: https://swiftcoder.wordpress.com/2008/12/19/simple-glsl-wrapper-for-pyglet/
+#
+
+from pyglet.gl import *
+
+
+class GLSLException(Exception):
+    pass
+
+
+class Shader(object):
+    # vert and frag take arrays of source strings the arrays will be
+    # concattenated into one string by OpenGL
+    def __init__(self, vert=None, frag=None):
+        # create the program handle
+        self.handle = glCreateProgram()
+        # we are not linked yet
+        self.linked = False
+
+        # cache the uniforms location
+        self.location_cache = {}
+
+        # create the vertex shader
+        self.createShader(vert, GL_VERTEX_SHADER)
+        # create the fragment shader
+        self.createShader(frag, GL_FRAGMENT_SHADER)
+
+        # attempt to link the program
+        self.link()
+
+    def load_source(self, path):
+        with open(path, 'rb') as file:
+            source = file.read()
+        return source
+
+    def createShader(self, strings, type):
+        count = len(strings)
+        # if we have no source code, ignore this shader
+        if count < 1:
+            return
+
+        # create the shader handle
+        shader = glCreateShader(type)
+
+        # convert the source strings into a ctypes pointer-to-char array, and upload them
+        # this is deep, dark, dangerous black magick - don't try stuff like this at home!
+        src = (c_char_p * count)(*strings)
+        glShaderSource(shader, count, cast(byref(src), POINTER(POINTER(c_char))), None)
+
+        # compile the shader
+        glCompileShader(shader)
+
+        temp = c_int(0)
+        # retrieve the compile status
+        glGetShaderiv(shader, GL_COMPILE_STATUS, byref(temp))
+
+        # if compilation failed, print the log
+        if not temp:
+            # retrieve the log length
+            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, byref(temp))
+            # create a buffer for the log
+            buffer = create_string_buffer(temp.value)
+            # retrieve the log text
+            glGetShaderInfoLog(shader, temp, None, buffer)
+            # print the log to the console
+            raise GLSLException(buffer.value)
+        else:
+            # all is well, so attach the shader to the program
+            glAttachShader(self.handle, shader);
+
+    def link(self):
+        # link the program
+        glLinkProgram(self.handle)
+
+        temp = c_int(0)
+        # retrieve the link status
+        glGetProgramiv(self.handle, GL_LINK_STATUS, byref(temp))
+
+        # if linking failed, print the log
+        if not temp:
+            #   retrieve the log length
+            glGetProgramiv(self.handle, GL_INFO_LOG_LENGTH, byref(temp))
+            # create a buffer for the log
+            buffer = create_string_buffer(temp.value)
+            # retrieve the log text
+            glGetProgramInfoLog(self.handle, temp, None, buffer)
+            # print the log to the console
+            raise GLSLException(buffer.value)
+        else:
+            # all is well, so we are linked
+            self.linked = True
+
+    def bind(self):
+        # bind the program
+        glUseProgram(self.handle)
+
+    @classmethod
+    def unbind(self):
+        # unbind whatever program is currently bound
+        glUseProgram(0)
+
+    def get_uniform_location(self, name):
+        try:
+            return self.location_cache[name]
+        except KeyError:
+            loc = glGetUniformLocation(self.handle, name)
+            if loc == -1:
+                raise GLSLException #TODO
+            self.location_cache[name] = loc
+            return loc
+
+    # upload a floating point uniform
+    # this program must be currently bound
+    def uniformf(self, name, *vals):
+        # check there are 1-4 values
+        if len(vals) in range(1, 5):
+            # select the correct function
+            { 1 : glUniform1f,
+                2 : glUniform2f,
+                3 : glUniform3f,
+                4 : glUniform4f
+                # retrieve the uniform location, and set
+            }[len(vals)](self.get_uniform_location(name), *vals)
+
+    # upload an integer uniform
+    # this program must be currently bound
+    def uniformi(self, name, *vals):
+        # check there are 1-4 values
+        if len(vals) in range(1, 5):
+            # select the correct function
+            { 1 : glUniform1i,
+                2 : glUniform2i,
+                3 : glUniform3i,
+                4 : glUniform4i
+                # retrieve the uniform location, and set
+            }[len(vals)](self.get_uniform_location(name), *vals)
+
+    # upload a uniform matrix
+    # works with matrices stored as lists,
+    # as well as euclid matrices
+    def uniform_matrixf(self, name, mat):
+        # obtian the uniform location
+        loc = self.get_uniform_location(name)
+        # uplaod the 4x4 floating point matrix
+        glUniformMatrix4fv(loc, 1, False, mat)
+
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/shaders/eosd.py
@@ -0,0 +1,78 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+
+from pytouhou.ui.shader import Shader
+
+
+class GameShader(Shader):
+    def __init__(self):
+        Shader.__init__(self, ['''
+            #version 120
+
+            uniform mat4 mvp;
+
+            void main()
+            {
+                gl_Position = mvp * gl_Vertex;
+                gl_FrontColor = gl_Color;
+                gl_TexCoord[0] = gl_MultiTexCoord0;
+            }
+        '''], [ '''
+            #version 120
+
+            uniform sampler2D color_map;
+
+            void main()
+            {
+                gl_FragColor = texture2D(color_map, gl_TexCoord[0].st) * gl_Color;
+            }
+        '''])
+
+
+class BackgroundShader(Shader):
+    def __init__(self):
+        Shader.__init__(self, ['''
+            #version 120
+
+            uniform mat4 model_view;
+            uniform mat4 projection;
+
+            varying float fog_density;
+
+            void main()
+            {
+                gl_Position = model_view * gl_Vertex;
+                gl_FrontColor = gl_Color;
+                gl_TexCoord[0] = gl_MultiTexCoord0;
+
+                float fog_position = -gl_Position.z / gl_Position.w;
+                fog_density = clamp((gl_Fog.end - fog_position) * gl_Fog.scale, 0.0f, 1.0f);
+
+                gl_Position = projection * gl_Position;
+            }
+        '''], [ '''
+            #version 120
+
+            uniform sampler2D color_map;
+
+            varying float fog_density;
+
+            void main()
+            {
+                vec4 color = texture2D(color_map, gl_TexCoord[0].st) * gl_Color;
+                gl_FragColor = mix(gl_Fog.color, color, fog_density);
+                gl_FragColor.w = color.w;
+            }
+        '''])
--- a/pytouhou/utils/matrix.pyx
+++ b/pytouhou/utils/matrix.pyx
@@ -13,11 +13,32 @@
 ##
 
 from libc.math cimport sin, cos
+from ctypes import c_float
 
 
 cdef class Matrix:
     def __init__(Matrix self, data=None):
-        self.data = data or [[0] * 4 for i in xrange(4)]
+        self.data = data or [[1, 0, 0, 0],
+                             [0, 1, 0, 0],
+                             [0, 0, 1, 0],
+                             [0, 0, 0, 1]]
+
+
+    def __getitem__(Matrix self, key):
+        return self.data[key]
+
+
+    def __mul__(Matrix self, Matrix other):
+        out = Matrix()
+        for i in xrange(4):
+            for j in xrange(4):
+                out[i][j] = sum(self[i][k] * other[k][j] for k in xrange(4))
+        return out
+
+
+    def get_c_data(Matrix self):
+        data = sum(self.data, [])
+        return (c_float * 16)(*data)
 
 
     cpdef flip(Matrix self):
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/vector.py
@@ -0,0 +1,43 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+from math import sqrt
+
+
+class Vector(list):
+    def __init__(self, data=None):
+        list.__init__(self, data or [0] * 3)
+
+
+    def __add__(self, other):
+        return Vector([a+b for a, b in zip(self, other)])
+
+
+    def __sub__(self, other):
+        return Vector([a-b for a, b in zip(self, other)])
+
+
+def cross(vec1, vec2):
+    return Vector([vec1[1] * vec2[2] - vec2[1] * vec1[2],
+                   vec1[2] * vec2[0] - vec2[2] * vec1[0],
+                   vec1[0] * vec2[1] - vec2[0] * vec1[1]])
+
+
+def dot(vec1, vec2):
+    return vec1[0] * vec2[0] + vec2[1] * vec1[1] + vec1[2] * vec2[2]
+
+
+def normalize(vec1):
+    normal = 1 / sqrt(vec1[0] * vec1[0] + vec1[1] * vec1[1] + vec1[2] * vec1[2])
+    return Vector(x * normal for x in vec1)