# HG changeset patch # User Emmanuel Gil Peyrot # Date 1343407428 -7200 # Node ID 74471afbac377ab08604511cd921303d2002da43 # Parent f305cdd6f6c5e29baa3812c2b7b9ca799af559a7 Add a programmable pipeline renderer, and a --fixed-pipeline switch to use the old one. diff --git a/eosd b/eosd --- 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) diff --git a/pytouhou/ui/gamerenderer.pyx b/pytouhou/ui/gamerenderer.pyx --- 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) diff --git a/pytouhou/ui/gamerunner.py b/pytouhou/ui/gamerunner.py --- 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] diff --git a/pytouhou/ui/renderer.pxd b/pytouhou/ui/renderer.pxd --- 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) diff --git a/pytouhou/ui/renderer.pyx b/pytouhou/ui/renderer.pyx --- 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.)) diff --git a/pytouhou/ui/shader.py b/pytouhou/ui/shader.py 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) + diff --git a/pytouhou/ui/shaders/__init__.py b/pytouhou/ui/shaders/__init__.py new file mode 100644 diff --git a/pytouhou/ui/shaders/eosd.py b/pytouhou/ui/shaders/eosd.py 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 +## +## 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; + } + ''']) diff --git a/pytouhou/utils/matrix.pyx b/pytouhou/utils/matrix.pyx --- 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): diff --git a/pytouhou/utils/vector.py b/pytouhou/utils/vector.py new file mode 100644 --- /dev/null +++ b/pytouhou/utils/vector.py @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## 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)