From 12d1fcd2fa8b7e029cd59c7bafdae28fbcbf2673 Mon Sep 17 00:00:00 2001 From: josch Date: Sat, 21 Jun 2014 14:41:07 +0200 Subject: [PATCH] cleanup --- hr.py | 7 +- hr_stable7_bigtex-nosplitanim-depthbuffer.py | 734 +++++ hr_stable7_bigtex-nosplitanim-new.py | 727 +++++ hr_stable7_bigtex-nosplitanim-overlay.py | 727 +++++ hr_stable7_bigtex-nosplitanim-rabbyt.py | 684 ++++ hr_stable7_bigtex-nosplitanim.py | 720 +++++ lib/batch.py | 1598 ++++++++++ lib/batch.py_ | 2979 ++++++++++++++++++ lib/batch.pyx | 1573 +++++++++ lib/blit_into.pyx | 28 + lib/include_gl.h | 11 + lib/mapset.py | 21 +- lib/mapview.py | 2 + setup.py | 9 + test-html.py | 13 + test.html | 1465 +++++++++ testtest.py | 110 + 17 files changed, 11392 insertions(+), 16 deletions(-) create mode 100644 hr_stable7_bigtex-nosplitanim-depthbuffer.py create mode 100644 hr_stable7_bigtex-nosplitanim-new.py create mode 100644 hr_stable7_bigtex-nosplitanim-overlay.py create mode 100644 hr_stable7_bigtex-nosplitanim-rabbyt.py create mode 100644 hr_stable7_bigtex-nosplitanim.py create mode 100644 lib/batch.py create mode 100644 lib/batch.py_ create mode 100644 lib/batch.pyx create mode 100644 lib/blit_into.pyx create mode 100644 lib/include_gl.h create mode 100644 setup.py create mode 100644 test-html.py create mode 100644 test.html create mode 100755 testtest.py diff --git a/hr.py b/hr.py index 4762734..9b6e1f0 100644 --- a/hr.py +++ b/hr.py @@ -45,15 +45,14 @@ class LoadScreen(object): self.window.push_handlers(interface) if __name__ == '__main__': + pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, + pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) + window = Window(width=1024, height=768) if len(sys.argv) < 2: sys.exit("specify the map you want to load from the map folder\nusage: python hr.py \"A Viking We Shall Go\"") if not os.path.exists(os.path.join(pyglet.resource._default_loader._script_home,"maps","%s.h3m" % sys.argv[1])): sys.exit("cannot find file %s" % os.path.join(pyglet.resource._default_loader._script_home,"maps","%s.h3m" % sys.argv[1])) - - pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, - pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) - window = Window(width=1024, height=768) window.push_handlers(LoadScreen(window, sys.argv[1])) img = pyglet.resource.image("data/cursors/cradvntr.def/0.png") window.set_mouse_cursor(pyglet.window.ImageMouseCursor(img, 0, 40)) diff --git a/hr_stable7_bigtex-nosplitanim-depthbuffer.py b/hr_stable7_bigtex-nosplitanim-depthbuffer.py new file mode 100644 index 0000000..611f109 --- /dev/null +++ b/hr_stable7_bigtex-nosplitanim-depthbuffer.py @@ -0,0 +1,734 @@ +#!/usr/bin/python +""" + copyright 2008 - Johannes 'josch' Schauer + + 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, either version 3 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import pyglet + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + import demjson as json + json.loads = json.decode + json.dumps = json.encode + +IF_BOTTOM = 48 +IF_RIGHT = 200 +IF_TOP = IF_LEFT = 8 + +class Animation(object): + def __init__(self, frames): + self.__frames = frames + self.__animation = 0 + self.width = frames[0].width + self.height = frames[0].height + self.z = frames[0].z + + def next_frame(self): + self.__animation = (self.__animation+1)%len(self.__frames) + + def get_tex_coords(self): + return self.__frames[self.__animation].tex_coords + + tex_coords = property(get_tex_coords) + + def get_group(self): + return self.__frames[self.__animation].group + + group = property(get_group) + +class MapSet(object): + def load_map_object(self, file, order=0): + image = pyglet.image.load(None, file=pyglet.resource.file(file)) + try: + texture_region = self.current_atlas.add(image) + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + texture_region = self.current_atlas.add(image) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture) + + if group not in self.groups: + self.groups.append(group) + + texture_region.group = self.groups.index(group) + texture_region.z = order + return texture_region + + def __init__(self, loaded_map, objects, tunedobj): + self.width = len(loaded_map[0]) + self.height = len(loaded_map) + + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + + self.groups = [] + + self.tiles = {} + tile_textures = {} + for y, line in enumerate(loaded_map): + for x, tile in enumerate(line): + if tile[0] == -1: #edge + if "edg" not in tile_textures.keys(): + tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 100) for i in xrange(36)] + self.tiles[x,y] = [tile_textures["edg"][tile[1]]] + elif tile[0] == 0: #dirt + if "dirttl" not in tile_textures.keys(): + tile_textures["dirttl"] = [self.load_map_object('data/advmap_tiles/dirttl.def/%d.png'%i, 0) for i in xrange(46)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["dirttl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirttl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["dirttl"][tile[1]]] + elif tile[0] == 1: #sand + if "sandtl" not in tile_textures.keys(): + tile_textures["sandtl"] = [self.load_map_object('data/advmap_tiles/sandtl.def/%d.png'%i, 0) for i in xrange(24)] + self.tiles[x,y] = [tile_textures["sandtl"][tile[1]]] + elif tile[0] == 2: #grass + if "grastl" not in tile_textures.keys(): + tile_textures["grastl"] = [self.load_map_object('data/advmap_tiles/grastl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["grastl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["grastl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["grastl"][tile[1]]] + elif tile[0] == 3: #snow + if "snowtl" not in tile_textures.keys(): + tile_textures["snowtl"] = [self.load_map_object('data/advmap_tiles/snowtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["snowtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["snowtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["snowtl"][tile[1]]] + elif tile[0] == 4: #swamp + if "swmptl" not in tile_textures.keys(): + tile_textures["swmptl"] = [self.load_map_object('data/advmap_tiles/swmptl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["swmptl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["swmptl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["swmptl"][tile[1]]] + elif tile[0] == 5: #rough + if "rougtl" not in tile_textures.keys(): + tile_textures["rougtl"] = [self.load_map_object('data/advmap_tiles/rougtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["rougtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["rougtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["rougtl"][tile[1]]] + elif tile[0] == 7: #lava + if "lavatl" not in tile_textures.keys(): + tile_textures["lavatl"] = [self.load_map_object('data/advmap_tiles/lavatl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["lavatl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["lavatl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["lavatl"][tile[1]]] + elif tile[0] == 8: #water + if "watrtl" not in tile_textures.keys(): + tile_textures["watrtl"] = [] + for j in xrange(33): + tile_textures["watrtl"].append([self.load_map_object('data/advmap_tiles/watrtl.def/%d/%d.png'%(j,i), 0) for i in xrange(12)]) + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + tiles = [] + for watrtl in tile_textures["watrtl"][tile[1]]: + new = watrtl.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = watrtl.group + tiles.append(new) + self.tiles[x,y] = [Animation(tiles)] + else: + self.tiles[x,y] = [Animation(tile_textures["watrtl"][tile[1]])] + elif tile[0] == 9: #rock + if "rocktl" not in tile_textures.keys(): + tile_textures["rocktl"] = [self.load_map_object('data/advmap_tiles/rocktl.def/%d.png'%i, 0) for i in xrange(48)] + self.tiles[x,y] = [tile_textures["rocktl"][tile[1]]] + else: + raise NotImplementedError + + if tile[2] == 0: #no river + pass + elif tile[2] == 1: #clrrvr + if "clrrvr" not in tile_textures.keys(): + tile_textures["clrrvr"] = [[self.load_map_object('data/advmap_tiles/clrrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for clrrvr in tile_textures["clrrvr"][tile[3]]: + new = clrrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = clrrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["clrrvr"][tile[3]])) + elif tile[2] == 2: #icyrvr + if "icyrvr" not in tile_textures.keys(): + tile_textures["icyrvr"] = [self.load_map_object('data/advmap_tiles/icyrvr.def/%d.png'%i, 1) for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + new = tile_textures["icyrvr"][tile[3]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["icyrvr"][tile[3]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["icyrvr"][tile[3]]) + elif tile[2] == 3: #mudrvr + if "mudrvr" not in tile_textures.keys(): + tile_textures["mudrvr"] = [[self.load_map_object('data/advmap_tiles/mudrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for mudrvr in tile_textures["mudrvr"][tile[3]]: + new = mudrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = mudrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["mudrvr"][tile[3]])) + elif tile[2] == 4: #lavrvr + if "lavrvr" not in tile_textures.keys(): + tile_textures["lavrvr"] = [[self.load_map_object('data/advmap_tiles/lavrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(9)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for lavrvr in tile_textures["lavrvr"][tile[3]]: + new = lavrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = lavrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["lavrvr"][tile[3]])) + else: + raise NotImplementedError, tile[2] + + if tile[4] == 0: #no road + pass + elif tile[4] == 1: #dirtrd + if "dirtrd" not in tile_textures.keys(): + tile_textures["dirtrd"] = [self.load_map_object('data/advmap_tiles/dirtrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["dirtrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirtrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["dirtrd"][tile[5]]) + elif tile[4] == 2: #gravrd + if "gravrd" not in tile_textures.keys(): + tile_textures["gravrd"] = [self.load_map_object('data/advmap_tiles/gravrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["gravrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["gravrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["gravrd"][tile[5]]) + elif tile[4] == 3: #cobbrd + if "cobbrd" not in tile_textures.keys(): + tile_textures["cobbrd"] = [self.load_map_object('data/advmap_tiles/cobbrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["cobbrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["cobbrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["cobbrd"][tile[5]]) + else: + raise NotImplementedError, tile[4] + + images = [] + for order, obj in enumerate(objects): + imgs = [] + i = 0 + while 1: + imgs.append(pyglet.image.load(None, file=pyglet.resource.file("data/advmap_objects/"+obj["filename"]+"/%d.png"%i))) + i+=1 + if "data/advmap_objects/"+obj["filename"]+"/%d.png"%i not in pyglet.resource._default_loader._index.keys(): + break; + images.append((imgs, order)) + + self.objects = [] + for imgs in sorted(images, key=lambda i:i[0][0].height, reverse=True): + textures = [] + try: + textures = [self.current_atlas.add(img) for img in imgs[0]] + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + textures = [self.current_atlas.add(img) for img in imgs[0]] + group = pyglet.graphics.TextureGroup(self.current_atlas.texture) + if group not in self.groups: + self.groups.append(group) + group = self.groups.index(group) + for texture in textures: + texture.group = group + texture.z = 2 + self.objects.append((textures, imgs[1])) + + self.objects = [i[0] for i in sorted(self.objects, key=lambda i:i[1])] + + self.tunedobj = {} + for obj in [i for i in tunedobj if i["z"]==0]: + if len(self.objects[obj["id"]]) == 1: + self.tiles[obj["x"]+9,obj["y"]+8].append(self.objects[obj["id"]][0]) + else: + self.tiles[obj["x"]+9,obj["y"]+8].append(Animation(self.objects[obj["id"]])) + +class MapView(object): + def __init__(self, mapset, window): + self.window = window + self.mapset = mapset + + self._first_time_init() + self._init_view() + + #mouse position + self.label = pyglet.text.Label('', + font_name="", + font_size=36, + bold=True, + color=(128, 128, 128, 128), + x=self.window.width-10, y=0, + anchor_x='right', anchor_y='bottom') + + #pyglet.clock.schedule_interval(self.animate_water, 1/6.0) + pyglet.clock.schedule_interval(self.update, 1/60.0) + + def _first_time_init(self): + self.tile_size = 32 + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + #center map + self.global_x = (self.mapset.width*self.tile_size-self.viewport_x+self.tile_size)//2 + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2)+(self.tile_size//2) + + self.mouse_x = self.mouse_dx = 0 + self.mouse_y = self.mouse_dy = 0 + + def _init_view(self): + #step one tile + self.steps = self.tile_size + + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + + #center map when viewport is too large, else check if map still fills + #whole viewport and if not adjust position accordingly + self.center_x = False + if self.mapset.width*self.tile_size < self.viewport_x: + self.center_x = True + self.global_x = (self.mapset.width*self.tile_size)//2-(self.viewport_x//2) + elif self.global_x > self.tile_size*self.mapset.width-self.viewport_x: + self.global_x = self.tile_size*self.mapset.width-self.viewport_x + elif self.global_x < 0: + self.global_x = 0 + + self.center_y = False + if self.mapset.height*self.tile_size < self.viewport_y: + self.center_y = True + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2) + elif self.global_y > self.tile_size*self.mapset.height-self.viewport_y: + self.global_y = self.tile_size*self.mapset.height-self.viewport_y + elif self.global_y < 0: + self.global_y = 0 + + #drawn tiles + self.tiles_x = min((self.viewport_x//self.tile_size)+2, self.mapset.width) + self.tiles_y = min((self.viewport_y//self.tile_size)+2, self.mapset.height) + + #undrawn map size + self.undrawn_x = self.tile_size*(self.mapset.width-self.tiles_x) + self.undrawn_y = self.tile_size*(self.mapset.height-self.tiles_y) + #size of full undrawn steps + self.undrawn_steps_x = self.steps*(self.undrawn_x//self.steps) + self.undrawn_steps_y = self.steps*(self.undrawn_y//self.steps) + + self.batch = pyglet.graphics.Batch() + + self.view_x = 0 + self.view_y = 0 + self.dx = 0 + self.dy = 0 + + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + if trans_global_x < -self.undrawn_steps_x: + mod_x = trans_global_x+self.undrawn_x + elif trans_global_x < self.steps: + mod_x = trans_global_x%self.steps + else: + mod_x = trans_global_x + + if trans_global_y < -self.undrawn_steps_y: + mod_y = trans_global_y+self.undrawn_y + elif trans_global_y < self.steps: + mod_y = trans_global_y%self.steps + else: + mod_y = trans_global_y + + self.div_x = (trans_global_x-mod_x)//self.tile_size + self.div_y = (trans_global_y-mod_y)//self.tile_size+self.mapset.height-1 + + self.vl_objects = [None for i, value in enumerate(self.mapset.groups)] + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + +# for y in xrange(self.tiles_y-1, -6, -1): + for y in xrange(self.tiles_y): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + z = obj.z*0.01 + vertices[obj.group].extend([x1, y1, z, x2, y1, z, x2, y2, z, x1, y2, z]) + tex_coords[obj.group].extend(obj.tex_coords) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] != 0: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v3f', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + + def on_draw(self): + pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT | pyglet.gl.GL_DEPTH_BUFFER_BIT) + #pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT) + pyglet.gl.glEnable(pyglet.gl.GL_DEPTH_TEST) +# pyglet.gl.glDepthFunc(pyglet.gl.GL_ALWAYS) + pyglet.gl.glDepthMask(pyglet.gl.GL_TRUE); + +# pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + #pyglet.gl.glBlendFunc(pyglet.gl.GL_ONE_MINUS_DST_ALPHA, pyglet.gl.GL_DST_ALPHA) + pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) + + #pyglet.gl.glAlphaFunc(pyglet.gl.GL_NOTEQUAL, 0) + #pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST) + + pyglet.gl.glPushMatrix() + pyglet.gl.glTranslatef(self.view_x, self.view_y, 0) + pyglet.gl.glScalef(self.tile_size/32.0, self.tile_size/32.0, 1.0) + self.batch.draw() + pyglet.gl.glPopMatrix() + pyglet.gl.glLoadIdentity() +# pyglet.gl.glEnable(pyglet.gl.GL_BLEND) +# pyglet.gl.glColor4f(1, 0, 1, 1) +# pyglet.gl.glRectf(0, 0, self.window.width, IF_BOTTOM) +# pyglet.gl.glRectf(self.window.width-IF_RIGHT, 0, self.window.width, self.window.height) +# pyglet.gl.glRectf(0, self.window.height-IF_TOP, self.window.width, self.window.height) +# pyglet.gl.glRectf(0, 0, IF_LEFT, self.window.height) + self.label.draw() + + def _move(self, dx, dy): + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + new_global_x = trans_global_x+dx + new_global_y = trans_global_y+dy + + if self.global_x-dx < 0: + new_global_x = self.steps + if self.global_y-dy < 0: + new_global_y = self.steps + if dx-self.global_x < -self.tile_size*self.mapset.width+self.viewport_x: + new_global_x = -self.tile_size*self.mapset.width+self.viewport_x+self.steps + if dy-self.global_y < -self.tile_size*self.mapset.height+self.viewport_y: + new_global_y = -self.tile_size*self.mapset.height+self.viewport_y+self.steps + + retex = False + + if new_global_x < -self.undrawn_steps_x: + mod_x = new_global_x+self.undrawn_x + if trans_global_x >= -self.undrawn_steps_x: + retex = True + elif new_global_x < self.steps: + div_x, mod_x = divmod(new_global_x, self.steps) + retex = div_x != trans_global_x//self.steps or retex + else: + mod_x = new_global_x + + if new_global_y < -self.undrawn_steps_y: + mod_y = new_global_y+self.undrawn_y + if trans_global_y >= -self.undrawn_steps_y: + retex = True + elif new_global_y < self.steps: + div_y, mod_y = divmod(new_global_y, self.steps) + retex = div_y != trans_global_y//self.steps or retex + else: + mod_y = new_global_y + + if retex: + self.div_x = (new_global_x-mod_x)//self.tile_size + self.div_y = (new_global_y-mod_y)//self.tile_size+self.mapset.height-1 + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + tex_coords[obj.group].extend(obj.tex_coords) + z = -y*0.01 + vertices[obj.group].extend([x1, y1, z, x2, y1, z, x2, y2, z, x1, y2, z]) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] == 0: + if self.vl_objects[i] is None: + pass + else: + self.vl_objects[i].delete() + self.vl_objects[i] = None + else: + if self.vl_objects[i] is None: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v3f', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + else: + self.vl_objects[i].resize(count[i]) + self.vl_objects[i].tex_coords = tex_coords[i] + self.vl_objects[i].vertices = vertices[i] + self.vl_objects[i].colors = (255,255,255,255)*count[i] + + if not self.center_x: + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.global_x = self.steps-new_global_x + if not self.center_y: + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + self.global_y = self.steps-new_global_y + + def update(self, dt): + try: + if self.window.keys[pyglet.window.key.LCTRL] and \ + any([self.window.keys[pyglet.window.key.UP], + self.window.keys[pyglet.window.key.DOWN], + self.window.keys[pyglet.window.key.LEFT], + self.window.keys[pyglet.window.key.RIGHT]]): + + if self.window.keys[pyglet.window.key.LEFT]: + x = 1 + elif self.window.keys[pyglet.window.key.RIGHT]: + x = -1 + else: + x = 0 + + if self.window.keys[pyglet.window.key.UP]: + y = -1 + elif self.window.keys[pyglet.window.key.DOWN]: + y = 1 + else: + y = 0 + self.dx += x*8 + self.dy += y*8 + elif self.window.keys[pyglet.window.key.PLUS] and \ + self.tile_size < 32: + self.tile_size+=8 + self._init_view() + elif self.window.keys[pyglet.window.key.MINUS] and \ + self.tile_size > 16: + self.tile_size-=8 + self._init_view() + except KeyError: + pass + if self.dx or self.dy: + self._move(self.dx, self.dy) + self.dx = 0 + self.dy = 0 + #mouse position: + if self.mouse_x != self.mouse_dx or self.mouse_y != self.mouse_dy: + self.mouse_x = self.mouse_dx + self.mouse_y = self.mouse_dy + x = (self.mouse_x-IF_LEFT-self.view_x + -(self.tile_size-32)//4)//self.tile_size + y = (self.mouse_y-IF_BOTTOM-self.view_y + -((self.tile_size-32)*3)//2)//self.tile_size + self.label.text = "%03d %03d"%(x-self.div_x, self.div_y-y) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.dx += dx + self.dy += dy + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_mouse_motion(self, x, y, dx, dy): + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_resize(self, width, height): + self._init_view() + + def animate_water(self, dt): + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + if isinstance(obj, Animation): + obj.next_frame() + tex_coords[obj.group].extend(obj.tex_coords) + for i, group in enumerate(self.mapset.groups): + if len(tex_coords[i]) != 0: + self.vl_objects[i].tex_coords = tex_coords[i] + +class Window(pyglet.window.Window): + def __init__(self, *args, **kwargs): + super(Window, self).__init__(1280, 1024, resizable=True, vsync=False) + self.keys = pyglet.window.key.KeyStateHandler() + self.push_handlers(self.keys) + self.fps = pyglet.clock.ClockDisplay() + pyglet.clock.schedule(lambda dt: None) + + def on_draw(self): + self.fps.draw() + + def on_key_press(self, symbol, modifiers): + if symbol == pyglet.window.key.F11: + self.set_fullscreen(fullscreen=not self.fullscreen) + elif symbol == pyglet.window.key.P: + pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png', encoder=PNGRGBEncoder()) + +class PNGRGBEncoder(pyglet.image.codecs.ImageEncoder): + def encode(self, image, file, filename): + import Image + image = image.get_image_data() + format = image.format + pitch = -(image.width * len(format)) + pil_image = Image.fromstring( + format, (image.width, image.height), image.get_data(format, pitch)) + try: + #.convert('P', palette=Image.WEB) + pil_image.convert("RGB").save(file) + except Exception, e: + raise ImageEncodeException(e) + + +class Interface(object): + def __init__(self, window): + self.window = window + + def on_mouse_motion(self, x, y, dx, dy): + if IF_LEFT < x < (self.window.width-IF_RIGHT): + pass + else: + return pyglet.event.EVENT_HANDLED + if IF_BOTTOM < y < (self.window.height-IF_TOP): + pass + else: + return pyglet.event.EVENT_HANDLED + +class LoadScreen(object): + def __init__(self, window): + self.window = window + self.label = pyglet.text.Label('', + font_name="Linux Libertine", + font_size=28, + x=self.window.width-10, y=10, + anchor_x='right', anchor_y='bottom') + + self.label.text = "PARSING MAP FILE..." + import lib.h3m as h3mlib + import os + h3m = h3mlib.extract(os.path.join(pyglet.resource._default_loader._script_home,"maps","A Warm and Familiar Place.h3m")) + self.label.text = "PARSING MAP FILE..." + edge_map = [[] for i in xrange(len(h3m["upper_terrain"])+16)] + for num in xrange(len(edge_map)): + if num < 7 or num > len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0])+18)]) + elif num == 7: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 16, 0, 0, 0, 0, 0]) + line.extend([[-1, 20+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 17, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + elif num == len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 19, 0, 0, 0, 0, 0]) + line.extend([[-1, 28+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 18, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + else: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 32+num%4, 0, 0, 0, 0, 0]) + line.extend(h3m["upper_terrain"][num-8]) + line.append([-1, 24+num%4, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + edge_map[num] = line + h3m["upper_terrain"] = edge_map + self.label.text = "INITIATING MAPSET..." + + mapset = MapSet(h3m["upper_terrain"], h3m["objects"], h3m["tunedobj"]) + self.label.text = "INITIATING MAPVIEW..." + mapview = MapView(mapset, self.window) + interface = Interface(self.window) + self.window.pop_handlers() + self.window.push_handlers(mapview) + self.window.push_handlers(interface) + self.window.push_handlers(self.window.keys) + +if __name__ == '__main__': + window = Window() + window.push_handlers(LoadScreen(window)) + img = pyglet.resource.image("data/cursors/cradvntr.def/0.png") + window.set_mouse_cursor(pyglet.window.ImageMouseCursor(img, 0, 40)) + pyglet.app.run() diff --git a/hr_stable7_bigtex-nosplitanim-new.py b/hr_stable7_bigtex-nosplitanim-new.py new file mode 100644 index 0000000..df36753 --- /dev/null +++ b/hr_stable7_bigtex-nosplitanim-new.py @@ -0,0 +1,727 @@ +#!/usr/bin/python +""" + copyright 2008 - Johannes 'josch' Schauer + + 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, either version 3 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import pyglet + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + import demjson as json + json.loads = json.decode + json.dumps = json.encode + +IF_BOTTOM = 48 +IF_RIGHT = 200 +IF_TOP = IF_LEFT = 8 + +class Animation(object): + def __init__(self, frames, group=None): + if group: + self.texgroup = group + else: + self.texgroup = frames[0].group + self.__frames = frames + self.__animation = 0 + self.width = frames[0].width + self.height = frames[0].height + + def next_frame(self): + self.__animation = (self.__animation+1)%len(self.__frames) + + def get_tex_coords(self): + return self.__frames[self.__animation].tex_coords + + tex_coords = property(get_tex_coords) + + def get_group(self): + return self.texgroup + + group = property(get_group) + +class MapSet(object): + def load_map_object(self, file, order=0): + image = pyglet.image.load(None, file=pyglet.resource.file(file)) + try: + texture_region = self.current_atlas.add(image) + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + texture_region = self.current_atlas.add(image) + ordered_group = pyglet.graphics.OrderedGroup(order) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture, ordered_group) + + if group not in self.groups: + self.groups.append(group) + + texture_region.group = self.groups.index(group) + return texture_region + + def __init__(self, loaded_map, objects, tunedobj): + self.width = len(loaded_map[0]) + self.height = len(loaded_map) + + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + + self.groups = [] + + self.tiles = {} + tile_textures = {} + for y, line in enumerate(loaded_map): + for x, tile in enumerate(line): + if tile[0] == -1: #edge + if "edg" not in tile_textures.keys(): + tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 100) for i in xrange(36)] + self.tiles[x,y] = [tile_textures["edg"][tile[1]]] + elif tile[0] == 0: #dirt + if "dirttl" not in tile_textures.keys(): + tile_textures["dirttl"] = [self.load_map_object('data/advmap_tiles/dirttl.def/%d.png'%i, 0) for i in xrange(46)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["dirttl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirttl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["dirttl"][tile[1]]] + elif tile[0] == 1: #sand + if "sandtl" not in tile_textures.keys(): + tile_textures["sandtl"] = [self.load_map_object('data/advmap_tiles/sandtl.def/%d.png'%i, 0) for i in xrange(24)] + self.tiles[x,y] = [tile_textures["sandtl"][tile[1]]] + elif tile[0] == 2: #grass + if "grastl" not in tile_textures.keys(): + tile_textures["grastl"] = [self.load_map_object('data/advmap_tiles/grastl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["grastl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["grastl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["grastl"][tile[1]]] + elif tile[0] == 3: #snow + if "snowtl" not in tile_textures.keys(): + tile_textures["snowtl"] = [self.load_map_object('data/advmap_tiles/snowtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["snowtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["snowtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["snowtl"][tile[1]]] + elif tile[0] == 4: #swamp + if "swmptl" not in tile_textures.keys(): + tile_textures["swmptl"] = [self.load_map_object('data/advmap_tiles/swmptl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["swmptl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["swmptl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["swmptl"][tile[1]]] + elif tile[0] == 5: #rough + if "rougtl" not in tile_textures.keys(): + tile_textures["rougtl"] = [self.load_map_object('data/advmap_tiles/rougtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["rougtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["rougtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["rougtl"][tile[1]]] + elif tile[0] == 7: #lava + if "lavatl" not in tile_textures.keys(): + tile_textures["lavatl"] = [self.load_map_object('data/advmap_tiles/lavatl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["lavatl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["lavatl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["lavatl"][tile[1]]] + elif tile[0] == 8: #water + if "watrtl" not in tile_textures.keys(): + tile_textures["watrtl"] = [] + for j in xrange(33): + tile_textures["watrtl"].append([self.load_map_object('data/advmap_tiles/watrtl.def/%d/%d.png'%(j,i), 0) for i in xrange(12)]) + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + tiles = [] + for watrtl in tile_textures["watrtl"][tile[1]]: + new = watrtl.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = watrtl.group + tiles.append(new) + self.tiles[x,y] = [Animation(tiles)] + else: + self.tiles[x,y] = [Animation(tile_textures["watrtl"][tile[1]])] + elif tile[0] == 9: #rock + if "rocktl" not in tile_textures.keys(): + tile_textures["rocktl"] = [self.load_map_object('data/advmap_tiles/rocktl.def/%d.png'%i, 0) for i in xrange(48)] + self.tiles[x,y] = [tile_textures["rocktl"][tile[1]]] + else: + raise NotImplementedError + + if tile[2] == 0: #no river + pass + elif tile[2] == 1: #clrrvr + if "clrrvr" not in tile_textures.keys(): + tile_textures["clrrvr"] = [[self.load_map_object('data/advmap_tiles/clrrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for clrrvr in tile_textures["clrrvr"][tile[3]]: + new = clrrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = clrrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["clrrvr"][tile[3]])) + elif tile[2] == 2: #icyrvr + if "icyrvr" not in tile_textures.keys(): + tile_textures["icyrvr"] = [self.load_map_object('data/advmap_tiles/icyrvr.def/%d.png'%i, 1) for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + new = tile_textures["icyrvr"][tile[3]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["icyrvr"][tile[3]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["icyrvr"][tile[3]]) + elif tile[2] == 3: #mudrvr + if "mudrvr" not in tile_textures.keys(): + tile_textures["mudrvr"] = [[self.load_map_object('data/advmap_tiles/mudrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for mudrvr in tile_textures["mudrvr"][tile[3]]: + new = mudrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = mudrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["mudrvr"][tile[3]])) + elif tile[2] == 4: #lavrvr + if "lavrvr" not in tile_textures.keys(): + tile_textures["lavrvr"] = [[self.load_map_object('data/advmap_tiles/lavrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(9)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for lavrvr in tile_textures["lavrvr"][tile[3]]: + new = lavrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = lavrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["lavrvr"][tile[3]])) + else: + raise NotImplementedError, tile[2] + + if tile[4] == 0: #no road + pass + elif tile[4] == 1: #dirtrd + if "dirtrd" not in tile_textures.keys(): + tile_textures["dirtrd"] = [self.load_map_object('data/advmap_tiles/dirtrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["dirtrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirtrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["dirtrd"][tile[5]]) + elif tile[4] == 2: #gravrd + if "gravrd" not in tile_textures.keys(): + tile_textures["gravrd"] = [self.load_map_object('data/advmap_tiles/gravrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["gravrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["gravrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["gravrd"][tile[5]]) + elif tile[4] == 3: #cobbrd + if "cobbrd" not in tile_textures.keys(): + tile_textures["cobbrd"] = [self.load_map_object('data/advmap_tiles/cobbrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["cobbrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["cobbrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["cobbrd"][tile[5]]) + else: + raise NotImplementedError, tile[4] + + images = [] + for order, obj in enumerate(objects): + imgs = [] + i = 0 + while 1: + imgs.append(pyglet.image.load(None, file=pyglet.resource.file("data/advmap_objects/"+obj["filename"]+"/%d.png"%i))) + i+=1 + if "data/advmap_objects/"+obj["filename"]+"/%d.png"%i not in pyglet.resource._default_loader._index.keys(): + break; + images.append((imgs, obj["class"], obj["overlay"], order)) + + self.objects = [] + for imgs in sorted(images, key=lambda i:i[0][0].height, reverse=True): + textures = [] + try: + textures = [self.current_atlas.add(img) for img in imgs[0]] + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + textures = [self.current_atlas.add(img) for img in imgs[0]] + for texture in textures: + texture.atlas = self.current_atlas + self.objects.append((textures, imgs[1], imgs[2], imgs[3])) + + self.objects = [(i[0], i[1], i[2]) for i in sorted(self.objects, key=lambda i:i[3])] + + self.tunedobj = {} + for obj in [i for i in tunedobj if i["z"]==0]: + order = obj["y"] + 8 + if self.objects[obj["id"]][2]: + order = order - self.objects[obj["id"]][0][0].height // 32 + order *= 2 + if self.objects[obj["id"]][1] in [119, 134, 135, 137, 155, 199]: + order -= 1 + group = pyglet.graphics.OrderedGroup(order) + group = pyglet.graphics.TextureGroup(self.objects[obj["id"]][0][0].atlas.texture, parent=group) + if group not in self.groups: + self.groups.append(group) + group = self.groups.index(group) + self.tiles[obj["x"]+9,obj["y"]+8].append(Animation(self.objects[obj["id"]][0], group)) + +class MapView(object): + def __init__(self, mapset, window): + self.window = window + self.mapset = mapset + + self._first_time_init() + self._init_view() + + #mouse position + self.label = pyglet.text.Label('', + font_name="", + font_size=36, + bold=True, + color=(128, 128, 128, 128), + x=self.window.width-10, y=0, + anchor_x='right', anchor_y='bottom') + + pyglet.clock.schedule_interval(self.animate_water, 1/6.0) + pyglet.clock.schedule_interval(self.update, 1/60.0) + + def _first_time_init(self): + self.tile_size = 32 + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + #center map + self.global_x = (self.mapset.width*self.tile_size-self.viewport_x+self.tile_size)//2 + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2)+(self.tile_size//2) + + self.mouse_x = self.mouse_dx = 0 + self.mouse_y = self.mouse_dy = 0 + + def _init_view(self): + #step one tile + self.steps = self.tile_size + + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + + #center map when viewport is too large, else check if map still fills + #whole viewport and if not adjust position accordingly + self.center_x = False + if self.mapset.width*self.tile_size < self.viewport_x: + self.center_x = True + self.global_x = (self.mapset.width*self.tile_size)//2-(self.viewport_x//2) + elif self.global_x > self.tile_size*self.mapset.width-self.viewport_x: + self.global_x = self.tile_size*self.mapset.width-self.viewport_x + elif self.global_x < 0: + self.global_x = 0 + + self.center_y = False + if self.mapset.height*self.tile_size < self.viewport_y: + self.center_y = True + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2) + elif self.global_y > self.tile_size*self.mapset.height-self.viewport_y: + self.global_y = self.tile_size*self.mapset.height-self.viewport_y + elif self.global_y < 0: + self.global_y = 0 + + #drawn tiles + self.tiles_x = min((self.viewport_x//self.tile_size)+2, self.mapset.width) + self.tiles_y = min((self.viewport_y//self.tile_size)+2, self.mapset.height) + + #undrawn map size + self.undrawn_x = self.tile_size*(self.mapset.width-self.tiles_x) + self.undrawn_y = self.tile_size*(self.mapset.height-self.tiles_y) + #size of full undrawn steps + self.undrawn_steps_x = self.steps*(self.undrawn_x//self.steps) + self.undrawn_steps_y = self.steps*(self.undrawn_y//self.steps) + + self.batch = pyglet.graphics.Batch() + + self.view_x = 0 + self.view_y = 0 + self.dx = 0 + self.dy = 0 + + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + if trans_global_x < -self.undrawn_steps_x: + mod_x = trans_global_x+self.undrawn_x + elif trans_global_x < self.steps: + mod_x = trans_global_x%self.steps + else: + mod_x = trans_global_x + + if trans_global_y < -self.undrawn_steps_y: + mod_y = trans_global_y+self.undrawn_y + elif trans_global_y < self.steps: + mod_y = trans_global_y%self.steps + else: + mod_y = trans_global_y + + self.div_x = (trans_global_x-mod_x)//self.tile_size + self.div_y = (trans_global_y-mod_y)//self.tile_size+self.mapset.height-1 + + self.vl_objects = [None for i, value in enumerate(self.mapset.groups)] + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + tex_coords[obj.group].extend(obj.tex_coords) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] != 0: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + + def on_draw(self): + pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT) + pyglet.gl.glPushMatrix() + pyglet.gl.glTranslatef(self.view_x, self.view_y, 0) + pyglet.gl.glScalef(self.tile_size/32.0, self.tile_size/32.0, 0.0) + self.batch.draw() + pyglet.gl.glPopMatrix() + pyglet.gl.glLoadIdentity() + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glColor4f(1, 0, 1, 1) + pyglet.gl.glRectf(0, 0, self.window.width, IF_BOTTOM) + pyglet.gl.glRectf(self.window.width-IF_RIGHT, 0, self.window.width, self.window.height) + pyglet.gl.glRectf(0, self.window.height-IF_TOP, self.window.width, self.window.height) + pyglet.gl.glRectf(0, 0, IF_LEFT, self.window.height) + self.label.draw() + + def _move(self, dx, dy): + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + new_global_x = trans_global_x+dx + new_global_y = trans_global_y+dy + + if self.global_x-dx < 0: + new_global_x = self.steps + if self.global_y-dy < 0: + new_global_y = self.steps + if dx-self.global_x < -self.tile_size*self.mapset.width+self.viewport_x: + new_global_x = -self.tile_size*self.mapset.width+self.viewport_x+self.steps + if dy-self.global_y < -self.tile_size*self.mapset.height+self.viewport_y: + new_global_y = -self.tile_size*self.mapset.height+self.viewport_y+self.steps + + retex = False + + if new_global_x < -self.undrawn_steps_x: + mod_x = new_global_x+self.undrawn_x + if trans_global_x >= -self.undrawn_steps_x: + retex = True + elif new_global_x < self.steps: + div_x, mod_x = divmod(new_global_x, self.steps) + retex = div_x != trans_global_x//self.steps or retex + else: + mod_x = new_global_x + + if new_global_y < -self.undrawn_steps_y: + mod_y = new_global_y+self.undrawn_y + if trans_global_y >= -self.undrawn_steps_y: + retex = True + elif new_global_y < self.steps: + div_y, mod_y = divmod(new_global_y, self.steps) + retex = div_y != trans_global_y//self.steps or retex + else: + mod_y = new_global_y + + if retex: + self.div_x = (new_global_x-mod_x)//self.tile_size + self.div_y = (new_global_y-mod_y)//self.tile_size+self.mapset.height-1 + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + tex_coords[obj.group].extend(obj.tex_coords) + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] == 0: + if self.vl_objects[i] is None: + pass + else: + self.vl_objects[i].delete() + self.vl_objects[i] = None + else: + if self.vl_objects[i] is None: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + else: + self.vl_objects[i].resize(count[i]) + self.vl_objects[i].tex_coords = tex_coords[i] + self.vl_objects[i].vertices = vertices[i] + self.vl_objects[i].colors = (255,255,255,255)*count[i] + + if not self.center_x: + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.global_x = self.steps-new_global_x + if not self.center_y: + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + self.global_y = self.steps-new_global_y + + def update(self, dt): + try: + if self.window.keys[pyglet.window.key.LCTRL] and \ + any([self.window.keys[pyglet.window.key.UP], + self.window.keys[pyglet.window.key.DOWN], + self.window.keys[pyglet.window.key.LEFT], + self.window.keys[pyglet.window.key.RIGHT]]): + + if self.window.keys[pyglet.window.key.LEFT]: + x = 1 + elif self.window.keys[pyglet.window.key.RIGHT]: + x = -1 + else: + x = 0 + + if self.window.keys[pyglet.window.key.UP]: + y = -1 + elif self.window.keys[pyglet.window.key.DOWN]: + y = 1 + else: + y = 0 + self.dx += x*8 + self.dy += y*8 + elif self.window.keys[pyglet.window.key.PLUS] and \ + self.tile_size < 32: + self.tile_size+=8 + self._init_view() + elif self.window.keys[pyglet.window.key.MINUS] and \ + self.tile_size > 16: + self.tile_size-=8 + self._init_view() + except KeyError: + pass + if self.dx or self.dy: + self._move(self.dx, self.dy) + self.dx = 0 + self.dy = 0 + #mouse position: + if self.mouse_x != self.mouse_dx or self.mouse_y != self.mouse_dy: + self.mouse_x = self.mouse_dx + self.mouse_y = self.mouse_dy + x = (self.mouse_x-IF_LEFT-self.view_x + -(self.tile_size-32)//4)//self.tile_size + y = (self.mouse_y-IF_BOTTOM-self.view_y + -((self.tile_size-32)*3)//2)//self.tile_size + self.label.text = "%03d %03d"%(x-self.div_x, self.div_y-y) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.dx += dx + self.dy += dy + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_mouse_motion(self, x, y, dx, dy): + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_resize(self, width, height): + self._init_view() + + def animate_water(self, dt): + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + if isinstance(obj, Animation): + obj.next_frame() + tex_coords[obj.group].extend(obj.tex_coords) + for i, group in enumerate(self.mapset.groups): + if len(tex_coords[i]) != 0: + self.vl_objects[i].tex_coords = tex_coords[i] + +class Window(pyglet.window.Window): + def __init__(self, *args, **kwargs): + super(Window, self).__init__(1280, 1024, resizable=True, vsync=False) + self.keys = pyglet.window.key.KeyStateHandler() + self.push_handlers(self.keys) + self.fps = pyglet.clock.ClockDisplay() + pyglet.clock.schedule(lambda dt: None) + + def on_draw(self): + self.fps.draw() + + def on_key_press(self, symbol, modifiers): + if symbol == pyglet.window.key.F11: + self.set_fullscreen(fullscreen=not self.fullscreen) + elif symbol == pyglet.window.key.P: + pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png', encoder=PNGRGBEncoder()) + +class PNGRGBEncoder(pyglet.image.codecs.ImageEncoder): + def encode(self, image, file, filename): + import Image + image = image.get_image_data() + format = image.format + pitch = -(image.width * len(format)) + pil_image = Image.fromstring( + format, (image.width, image.height), image.get_data(format, pitch)) + try: + #.convert('P', palette=Image.WEB) + pil_image.convert("RGB").save(file) + except Exception, e: + raise ImageEncodeException(e) + + +class Interface(object): + def __init__(self, window): + self.window = window + + def on_mouse_motion(self, x, y, dx, dy): + if IF_LEFT < x < (self.window.width-IF_RIGHT): + pass + else: + return pyglet.event.EVENT_HANDLED + if IF_BOTTOM < y < (self.window.height-IF_TOP): + pass + else: + return pyglet.event.EVENT_HANDLED + +class LoadScreen(object): + def __init__(self, window): + self.window = window + self.label = pyglet.text.Label('', + font_name="Linux Libertine", + font_size=28, + x=self.window.width-10, y=10, + anchor_x='right', anchor_y='bottom') + + self.label.text = "PARSING MAP FILE..." + import lib.h3m as h3mlib + import os + h3m = h3mlib.extract(os.path.join(pyglet.resource._default_loader._script_home,"maps","A Warm and Familiar Place.h3m")) + self.label.text = "PARSING MAP FILE..." + edge_map = [[] for i in xrange(len(h3m["upper_terrain"])+16)] + for num in xrange(len(edge_map)): + if num < 7 or num > len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0])+18)]) + elif num == 7: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 16, 0, 0, 0, 0, 0]) + line.extend([[-1, 20+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 17, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + elif num == len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 19, 0, 0, 0, 0, 0]) + line.extend([[-1, 28+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 18, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + else: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 32+num%4, 0, 0, 0, 0, 0]) + line.extend(h3m["upper_terrain"][num-8]) + line.append([-1, 24+num%4, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + edge_map[num] = line + h3m["upper_terrain"] = edge_map + self.label.text = "INITIATING MAPSET..." + + mapset = MapSet(h3m["upper_terrain"], h3m["objects"], h3m["tunedobj"]) + self.label.text = "INITIATING MAPVIEW..." + mapview = MapView(mapset, self.window) + interface = Interface(self.window) + self.window.pop_handlers() + self.window.push_handlers(mapview) + self.window.push_handlers(interface) + self.window.push_handlers(self.window.keys) + +if __name__ == '__main__': + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) + window = Window() + window.push_handlers(LoadScreen(window)) + img = pyglet.resource.image("data/cursors/cradvntr.def/0.png") + window.set_mouse_cursor(pyglet.window.ImageMouseCursor(img, 0, 40)) + pyglet.app.run() diff --git a/hr_stable7_bigtex-nosplitanim-overlay.py b/hr_stable7_bigtex-nosplitanim-overlay.py new file mode 100644 index 0000000..df36753 --- /dev/null +++ b/hr_stable7_bigtex-nosplitanim-overlay.py @@ -0,0 +1,727 @@ +#!/usr/bin/python +""" + copyright 2008 - Johannes 'josch' Schauer + + 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, either version 3 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import pyglet + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + import demjson as json + json.loads = json.decode + json.dumps = json.encode + +IF_BOTTOM = 48 +IF_RIGHT = 200 +IF_TOP = IF_LEFT = 8 + +class Animation(object): + def __init__(self, frames, group=None): + if group: + self.texgroup = group + else: + self.texgroup = frames[0].group + self.__frames = frames + self.__animation = 0 + self.width = frames[0].width + self.height = frames[0].height + + def next_frame(self): + self.__animation = (self.__animation+1)%len(self.__frames) + + def get_tex_coords(self): + return self.__frames[self.__animation].tex_coords + + tex_coords = property(get_tex_coords) + + def get_group(self): + return self.texgroup + + group = property(get_group) + +class MapSet(object): + def load_map_object(self, file, order=0): + image = pyglet.image.load(None, file=pyglet.resource.file(file)) + try: + texture_region = self.current_atlas.add(image) + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + texture_region = self.current_atlas.add(image) + ordered_group = pyglet.graphics.OrderedGroup(order) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture, ordered_group) + + if group not in self.groups: + self.groups.append(group) + + texture_region.group = self.groups.index(group) + return texture_region + + def __init__(self, loaded_map, objects, tunedobj): + self.width = len(loaded_map[0]) + self.height = len(loaded_map) + + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + + self.groups = [] + + self.tiles = {} + tile_textures = {} + for y, line in enumerate(loaded_map): + for x, tile in enumerate(line): + if tile[0] == -1: #edge + if "edg" not in tile_textures.keys(): + tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 100) for i in xrange(36)] + self.tiles[x,y] = [tile_textures["edg"][tile[1]]] + elif tile[0] == 0: #dirt + if "dirttl" not in tile_textures.keys(): + tile_textures["dirttl"] = [self.load_map_object('data/advmap_tiles/dirttl.def/%d.png'%i, 0) for i in xrange(46)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["dirttl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirttl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["dirttl"][tile[1]]] + elif tile[0] == 1: #sand + if "sandtl" not in tile_textures.keys(): + tile_textures["sandtl"] = [self.load_map_object('data/advmap_tiles/sandtl.def/%d.png'%i, 0) for i in xrange(24)] + self.tiles[x,y] = [tile_textures["sandtl"][tile[1]]] + elif tile[0] == 2: #grass + if "grastl" not in tile_textures.keys(): + tile_textures["grastl"] = [self.load_map_object('data/advmap_tiles/grastl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["grastl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["grastl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["grastl"][tile[1]]] + elif tile[0] == 3: #snow + if "snowtl" not in tile_textures.keys(): + tile_textures["snowtl"] = [self.load_map_object('data/advmap_tiles/snowtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["snowtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["snowtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["snowtl"][tile[1]]] + elif tile[0] == 4: #swamp + if "swmptl" not in tile_textures.keys(): + tile_textures["swmptl"] = [self.load_map_object('data/advmap_tiles/swmptl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["swmptl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["swmptl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["swmptl"][tile[1]]] + elif tile[0] == 5: #rough + if "rougtl" not in tile_textures.keys(): + tile_textures["rougtl"] = [self.load_map_object('data/advmap_tiles/rougtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["rougtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["rougtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["rougtl"][tile[1]]] + elif tile[0] == 7: #lava + if "lavatl" not in tile_textures.keys(): + tile_textures["lavatl"] = [self.load_map_object('data/advmap_tiles/lavatl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["lavatl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["lavatl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["lavatl"][tile[1]]] + elif tile[0] == 8: #water + if "watrtl" not in tile_textures.keys(): + tile_textures["watrtl"] = [] + for j in xrange(33): + tile_textures["watrtl"].append([self.load_map_object('data/advmap_tiles/watrtl.def/%d/%d.png'%(j,i), 0) for i in xrange(12)]) + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + tiles = [] + for watrtl in tile_textures["watrtl"][tile[1]]: + new = watrtl.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = watrtl.group + tiles.append(new) + self.tiles[x,y] = [Animation(tiles)] + else: + self.tiles[x,y] = [Animation(tile_textures["watrtl"][tile[1]])] + elif tile[0] == 9: #rock + if "rocktl" not in tile_textures.keys(): + tile_textures["rocktl"] = [self.load_map_object('data/advmap_tiles/rocktl.def/%d.png'%i, 0) for i in xrange(48)] + self.tiles[x,y] = [tile_textures["rocktl"][tile[1]]] + else: + raise NotImplementedError + + if tile[2] == 0: #no river + pass + elif tile[2] == 1: #clrrvr + if "clrrvr" not in tile_textures.keys(): + tile_textures["clrrvr"] = [[self.load_map_object('data/advmap_tiles/clrrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for clrrvr in tile_textures["clrrvr"][tile[3]]: + new = clrrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = clrrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["clrrvr"][tile[3]])) + elif tile[2] == 2: #icyrvr + if "icyrvr" not in tile_textures.keys(): + tile_textures["icyrvr"] = [self.load_map_object('data/advmap_tiles/icyrvr.def/%d.png'%i, 1) for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + new = tile_textures["icyrvr"][tile[3]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["icyrvr"][tile[3]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["icyrvr"][tile[3]]) + elif tile[2] == 3: #mudrvr + if "mudrvr" not in tile_textures.keys(): + tile_textures["mudrvr"] = [[self.load_map_object('data/advmap_tiles/mudrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for mudrvr in tile_textures["mudrvr"][tile[3]]: + new = mudrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = mudrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["mudrvr"][tile[3]])) + elif tile[2] == 4: #lavrvr + if "lavrvr" not in tile_textures.keys(): + tile_textures["lavrvr"] = [[self.load_map_object('data/advmap_tiles/lavrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(9)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for lavrvr in tile_textures["lavrvr"][tile[3]]: + new = lavrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = lavrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["lavrvr"][tile[3]])) + else: + raise NotImplementedError, tile[2] + + if tile[4] == 0: #no road + pass + elif tile[4] == 1: #dirtrd + if "dirtrd" not in tile_textures.keys(): + tile_textures["dirtrd"] = [self.load_map_object('data/advmap_tiles/dirtrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["dirtrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirtrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["dirtrd"][tile[5]]) + elif tile[4] == 2: #gravrd + if "gravrd" not in tile_textures.keys(): + tile_textures["gravrd"] = [self.load_map_object('data/advmap_tiles/gravrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["gravrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["gravrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["gravrd"][tile[5]]) + elif tile[4] == 3: #cobbrd + if "cobbrd" not in tile_textures.keys(): + tile_textures["cobbrd"] = [self.load_map_object('data/advmap_tiles/cobbrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["cobbrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["cobbrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["cobbrd"][tile[5]]) + else: + raise NotImplementedError, tile[4] + + images = [] + for order, obj in enumerate(objects): + imgs = [] + i = 0 + while 1: + imgs.append(pyglet.image.load(None, file=pyglet.resource.file("data/advmap_objects/"+obj["filename"]+"/%d.png"%i))) + i+=1 + if "data/advmap_objects/"+obj["filename"]+"/%d.png"%i not in pyglet.resource._default_loader._index.keys(): + break; + images.append((imgs, obj["class"], obj["overlay"], order)) + + self.objects = [] + for imgs in sorted(images, key=lambda i:i[0][0].height, reverse=True): + textures = [] + try: + textures = [self.current_atlas.add(img) for img in imgs[0]] + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + textures = [self.current_atlas.add(img) for img in imgs[0]] + for texture in textures: + texture.atlas = self.current_atlas + self.objects.append((textures, imgs[1], imgs[2], imgs[3])) + + self.objects = [(i[0], i[1], i[2]) for i in sorted(self.objects, key=lambda i:i[3])] + + self.tunedobj = {} + for obj in [i for i in tunedobj if i["z"]==0]: + order = obj["y"] + 8 + if self.objects[obj["id"]][2]: + order = order - self.objects[obj["id"]][0][0].height // 32 + order *= 2 + if self.objects[obj["id"]][1] in [119, 134, 135, 137, 155, 199]: + order -= 1 + group = pyglet.graphics.OrderedGroup(order) + group = pyglet.graphics.TextureGroup(self.objects[obj["id"]][0][0].atlas.texture, parent=group) + if group not in self.groups: + self.groups.append(group) + group = self.groups.index(group) + self.tiles[obj["x"]+9,obj["y"]+8].append(Animation(self.objects[obj["id"]][0], group)) + +class MapView(object): + def __init__(self, mapset, window): + self.window = window + self.mapset = mapset + + self._first_time_init() + self._init_view() + + #mouse position + self.label = pyglet.text.Label('', + font_name="", + font_size=36, + bold=True, + color=(128, 128, 128, 128), + x=self.window.width-10, y=0, + anchor_x='right', anchor_y='bottom') + + pyglet.clock.schedule_interval(self.animate_water, 1/6.0) + pyglet.clock.schedule_interval(self.update, 1/60.0) + + def _first_time_init(self): + self.tile_size = 32 + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + #center map + self.global_x = (self.mapset.width*self.tile_size-self.viewport_x+self.tile_size)//2 + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2)+(self.tile_size//2) + + self.mouse_x = self.mouse_dx = 0 + self.mouse_y = self.mouse_dy = 0 + + def _init_view(self): + #step one tile + self.steps = self.tile_size + + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + + #center map when viewport is too large, else check if map still fills + #whole viewport and if not adjust position accordingly + self.center_x = False + if self.mapset.width*self.tile_size < self.viewport_x: + self.center_x = True + self.global_x = (self.mapset.width*self.tile_size)//2-(self.viewport_x//2) + elif self.global_x > self.tile_size*self.mapset.width-self.viewport_x: + self.global_x = self.tile_size*self.mapset.width-self.viewport_x + elif self.global_x < 0: + self.global_x = 0 + + self.center_y = False + if self.mapset.height*self.tile_size < self.viewport_y: + self.center_y = True + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2) + elif self.global_y > self.tile_size*self.mapset.height-self.viewport_y: + self.global_y = self.tile_size*self.mapset.height-self.viewport_y + elif self.global_y < 0: + self.global_y = 0 + + #drawn tiles + self.tiles_x = min((self.viewport_x//self.tile_size)+2, self.mapset.width) + self.tiles_y = min((self.viewport_y//self.tile_size)+2, self.mapset.height) + + #undrawn map size + self.undrawn_x = self.tile_size*(self.mapset.width-self.tiles_x) + self.undrawn_y = self.tile_size*(self.mapset.height-self.tiles_y) + #size of full undrawn steps + self.undrawn_steps_x = self.steps*(self.undrawn_x//self.steps) + self.undrawn_steps_y = self.steps*(self.undrawn_y//self.steps) + + self.batch = pyglet.graphics.Batch() + + self.view_x = 0 + self.view_y = 0 + self.dx = 0 + self.dy = 0 + + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + if trans_global_x < -self.undrawn_steps_x: + mod_x = trans_global_x+self.undrawn_x + elif trans_global_x < self.steps: + mod_x = trans_global_x%self.steps + else: + mod_x = trans_global_x + + if trans_global_y < -self.undrawn_steps_y: + mod_y = trans_global_y+self.undrawn_y + elif trans_global_y < self.steps: + mod_y = trans_global_y%self.steps + else: + mod_y = trans_global_y + + self.div_x = (trans_global_x-mod_x)//self.tile_size + self.div_y = (trans_global_y-mod_y)//self.tile_size+self.mapset.height-1 + + self.vl_objects = [None for i, value in enumerate(self.mapset.groups)] + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + tex_coords[obj.group].extend(obj.tex_coords) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] != 0: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + + def on_draw(self): + pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT) + pyglet.gl.glPushMatrix() + pyglet.gl.glTranslatef(self.view_x, self.view_y, 0) + pyglet.gl.glScalef(self.tile_size/32.0, self.tile_size/32.0, 0.0) + self.batch.draw() + pyglet.gl.glPopMatrix() + pyglet.gl.glLoadIdentity() + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glColor4f(1, 0, 1, 1) + pyglet.gl.glRectf(0, 0, self.window.width, IF_BOTTOM) + pyglet.gl.glRectf(self.window.width-IF_RIGHT, 0, self.window.width, self.window.height) + pyglet.gl.glRectf(0, self.window.height-IF_TOP, self.window.width, self.window.height) + pyglet.gl.glRectf(0, 0, IF_LEFT, self.window.height) + self.label.draw() + + def _move(self, dx, dy): + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + new_global_x = trans_global_x+dx + new_global_y = trans_global_y+dy + + if self.global_x-dx < 0: + new_global_x = self.steps + if self.global_y-dy < 0: + new_global_y = self.steps + if dx-self.global_x < -self.tile_size*self.mapset.width+self.viewport_x: + new_global_x = -self.tile_size*self.mapset.width+self.viewport_x+self.steps + if dy-self.global_y < -self.tile_size*self.mapset.height+self.viewport_y: + new_global_y = -self.tile_size*self.mapset.height+self.viewport_y+self.steps + + retex = False + + if new_global_x < -self.undrawn_steps_x: + mod_x = new_global_x+self.undrawn_x + if trans_global_x >= -self.undrawn_steps_x: + retex = True + elif new_global_x < self.steps: + div_x, mod_x = divmod(new_global_x, self.steps) + retex = div_x != trans_global_x//self.steps or retex + else: + mod_x = new_global_x + + if new_global_y < -self.undrawn_steps_y: + mod_y = new_global_y+self.undrawn_y + if trans_global_y >= -self.undrawn_steps_y: + retex = True + elif new_global_y < self.steps: + div_y, mod_y = divmod(new_global_y, self.steps) + retex = div_y != trans_global_y//self.steps or retex + else: + mod_y = new_global_y + + if retex: + self.div_x = (new_global_x-mod_x)//self.tile_size + self.div_y = (new_global_y-mod_y)//self.tile_size+self.mapset.height-1 + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + tex_coords[obj.group].extend(obj.tex_coords) + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] == 0: + if self.vl_objects[i] is None: + pass + else: + self.vl_objects[i].delete() + self.vl_objects[i] = None + else: + if self.vl_objects[i] is None: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + else: + self.vl_objects[i].resize(count[i]) + self.vl_objects[i].tex_coords = tex_coords[i] + self.vl_objects[i].vertices = vertices[i] + self.vl_objects[i].colors = (255,255,255,255)*count[i] + + if not self.center_x: + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.global_x = self.steps-new_global_x + if not self.center_y: + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + self.global_y = self.steps-new_global_y + + def update(self, dt): + try: + if self.window.keys[pyglet.window.key.LCTRL] and \ + any([self.window.keys[pyglet.window.key.UP], + self.window.keys[pyglet.window.key.DOWN], + self.window.keys[pyglet.window.key.LEFT], + self.window.keys[pyglet.window.key.RIGHT]]): + + if self.window.keys[pyglet.window.key.LEFT]: + x = 1 + elif self.window.keys[pyglet.window.key.RIGHT]: + x = -1 + else: + x = 0 + + if self.window.keys[pyglet.window.key.UP]: + y = -1 + elif self.window.keys[pyglet.window.key.DOWN]: + y = 1 + else: + y = 0 + self.dx += x*8 + self.dy += y*8 + elif self.window.keys[pyglet.window.key.PLUS] and \ + self.tile_size < 32: + self.tile_size+=8 + self._init_view() + elif self.window.keys[pyglet.window.key.MINUS] and \ + self.tile_size > 16: + self.tile_size-=8 + self._init_view() + except KeyError: + pass + if self.dx or self.dy: + self._move(self.dx, self.dy) + self.dx = 0 + self.dy = 0 + #mouse position: + if self.mouse_x != self.mouse_dx or self.mouse_y != self.mouse_dy: + self.mouse_x = self.mouse_dx + self.mouse_y = self.mouse_dy + x = (self.mouse_x-IF_LEFT-self.view_x + -(self.tile_size-32)//4)//self.tile_size + y = (self.mouse_y-IF_BOTTOM-self.view_y + -((self.tile_size-32)*3)//2)//self.tile_size + self.label.text = "%03d %03d"%(x-self.div_x, self.div_y-y) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.dx += dx + self.dy += dy + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_mouse_motion(self, x, y, dx, dy): + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_resize(self, width, height): + self._init_view() + + def animate_water(self, dt): + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + if isinstance(obj, Animation): + obj.next_frame() + tex_coords[obj.group].extend(obj.tex_coords) + for i, group in enumerate(self.mapset.groups): + if len(tex_coords[i]) != 0: + self.vl_objects[i].tex_coords = tex_coords[i] + +class Window(pyglet.window.Window): + def __init__(self, *args, **kwargs): + super(Window, self).__init__(1280, 1024, resizable=True, vsync=False) + self.keys = pyglet.window.key.KeyStateHandler() + self.push_handlers(self.keys) + self.fps = pyglet.clock.ClockDisplay() + pyglet.clock.schedule(lambda dt: None) + + def on_draw(self): + self.fps.draw() + + def on_key_press(self, symbol, modifiers): + if symbol == pyglet.window.key.F11: + self.set_fullscreen(fullscreen=not self.fullscreen) + elif symbol == pyglet.window.key.P: + pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png', encoder=PNGRGBEncoder()) + +class PNGRGBEncoder(pyglet.image.codecs.ImageEncoder): + def encode(self, image, file, filename): + import Image + image = image.get_image_data() + format = image.format + pitch = -(image.width * len(format)) + pil_image = Image.fromstring( + format, (image.width, image.height), image.get_data(format, pitch)) + try: + #.convert('P', palette=Image.WEB) + pil_image.convert("RGB").save(file) + except Exception, e: + raise ImageEncodeException(e) + + +class Interface(object): + def __init__(self, window): + self.window = window + + def on_mouse_motion(self, x, y, dx, dy): + if IF_LEFT < x < (self.window.width-IF_RIGHT): + pass + else: + return pyglet.event.EVENT_HANDLED + if IF_BOTTOM < y < (self.window.height-IF_TOP): + pass + else: + return pyglet.event.EVENT_HANDLED + +class LoadScreen(object): + def __init__(self, window): + self.window = window + self.label = pyglet.text.Label('', + font_name="Linux Libertine", + font_size=28, + x=self.window.width-10, y=10, + anchor_x='right', anchor_y='bottom') + + self.label.text = "PARSING MAP FILE..." + import lib.h3m as h3mlib + import os + h3m = h3mlib.extract(os.path.join(pyglet.resource._default_loader._script_home,"maps","A Warm and Familiar Place.h3m")) + self.label.text = "PARSING MAP FILE..." + edge_map = [[] for i in xrange(len(h3m["upper_terrain"])+16)] + for num in xrange(len(edge_map)): + if num < 7 or num > len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0])+18)]) + elif num == 7: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 16, 0, 0, 0, 0, 0]) + line.extend([[-1, 20+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 17, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + elif num == len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 19, 0, 0, 0, 0, 0]) + line.extend([[-1, 28+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 18, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + else: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 32+num%4, 0, 0, 0, 0, 0]) + line.extend(h3m["upper_terrain"][num-8]) + line.append([-1, 24+num%4, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + edge_map[num] = line + h3m["upper_terrain"] = edge_map + self.label.text = "INITIATING MAPSET..." + + mapset = MapSet(h3m["upper_terrain"], h3m["objects"], h3m["tunedobj"]) + self.label.text = "INITIATING MAPVIEW..." + mapview = MapView(mapset, self.window) + interface = Interface(self.window) + self.window.pop_handlers() + self.window.push_handlers(mapview) + self.window.push_handlers(interface) + self.window.push_handlers(self.window.keys) + +if __name__ == '__main__': + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) + window = Window() + window.push_handlers(LoadScreen(window)) + img = pyglet.resource.image("data/cursors/cradvntr.def/0.png") + window.set_mouse_cursor(pyglet.window.ImageMouseCursor(img, 0, 40)) + pyglet.app.run() diff --git a/hr_stable7_bigtex-nosplitanim-rabbyt.py b/hr_stable7_bigtex-nosplitanim-rabbyt.py new file mode 100644 index 0000000..9c5f9bd --- /dev/null +++ b/hr_stable7_bigtex-nosplitanim-rabbyt.py @@ -0,0 +1,684 @@ +#!/usr/bin/python +""" + copyright 2008 - Johannes 'josch' Schauer + + 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, either version 3 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import pyglet +import rabbyt + +IF_BOTTOM = 48 +IF_RIGHT = 200 +IF_TOP = IF_LEFT = 8 + +class MapSet(object): + def load_map_object(self, file, order=0): + image = pyglet.image.load(None, file=pyglet.resource.file(file)) + try: + texture_region = self.current_atlas.add(image) + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + texture_region = self.current_atlas.add(image) + ordered_group = pyglet.graphics.OrderedGroup(order) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture, ordered_group) + + if group not in self.groups: + self.groups.append(group) + + texture_region.group = self.groups.index(group) + return texture_region + + def __init__(self, loaded_map, objects, tunedobj): + self.width = len(loaded_map[0]) + self.height = len(loaded_map) + + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + + self.groups = [] + + self.tiles = {} + tile_textures = {} + for y, line in enumerate(loaded_map): + for x, tile in enumerate(line): + if tile[0] == -1: #edge + if "edg" not in tile_textures.keys(): + tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 100) for i in xrange(36)] + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["edg"][tile[1]])] + elif tile[0] == 0: #dirt + if "dirttl" not in tile_textures.keys(): + tile_textures["dirttl"] = [self.load_map_object('data/advmap_tiles/dirttl.def/%d.png'%i, 0) for i in xrange(46)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["dirttl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirttl"][tile[1]].group + self.tiles[x,y] = [rabbyt.Sprite(new)] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["dirttl"][tile[1]])] + elif tile[0] == 1: #sand + if "sandtl" not in tile_textures.keys(): + tile_textures["sandtl"] = [self.load_map_object('data/advmap_tiles/sandtl.def/%d.png'%i, 0) for i in xrange(24)] + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["sandtl"][tile[1]])] + elif tile[0] == 2: #grass + if "grastl" not in tile_textures.keys(): + tile_textures["grastl"] = [self.load_map_object('data/advmap_tiles/grastl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["grastl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["grastl"][tile[1]].group + self.tiles[x,y] = [rabbyt.Sprite(new)] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["grastl"][tile[1]])] + elif tile[0] == 3: #snow + if "snowtl" not in tile_textures.keys(): + tile_textures["snowtl"] = [self.load_map_object('data/advmap_tiles/snowtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["snowtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["snowtl"][tile[1]].group + self.tiles[x,y] = [rabbyt.Sprite(new)] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["snowtl"][tile[1]])] + elif tile[0] == 4: #swamp + if "swmptl" not in tile_textures.keys(): + tile_textures["swmptl"] = [self.load_map_object('data/advmap_tiles/swmptl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["swmptl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["swmptl"][tile[1]].group + self.tiles[x,y] = [rabbyt.Sprite(new)] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["swmptl"][tile[1]])] + elif tile[0] == 5: #rough + if "rougtl" not in tile_textures.keys(): + tile_textures["rougtl"] = [self.load_map_object('data/advmap_tiles/rougtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["rougtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["rougtl"][tile[1]].group + self.tiles[x,y] = [rabbyt.Sprite(new)] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["rougtl"][tile[1]])] + elif tile[0] == 7: #lava + if "lavatl" not in tile_textures.keys(): + tile_textures["lavatl"] = [self.load_map_object('data/advmap_tiles/lavatl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["lavatl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["lavatl"][tile[1]].group + self.tiles[x,y] = [rabbyt.Sprite(new)] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["lavatl"][tile[1]])] + elif tile[0] == 8: #water + if "watrtl" not in tile_textures.keys(): + tile_textures["watrtl"] = [] + for j in xrange(33): + tile_textures["watrtl"].append([self.load_map_object('data/advmap_tiles/watrtl.def/%d/%d.png'%(j,i), 0) for i in xrange(12)]) + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + tiles = [] + for watrtl in tile_textures["watrtl"][tile[1]]: + new = watrtl.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = watrtl.group + tiles.append(new) + self.tiles[x,y] = [rabbyt.Sprite(tiles[0])] + else: + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["watrtl"][tile[1]][0])] + elif tile[0] == 9: #rock + if "rocktl" not in tile_textures.keys(): + tile_textures["rocktl"] = [self.load_map_object('data/advmap_tiles/rocktl.def/%d.png'%i, 0) for i in xrange(48)] + self.tiles[x,y] = [rabbyt.Sprite(tile_textures["rocktl"][tile[1]])] + else: + raise NotImplementedError + + if tile[2] == 0: #no river + pass + elif tile[2] == 1: #clrrvr + if "clrrvr" not in tile_textures.keys(): + tile_textures["clrrvr"] = [[self.load_map_object('data/advmap_tiles/clrrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for clrrvr in tile_textures["clrrvr"][tile[3]]: + new = clrrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = clrrvr.group + tiles.append(new) + self.tiles[x, y].append(rabbyt.Sprite(tiles[0])) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["clrrvr"][tile[3]][0])) + elif tile[2] == 2: #icyrvr + if "icyrvr" not in tile_textures.keys(): + tile_textures["icyrvr"] = [self.load_map_object('data/advmap_tiles/icyrvr.def/%d.png'%i, 1) for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + new = tile_textures["icyrvr"][tile[3]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["icyrvr"][tile[3]].group + self.tiles[x, y].append(rabbyt.Sprite(new)) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["icyrvr"][tile[3]])) + elif tile[2] == 3: #mudrvr + if "mudrvr" not in tile_textures.keys(): + tile_textures["mudrvr"] = [[self.load_map_object('data/advmap_tiles/mudrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for mudrvr in tile_textures["mudrvr"][tile[3]]: + new = mudrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = mudrvr.group + tiles.append(new) + self.tiles[x, y].append(rabbyt.Sprite(tiles[0])) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["mudrvr"][tile[3]][0])) + elif tile[2] == 4: #lavrvr + if "lavrvr" not in tile_textures.keys(): + tile_textures["lavrvr"] = [[self.load_map_object('data/advmap_tiles/lavrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(9)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for lavrvr in tile_textures["lavrvr"][tile[3]]: + new = lavrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = lavrvr.group + tiles.append(new) + self.tiles[x, y].append(rabbyt.Sprite(tiles[0])) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["lavrvr"][tile[3]][0])) + else: + raise NotImplementedError, tile[2] + + if tile[4] == 0: #no road + pass + elif tile[4] == 1: #dirtrd + if "dirtrd" not in tile_textures.keys(): + tile_textures["dirtrd"] = [self.load_map_object('data/advmap_tiles/dirtrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["dirtrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirtrd"][tile[5]].group + self.tiles[x, y].append(rabbyt.Sprite(new)) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["dirtrd"][tile[5]])) + elif tile[4] == 2: #gravrd + if "gravrd" not in tile_textures.keys(): + tile_textures["gravrd"] = [self.load_map_object('data/advmap_tiles/gravrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["gravrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["gravrd"][tile[5]].group + self.tiles[x, y].append(rabbyt.Sprite(new)) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["gravrd"][tile[5]])) + elif tile[4] == 3: #cobbrd + if "cobbrd" not in tile_textures.keys(): + tile_textures["cobbrd"] = [self.load_map_object('data/advmap_tiles/cobbrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["cobbrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["cobbrd"][tile[5]].group + self.tiles[x, y].append(rabbyt.Sprite(new)) + else: + self.tiles[x, y].append(rabbyt.Sprite(tile_textures["cobbrd"][tile[5]])) + else: + raise NotImplementedError, tile[4] + + images = [] + for order, obj in enumerate(objects): + imgs = [] + i = 0 + while 1: + imgs.append(pyglet.image.load(None, file=pyglet.resource.file("data/advmap_objects/"+obj["filename"]+"/%d.png"%i))) + i+=1 + break + if "data/advmap_objects/"+obj["filename"]+"/%d.png"%i not in pyglet.resource._default_loader._index.keys(): + break; + images.append((imgs, order)) + + self.objects = [] + for imgs in sorted(images, key=lambda i:i[0][0].height, reverse=True): + textures = [] + try: + textures = [self.current_atlas.add(img) for img in imgs[0]] + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + textures = [self.current_atlas.add(img) for img in imgs[0]] + ordered_group = pyglet.graphics.OrderedGroup(2) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture, ordered_group) + if group not in self.groups: + self.groups.append(group) + group = self.groups.index(group) + for texture in textures: + texture.group = group + self.objects.append((textures, imgs[1])) + + self.objects = [i[0] for i in sorted(self.objects, key=lambda i:i[1])] + + self.tunedobj = {} + for obj in [i for i in tunedobj if i["z"]==0]: + self.tiles[obj["x"]+9,obj["y"]+8].append(rabbyt.Sprite(self.objects[obj["id"]][0])) + +class MapView(object): + def __init__(self, mapset, window): + self.window = window + self.mapset = mapset + + self._first_time_init() + self._init_view() + + #mouse position + self.label = pyglet.text.Label('', + font_name="", + font_size=36, + bold=True, + color=(128, 128, 128, 128), + x=self.window.width-10, y=0, + anchor_x='right', anchor_y='bottom') + + #pyglet.clock.schedule_interval(self.animate_water, 1/6.0) + pyglet.clock.schedule_interval(self.update, 1/60.0) + + def _first_time_init(self): + self.tile_size = 32 + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + #center map + self.global_x = (self.mapset.width*self.tile_size-self.viewport_x+self.tile_size)//2 + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2)+(self.tile_size//2) + + self.mouse_x = self.mouse_dx = 0 + self.mouse_y = self.mouse_dy = 0 + + def _init_view(self): + #step one tile + self.steps = self.tile_size + + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + + #center map when viewport is too large, else check if map still fills + #whole viewport and if not adjust position accordingly + self.center_x = False + if self.mapset.width*self.tile_size < self.viewport_x: + self.center_x = True + self.global_x = (self.mapset.width*self.tile_size)//2-(self.viewport_x//2) + elif self.global_x > self.tile_size*self.mapset.width-self.viewport_x: + self.global_x = self.tile_size*self.mapset.width-self.viewport_x + elif self.global_x < 0: + self.global_x = 0 + + self.center_y = False + if self.mapset.height*self.tile_size < self.viewport_y: + self.center_y = True + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2) + elif self.global_y > self.tile_size*self.mapset.height-self.viewport_y: + self.global_y = self.tile_size*self.mapset.height-self.viewport_y + elif self.global_y < 0: + self.global_y = 0 + + #drawn tiles + self.tiles_x = min((self.viewport_x//self.tile_size)+2, self.mapset.width) + self.tiles_y = min((self.viewport_y//self.tile_size)+2, self.mapset.height) + + #undrawn map size + self.undrawn_x = self.tile_size*(self.mapset.width-self.tiles_x) + self.undrawn_y = self.tile_size*(self.mapset.height-self.tiles_y) + #size of full undrawn steps + self.undrawn_steps_x = self.steps*(self.undrawn_x//self.steps) + self.undrawn_steps_y = self.steps*(self.undrawn_y//self.steps) + + self.batch = pyglet.graphics.Batch() + + self.view_x = 0 + self.view_y = 0 + self.dx = 0 + self.dy = 0 + + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + if trans_global_x < -self.undrawn_steps_x: + mod_x = trans_global_x+self.undrawn_x + elif trans_global_x < self.steps: + mod_x = trans_global_x%self.steps + else: + mod_x = trans_global_x + + if trans_global_y < -self.undrawn_steps_y: + mod_y = trans_global_y+self.undrawn_y + elif trans_global_y < self.steps: + mod_y = trans_global_y%self.steps + else: + mod_y = trans_global_y + + self.div_x = (trans_global_x-mod_x)//self.tile_size + self.div_y = (trans_global_y-mod_y)//self.tile_size+self.mapset.height-1 + + self.vl_objects = [None for i, value in enumerate(self.mapset.groups)] + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + + def on_draw(self): + pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT) + pyglet.gl.glPushMatrix() + pyglet.gl.glTranslatef(self.view_x, self.view_y, 0) + pyglet.gl.glScalef(self.tile_size/32.0, self.tile_size/32.0, 0.0) + #import time + #before = time.time() + #i=0 + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.shape[2][0]+32 + obj.left = x1 + obj.bottom = y1 + obj.render() + #i+=1 + #print i + #print time.time()-before + + pyglet.gl.glPopMatrix() + pyglet.gl.glLoadIdentity() + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glColor4f(1, 0, 1, 1) + pyglet.gl.glRectf(0, 0, self.window.width, IF_BOTTOM) + pyglet.gl.glRectf(self.window.width-IF_RIGHT, 0, self.window.width, self.window.height) + pyglet.gl.glRectf(0, self.window.height-IF_TOP, self.window.width, self.window.height) + pyglet.gl.glRectf(0, 0, IF_LEFT, self.window.height) + self.label.draw() + + def _move(self, dx, dy): + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + new_global_x = trans_global_x+dx + new_global_y = trans_global_y+dy + + if self.global_x-dx < 0: + new_global_x = self.steps + if self.global_y-dy < 0: + new_global_y = self.steps + if dx-self.global_x < -self.tile_size*self.mapset.width+self.viewport_x: + new_global_x = -self.tile_size*self.mapset.width+self.viewport_x+self.steps + if dy-self.global_y < -self.tile_size*self.mapset.height+self.viewport_y: + new_global_y = -self.tile_size*self.mapset.height+self.viewport_y+self.steps + + retex = False + + if new_global_x < -self.undrawn_steps_x: + mod_x = new_global_x+self.undrawn_x + if trans_global_x >= -self.undrawn_steps_x: + retex = True + elif new_global_x < self.steps: + div_x, mod_x = divmod(new_global_x, self.steps) + retex = div_x != trans_global_x//self.steps or retex + else: + mod_x = new_global_x + + if new_global_y < -self.undrawn_steps_y: + mod_y = new_global_y+self.undrawn_y + if trans_global_y >= -self.undrawn_steps_y: + retex = True + elif new_global_y < self.steps: + div_y, mod_y = divmod(new_global_y, self.steps) + retex = div_y != trans_global_y//self.steps or retex + else: + mod_y = new_global_y + + if retex: + self.div_x = (new_global_x-mod_x)//self.tile_size + self.div_y = (new_global_y-mod_y)//self.tile_size+self.mapset.height-1 + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + tex_coords[obj.group].extend(obj.tex_coords) + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] == 0: + if self.vl_objects[i] is None: + pass + else: + self.vl_objects[i].delete() + self.vl_objects[i] = None + else: + if self.vl_objects[i] is None: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + else: + self.vl_objects[i].resize(count[i]) + self.vl_objects[i].tex_coords = tex_coords[i] + self.vl_objects[i].vertices = vertices[i] + self.vl_objects[i].colors = (255,255,255,255)*count[i] + + if not self.center_x: + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.global_x = self.steps-new_global_x + if not self.center_y: + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + self.global_y = self.steps-new_global_y + + def update(self, dt): + try: + if self.window.keys[pyglet.window.key.LCTRL] and \ + any([self.window.keys[pyglet.window.key.UP], + self.window.keys[pyglet.window.key.DOWN], + self.window.keys[pyglet.window.key.LEFT], + self.window.keys[pyglet.window.key.RIGHT]]): + + if self.window.keys[pyglet.window.key.LEFT]: + x = 1 + elif self.window.keys[pyglet.window.key.RIGHT]: + x = -1 + else: + x = 0 + + if self.window.keys[pyglet.window.key.UP]: + y = -1 + elif self.window.keys[pyglet.window.key.DOWN]: + y = 1 + else: + y = 0 + self.dx += x*8 + self.dy += y*8 + elif self.window.keys[pyglet.window.key.PLUS] and \ + self.tile_size < 32: + self.tile_size+=8 + self._init_view() + elif self.window.keys[pyglet.window.key.MINUS] and \ + self.tile_size > 16: + self.tile_size-=8 + self._init_view() + except KeyError: + pass + if self.dx or self.dy: + self._move(self.dx, self.dy) + self.dx = 0 + self.dy = 0 + #mouse position: + if self.mouse_x != self.mouse_dx or self.mouse_y != self.mouse_dy: + self.mouse_x = self.mouse_dx + self.mouse_y = self.mouse_dy + x = (self.mouse_x-IF_LEFT-self.view_x + -(self.tile_size-32)//4)//self.tile_size + y = (self.mouse_y-IF_BOTTOM-self.view_y + -((self.tile_size-32)*3)//2)//self.tile_size + self.label.text = "%03d %03d"%(x-self.div_x, self.div_y-y) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.dx += dx + self.dy += dy + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_mouse_motion(self, x, y, dx, dy): + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_resize(self, width, height): + self._init_view() + + def animate_water(self, dt): + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + if isinstance(obj, Animation): + obj.next_frame() + tex_coords[obj.group].extend(obj.tex_coords) + for i, group in enumerate(self.mapset.groups): + if len(tex_coords[i]) != 0: + self.vl_objects[i].tex_coords = tex_coords[i] + +class Window(pyglet.window.Window): + def __init__(self, *args, **kwargs): + super(Window, self).__init__(1280, 1024, resizable=True, vsync=False) + self.keys = pyglet.window.key.KeyStateHandler() + self.push_handlers(self.keys) + self.fps = pyglet.clock.ClockDisplay() + pyglet.clock.schedule(lambda dt: None) + + def on_draw(self): + self.fps.draw() + + def on_key_press(self, symbol, modifiers): + if symbol == pyglet.window.key.F11: + self.set_fullscreen(fullscreen=not self.fullscreen) + elif symbol == pyglet.window.key.P: + pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png', encoder=PNGRGBEncoder()) + +class PNGRGBEncoder(pyglet.image.codecs.ImageEncoder): + def encode(self, image, file, filename): + import Image + image = image.get_image_data() + format = image.format + pitch = -(image.width * len(format)) + pil_image = Image.fromstring( + format, (image.width, image.height), image.get_data(format, pitch)) + try: + #.convert('P', palette=Image.WEB) + pil_image.convert("RGB").save(file) + except Exception, e: + raise ImageEncodeException(e) + + +class Interface(object): + def __init__(self, window): + self.window = window + + def on_mouse_motion(self, x, y, dx, dy): + if IF_LEFT < x < (self.window.width-IF_RIGHT): + pass + else: + return pyglet.event.EVENT_HANDLED + if IF_BOTTOM < y < (self.window.height-IF_TOP): + pass + else: + return pyglet.event.EVENT_HANDLED + +class LoadScreen(object): + def __init__(self, window): + self.window = window + self.label = pyglet.text.Label('', + font_name="Linux Libertine", + font_size=28, + x=self.window.width-10, y=10, + anchor_x='right', anchor_y='bottom') + + self.label.text = "PARSING MAP FILE..." + import lib.h3m as h3mlib + import os + h3m = h3mlib.extract(os.path.join(pyglet.resource._default_loader._script_home,"maps","A Viking We Shall Go.h3m")) + self.label.text = "PARSING MAP FILE..." + edge_map = [[] for i in xrange(len(h3m["upper_terrain"])+16)] + for num in xrange(len(edge_map)): + if num < 7 or num > len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0])+18)]) + elif num == 7: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 16, 0, 0, 0, 0, 0]) + line.extend([[-1, 20+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 17, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + elif num == len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 19, 0, 0, 0, 0, 0]) + line.extend([[-1, 28+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 18, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + else: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 32+num%4, 0, 0, 0, 0, 0]) + line.extend(h3m["upper_terrain"][num-8]) + line.append([-1, 24+num%4, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + edge_map[num] = line + h3m["upper_terrain"] = edge_map + self.label.text = "INITIATING MAPSET..." + + mapset = MapSet(h3m["upper_terrain"], h3m["objects"], h3m["tunedobj"]) + self.label.text = "INITIATING MAPVIEW..." + mapview = MapView(mapset, self.window) + interface = Interface(self.window) + self.window.pop_handlers() + self.window.push_handlers(mapview) + self.window.push_handlers(interface) + self.window.push_handlers(self.window.keys) + +if __name__ == '__main__': + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) + window = Window() + window.push_handlers(LoadScreen(window)) + img = pyglet.resource.image("data/cursors/cradvntr.def/0.png") + window.set_mouse_cursor(pyglet.window.ImageMouseCursor(img, 0, 40)) + pyglet.app.run() diff --git a/hr_stable7_bigtex-nosplitanim.py b/hr_stable7_bigtex-nosplitanim.py new file mode 100644 index 0000000..cec6c88 --- /dev/null +++ b/hr_stable7_bigtex-nosplitanim.py @@ -0,0 +1,720 @@ +#!/usr/bin/python +""" + copyright 2008 - Johannes 'josch' Schauer + + 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, either version 3 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import pyglet + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + import demjson as json + json.loads = json.decode + json.dumps = json.encode + +IF_BOTTOM = 48 +IF_RIGHT = 200 +IF_TOP = IF_LEFT = 8 + +class Animation(object): + def __init__(self, frames): + self.__frames = frames + self.__animation = 0 + self.width = frames[0].width + self.height = frames[0].height + + def next_frame(self): + self.__animation = (self.__animation+1)%len(self.__frames) + + def get_tex_coords(self): + return self.__frames[self.__animation].tex_coords + + tex_coords = property(get_tex_coords) + + def get_group(self): + return self.__frames[self.__animation].group + + group = property(get_group) + +class MapSet(object): + def load_map_object(self, file, order=0): + image = pyglet.image.load(None, file=pyglet.resource.file(file)) + try: + texture_region = self.current_atlas.add(image) + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + texture_region = self.current_atlas.add(image) + ordered_group = pyglet.graphics.OrderedGroup(order) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture, ordered_group) + + if group not in self.groups: + self.groups.append(group) + + texture_region.group = self.groups.index(group) + return texture_region + + def __init__(self, loaded_map, objects, tunedobj): + self.width = len(loaded_map[0]) + self.height = len(loaded_map) + + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + + self.groups = [] + + self.tiles = {} + tile_textures = {} + for y, line in enumerate(loaded_map): + for x, tile in enumerate(line): + if tile[0] == -1: #edge + if "edg" not in tile_textures.keys(): + tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 100) for i in xrange(36)] + self.tiles[x,y] = [tile_textures["edg"][tile[1]]] + elif tile[0] == 0: #dirt + if "dirttl" not in tile_textures.keys(): + tile_textures["dirttl"] = [self.load_map_object('data/advmap_tiles/dirttl.def/%d.png'%i, 0) for i in xrange(46)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["dirttl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirttl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["dirttl"][tile[1]]] + elif tile[0] == 1: #sand + if "sandtl" not in tile_textures.keys(): + tile_textures["sandtl"] = [self.load_map_object('data/advmap_tiles/sandtl.def/%d.png'%i, 0) for i in xrange(24)] + self.tiles[x,y] = [tile_textures["sandtl"][tile[1]]] + elif tile[0] == 2: #grass + if "grastl" not in tile_textures.keys(): + tile_textures["grastl"] = [self.load_map_object('data/advmap_tiles/grastl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["grastl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["grastl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["grastl"][tile[1]]] + elif tile[0] == 3: #snow + if "snowtl" not in tile_textures.keys(): + tile_textures["snowtl"] = [self.load_map_object('data/advmap_tiles/snowtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["snowtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["snowtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["snowtl"][tile[1]]] + elif tile[0] == 4: #swamp + if "swmptl" not in tile_textures.keys(): + tile_textures["swmptl"] = [self.load_map_object('data/advmap_tiles/swmptl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["swmptl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["swmptl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["swmptl"][tile[1]]] + elif tile[0] == 5: #rough + if "rougtl" not in tile_textures.keys(): + tile_textures["rougtl"] = [self.load_map_object('data/advmap_tiles/rougtl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["rougtl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["rougtl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["rougtl"][tile[1]]] + elif tile[0] == 7: #lava + if "lavatl" not in tile_textures.keys(): + tile_textures["lavatl"] = [self.load_map_object('data/advmap_tiles/lavatl.def/%d.png'%i, 0) for i in xrange(79)] + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + new = tile_textures["lavatl"][tile[1]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["lavatl"][tile[1]].group + self.tiles[x,y] = [new] + else: + self.tiles[x,y] = [tile_textures["lavatl"][tile[1]]] + elif tile[0] == 8: #water + if "watrtl" not in tile_textures.keys(): + tile_textures["watrtl"] = [] + for j in xrange(33): + tile_textures["watrtl"].append([self.load_map_object('data/advmap_tiles/watrtl.def/%d/%d.png'%(j,i), 0) for i in xrange(12)]) + flip_x = bool(tile[6] & 1) + flip_y = bool(tile[6] & 2) + if flip_x or flip_y: + tiles = [] + for watrtl in tile_textures["watrtl"][tile[1]]: + new = watrtl.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = watrtl.group + tiles.append(new) + self.tiles[x,y] = [Animation(tiles)] + else: + self.tiles[x,y] = [Animation(tile_textures["watrtl"][tile[1]])] + elif tile[0] == 9: #rock + if "rocktl" not in tile_textures.keys(): + tile_textures["rocktl"] = [self.load_map_object('data/advmap_tiles/rocktl.def/%d.png'%i, 0) for i in xrange(48)] + self.tiles[x,y] = [tile_textures["rocktl"][tile[1]]] + else: + raise NotImplementedError + + if tile[2] == 0: #no river + pass + elif tile[2] == 1: #clrrvr + if "clrrvr" not in tile_textures.keys(): + tile_textures["clrrvr"] = [[self.load_map_object('data/advmap_tiles/clrrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for clrrvr in tile_textures["clrrvr"][tile[3]]: + new = clrrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = clrrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["clrrvr"][tile[3]])) + elif tile[2] == 2: #icyrvr + if "icyrvr" not in tile_textures.keys(): + tile_textures["icyrvr"] = [self.load_map_object('data/advmap_tiles/icyrvr.def/%d.png'%i, 1) for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + new = tile_textures["icyrvr"][tile[3]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["icyrvr"][tile[3]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["icyrvr"][tile[3]]) + elif tile[2] == 3: #mudrvr + if "mudrvr" not in tile_textures.keys(): + tile_textures["mudrvr"] = [[self.load_map_object('data/advmap_tiles/mudrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(12)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for mudrvr in tile_textures["mudrvr"][tile[3]]: + new = mudrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = mudrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["mudrvr"][tile[3]])) + elif tile[2] == 4: #lavrvr + if "lavrvr" not in tile_textures.keys(): + tile_textures["lavrvr"] = [[self.load_map_object('data/advmap_tiles/lavrvr.def/%d/%d.png'%(i, j), 1) for j in xrange(9)] for i in xrange(13)] + flip_x = bool(tile[6] & 4) + flip_y = bool(tile[6] & 8) + if flip_x or flip_y: + tiles = [] + for lavrvr in tile_textures["lavrvr"][tile[3]]: + new = lavrvr.get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = lavrvr.group + tiles.append(new) + self.tiles[x, y].append(Animation(tiles)) + else: + self.tiles[x, y].append(Animation(tile_textures["lavrvr"][tile[3]])) + else: + raise NotImplementedError, tile[2] + + if tile[4] == 0: #no road + pass + elif tile[4] == 1: #dirtrd + if "dirtrd" not in tile_textures.keys(): + tile_textures["dirtrd"] = [self.load_map_object('data/advmap_tiles/dirtrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["dirtrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["dirtrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["dirtrd"][tile[5]]) + elif tile[4] == 2: #gravrd + if "gravrd" not in tile_textures.keys(): + tile_textures["gravrd"] = [self.load_map_object('data/advmap_tiles/gravrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["gravrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["gravrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["gravrd"][tile[5]]) + elif tile[4] == 3: #cobbrd + if "cobbrd" not in tile_textures.keys(): + tile_textures["cobbrd"] = [self.load_map_object('data/advmap_tiles/cobbrd.def/%d.png'%i, 1) for i in xrange(17)] + flip_x = bool(tile[6] & 16) + flip_y = bool(tile[6] & 32) + if flip_x or flip_y: + new = tile_textures["cobbrd"][tile[5]].get_transform(flip_x=flip_x, flip_y=flip_y) + new.group = tile_textures["cobbrd"][tile[5]].group + self.tiles[x, y].append(new) + else: + self.tiles[x, y].append(tile_textures["cobbrd"][tile[5]]) + else: + raise NotImplementedError, tile[4] + + images = [] + for order, obj in enumerate(objects): + imgs = [] + i = 0 + while 1: + imgs.append(pyglet.image.load(None, file=pyglet.resource.file("data/advmap_objects/"+obj["filename"]+"/%d.png"%i))) + i+=1 + if "data/advmap_objects/"+obj["filename"]+"/%d.png"%i not in pyglet.resource._default_loader._index.keys(): + break; + images.append((imgs, order)) + + self.objects = [] + for imgs in sorted(images, key=lambda i:i[0][0].height, reverse=True): + textures = [] + try: + textures = [self.current_atlas.add(img) for img in imgs[0]] + except pyglet.image.atlas.AllocatorException: + self.current_atlas = pyglet.image.atlas.TextureAtlas(1024, 1024) + print "atlas" + textures = [self.current_atlas.add(img) for img in imgs[0]] + ordered_group = pyglet.graphics.OrderedGroup(2) + group = pyglet.graphics.TextureGroup(self.current_atlas.texture, ordered_group) + if group not in self.groups: + self.groups.append(group) + group = self.groups.index(group) + for texture in textures: + texture.group = group + self.objects.append((textures, imgs[1])) + + self.objects = [i[0] for i in sorted(self.objects, key=lambda i:i[1])] + + self.tunedobj = {} + for obj in [i for i in tunedobj if i["z"]==0]: + if len(self.objects[obj["id"]]) == 1: + self.tiles[obj["x"]+9,obj["y"]+8].append(self.objects[obj["id"]][0]) + else: + self.tiles[obj["x"]+9,obj["y"]+8].append(Animation(self.objects[obj["id"]])) + +class MapView(object): + def __init__(self, mapset, window): + self.window = window + self.mapset = mapset + + self._first_time_init() + self._init_view() + + #mouse position + self.label = pyglet.text.Label('', + font_name="", + font_size=36, + bold=True, + color=(128, 128, 128, 128), + x=self.window.width-10, y=0, + anchor_x='right', anchor_y='bottom') + + pyglet.clock.schedule_interval(self.animate_water, 1/6.0) + pyglet.clock.schedule_interval(self.update, 1/60.0) + + def _first_time_init(self): + self.tile_size = 32 + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + #center map + self.global_x = (self.mapset.width*self.tile_size-self.viewport_x+self.tile_size)//2 + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2)+(self.tile_size//2) + + self.mouse_x = self.mouse_dx = 0 + self.mouse_y = self.mouse_dy = 0 + + def _init_view(self): + #step one tile + self.steps = self.tile_size + + self.viewport_x = self.window.width-IF_RIGHT-IF_LEFT + self.viewport_y = self.window.height-IF_BOTTOM-IF_TOP + + #center map when viewport is too large, else check if map still fills + #whole viewport and if not adjust position accordingly + self.center_x = False + if self.mapset.width*self.tile_size < self.viewport_x: + self.center_x = True + self.global_x = (self.mapset.width*self.tile_size)//2-(self.viewport_x//2) + elif self.global_x > self.tile_size*self.mapset.width-self.viewport_x: + self.global_x = self.tile_size*self.mapset.width-self.viewport_x + elif self.global_x < 0: + self.global_x = 0 + + self.center_y = False + if self.mapset.height*self.tile_size < self.viewport_y: + self.center_y = True + self.global_y = (self.mapset.height*self.tile_size)//2-(self.viewport_y//2) + elif self.global_y > self.tile_size*self.mapset.height-self.viewport_y: + self.global_y = self.tile_size*self.mapset.height-self.viewport_y + elif self.global_y < 0: + self.global_y = 0 + + #drawn tiles + self.tiles_x = min((self.viewport_x//self.tile_size)+2, self.mapset.width) + self.tiles_y = min((self.viewport_y//self.tile_size)+2, self.mapset.height) + + #undrawn map size + self.undrawn_x = self.tile_size*(self.mapset.width-self.tiles_x) + self.undrawn_y = self.tile_size*(self.mapset.height-self.tiles_y) + #size of full undrawn steps + self.undrawn_steps_x = self.steps*(self.undrawn_x//self.steps) + self.undrawn_steps_y = self.steps*(self.undrawn_y//self.steps) + + self.batch = pyglet.graphics.Batch() + + self.view_x = 0 + self.view_y = 0 + self.dx = 0 + self.dy = 0 + + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + if trans_global_x < -self.undrawn_steps_x: + mod_x = trans_global_x+self.undrawn_x + elif trans_global_x < self.steps: + mod_x = trans_global_x%self.steps + else: + mod_x = trans_global_x + + if trans_global_y < -self.undrawn_steps_y: + mod_y = trans_global_y+self.undrawn_y + elif trans_global_y < self.steps: + mod_y = trans_global_y%self.steps + else: + mod_y = trans_global_y + + self.div_x = (trans_global_x-mod_x)//self.tile_size + self.div_y = (trans_global_y-mod_y)//self.tile_size+self.mapset.height-1 + + self.vl_objects = [None for i, value in enumerate(self.mapset.groups)] + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + tex_coords[obj.group].extend(obj.tex_coords) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] != 0: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + + def on_draw(self): + pyglet.gl.glClear(pyglet.gl.GL_COLOR_BUFFER_BIT) + pyglet.gl.glPushMatrix() + pyglet.gl.glTranslatef(self.view_x, self.view_y, 0) + pyglet.gl.glScalef(self.tile_size/32.0, self.tile_size/32.0, 0.0) + self.batch.draw() + pyglet.gl.glPopMatrix() + pyglet.gl.glLoadIdentity() + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glColor4f(1, 0, 1, 1) + pyglet.gl.glRectf(0, 0, self.window.width, IF_BOTTOM) + pyglet.gl.glRectf(self.window.width-IF_RIGHT, 0, self.window.width, self.window.height) + pyglet.gl.glRectf(0, self.window.height-IF_TOP, self.window.width, self.window.height) + pyglet.gl.glRectf(0, 0, IF_LEFT, self.window.height) + self.label.draw() + + def _move(self, dx, dy): + #here we translate the global map position so we can draw with it + trans_global_x = self.steps-self.global_x + trans_global_y = self.steps-self.global_y + + new_global_x = trans_global_x+dx + new_global_y = trans_global_y+dy + + if self.global_x-dx < 0: + new_global_x = self.steps + if self.global_y-dy < 0: + new_global_y = self.steps + if dx-self.global_x < -self.tile_size*self.mapset.width+self.viewport_x: + new_global_x = -self.tile_size*self.mapset.width+self.viewport_x+self.steps + if dy-self.global_y < -self.tile_size*self.mapset.height+self.viewport_y: + new_global_y = -self.tile_size*self.mapset.height+self.viewport_y+self.steps + + retex = False + + if new_global_x < -self.undrawn_steps_x: + mod_x = new_global_x+self.undrawn_x + if trans_global_x >= -self.undrawn_steps_x: + retex = True + elif new_global_x < self.steps: + div_x, mod_x = divmod(new_global_x, self.steps) + retex = div_x != trans_global_x//self.steps or retex + else: + mod_x = new_global_x + + if new_global_y < -self.undrawn_steps_y: + mod_y = new_global_y+self.undrawn_y + if trans_global_y >= -self.undrawn_steps_y: + retex = True + elif new_global_y < self.steps: + div_y, mod_y = divmod(new_global_y, self.steps) + retex = div_y != trans_global_y//self.steps or retex + else: + mod_y = new_global_y + + if retex: + self.div_x = (new_global_x-mod_x)//self.tile_size + self.div_y = (new_global_y-mod_y)//self.tile_size+self.mapset.height-1 + vertices = [[] for i, value in enumerate(self.mapset.groups)] + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + count = [0 for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + y1 = y*32+IF_BOTTOM + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + x1 = x*32+IF_LEFT-obj.width+32 + x2 = x1+obj.width + y2 = y1+obj.height + tex_coords[obj.group].extend(obj.tex_coords) + vertices[obj.group].extend([x1, y1, x2, y1, x2, y2, x1, y2]) + count[obj.group]+=4 + + for i, group in enumerate(self.mapset.groups): + if count[i] == 0: + if self.vl_objects[i] is None: + pass + else: + self.vl_objects[i].delete() + self.vl_objects[i] = None + else: + if self.vl_objects[i] is None: + self.vl_objects[i] = self.batch.add(count[i], pyglet.gl.GL_QUADS, + group, + ('v2i', vertices[i]), + ('t3f', tex_coords[i]), + ('c4B', (255,255,255,255)*count[i])) + else: + self.vl_objects[i].resize(count[i]) + self.vl_objects[i].tex_coords = tex_coords[i] + self.vl_objects[i].vertices = vertices[i] + self.vl_objects[i].colors = (255,255,255,255)*count[i] + + if not self.center_x: + self.view_x = mod_x-self.steps-(self.tile_size-32)//4 + self.global_x = self.steps-new_global_x + if not self.center_y: + self.view_y = mod_y-self.steps-((self.tile_size-32)*3)//2 + self.global_y = self.steps-new_global_y + + def update(self, dt): + try: + if self.window.keys[pyglet.window.key.LCTRL] and \ + any([self.window.keys[pyglet.window.key.UP], + self.window.keys[pyglet.window.key.DOWN], + self.window.keys[pyglet.window.key.LEFT], + self.window.keys[pyglet.window.key.RIGHT]]): + + if self.window.keys[pyglet.window.key.LEFT]: + x = 1 + elif self.window.keys[pyglet.window.key.RIGHT]: + x = -1 + else: + x = 0 + + if self.window.keys[pyglet.window.key.UP]: + y = -1 + elif self.window.keys[pyglet.window.key.DOWN]: + y = 1 + else: + y = 0 + self.dx += x*8 + self.dy += y*8 + elif self.window.keys[pyglet.window.key.PLUS] and \ + self.tile_size < 32: + self.tile_size+=8 + self._init_view() + elif self.window.keys[pyglet.window.key.MINUS] and \ + self.tile_size > 16: + self.tile_size-=8 + self._init_view() + except KeyError: + pass + if self.dx or self.dy: + self._move(self.dx, self.dy) + self.dx = 0 + self.dy = 0 + #mouse position: + if self.mouse_x != self.mouse_dx or self.mouse_y != self.mouse_dy: + self.mouse_x = self.mouse_dx + self.mouse_y = self.mouse_dy + x = (self.mouse_x-IF_LEFT-self.view_x + -(self.tile_size-32)//4)//self.tile_size + y = (self.mouse_y-IF_BOTTOM-self.view_y + -((self.tile_size-32)*3)//2)//self.tile_size + self.label.text = "%03d %03d"%(x-self.div_x, self.div_y-y) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.dx += dx + self.dy += dy + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_mouse_motion(self, x, y, dx, dy): + self.mouse_dx = x + self.mouse_dy = y + return pyglet.event.EVENT_HANDLED + + def on_resize(self, width, height): + self._init_view() + + def animate_water(self, dt): + tex_coords = [[] for i, value in enumerate(self.mapset.groups)] + for y in xrange(self.tiles_y-1, -6, -1): + for x in xrange(self.tiles_x+5-1, -1, -1): + for obj in self.mapset.tiles.get((x-self.div_x, self.div_y-y), []): + if isinstance(obj, Animation): + obj.next_frame() + tex_coords[obj.group].extend(obj.tex_coords) + for i, group in enumerate(self.mapset.groups): + if len(tex_coords[i]) != 0: + self.vl_objects[i].tex_coords = tex_coords[i] + +class Window(pyglet.window.Window): + def __init__(self, *args, **kwargs): + super(Window, self).__init__(1280, 1024, resizable=True, vsync=False) + self.keys = pyglet.window.key.KeyStateHandler() + self.push_handlers(self.keys) + self.fps = pyglet.clock.ClockDisplay() + pyglet.clock.schedule(lambda dt: None) + + def on_draw(self): + self.fps.draw() + + def on_key_press(self, symbol, modifiers): + if symbol == pyglet.window.key.F11: + self.set_fullscreen(fullscreen=not self.fullscreen) + elif symbol == pyglet.window.key.P: + pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png', encoder=PNGRGBEncoder()) + +class PNGRGBEncoder(pyglet.image.codecs.ImageEncoder): + def encode(self, image, file, filename): + import Image + image = image.get_image_data() + format = image.format + pitch = -(image.width * len(format)) + pil_image = Image.fromstring( + format, (image.width, image.height), image.get_data(format, pitch)) + try: + #.convert('P', palette=Image.WEB) + pil_image.convert("RGB").save(file) + except Exception, e: + raise ImageEncodeException(e) + + +class Interface(object): + def __init__(self, window): + self.window = window + + def on_mouse_motion(self, x, y, dx, dy): + if IF_LEFT < x < (self.window.width-IF_RIGHT): + pass + else: + return pyglet.event.EVENT_HANDLED + if IF_BOTTOM < y < (self.window.height-IF_TOP): + pass + else: + return pyglet.event.EVENT_HANDLED + +class LoadScreen(object): + def __init__(self, window): + self.window = window + self.label = pyglet.text.Label('', + font_name="Linux Libertine", + font_size=28, + x=self.window.width-10, y=10, + anchor_x='right', anchor_y='bottom') + + self.label.text = "PARSING MAP FILE..." + import lib.h3m as h3mlib + import os + h3m = h3mlib.extract(os.path.join(pyglet.resource._default_loader._script_home,"maps","Kingdom in peaces MPgame.h3m")) + self.label.text = "PARSING MAP FILE..." + edge_map = [[] for i in xrange(len(h3m["upper_terrain"])+16)] + for num in xrange(len(edge_map)): + if num < 7 or num > len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0])+18)]) + elif num == 7: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 16, 0, 0, 0, 0, 0]) + line.extend([[-1, 20+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 17, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + elif num == len(edge_map)-8: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 19, 0, 0, 0, 0, 0]) + line.extend([[-1, 28+i%4, 0, 0, 0, 0, 0] for i in xrange(len(h3m["upper_terrain"][0]))]) + line.append([-1, 18, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + else: + line = [] + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + line.append([-1, 32+num%4, 0, 0, 0, 0, 0]) + line.extend(h3m["upper_terrain"][num-8]) + line.append([-1, 24+num%4, 0, 0, 0, 0, 0]) + line.extend([[-1, 0+(i-1)%4+4*(num%4), 0, 0, 0, 0, 0] for i in xrange(8)]) + edge_map[num] = line + h3m["upper_terrain"] = edge_map + self.label.text = "INITIATING MAPSET..." + + mapset = MapSet(h3m["upper_terrain"], h3m["objects"], h3m["tunedobj"]) + self.label.text = "INITIATING MAPVIEW..." + mapview = MapView(mapset, self.window) + interface = Interface(self.window) + self.window.pop_handlers() + self.window.push_handlers(mapview) + self.window.push_handlers(interface) + self.window.push_handlers(self.window.keys) + +if __name__ == '__main__': + pyglet.gl.glEnable(pyglet.gl.GL_BLEND) + pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA) + window = Window() + window.push_handlers(LoadScreen(window)) + img = pyglet.resource.image("data/cursors/cradvntr.def/0.png") + window.set_mouse_cursor(pyglet.window.ImageMouseCursor(img, 0, 40)) + pyglet.app.run() diff --git a/lib/batch.py b/lib/batch.py new file mode 100644 index 0000000..0efa6aa --- /dev/null +++ b/lib/batch.py @@ -0,0 +1,1598 @@ +############################################################################### +## __init__.py ## +############################################################################### + +import ctypes + +import pyglet +from pyglet.gl import * +import ctypes +import re + +_debug_graphics_batch = pyglet.options['debug_graphics_batch'] + +def _parse_data(data): + '''Given a list of data items, returns (formats, initial_arrays).''' + assert data, 'No attribute formats given' + + # Return tuple (formats, initial_arrays). + formats = [] + initial_arrays = [] + for i, format in enumerate(data): + if isinstance(format, tuple): + format, array = format + initial_arrays.append((i, array)) + formats.append(format) + formats = tuple(formats) + return formats, initial_arrays + +def _get_default_batch(): + shared_object_space = gl.current_context.object_space + try: + return shared_object_space.pyglet_graphics_default_batch + except AttributeError: + shared_object_space.pyglet_graphics_default_batch = Batch() + return shared_object_space.pyglet_graphics_default_batch + +def vertex_list(count, *data): + '''Create a `VertexList` not associated with a batch, group or mode. + + :Parameters: + `count` : int + The number of vertices in the list. + `data` : data items + Attribute formats and initial data for the vertex list. See the + module summary for details. + + :rtype: `VertexList` + ''' + # Note that mode=0 because the default batch is never drawn: vertex lists + # returned from this function are drawn directly by the app. + return _get_default_batch().add(count, 0, None, *data) + +class DomainDraw(object): + def __init__(self, domain, mode, group): + self.domain = domain + self.mode = mode + self.group = group + + def draw(self): + self.group.set_state() + self.domain.draw(self.mode) + self.group.unset_state() + +class Batch(object): + '''Manage a collection of vertex lists for batched rendering. + + Vertex lists are added to a `Batch` using the `add` and `add_indexed` + methods. An optional group can be specified along with the vertex list, + which gives the OpenGL state required for its rendering. Vertex lists + with shared mode and group are allocated into adjacent areas of memory and + sent to the graphics card in a single operation. + + Call `VertexList.delete` to remove a vertex list from the batch. + ''' + def __init__(self): + '''Create a graphics batch.''' + # Mapping to find domain. + # group -> (attributes, mode, indexed) -> domain + self.group_map = {} + + # Mapping of group to list of children. + self.group_children = {} + + # List of top-level groups + self.top_groups = [] + + self._draw_list = [] + self._draw_list_dirty = False + + def add(self, count, mode, group, *data): + '''Add a vertex list to the batch. + + :Parameters: + `count` : int + The number of vertices in the list. + `mode` : int + OpenGL drawing mode enumeration; for example, one of + ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. + See the module summary for additional information. + `group` : `Group` + Group of the vertex list, or ``None`` if no group is required. + `data` : data items + Attribute formats and initial data for the vertex list. See + the module summary for details. + + :rtype: `VertexList` + ''' + formats, initial_arrays = _parse_data(data) + domain = self._get_domain(False, mode, group, formats) + domain.__formats = formats + + # Create vertex list and initialize + vlist = domain.create(count) + for i, array in initial_arrays: + vlist._set_attribute_data(i, array) + + return vlist + + def _get_domain(self, indexed, mode, group, formats): + if group is None: + group = null_group + + # Batch group + if group not in self.group_map: + self._add_group(group) + + domain_map = self.group_map[group] + + # Find domain given formats, indices and mode + key = (formats, mode, indexed) + try: + domain = domain_map[key] + except KeyError: + # Create domain + if indexed: + domain = vertexdomain.create_indexed_domain(*formats) + else: + domain = create_domain(*formats) + domain_map[key] = domain + self._draw_list_dirty = True + + return domain + + def _add_group(self, group): + self.group_map[group] = {} + if group.parent is None: + self.top_groups.append(group) + else: + if group.parent not in self.group_map: + self._add_group(group.parent) + if group.parent not in self.group_children: + self.group_children[group.parent] = [] + self.group_children[group.parent].append(group) + self._draw_list_dirty = True + + def visit(self, group): + draw_list = [] + + # Draw domains using this group + domain_map = self.group_map[group] + for (formats, mode, indexed), domain in list(domain_map.items()): + # Remove unused domains from batch + if domain._is_empty(): + del domain_map[(formats, mode, indexed)] + continue + draw_list.append(DomainDraw(domain, mode, group)) + + # Sort and visit child groups of this group + children = self.group_children.get(group) + if children: + children.sort() + for child in list(children): + draw_list.extend(visit(child)) + + if children or domain_map: + return draw_list + else: + # Remove unused group from batch + del self.group_map[group] + if group.parent: + self.group_children[group.parent].remove(group) + try: + del self.group_children[group] + except KeyError: + pass + try: + self.top_groups.remove(group) + except ValueError: + pass + return [] + + def _update_draw_list(self): + '''Visit group tree in preorder and create a list of bound methods + to call. + ''' + + self._draw_list = [] + + self.top_groups.sort() + for group in list(self.top_groups): + self._draw_list.extend(self.visit(group)) + + self._draw_list_dirty = False + + if _debug_graphics_batch: + self._dump_draw_list() + + def draw(self): + '''Draw the batch. + ''' + if self._draw_list_dirty: + self._update_draw_list() + + for func in self._draw_list: + func.draw() + +class Group(object): + '''Group of common OpenGL state. + + Before a vertex list is rendered, its group's OpenGL state is set; as are + that state's ancestors' states. This can be defined arbitrarily on + subclasses; the default state change has no effect, and groups vertex + lists only in the order in which they are drawn. + ''' + def __init__(self, parent=None): + '''Create a group. + + :Parameters: + `parent` : `Group` + Group to contain this group; its state will be set before this + state's. + + ''' + self.parent = parent + + def set_state(self): + '''Apply the OpenGL state change. + + The default implementation does nothing.''' + pass + + def unset_state(self): + '''Repeal the OpenGL state change. + + The default implementation does nothing.''' + pass + + def set_state_recursive(self): + '''Set this group and its ancestry. + + Call this method if you are using a group in isolation: the + parent groups will be called in top-down order, with this class's + `set` being called last. + ''' + if self.parent: + self.parent.set_state_recursive() + self.set_state() + + def unset_state_recursive(self): + '''Unset this group and its ancestry. + + The inverse of `set_state_recursive`. + ''' + self.unset_state() + if self.parent: + self.parent.unset_state_recursive() + +class NullGroup(Group): + '''The default group class used when ``None`` is given to a batch. + + This implementation has no effect. + ''' + pass + +#: The default group. +#: +#: :type: `Group` +null_group = NullGroup() + + +############################################################################### +## allocation.py ## +############################################################################### + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +# Common cases: +# -regions will be the same size (instances of same object, e.g. sprites) +# -regions will not usually be resized (only exception is text) +# -alignment of 4 vertices (glyphs, sprites, images, ...) +# +# Optimise for: +# -keeping regions adjacent, reduce the number of entries in glMultiDrawArrays +# -finding large blocks of allocated regions quickly (for drawing) +# -finding block of unallocated space is the _uncommon_ case! +# +# Decisions: +# -don't over-allocate regions to any alignment -- this would require more +# work in finding the allocated spaces (for drawing) and would result in +# more entries in glMultiDrawArrays +# -don't move blocks when they truncate themselves. try not to allocate the +# space they freed too soon (they will likely need grow back into it later, +# and growing will usually require a reallocation). +# -allocator does not track individual allocated regions. Trusts caller +# to provide accurate (start, size) tuple, which completely describes +# a region from the allocator's point of view. +# -this means that compacting is probably not feasible, or would be hideously +# expensive + +class AllocatorMemoryException(Exception): + '''The buffer is not large enough to fulfil an allocation. + + Raised by `Allocator` methods when the operation failed due to lack of + buffer space. The buffer should be increased to at least + requested_capacity and then the operation retried (guaranteed to + pass second time). + ''' + + def __init__(self, requested_capacity): + self.requested_capacity = requested_capacity + +class Allocator(object): + '''Buffer space allocation implementation.''' + def __init__(self, capacity): + '''Create an allocator for a buffer of the specified capacity. + + :Parameters: + `capacity` : int + Maximum size of the buffer. + + ''' + self.capacity = capacity + + # Allocated blocks. Start index and size in parallel lists. + # + # # = allocated, - = free + # + # 0 3 5 15 20 24 40 + # |###--##########-----####----------------------| + # + # starts = [0, 5, 20] + # sizes = [3, 10, 4] + # + # To calculate free blocks: + # for i in range(0, len(starts)): + # free_start[i] = starts[i] + sizes[i] + # free_size[i] = starts[i+1] - free_start[i] + # free_size[i+1] = self.capacity - free_start[-1] + + self.starts = [] + self.sizes = [] + + def set_capacity(self, size): + '''Resize the maximum buffer size. + + The capaity cannot be reduced. + + :Parameters: + `size` : int + New maximum size of the buffer. + + ''' + assert size > self.capacity + self.capacity = size + + def alloc(self, size): + '''Allocate memory in the buffer. + + Raises `AllocatorMemoryException` if the allocation cannot be + fulfilled. + + :Parameters: + `size` : int + Size of region to allocate. + + :rtype: int + :return: Starting index of the allocated region. + ''' + assert size > 0 + + # return start + # or raise AllocatorMemoryException + + if not self.starts: + if size <= self.capacity: + self.starts.append(0) + self.sizes.append(size) + return 0 + else: + raise AllocatorMemoryException(size) + + # Allocate in a free space + free_start = self.starts[0] + self.sizes[0] + for i, (alloc_start, alloc_size) in \ + enumerate(zip(self.starts[1:], self.sizes[1:])): + # Danger! + # i is actually index - 1 because of slicing above... + # starts[i] points to the block before this free space + # starts[i+1] points to the block after this free space, and is + # always valid. + free_size = alloc_start - free_start + if free_size == size: + # Merge previous block with this one (removing this free space) + self.sizes[i] += free_size + alloc_size + del self.starts[i+1] + del self.sizes[i+1] + return free_start + elif free_size > size: + # Increase size of previous block to intrude into this free + # space. + self.sizes[i] += size + return free_start + free_start = alloc_start + alloc_size + + # Allocate at end of capacity + free_size = self.capacity - free_start + if free_size >= size: + self.sizes[-1] += size + return free_start + + raise AllocatorMemoryException(self.capacity + size - free_size) + + def realloc(self, start, size, new_size): + '''Reallocate a region of the buffer. + + This is more efficient than separate `dealloc` and `alloc` calls, as + the region can often be resized in-place. + + Raises `AllocatorMemoryException` if the allocation cannot be + fulfilled. + + :Parameters: + `start` : int + Current starting index of the region. + `size` : int + Current size of the region. + `new_size` : int + New size of the region. + + ''' + assert size > 0 and new_size > 0 + + # return start + # or raise AllocatorMemoryException + + # Truncation is the same as deallocating the tail cruft + if new_size < size: + self.dealloc(start + new_size, size - new_size) + return start + + # Find which block it lives in + for i, (alloc_start, alloc_size) in \ + enumerate(zip(*(self.starts, self.sizes))): + p = start - alloc_start + if p >= 0 and size <= alloc_size - p: + break + if not (p >= 0 and size <= alloc_size - p): + print zip(self.starts, self.sizes) + print start, size, new_size + print p, alloc_start, alloc_size + assert p >= 0 and size <= alloc_size - p, 'Region not allocated' + + if size == alloc_size - p: + # Region is at end of block. Find how much free space is after + # it. + is_final_block = i == len(self.starts) - 1 + if not is_final_block: + free_size = self.starts[i + 1] - (start + size) + else: + free_size = self.capacity - (start + size) + + # TODO If region is an entire block being an island in free space, + # can possibly extend in both directions. + + if free_size == new_size - size and not is_final_block: + # Merge block with next (region is expanded in place to + # exactly fill the free space) + self.sizes[i] += free_size + self.sizes[i + 1] + del self.starts[i + 1] + del self.sizes[i + 1] + return start + elif free_size > new_size - size: + # Expand region in place + self.sizes[i] += new_size - size + return start + + # The block must be repositioned. Dealloc then alloc. + + # But don't do this! If alloc fails, we've already silently dealloc'd + # the original block. + # self.dealloc(start, size) + # return self.alloc(new_size) + + # It must be alloc'd first. We're not missing an optimisation + # here, because if freeing the block would've allowed for the block to + # be placed in the resulting free space, one of the above in-place + # checks would've found it. + result = self.alloc(new_size) + self.dealloc(start, size) + return result + + def dealloc(self, start, size): + '''Free a region of the buffer. + + :Parameters: + `start` : int + Starting index of the region. + `size` : int + Size of the region. + + ''' + assert size > 0 + assert self.starts + + # Find which block needs to be split + for i, (alloc_start, alloc_size) in \ + enumerate(zip(*(self.starts, self.sizes))): + p = start - alloc_start + if p >= 0 and size <= alloc_size - p: + break + + # Assert we left via the break + assert p >= 0 and size <= alloc_size - p, 'Region not allocated' + + if p == 0 and size == alloc_size: + # Remove entire block + del self.starts[i] + del self.sizes[i] + elif p == 0: + # Truncate beginning of block + self.starts[i] += size + self.sizes[i] -= size + elif size == alloc_size - p: + # Truncate end of block + self.sizes[i] -= size + else: + # Reduce size of left side, insert block at right side + # $ = dealloc'd block, # = alloc'd region from same block + # + # <------8------> + # <-5-><-6-><-7-> + # 1 2 3 4 + # #####$$$$$##### + # + # 1 = alloc_start + # 2 = start + # 3 = start + size + # 4 = alloc_start + alloc_size + # 5 = start - alloc_start = p + # 6 = size + # 7 = {8} - ({5} + {6}) = alloc_size - (p + size) + # 8 = alloc_size + # + self.sizes[i] = p + self.starts.insert(i + 1, start + size) + self.sizes.insert(i + 1, alloc_size - (p + size)) + + def get_allocated_regions(self): + '''Get a list of (aggregate) allocated regions. + + The result of this method is ``(starts, sizes)``, where ``starts`` is + a list of starting indices of the regions and ``sizes`` their + corresponding lengths. + + :rtype: (list, list) + ''' + # return (starts, sizes); len(starts) == len(sizes) + return (self.starts, self.sizes) + + def get_fragmented_free_size(self): + '''Returns the amount of space unused, not including the final + free block. + + :rtype: int + ''' + if not self.starts: + return 0 + + # Variation of search for free block. + total_free = 0 + free_start = self.starts[0] + self.sizes[0] + for i, (alloc_start, alloc_size) in \ + enumerate(zip(self.starts[1:], self.sizes[1:])): + total_free += alloc_start - free_start + free_start = alloc_start + alloc_size + + return total_free + + def get_free_size(self): + '''Return the amount of space unused. + + :rtype: int + ''' + if not self.starts: + return self.capacity + + free_end = self.capacity - (self.starts[-1] + self.sizes[-1]) + return self.get_fragmented_free_size() + free_end + + def get_usage(self): + '''Return fraction of capacity currently allocated. + + :rtype: float + ''' + return 1. - self.get_free_size() / float(self.capacity) + + def get_fragmentation(self): + '''Return fraction of free space that is not expandable. + + :rtype: float + ''' + free_size = self.get_free_size() + if free_size == 0: + return 0. + return self.get_fragmented_free_size() / float(self.get_free_size()) + + def _is_empty(self): + return not self.starts + + def __str__(self): + return 'allocs=' + repr(zip(self.starts, self.sizes)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, str(self)) + + +############################################################################### +## vertexattribute.py ## +############################################################################### + +_c_types = { + GL_BYTE: ctypes.c_byte, + GL_UNSIGNED_BYTE: ctypes.c_ubyte, + GL_SHORT: ctypes.c_short, + GL_UNSIGNED_SHORT: ctypes.c_ushort, + GL_INT: ctypes.c_int, + GL_UNSIGNED_INT: ctypes.c_uint, + GL_FLOAT: ctypes.c_float, + GL_DOUBLE: ctypes.c_double, +} + +_gl_types = { + 'b': GL_BYTE, + 'B': GL_UNSIGNED_BYTE, + 's': GL_SHORT, + 'S': GL_UNSIGNED_SHORT, + 'i': GL_INT, + 'I': GL_UNSIGNED_INT, + 'f': GL_FLOAT, + 'd': GL_DOUBLE, +} + +_attribute_format_re = re.compile(r''' + (?P + [cefnstv] | + (?P[0-9]+) g + (?Pn?)) + (?P[1234]) + (?P[bBsSiIfd]) +''', re.VERBOSE) + +_attribute_cache = {} + +def create_attribute(format): + '''Create a vertex attribute description from a format string. + + The initial stride and offset of the attribute will be 0. + + :Parameters: + `format` : str + Attribute format string. See the module summary for details. + + :rtype: `AbstractAttribute` + ''' + try: + cls, args = _attribute_cache[format] + return cls(*args) + except KeyError: + pass + + match = _attribute_format_re.match(format) + assert match, 'Invalid attribute format %r' % format + count = int(match.group('count')) + gl_type = _gl_types[match.group('type')] + generic_index = match.group('generic_index') + if generic_index: + normalized = match.group('generic_normalized') + attr_class = GenericAttribute + args = int(generic_index), normalized, count, gl_type + else: + name = match.group('name') + attr_class = _attribute_classes[name] + if attr_class._fixed_count: + assert count == attr_class._fixed_count, \ + 'Attributes named "%s" must have count of %d' % ( + name, attr_class._fixed_count) + args = (gl_type,) + else: + args = (count, gl_type) + + _attribute_cache[format] = attr_class, args + return attr_class(*args) + +class AbstractAttribute(object): + '''Abstract accessor for an attribute in a mapped buffer. + ''' + + _fixed_count = None + + def __init__(self, count, gl_type): + '''Create the attribute accessor. + + :Parameters: + `count` : int + Number of components in the attribute. + `gl_type` : int + OpenGL type enumerant; for example, ``GL_FLOAT`` + + ''' + assert count in (1, 2, 3, 4), 'Component count out of range' + self.gl_type = gl_type + self.c_type = _c_types[gl_type] + self.count = count + self.align = ctypes.sizeof(self.c_type) + self.size = count * self.align + self.stride = self.size + self.offset = 0 + + def enable(self): + '''Enable the attribute using ``glEnableClientState``.''' + raise NotImplementedError('abstract') + + def set_pointer(self, offset): + '''Setup this attribute to point to the currently bound buffer at + the given offset. + + ``offset`` should be based on the currently bound buffer's ``ptr`` + member. + + :Parameters: + `offset` : int + Pointer offset to the currently bound buffer for this + attribute. + + ''' + raise NotImplementedError('abstract') + + def get_region(self, buffer, start, count): + '''Map a buffer region using this attribute as an accessor. + + The returned region can be modified as if the buffer was a contiguous + array of this attribute (though it may actually be interleaved or + otherwise non-contiguous). + + The returned region consists of a contiguous array of component + data elements. For example, if this attribute uses 3 floats per + vertex, and the `count` parameter is 4, the number of floats mapped + will be ``3 * 4 = 12``. + + :Parameters: + `buffer` : `AbstractMappable` + The buffer to map. + `start` : int + Offset of the first vertex to map. + `count` : int + Number of vertices to map + + :rtype: `AbstractBufferRegion` + ''' + byte_start = self.stride * start + byte_size = self.stride * count + array_count = self.count * count + if self.stride == self.size: + # non-interleaved + ptr_type = ctypes.POINTER(self.c_type * array_count) + return buffer.get_region(byte_start, byte_size, ptr_type) + else: + # interleaved + byte_start += self.offset + byte_size -= self.offset + elem_stride = self.stride // ctypes.sizeof(self.c_type) + elem_offset = self.offset // ctypes.sizeof(self.c_type) + ptr_type = ctypes.POINTER( + self.c_type * (count * elem_stride - elem_offset)) + region = buffer.get_region(byte_start, byte_size, ptr_type) + return vertexbuffer.IndirectArrayRegion( + region, array_count, self.count, elem_stride) + + def set_region(self, buffer, start, count, data): + '''Set the data over a region of the buffer. + + :Parameters: + `buffer` : AbstractMappable` + The buffer to modify. + `start` : int + Offset of the first vertex to set. + `count` : int + Number of vertices to set. + `data` : sequence + Sequence of data components. + + ''' + if self.stride == self.size: + # non-interleaved + byte_start = self.stride * start + byte_size = self.stride * count + array_count = self.count * count + data = (self.c_type * array_count)(*data) + buffer.set_data_region(data, byte_start, byte_size) + else: + # interleaved + region = self.get_region(buffer, start, count) + region[:] = data + +class ColorAttribute(AbstractAttribute): + '''Color vertex attribute.''' + + plural = 'colors' + + def __init__(self, count, gl_type): + assert count in (3, 4), 'Color attributes must have count of 3 or 4' + super(ColorAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_COLOR_ARRAY) + + def set_pointer(self, pointer): + glColorPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class TexCoordAttribute(AbstractAttribute): + '''Texture coordinate attribute.''' + + plural = 'tex_coords' + + def __init__(self, count, gl_type): + assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Texture coord attribute must have non-byte signed type' + super(TexCoordAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_TEXTURE_COORD_ARRAY) + + def set_pointer(self, pointer): + glTexCoordPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class VertexAttribute(AbstractAttribute): + '''Vertex coordinate attribute.''' + + plural = 'vertices' + + def __init__(self, count, gl_type): + assert count > 1, \ + 'Vertex attribute must have count of 2, 3 or 4' + assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Vertex attribute must have signed type larger than byte' + super(VertexAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_VERTEX_ARRAY) + + def set_pointer(self, pointer): + glVertexPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class GenericAttribute(AbstractAttribute): + '''Generic vertex attribute, used by shader programs.''' + + def __init__(self, index, normalized, count, gl_type): + self.normalized = bool(normalized) + self.index = index + super(GenericAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableVertexAttribArray(self.index) + + def set_pointer(self, pointer): + glVertexAttribPointer(self.index, self.count, self.gl_type, + self.normalized, self.stride, + self.offset + pointer) + +_attribute_classes = { + 'c': ColorAttribute, + 't': TexCoordAttribute, + 'v': VertexAttribute, +} + + +############################################################################### +## vertexbuffer.py ## +############################################################################### + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +_enable_vbo = pyglet.options['graphics_vbo'] + +# Enable workaround permanently if any VBO is created on a context that has +# this workaround. (On systems with multiple contexts where one is +# unaffected, the workaround will be enabled unconditionally on all of the +# contexts anyway. This is completely unlikely anyway). +_workaround_vbo_finish = False + +def create_buffer(size, + target=GL_ARRAY_BUFFER, + usage=GL_DYNAMIC_DRAW, + vbo=True): + '''Create a buffer of vertex data. + + :Parameters: + `size` : int + Size of the buffer, in bytes + `target` : int + OpenGL target buffer + `usage` : int + OpenGL usage constant + `vbo` : bool + True if a `VertexBufferObject` should be created if the driver + supports it; otherwise only a `VertexArray` is created. + + :rtype: `AbstractBuffer` + ''' + from pyglet import gl + if (vbo and + gl_info.have_version(1, 5) and + _enable_vbo and + not gl.current_context._workaround_vbo): + return VertexBufferObject(size, target, usage) + else: + return VertexArray(size) + +def create_mappable_buffer(size, + target=GL_ARRAY_BUFFER, + usage=GL_DYNAMIC_DRAW, + vbo=True): + '''Create a mappable buffer of vertex data. + + :Parameters: + `size` : int + Size of the buffer, in bytes + `target` : int + OpenGL target buffer + `usage` : int + OpenGL usage constant + `vbo` : bool + True if a `VertexBufferObject` should be created if the driver + supports it; otherwise only a `VertexArray` is created. + + :rtype: `AbstractBuffer` with `AbstractMappable` + ''' + from pyglet import gl + if (vbo and + gl_info.have_version(1, 5) and + _enable_vbo and + not gl.current_context._workaround_vbo): + return MappableVertexBufferObject(size, target, usage) + else: + return VertexArray(size) + +class AbstractBuffer(object): + '''Abstract buffer of byte data. + + :Ivariables: + `size` : int + Size of buffer, in bytes + `ptr` : int + Memory offset of the buffer, as used by the ``glVertexPointer`` + family of functions + `target` : int + OpenGL buffer target, for example ``GL_ARRAY_BUFFER`` + `usage` : int + OpenGL buffer usage, for example ``GL_DYNAMIC_DRAW`` + + ''' + + ptr = 0 + size = 0 + + def bind(self): + '''Bind this buffer to its OpenGL target.''' + raise NotImplementedError('abstract') + + def unbind(self): + '''Reset the buffer's OpenGL target.''' + raise NotImplementedError('abstract') + + def set_data(self, data): + '''Set the entire contents of the buffer. + + :Parameters: + `data` : sequence of int or ctypes pointer + The byte array to set. + + ''' + raise NotImplementedError('abstract') + + def set_data_region(self, data, start, length): + '''Set part of the buffer contents. + + :Parameters: + `data` : sequence of int or ctypes pointer + The byte array of data to set + `start` : int + Offset to start replacing data + `length` : int + Length of region to replace + + ''' + raise NotImplementedError('abstract') + + def map(self, invalidate=False): + '''Map the entire buffer into system memory. + + The mapped region must be subsequently unmapped with `unmap` before + performing any other operations on the buffer. + + :Parameters: + `invalidate` : bool + If True, the initial contents of the mapped block need not + reflect the actual contents of the buffer. + + :rtype: ``POINTER(ctypes.c_ubyte)`` + :return: Pointer to the mapped block in memory + ''' + raise NotImplementedError('abstract') + + def unmap(self): + '''Unmap a previously mapped memory block.''' + raise NotImplementedError('abstract') + + def resize(self, size): + '''Resize the buffer to a new size. + + :Parameters: + `size` : int + New size of the buffer, in bytes + + ''' + + def delete(self): + '''Delete this buffer, reducing system resource usage.''' + raise NotImplementedError('abstract') + +class AbstractMappable(object): + def get_region(self, start, size, ptr_type): + '''Map a region of the buffer into a ctypes array of the desired + type. This region does not need to be unmapped, but will become + invalid if the buffer is resized. + + Note that although a pointer type is required, an array is mapped. + For example:: + + get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20)) + + will map bytes 0 to 80 of the buffer to an array of 20 ints. + + Changes to the array may not be recognised until the region's + `AbstractBufferRegion.invalidate` method is called. + + :Parameters: + `start` : int + Offset into the buffer to map from, in bytes + `size` : int + Size of the buffer region to map, in bytes + `ptr_type` : ctypes pointer type + Pointer type describing the array format to create + + :rtype: `AbstractBufferRegion` + ''' + raise NotImplementedError('abstract') + +class VertexArray(AbstractBuffer, AbstractMappable): + '''A ctypes implementation of a vertex array. + + Many of the methods on this class are effectively no-op's, such as `bind`, + `unbind`, `map`, `unmap` and `delete`; they exist in order to present + a consistent interface with `VertexBufferObject`. + + This buffer type is also mappable, and so `get_region` can be used. + ''' + + def __init__(self, size): + self.size = size + + self.array = (ctypes.c_byte * size)() + self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value + + def bind(self): + pass + + def unbind(self): + pass + + def set_data(self, data): + ctypes.memmove(self.ptr, data, self.size) + + def set_data_region(self, data, start, length): + ctypes.memmove(self.ptr + start, data, length) + + def map(self, invalidate=False): + return self.array + + def unmap(self): + pass + + def get_region(self, start, size, ptr_type): + array = ctypes.cast(self.ptr + start, ptr_type).contents + return VertexArrayRegion(array) + + def delete(self): + pass + + def resize(self, size): + array = (ctypes.c_byte * size)() + ctypes.memmove(array, self.array, min(size, self.size)) + self.size = size + self.array = array + self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value + + +class VertexBufferObject(AbstractBuffer): + '''Lightweight representation of an OpenGL VBO. + + The data in the buffer is not replicated in any system memory (unless it + is done so by the video driver). While this can improve memory usage and + possibly performance, updates to the buffer are relatively slow. + + This class does not implement `AbstractMappable`, and so has no + ``get_region`` method. See `MappableVertexBufferObject` for a VBO class + that does implement ``get_region``. + ''' + + def __init__(self, size, target, usage): + self.size = size + self.target = target + self.usage = usage + self._context = pyglet.gl.current_context + + id = GLuint() + glGenBuffers(1, id) + self.id = id.value + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(target, self.id) + glBufferData(target, self.size, None, self.usage) + glPopClientAttrib() + + global _workaround_vbo_finish + if pyglet.gl.current_context._workaround_vbo_finish: + _workaround_vbo_finish = True + + def bind(self): + glBindBuffer(self.target, self.id) + + def unbind(self): + glBindBuffer(self.target, 0) + + def set_data(self, data): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferData(self.target, self.size, data, self.usage) + glPopClientAttrib() + + def set_data_region(self, data, start, length): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferSubData(self.target, start, length, data) + glPopClientAttrib() + + def map(self, invalidate=False): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + if invalidate: + glBufferData(self.target, self.size, None, self.usage) + ptr = ctypes.cast(glMapBuffer(self.target, GL_WRITE_ONLY), + ctypes.POINTER(ctypes.c_byte * self.size)).contents + glPopClientAttrib() + return ptr + + def unmap(self): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glUnmapBuffer(self.target) + glPopClientAttrib() + + def __del__(self): + try: + if self.id is not None: + self._context.delete_buffer(self.id) + except: + pass + + def delete(self): + id = GLuint(self.id) + glDeleteBuffers(1, id) + self.id = None + + def resize(self, size): + # Map, create a copy, then reinitialize. + temp = (ctypes.c_byte * size)() + + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + data = glMapBuffer(self.target, GL_READ_ONLY) + ctypes.memmove(temp, data, min(size, self.size)) + glUnmapBuffer(self.target) + + self.size = size + glBufferData(self.target, self.size, temp, self.usage) + glPopClientAttrib() + +class AbstractBufferRegion(object): + '''A mapped region of a buffer. + + Buffer regions are obtained using `AbstractMappable.get_region`. + + :Ivariables: + `array` : ctypes array + Array of data, of the type and count requested by ``get_region``. + + ''' + def invalidate(self): + '''Mark this region as changed. + + The buffer may not be updated with the latest contents of the + array until this method is called. (However, it may not be updated + until the next time the buffer is used, for efficiency). + ''' + pass + +class VertexBufferObjectRegion(AbstractBufferRegion): + '''A mapped region of a VBO.''' + def __init__(self, buffer, start, end, array): + self.buffer = buffer + self.start = start + self.end = end + self.array = array + + def invalidate(self): + buffer = self.buffer + buffer._dirty_min = min(buffer._dirty_min, self.start) + buffer._dirty_max = max(buffer._dirty_max, self.end) + +class VertexArrayRegion(AbstractBufferRegion): + '''A mapped region of a vertex array. + + The `invalidate` method is a no-op but is provided in order to present + a consistent interface with `VertexBufferObjectRegion`. + ''' + def __init__(self, array): + self.array = array + +############################################################################### +## vertexdomain.py ## +############################################################################### + + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + + +_usage_format_re = re.compile(r''' + (?P[^/]*) + (/ (?P static|dynamic|stream|none))? +''', re.VERBOSE) + +_gl_usages = { + 'static': GL_STATIC_DRAW, + 'dynamic': GL_DYNAMIC_DRAW, + 'stream': GL_STREAM_DRAW, + 'none': GL_STREAM_DRAW_ARB, # Force no VBO +} + +def _nearest_pow2(v): + # From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + # Credit: Sean Anderson + v -= 1 + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + return v + 1 + +def create_attribute_usage(format): + '''Create an attribute and usage pair from a format string. The + format string is as documented in `pyglet.graphics.vertexattribute`, with + the addition of an optional usage component:: + + usage ::= attribute ( '/' ('static' | 'dynamic' | 'stream' | 'none') )? + + If the usage is not given it defaults to 'dynamic'. The usage corresponds + to the OpenGL VBO usage hint, and for ``static`` also indicates a + preference for interleaved arrays. If ``none`` is specified a buffer + object is not created, and vertex data is stored in system memory. + + Some examples: + + ``v3f/stream`` + 3D vertex position using floats, for stream usage + ``c4b/static`` + 4-byte color attribute, for static usage + + :return: attribute, usage + ''' + match = _usage_format_re.match(format) + attribute_format = match.group('attribute') + attribute = create_attribute(attribute_format) + usage = match.group('usage') + if usage: + vbo = not usage == 'none' + usage = _gl_usages[usage] + else: + usage = GL_DYNAMIC_DRAW + vbo = True + + return (attribute, usage, vbo) + +def create_domain(*attribute_usage_formats): + '''Create a vertex domain covering the given attribute usage formats. + See documentation for `create_attribute_usage` and + `pyglet.graphics.vertexattribute.create_attribute` for the grammar of + these format strings. + + :rtype: `VertexDomain` + ''' + attribute_usages = [create_attribute_usage(f) \ + for f in attribute_usage_formats] + return VertexDomain(attribute_usages) + +class VertexDomain(object): + '''Management of a set of vertex lists. + + Construction of a vertex domain is usually done with the `create_domain` + function. + ''' + _version = 0 + _initial_count = 16 + + def __init__(self, attribute_usages): + self.allocator = Allocator(self._initial_count) + + static_attributes = [] + attributes = [] + self.buffer_attributes = [] # list of (buffer, attributes) + for attribute, usage, vbo in attribute_usages: + # Create non-interleaved buffer + attributes.append(attribute) + attribute.buffer = create_mappable_buffer( + attribute.stride * self.allocator.capacity, + usage=usage, vbo=vbo) + attribute.buffer.element_size = attribute.stride + #attribute.buffer.attributes = (attribute,) + self.buffer_attributes.append( + (attribute.buffer, attribute)) + + # Create named attributes for each attribute + self.attributes = attributes + self.attribute_names = {} + for attribute in attributes: + if isinstance(attribute, GenericAttribute): + index = attribute.index + if 'generic' not in self.attributes: + self.attribute_names['generic'] = {} + assert index not in self.attribute_names['generic'], \ + 'More than one generic attribute with index %d' % index + self.attribute_names['generic'][index] = attribute + else: + name = attribute.plural + assert name not in self.attributes, \ + 'More than one "%s" attribute given' % name + self.attribute_names[name] = attribute + + def __del__(self): + # Break circular refs that Python GC seems to miss even when forced + # collection. + for attribute in self.attributes: + del attribute.buffer + + def _safe_alloc(self, count): + '''Allocate vertices, resizing the buffers if necessary.''' + try: + return self.allocator.alloc(count) + except AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + for buffer, _ in self.buffer_attributes: + buffer.resize(capacity * buffer.element_size) + self.allocator.set_capacity(capacity) + return self.allocator.alloc(count) + + def _safe_realloc(self, start, count, new_count): + '''Reallocate vertices, resizing the buffers if necessary.''' + try: + return self.allocator.realloc(start, count, new_count) + except AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + for buffer, _ in self.buffer_attributes: + buffer.resize(capacity * buffer.element_size) + self.allocator.set_capacity(capacity) + return self.allocator.realloc(start, count, new_count) + + def create(self, count): + '''Create a `VertexList` in this domain. + + :Parameters: + `count` : int + Number of vertices to create. + + :rtype: `VertexList` + ''' + start = self._safe_alloc(count) + return VertexList(self, start, count) + + def draw(self, mode): + '''Draw vertices in the domain. + + If `vertex_list` is not specified, all vertices in the domain are + drawn. This is the most efficient way to render primitives. + + If `vertex_list` specifies a `VertexList`, only primitives in that + list will be drawn. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + `vertex_list` : `VertexList` + Vertex list to draw, or ``None`` for all lists in this domain. + + ''' + + starts, sizes = self.allocator.get_allocated_regions() + primcount = len(starts) + + if primcount == 0: + pass + elif primcount == 1: + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + for buffer, attribute in self.buffer_attributes: + attribute.enable() + attribute.set_pointer(attribute.buffer.ptr) + if _workaround_vbo_finish: + glFinish() + + # Common case + glDrawArrays(mode, starts[0], sizes[0]) + glPopClientAttrib() + + def _is_empty(self): + return not self.allocator.starts + + def __repr__(self): + return '<%s@%x %s>' % (self.__class__.__name__, id(self), + self.allocator) + +class VertexList(object): + '''A list of vertices within a `VertexDomain`. Use + `VertexDomain.create` to construct this list. + ''' + + def __init__(self, domain, start, count): + # TODO make private + self.domain = domain + self.start = start + self.count = count + + def get_size(self): + '''Get the number of vertices in the list. + + :rtype: int + ''' + return self.count + + def get_domain(self): + '''Get the domain this vertex list belongs to. + + :rtype: `VertexDomain` + ''' + return self.domain + + def draw(self, mode): + '''Draw this vertex list in the given OpenGL mode. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + + ''' + self.domain.draw(mode, self) + + def resize(self, count): + '''Resize this group. + + :Parameters: + `count` : int + New number of vertices in the list. + + ''' + new_start = self.domain._safe_realloc(self.start, self.count, count) + if new_start != self.start: + # Copy contents to new location + for attribute in self.domain.attributes: + old = attribute.get_region(attribute.buffer, + self.start, self.count) + new = attribute.get_region(attribute.buffer, + new_start, self.count) + new.array[:] = old.array[:] + new.invalidate() + self.start = new_start + self.count = count + + self._colors_cache_version = None + self._fog_coords_cache_version = None + self._edge_flags_cache_version = None + self._normals_cache_version = None + self._secondary_colors_cache_version = None + self._tex_coords_cache_version = None + self._vertices_cache_version = None + + def delete(self): + '''Delete this group.''' + self.domain.allocator.dealloc(self.start, self.count) + + def _set_attribute_data(self, i, data): + attribute = self.domain.attributes[i] + # TODO without region + region = attribute.get_region(attribute.buffer, self.start, self.count) + region.array[:] = data + region.invalidate() + + # --- + + def _get_colors(self): + if (self._colors_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['colors'] + self._colors_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._colors_cache_version = domain._version + + region = self._colors_cache + region.invalidate() + return region.array + + def _set_colors(self, data): + self._get_colors()[:] = data + + _colors_cache = None + _colors_cache_version = None + colors = property(_get_colors, _set_colors, + doc='''Array of color data.''') + + # --- + + _tex_coords_cache = None + _tex_coords_cache_version = None + + def _get_tex_coords(self): + if (self._tex_coords_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['tex_coords'] + self._tex_coords_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._tex_coords_cache_version = domain._version + + region = self._tex_coords_cache + region.invalidate() + return region.array + + def _set_tex_coords(self, data): + self._get_tex_coords()[:] = data + + tex_coords = property(_get_tex_coords, _set_tex_coords, + doc='''Array of texture coordinate data.''') + + # --- + + _vertices_cache = None + _vertices_cache_version = None + + def _get_vertices(self): + if (self._vertices_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['vertices'] + self._vertices_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._vertices_cache_version = domain._version + + region = self._vertices_cache + region.invalidate() + return region.array + + def _set_vertices(self, data): + self._get_vertices()[:] = data + + vertices = property(_get_vertices, _set_vertices, + doc='''Array of vertex coordinate data.''') diff --git a/lib/batch.py_ b/lib/batch.py_ new file mode 100644 index 0000000..7ed63c0 --- /dev/null +++ b/lib/batch.py_ @@ -0,0 +1,2979 @@ +# ---------------------------------------------------------------------------- +# pyglet +# Copyright (c) 2006-2008 Alex Holkner +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of pyglet nor the names of its +# contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- +# $Id:$ + +'''Low-level graphics rendering. + +This module provides an efficient low-level abstraction over OpenGL. It gives +very good performance for rendering OpenGL primitives; far better than the +typical immediate-mode usage and, on modern graphics cards, better than using +display lists in many cases. The module is used internally by other areas of +pyglet. + +See the Programming Guide for details on how to use this graphics API. + +Batches and groups +================== + +Without even needing to understand the details on how to draw primitives with +the graphics API, developers can make use of `Batch` and `Group` +objects to improve performance of sprite and text rendering. + +The `Sprite`, `Label` and `TextLayout` classes all accept a ``batch`` and +``group`` parameter in their constructors. A batch manages a set of objects +that will be drawn all at once, and a group describes the manner in which an +object is drawn. + +The following example creates a batch, adds two sprites to the batch, and then +draws the entire batch:: + + batch = pyglet.graphics.Batch() + car = pyglet.sprite.Sprite(car_image, batch=batch) + boat = pyglet.sprite.Sprite(boat_image, batch=batch) + + def on_draw() + batch.draw() + +Drawing a complete batch is much faster than drawing the items in the batch +individually, especially when those items belong to a common group. + +Groups describe the OpenGL state required for an item. This is for the most +part managed by the sprite and text classes, however you can also use groups +to ensure items are drawn in a particular order. For example, the following +example adds a background sprite which is guaranteed to be drawn before the +car and the boat:: + + batch = pyglet.graphics.Batch() + background = pyglet.graphics.OrderedGroup(0) + foreground = pyglet.graphics.OrderedGroup(1) + + background = pyglet.sprite.Sprite(background_image, + batch=batch, group=background) + car = pyglet.sprite.Sprite(car_image, batch=batch, group=foreground) + boat = pyglet.sprite.Sprite(boat_image, batch=batch, group=foreground) + + def on_draw() + batch.draw() + +It's preferable to manage sprites and text objects within as few batches as +possible. If the drawing of sprites or text objects need to be interleaved +with other drawing that does not use the graphics API, multiple batches will +be required. + +Data item parameters +==================== + +Many of the functions and methods in this module accept any number of ``data`` +parameters as their final parameters. In the documentation these are notated +as ``*data`` in the formal parameter list. + +A data parameter describes a vertex attribute format and an optional sequence +to initialise that attribute. Examples of common attribute formats are: + +``"v3f"`` + Vertex position, specified as three floats. +``"c4B"`` + Vertex color, specifed as four unsigned bytes. +``"t2f"`` + Texture coordinate, specified as two floats. + +See `pyglet.graphics.vertexattribute` for the complete syntax of the vertex +format string. + +When no initial data is to be given, the data item is just the format string. +For example, the following creates a 2 element vertex list with position and +color attributes:: + + vertex_list = pyglet.graphics.vertex_list(2, 'v2f', 'c4B') + +When initial data is required, wrap the format string and the initial data in +a tuple, for example:: + + vertex_list = pyglet.graphics.vertex_list(2, + ('v2f', (0.0, 1.0, 1.0, 0.0)), + ('c4B', (255, 255, 255, 255) * 2)) + +Drawing modes +============= + +Methods in this module that accept a ``mode`` parameter will accept any value +in the OpenGL drawing mode enumeration; for example, ``GL_POINTS``, +``GL_LINES``, ``GL_TRIANGLES``, etc. + +Because of the way the graphics API renders multiple primitives with shared +state, ``GL_POLYGON``, ``GL_LINE_LOOP`` and ``GL_TRIANGLE_FAN`` cannot be used +--- the results are undefined. + +When using ``GL_LINE_STRIP``, ``GL_TRIANGLE_STRIP`` or ``GL_QUAD_STRIP`` care +must be taken to insert degenrate vertices at the beginning and end of each +vertex list. For example, given the vertex list:: + + A, B, C, D + +the correct vertex list to provide the vertex list is:: + + A, A, B, C, D, D + +Alternatively, the ``NV_primitive_restart`` extension can be used if it is +present. This also permits use of ``GL_POLYGON``, ``GL_LINE_LOOP`` and +``GL_TRIANGLE_FAN``. Unfortunatley the extension is not provided by older +video drivers, and requires indexed vertex lists. + +:since: pyglet 1.1 +''' + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +import ctypes + +import pyglet +from pyglet.gl import * +import ctypes +import re + +_debug_graphics_batch = pyglet.options['debug_graphics_batch'] + +def draw(size, mode, *data): + '''Draw a primitive immediately. + + :Parameters: + `size` : int + Number of vertices given + `mode` : int + OpenGL drawing mode, e.g. ``GL_TRIANGLES`` + `data` : data items + Attribute formats and data. See the module summary for + details. + + ''' + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + + buffers = [] + for format, array in data: + attribute = vertexattribute.create_attribute(format) + assert size == len(array) // attribute.count, \ + 'Data for %s is incorrect length' % format + buffer = vertexbuffer.create_mappable_buffer( + size * attribute.stride, vbo=False) + + attribute.set_region(buffer, 0, size, array) + attribute.enable() + attribute.set_pointer(buffer.ptr) + buffers.append(buffer) + + glDrawArrays(mode, 0, size) + glFlush() + + glPopClientAttrib() + +def draw_indexed(size, mode, indices, *data): + '''Draw a primitive with indexed vertices immediately. + + :Parameters: + `size` : int + Number of vertices given + `mode` : int + OpenGL drawing mode, e.g. ``GL_TRIANGLES`` + `indices` : sequence of int + Sequence of integers giving indices into the vertex list. + `data` : data items + Attribute formats and data. See the module summary for details. + + ''' + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + + buffers = [] + for format, array in data: + attribute = vertexattribute.create_attribute(format) + assert size == len(array) // attribute.count, \ + 'Data for %s is incorrect length' % format + buffer = vertexbuffer.create_mappable_buffer( + size * attribute.stride, vbo=False) + + attribute.set_region(buffer, 0, size, array) + attribute.enable() + attribute.set_pointer(buffer.ptr) + buffers.append(buffer) + + if size <= 0xff: + index_type = GL_UNSIGNED_BYTE + index_c_type = ctypes.c_ubyte + elif size <= 0xffff: + index_type = GL_UNSIGNED_SHORT + index_c_type = ctypes.c_ushort + else: + index_type = GL_UNSIGNED_INT + index_c_type = ctypes.c_uint + + index_array = (index_c_type * len(indices))(*indices) + glDrawElements(mode, len(indices), index_type, index_array) + glFlush() + + glPopClientAttrib() + +def _parse_data(data): + '''Given a list of data items, returns (formats, initial_arrays).''' + assert data, 'No attribute formats given' + + # Return tuple (formats, initial_arrays). + formats = [] + initial_arrays = [] + for i, format in enumerate(data): + if isinstance(format, tuple): + format, array = format + initial_arrays.append((i, array)) + formats.append(format) + formats = tuple(formats) + return formats, initial_arrays + +def _get_default_batch(): + shared_object_space = gl.current_context.object_space + try: + return shared_object_space.pyglet_graphics_default_batch + except AttributeError: + shared_object_space.pyglet_graphics_default_batch = Batch() + return shared_object_space.pyglet_graphics_default_batch + +def vertex_list(count, *data): + '''Create a `VertexList` not associated with a batch, group or mode. + + :Parameters: + `count` : int + The number of vertices in the list. + `data` : data items + Attribute formats and initial data for the vertex list. See the + module summary for details. + + :rtype: `VertexList` + ''' + # Note that mode=0 because the default batch is never drawn: vertex lists + # returned from this function are drawn directly by the app. + return _get_default_batch().add(count, 0, None, *data) + +def vertex_list_indexed(count, indices, *data): + '''Create an `IndexedVertexList` not associated with a batch, group or mode. + + :Parameters: + `count` : int + The number of vertices in the list. + `indices` : sequence + Sequence of integers giving indices into the vertex list. + `data` : data items + Attribute formats and initial data for the vertex list. See the + module summary for details. + + :rtype: `IndexedVertexList` + ''' + # Note that mode=0 because the default batch is never drawn: vertex lists + # returned from this function are drawn directly by the app. + return _get_default_batch().add_indexed(count, 0, None, indices, *data) + +class Batch(object): + '''Manage a collection of vertex lists for batched rendering. + + Vertex lists are added to a `Batch` using the `add` and `add_indexed` + methods. An optional group can be specified along with the vertex list, + which gives the OpenGL state required for its rendering. Vertex lists + with shared mode and group are allocated into adjacent areas of memory and + sent to the graphics card in a single operation. + + Call `VertexList.delete` to remove a vertex list from the batch. + ''' + def __init__(self): + '''Create a graphics batch.''' + # Mapping to find domain. + # group -> (attributes, mode, indexed) -> domain + self.group_map = {} + + # Mapping of group to list of children. + self.group_children = {} + + # List of top-level groups + self.top_groups = [] + + self._draw_list = [] + self._draw_list_dirty = False + + def add(self, count, mode, group, *data): + '''Add a vertex list to the batch. + + :Parameters: + `count` : int + The number of vertices in the list. + `mode` : int + OpenGL drawing mode enumeration; for example, one of + ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. + See the module summary for additional information. + `group` : `Group` + Group of the vertex list, or ``None`` if no group is required. + `data` : data items + Attribute formats and initial data for the vertex list. See + the module summary for details. + + :rtype: `VertexList` + ''' + formats, initial_arrays = _parse_data(data) + domain = self._get_domain(False, mode, group, formats) + domain.__formats = formats + + # Create vertex list and initialize + vlist = domain.create(count) + for i, array in initial_arrays: + vlist._set_attribute_data(i, array) + + return vlist + + def add_indexed(self, count, mode, group, indices, *data): + '''Add an indexed vertex list to the batch. + + :Parameters: + `count` : int + The number of vertices in the list. + `mode` : int + OpenGL drawing mode enumeration; for example, one of + ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. + See the module summary for additional information. + `group` : `Group` + Group of the vertex list, or ``None`` if no group is required. + `indices` : sequence + Sequence of integers giving indices into the vertex list. + `data` : data items + Attribute formats and initial data for the vertex list. See + the module summary for details. + + :rtype: `IndexedVertexList` + ''' + formats, initial_arrays = _parse_data(data) + domain = self._get_domain(True, mode, group, formats) + + # Create vertex list and initialize + vlist = domain.create(count, len(indices)) + start = vlist.start + vlist._set_index_data(map(lambda i: i + start, indices)) + for i, array in initial_arrays: + vlist._set_attribute_data(i, array) + + return vlist + + def migrate(self, vertex_list, mode, group, batch): + '''Migrate a vertex list to another batch and/or group. + + `vertex_list` and `mode` together identify the vertex list to migrate. + `group` and `batch` are new owners of the vertex list after migration. + + The results are undefined if `mode` is not correct or if `vertex_list` + does not belong to this batch (they are not checked and will not + necessarily throw an exception immediately). + + `batch` can remain unchanged if only a group change is desired. + + :Parameters: + `vertex_list` : `VertexList` + A vertex list currently belonging to this batch. + `mode` : int + The current GL drawing mode of the vertex list. + `group` : `Group` + The new group to migrate to. + `batch` : `Batch` + The batch to migrate to (or the current batch). + + ''' + formats = vertex_list.domain.__formats + domain = batch._get_domain(False, mode, group, formats) + vertex_list.migrate(domain) + + def _get_domain(self, indexed, mode, group, formats): + if group is None: + group = null_group + + # Batch group + if group not in self.group_map: + self._add_group(group) + + domain_map = self.group_map[group] + + # Find domain given formats, indices and mode + key = (formats, mode, indexed) + try: + domain = domain_map[key] + except KeyError: + # Create domain + if indexed: + domain = vertexdomain.create_indexed_domain(*formats) + else: + domain = create_domain(*formats) + domain_map[key] = domain + self._draw_list_dirty = True + + return domain + + def _add_group(self, group): + self.group_map[group] = {} + if group.parent is None: + self.top_groups.append(group) + else: + if group.parent not in self.group_map: + self._add_group(group.parent) + if group.parent not in self.group_children: + self.group_children[group.parent] = [] + self.group_children[group.parent].append(group) + self._draw_list_dirty = True + + def _update_draw_list(self): + '''Visit group tree in preorder and create a list of bound methods + to call. + ''' + + def visit(group): + draw_list = [] + + # Draw domains using this group + domain_map = self.group_map[group] + for (formats, mode, indexed), domain in list(domain_map.items()): + # Remove unused domains from batch + if domain._is_empty(): + del domain_map[(formats, mode, indexed)] + continue + draw_list.append( + (lambda d, m: lambda: d.draw(m))(domain, mode)) + + # Sort and visit child groups of this group + children = self.group_children.get(group) + if children: + children.sort() + for child in list(children): + draw_list.extend(visit(child)) + + if children or domain_map: + return [group.set_state] + draw_list + [group.unset_state] + else: + # Remove unused group from batch + del self.group_map[group] + if group.parent: + self.group_children[group.parent].remove(group) + try: + del self.group_children[group] + except KeyError: + pass + try: + self.top_groups.remove(group) + except ValueError: + pass + return [] + + self._draw_list = [] + + self.top_groups.sort() + for group in list(self.top_groups): + self._draw_list.extend(visit(group)) + + self._draw_list_dirty = False + + if _debug_graphics_batch: + self._dump_draw_list() + + def _dump_draw_list(self): + def dump(group, indent=''): + print indent, 'Begin group', group + domain_map = self.group_map[group] + for _, domain in domain_map.items(): + print indent, ' ', domain + for start, size in zip(*domain.allocator.get_allocated_regions()): + print indent, ' ', 'Region %d size %d:' % (start, size) + for key, attribute in domain.attribute_names.items(): + print indent, ' ', + try: + region = attribute.get_region(attribute.buffer, + start, size) + print key, region.array[:] + except: + print key, '(unmappable)' + for child in self.group_children.get(group, ()): + dump(child, indent + ' ') + print indent, 'End group', group + + print 'Draw list for %r:' % self + for group in self.top_groups: + dump(group) + + def draw(self): + '''Draw the batch. + ''' + if self._draw_list_dirty: + self._update_draw_list() + + for func in self._draw_list: + func() + + def draw_subset(self, vertex_lists): + '''Draw only some vertex lists in the batch. + + The use of this method is highly discouraged, as it is quite + inefficient. Usually an application can be redesigned so that batches + can always be drawn in their entirety, using `draw`. + + The given vertex lists must belong to this batch; behaviour is + undefined if this condition is not met. + + :Parameters: + `vertex_lists` : sequence of `VertexList` or `IndexedVertexList` + Vertex lists to draw. + + ''' + # Horrendously inefficient. + def visit(group): + group.set_state() + + # Draw domains using this group + domain_map = self.group_map[group] + for (_, mode, _), domain in domain_map.items(): + for list in vertex_lists: + if list.domain is domain: + list.draw(mode) + + # Sort and visit child groups of this group + children = self.group_children.get(group) + if children: + children.sort() + for child in children: + visit(child) + + group.unset_state() + + self.top_groups.sort() + for group in self.top_groups: + visit(group) + +class Group(object): + '''Group of common OpenGL state. + + Before a vertex list is rendered, its group's OpenGL state is set; as are + that state's ancestors' states. This can be defined arbitrarily on + subclasses; the default state change has no effect, and groups vertex + lists only in the order in which they are drawn. + ''' + def __init__(self, parent=None): + '''Create a group. + + :Parameters: + `parent` : `Group` + Group to contain this group; its state will be set before this + state's. + + ''' + self.parent = parent + + def set_state(self): + '''Apply the OpenGL state change. + + The default implementation does nothing.''' + pass + + def unset_state(self): + '''Repeal the OpenGL state change. + + The default implementation does nothing.''' + pass + + def set_state_recursive(self): + '''Set this group and its ancestry. + + Call this method if you are using a group in isolation: the + parent groups will be called in top-down order, with this class's + `set` being called last. + ''' + if self.parent: + self.parent.set_state_recursive() + self.set_state() + + def unset_state_recursive(self): + '''Unset this group and its ancestry. + + The inverse of `set_state_recursive`. + ''' + self.unset_state() + if self.parent: + self.parent.unset_state_recursive() + +class NullGroup(Group): + '''The default group class used when ``None`` is given to a batch. + + This implementation has no effect. + ''' + pass + +#: The default group. +#: +#: :type: `Group` +null_group = NullGroup() + +class TextureGroup(Group): + '''A group that enables and binds a texture. + + Texture groups are equal if their textures' targets and names are equal. + ''' + # Don't use this, create your own group classes that are more specific. + # This is just an example. + def __init__(self, texture, parent=None): + '''Create a texture group. + + :Parameters: + `texture` : `Texture` + Texture to bind. + `parent` : `Group` + Parent group. + + ''' + super(TextureGroup, self).__init__(parent) + self.texture = texture + + def set_state(self): + glEnable(self.texture.target) + glBindTexture(self.texture.target, self.texture.id) + + def unset_state(self): + glDisable(self.texture.target) + + def __hash__(self): + return hash((self.texture.target, self.texture.id, self.parent)) + + def __eq__(self, other): + return (self.__class__ is other.__class__ and + self.texture.target == other.texture.target and + self.texture.id == other.texture.id and + self.parent == other.parent) + + def __repr__(self): + return '%s(id=%d)' % (self.__class__.__name__, self.texture.id) + +class OrderedGroup(Group): + '''A group with partial order. + + Ordered groups with a common parent are rendered in ascending order of + their ``order`` field. This is a useful way to render multiple layers of + a scene within a single batch. + ''' + # This can be useful as a top-level group, or as a superclass for other + # groups that need to be ordered. + # + # As a top-level group it's useful because graphics can be composited in a + # known order even if they don't know about each other or share any known + # group. + def __init__(self, order, parent=None): + '''Create an ordered group. + + :Parameters: + `order` : int + Order of this group. + `parent` : `Group` + Parent of this group. + + ''' + super(OrderedGroup, self).__init__(parent) + self.order = order + + def __cmp__(self, other): + if isinstance(other, OrderedGroup): + return cmp(self.order, other.order) + return -1 + + def __eq__(self, other): + return (self.__class__ is other.__class__ and + self.order == other.order and + self.parent == other.parent) + + def __hash__(self): + return hash((self.order, self.parent)) + + def __repr__(self): + return '%s(%d)' % (self.__class__.__name__, self.order) + +# ---------------------------------------------------------------------------- +# pyglet +# Copyright (c) 2006-2008 Alex Holkner +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of pyglet nor the names of its +# contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- +# $Id:$ + +'''Memory allocation algorithm for vertex arrays and buffers. + +The region allocator is used to allocate vertex indices within a vertex +domain's multiple buffers. ("Buffer" refers to any abstract buffer presented +by `pyglet.graphics.vertexbuffer`. + +The allocator will at times request more space from the buffers. The current +policy is to double the buffer size when there is not enough room to fulfil an +allocation. The buffer is never resized smaller. + +The allocator maintains references to free space only; it is the caller's +responsibility to mantain the allocated regions. +''' + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +# Common cases: +# -regions will be the same size (instances of same object, e.g. sprites) +# -regions will not usually be resized (only exception is text) +# -alignment of 4 vertices (glyphs, sprites, images, ...) +# +# Optimise for: +# -keeping regions adjacent, reduce the number of entries in glMultiDrawArrays +# -finding large blocks of allocated regions quickly (for drawing) +# -finding block of unallocated space is the _uncommon_ case! +# +# Decisions: +# -don't over-allocate regions to any alignment -- this would require more +# work in finding the allocated spaces (for drawing) and would result in +# more entries in glMultiDrawArrays +# -don't move blocks when they truncate themselves. try not to allocate the +# space they freed too soon (they will likely need grow back into it later, +# and growing will usually require a reallocation). +# -allocator does not track individual allocated regions. Trusts caller +# to provide accurate (start, size) tuple, which completely describes +# a region from the allocator's point of view. +# -this means that compacting is probably not feasible, or would be hideously +# expensive + +class AllocatorMemoryException(Exception): + '''The buffer is not large enough to fulfil an allocation. + + Raised by `Allocator` methods when the operation failed due to lack of + buffer space. The buffer should be increased to at least + requested_capacity and then the operation retried (guaranteed to + pass second time). + ''' + + def __init__(self, requested_capacity): + self.requested_capacity = requested_capacity + +class Allocator(object): + '''Buffer space allocation implementation.''' + def __init__(self, capacity): + '''Create an allocator for a buffer of the specified capacity. + + :Parameters: + `capacity` : int + Maximum size of the buffer. + + ''' + self.capacity = capacity + + # Allocated blocks. Start index and size in parallel lists. + # + # # = allocated, - = free + # + # 0 3 5 15 20 24 40 + # |###--##########-----####----------------------| + # + # starts = [0, 5, 20] + # sizes = [3, 10, 4] + # + # To calculate free blocks: + # for i in range(0, len(starts)): + # free_start[i] = starts[i] + sizes[i] + # free_size[i] = starts[i+1] - free_start[i] + # free_size[i+1] = self.capacity - free_start[-1] + + self.starts = [] + self.sizes = [] + + def set_capacity(self, size): + '''Resize the maximum buffer size. + + The capaity cannot be reduced. + + :Parameters: + `size` : int + New maximum size of the buffer. + + ''' + assert size > self.capacity + self.capacity = size + + def alloc(self, size): + '''Allocate memory in the buffer. + + Raises `AllocatorMemoryException` if the allocation cannot be + fulfilled. + + :Parameters: + `size` : int + Size of region to allocate. + + :rtype: int + :return: Starting index of the allocated region. + ''' + assert size > 0 + + # return start + # or raise AllocatorMemoryException + + if not self.starts: + if size <= self.capacity: + self.starts.append(0) + self.sizes.append(size) + return 0 + else: + raise AllocatorMemoryException(size) + + # Allocate in a free space + free_start = self.starts[0] + self.sizes[0] + for i, (alloc_start, alloc_size) in \ + enumerate(zip(self.starts[1:], self.sizes[1:])): + # Danger! + # i is actually index - 1 because of slicing above... + # starts[i] points to the block before this free space + # starts[i+1] points to the block after this free space, and is + # always valid. + free_size = alloc_start - free_start + if free_size == size: + # Merge previous block with this one (removing this free space) + self.sizes[i] += free_size + alloc_size + del self.starts[i+1] + del self.sizes[i+1] + return free_start + elif free_size > size: + # Increase size of previous block to intrude into this free + # space. + self.sizes[i] += size + return free_start + free_start = alloc_start + alloc_size + + # Allocate at end of capacity + free_size = self.capacity - free_start + if free_size >= size: + self.sizes[-1] += size + return free_start + + raise AllocatorMemoryException(self.capacity + size - free_size) + + def realloc(self, start, size, new_size): + '''Reallocate a region of the buffer. + + This is more efficient than separate `dealloc` and `alloc` calls, as + the region can often be resized in-place. + + Raises `AllocatorMemoryException` if the allocation cannot be + fulfilled. + + :Parameters: + `start` : int + Current starting index of the region. + `size` : int + Current size of the region. + `new_size` : int + New size of the region. + + ''' + assert size > 0 and new_size > 0 + + # return start + # or raise AllocatorMemoryException + + # Truncation is the same as deallocating the tail cruft + if new_size < size: + self.dealloc(start + new_size, size - new_size) + return start + + # Find which block it lives in + for i, (alloc_start, alloc_size) in \ + enumerate(zip(*(self.starts, self.sizes))): + p = start - alloc_start + if p >= 0 and size <= alloc_size - p: + break + if not (p >= 0 and size <= alloc_size - p): + print zip(self.starts, self.sizes) + print start, size, new_size + print p, alloc_start, alloc_size + assert p >= 0 and size <= alloc_size - p, 'Region not allocated' + + if size == alloc_size - p: + # Region is at end of block. Find how much free space is after + # it. + is_final_block = i == len(self.starts) - 1 + if not is_final_block: + free_size = self.starts[i + 1] - (start + size) + else: + free_size = self.capacity - (start + size) + + # TODO If region is an entire block being an island in free space, + # can possibly extend in both directions. + + if free_size == new_size - size and not is_final_block: + # Merge block with next (region is expanded in place to + # exactly fill the free space) + self.sizes[i] += free_size + self.sizes[i + 1] + del self.starts[i + 1] + del self.sizes[i + 1] + return start + elif free_size > new_size - size: + # Expand region in place + self.sizes[i] += new_size - size + return start + + # The block must be repositioned. Dealloc then alloc. + + # But don't do this! If alloc fails, we've already silently dealloc'd + # the original block. + # self.dealloc(start, size) + # return self.alloc(new_size) + + # It must be alloc'd first. We're not missing an optimisation + # here, because if freeing the block would've allowed for the block to + # be placed in the resulting free space, one of the above in-place + # checks would've found it. + result = self.alloc(new_size) + self.dealloc(start, size) + return result + + def dealloc(self, start, size): + '''Free a region of the buffer. + + :Parameters: + `start` : int + Starting index of the region. + `size` : int + Size of the region. + + ''' + assert size > 0 + assert self.starts + + # Find which block needs to be split + for i, (alloc_start, alloc_size) in \ + enumerate(zip(*(self.starts, self.sizes))): + p = start - alloc_start + if p >= 0 and size <= alloc_size - p: + break + + # Assert we left via the break + assert p >= 0 and size <= alloc_size - p, 'Region not allocated' + + if p == 0 and size == alloc_size: + # Remove entire block + del self.starts[i] + del self.sizes[i] + elif p == 0: + # Truncate beginning of block + self.starts[i] += size + self.sizes[i] -= size + elif size == alloc_size - p: + # Truncate end of block + self.sizes[i] -= size + else: + # Reduce size of left side, insert block at right side + # $ = dealloc'd block, # = alloc'd region from same block + # + # <------8------> + # <-5-><-6-><-7-> + # 1 2 3 4 + # #####$$$$$##### + # + # 1 = alloc_start + # 2 = start + # 3 = start + size + # 4 = alloc_start + alloc_size + # 5 = start - alloc_start = p + # 6 = size + # 7 = {8} - ({5} + {6}) = alloc_size - (p + size) + # 8 = alloc_size + # + self.sizes[i] = p + self.starts.insert(i + 1, start + size) + self.sizes.insert(i + 1, alloc_size - (p + size)) + + def get_allocated_regions(self): + '''Get a list of (aggregate) allocated regions. + + The result of this method is ``(starts, sizes)``, where ``starts`` is + a list of starting indices of the regions and ``sizes`` their + corresponding lengths. + + :rtype: (list, list) + ''' + # return (starts, sizes); len(starts) == len(sizes) + return (self.starts, self.sizes) + + def get_fragmented_free_size(self): + '''Returns the amount of space unused, not including the final + free block. + + :rtype: int + ''' + if not self.starts: + return 0 + + # Variation of search for free block. + total_free = 0 + free_start = self.starts[0] + self.sizes[0] + for i, (alloc_start, alloc_size) in \ + enumerate(zip(self.starts[1:], self.sizes[1:])): + total_free += alloc_start - free_start + free_start = alloc_start + alloc_size + + return total_free + + def get_free_size(self): + '''Return the amount of space unused. + + :rtype: int + ''' + if not self.starts: + return self.capacity + + free_end = self.capacity - (self.starts[-1] + self.sizes[-1]) + return self.get_fragmented_free_size() + free_end + + def get_usage(self): + '''Return fraction of capacity currently allocated. + + :rtype: float + ''' + return 1. - self.get_free_size() / float(self.capacity) + + def get_fragmentation(self): + '''Return fraction of free space that is not expandable. + + :rtype: float + ''' + free_size = self.get_free_size() + if free_size == 0: + return 0. + return self.get_fragmented_free_size() / float(self.get_free_size()) + + def _is_empty(self): + return not self.starts + + def __str__(self): + return 'allocs=' + repr(zip(self.starts, self.sizes)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, str(self)) + +# ---------------------------------------------------------------------------- +# pyglet +# Copyright (c) 2006-2008 Alex Holkner +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of pyglet nor the names of its +# contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- +# $Id:$ + +'''Access byte arrays as arrays of vertex attributes. + +Use `create_attribute` to create an attribute accessor given a simple format +string. Alternatively, the classes may be constructed directly. + +Attribute format strings +======================== + +An attribute format string specifies the format of a vertex attribute. Format +strings are accepted by the `create_attribute` function as well as most +methods in the `pyglet.graphics` module. + +Format strings have the following (BNF) syntax:: + + attribute ::= ( name | index 'g' 'n'? ) count type + +``name`` describes the vertex attribute, and is one of the following +constants for the predefined attributes: + +``c`` + Vertex color +``e`` + Edge flag +``f`` + Fog coordinate +``n`` + Normal vector +``s`` + Secondary color +``t`` + Texture coordinate +``v`` + Vertex coordinate + +You can alternatively create a generic indexed vertex attribute by +specifying its index in decimal followed by the constant ``g``. For +example, ``0g`` specifies the generic vertex attribute with index 0. +If the optional constant ``n`` is present after the ``g``, the +attribute is normalised to the range ``[0, 1]`` or ``[-1, 1]`` within +the range of the data type. + +``count`` gives the number of data components in the attribute. For +example, a 3D vertex position has a count of 3. Some attributes +constrain the possible counts that can be used; for example, a normal +vector must have a count of 3. + +``type`` gives the data type of each component of the attribute. The +following types can be used: + +``b`` + ``GLbyte`` +``B`` + ``GLubyte`` +``s`` + ``GLshort`` +``S`` + ``GLushort`` +``i`` + ``GLint`` +``I`` + ``GLuint`` +``f`` + ``GLfloat`` +``d`` + ``GLdouble`` + +Some attributes constrain the possible data types; for example, +normal vectors must use one of the signed data types. The use of +some data types, while not illegal, may have severe performance +concerns. For example, the use of ``GLdouble`` is discouraged, +and colours should be specified with ``GLubyte``. + +Whitespace is prohibited within the format string. + +Some examples follow: + +``v3f`` + 3-float vertex position +``c4b`` + 4-byte colour +``1eb`` + Edge flag +``0g3f`` + 3-float generic vertex attribute 0 +``1gn1i`` + Integer generic vertex attribute 1, normalized to [-1, 1] +``2gn4B`` + 4-byte generic vertex attribute 2, normalized to [0, 1] (because + the type is unsigned) + +''' + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +_c_types = { + GL_BYTE: ctypes.c_byte, + GL_UNSIGNED_BYTE: ctypes.c_ubyte, + GL_SHORT: ctypes.c_short, + GL_UNSIGNED_SHORT: ctypes.c_ushort, + GL_INT: ctypes.c_int, + GL_UNSIGNED_INT: ctypes.c_uint, + GL_FLOAT: ctypes.c_float, + GL_DOUBLE: ctypes.c_double, +} + +_gl_types = { + 'b': GL_BYTE, + 'B': GL_UNSIGNED_BYTE, + 's': GL_SHORT, + 'S': GL_UNSIGNED_SHORT, + 'i': GL_INT, + 'I': GL_UNSIGNED_INT, + 'f': GL_FLOAT, + 'd': GL_DOUBLE, +} + +_attribute_format_re = re.compile(r''' + (?P + [cefnstv] | + (?P[0-9]+) g + (?Pn?)) + (?P[1234]) + (?P[bBsSiIfd]) +''', re.VERBOSE) + +_attribute_cache = {} + +def _align(v, align): + return ((v - 1) & ~(align - 1)) + align + +def interleave_attributes(attributes): + '''Interleave attribute offsets. + + Adjusts the offsets and strides of the given attributes so that + they are interleaved. Alignment constraints are respected. + + :Parameters: + `attributes` : sequence of `AbstractAttribute` + Attributes to interleave in-place. + + ''' + stride = 0 + max_size = 0 + for attribute in attributes: + stride = _align(stride, attribute.align) + attribute.offset = stride + stride += attribute.size + max_size = max(max_size, attribute.size) + stride = _align(stride, max_size) + for attribute in attributes: + attribute.stride = stride + +def serialize_attributes(count, attributes): + '''Serialize attribute offsets. + + Adjust the offsets of the given attributes so that they are + packed serially against each other for `count` vertices. + + :Parameters: + `count` : int + Number of vertices. + `attributes` : sequence of `AbstractAttribute` + Attributes to serialze in-place. + + ''' + offset = 0 + for attribute in attributes: + offset = _align(offset, attribute.align) + attribute.offset = offset + offset += count * attribute.stride + +def create_attribute(format): + '''Create a vertex attribute description from a format string. + + The initial stride and offset of the attribute will be 0. + + :Parameters: + `format` : str + Attribute format string. See the module summary for details. + + :rtype: `AbstractAttribute` + ''' + try: + cls, args = _attribute_cache[format] + return cls(*args) + except KeyError: + pass + + match = _attribute_format_re.match(format) + assert match, 'Invalid attribute format %r' % format + count = int(match.group('count')) + gl_type = _gl_types[match.group('type')] + generic_index = match.group('generic_index') + if generic_index: + normalized = match.group('generic_normalized') + attr_class = GenericAttribute + args = int(generic_index), normalized, count, gl_type + else: + name = match.group('name') + attr_class = _attribute_classes[name] + if attr_class._fixed_count: + assert count == attr_class._fixed_count, \ + 'Attributes named "%s" must have count of %d' % ( + name, attr_class._fixed_count) + args = (gl_type,) + else: + args = (count, gl_type) + + _attribute_cache[format] = attr_class, args + return attr_class(*args) + +class AbstractAttribute(object): + '''Abstract accessor for an attribute in a mapped buffer. + ''' + + _fixed_count = None + + def __init__(self, count, gl_type): + '''Create the attribute accessor. + + :Parameters: + `count` : int + Number of components in the attribute. + `gl_type` : int + OpenGL type enumerant; for example, ``GL_FLOAT`` + + ''' + assert count in (1, 2, 3, 4), 'Component count out of range' + self.gl_type = gl_type + self.c_type = _c_types[gl_type] + self.count = count + self.align = ctypes.sizeof(self.c_type) + self.size = count * self.align + self.stride = self.size + self.offset = 0 + + def enable(self): + '''Enable the attribute using ``glEnableClientState``.''' + raise NotImplementedError('abstract') + + def set_pointer(self, offset): + '''Setup this attribute to point to the currently bound buffer at + the given offset. + + ``offset`` should be based on the currently bound buffer's ``ptr`` + member. + + :Parameters: + `offset` : int + Pointer offset to the currently bound buffer for this + attribute. + + ''' + raise NotImplementedError('abstract') + + def get_region(self, buffer, start, count): + '''Map a buffer region using this attribute as an accessor. + + The returned region can be modified as if the buffer was a contiguous + array of this attribute (though it may actually be interleaved or + otherwise non-contiguous). + + The returned region consists of a contiguous array of component + data elements. For example, if this attribute uses 3 floats per + vertex, and the `count` parameter is 4, the number of floats mapped + will be ``3 * 4 = 12``. + + :Parameters: + `buffer` : `AbstractMappable` + The buffer to map. + `start` : int + Offset of the first vertex to map. + `count` : int + Number of vertices to map + + :rtype: `AbstractBufferRegion` + ''' + byte_start = self.stride * start + byte_size = self.stride * count + array_count = self.count * count + if self.stride == self.size: + # non-interleaved + ptr_type = ctypes.POINTER(self.c_type * array_count) + return buffer.get_region(byte_start, byte_size, ptr_type) + else: + # interleaved + byte_start += self.offset + byte_size -= self.offset + elem_stride = self.stride // ctypes.sizeof(self.c_type) + elem_offset = self.offset // ctypes.sizeof(self.c_type) + ptr_type = ctypes.POINTER( + self.c_type * (count * elem_stride - elem_offset)) + region = buffer.get_region(byte_start, byte_size, ptr_type) + return vertexbuffer.IndirectArrayRegion( + region, array_count, self.count, elem_stride) + + def set_region(self, buffer, start, count, data): + '''Set the data over a region of the buffer. + + :Parameters: + `buffer` : AbstractMappable` + The buffer to modify. + `start` : int + Offset of the first vertex to set. + `count` : int + Number of vertices to set. + `data` : sequence + Sequence of data components. + + ''' + if self.stride == self.size: + # non-interleaved + byte_start = self.stride * start + byte_size = self.stride * count + array_count = self.count * count + data = (self.c_type * array_count)(*data) + buffer.set_data_region(data, byte_start, byte_size) + else: + # interleaved + region = self.get_region(buffer, start, count) + region[:] = data + +class ColorAttribute(AbstractAttribute): + '''Color vertex attribute.''' + + plural = 'colors' + + def __init__(self, count, gl_type): + assert count in (3, 4), 'Color attributes must have count of 3 or 4' + super(ColorAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_COLOR_ARRAY) + + def set_pointer(self, pointer): + glColorPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class EdgeFlagAttribute(AbstractAttribute): + '''Edge flag attribute.''' + + plural = 'edge_flags' + _fixed_count = 1 + + def __init__(self, gl_type): + assert gl_type in (GL_BYTE, GL_UNSIGNED_BYTE, GL_BOOL), \ + 'Edge flag attribute must have boolean type' + super(EdgeFlagAttribute, self).__init__(1, gl_type) + + def enable(self): + glEnableClientState(GL_EDGE_FLAG_ARRAY) + + def set_pointer(self, pointer): + glEdgeFlagPointer(self.stride, self.offset + pointer) + +class FogCoordAttribute(AbstractAttribute): + '''Fog coordinate attribute.''' + + plural = 'fog_coords' + + def __init__(self, count, gl_type): + super(FogCoordAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_FOG_COORD_ARRAY) + + def set_pointer(self, pointer): + glFogCoordPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class NormalAttribute(AbstractAttribute): + '''Normal vector attribute.''' + + plural = 'normals' + _fixed_count = 3 + + def __init__(self, gl_type): + assert gl_type in (GL_BYTE, GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Normal attribute must have signed type' + super(NormalAttribute, self).__init__(3, gl_type) + + def enable(self): + glEnableClientState(GL_NORMAL_ARRAY) + + def set_pointer(self, pointer): + glNormalPointer(self.gl_type, self.stride, self.offset + pointer) + +class SecondaryColorAttribute(AbstractAttribute): + '''Secondary color attribute.''' + + plural = 'secondary_colors' + _fixed_count = 3 + + def __init__(self, gl_type): + super(SecondaryColorAttribute, self).__init__(3, gl_type) + + def enable(self): + glEnableClientState(GL_SECONDARY_COLOR_ARRAY) + + def set_pointer(self, pointer): + glSecondaryColorPointer(3, self.gl_type, self.stride, + self.offset + pointer) + +class TexCoordAttribute(AbstractAttribute): + '''Texture coordinate attribute.''' + + plural = 'tex_coords' + + def __init__(self, count, gl_type): + assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Texture coord attribute must have non-byte signed type' + super(TexCoordAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_TEXTURE_COORD_ARRAY) + + def set_pointer(self, pointer): + glTexCoordPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class VertexAttribute(AbstractAttribute): + '''Vertex coordinate attribute.''' + + plural = 'vertices' + + def __init__(self, count, gl_type): + assert count > 1, \ + 'Vertex attribute must have count of 2, 3 or 4' + assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Vertex attribute must have signed type larger than byte' + super(VertexAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_VERTEX_ARRAY) + + def set_pointer(self, pointer): + glVertexPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class GenericAttribute(AbstractAttribute): + '''Generic vertex attribute, used by shader programs.''' + + def __init__(self, index, normalized, count, gl_type): + self.normalized = bool(normalized) + self.index = index + super(GenericAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableVertexAttribArray(self.index) + + def set_pointer(self, pointer): + glVertexAttribPointer(self.index, self.count, self.gl_type, + self.normalized, self.stride, + self.offset + pointer) + +_attribute_classes = { + 'c': ColorAttribute, + 'e': EdgeFlagAttribute, + 'f': FogCoordAttribute, + 'n': NormalAttribute, + 's': SecondaryColorAttribute, + 't': TexCoordAttribute, + 'v': VertexAttribute, +} + +# ---------------------------------------------------------------------------- +# pyglet +# Copyright (c) 2006-2008 Alex Holkner +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of pyglet nor the names of its +# contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- +# $Id:$ + +'''Byte abstractions of Vertex Buffer Objects and vertex arrays. + +Use `create_buffer` or `create_mappable_buffer` to create a Vertex Buffer +Object, or a vertex array if VBOs are not supported by the current context. + +Buffers can optionally be created "mappable" (incorporating the +`AbstractMappable` mix-in). In this case the buffer provides a ``get_region`` +method which provides the most efficient path for updating partial data within +the buffer. +''' + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +_enable_vbo = pyglet.options['graphics_vbo'] + +# Enable workaround permanently if any VBO is created on a context that has +# this workaround. (On systems with multiple contexts where one is +# unaffected, the workaround will be enabled unconditionally on all of the +# contexts anyway. This is completely unlikely anyway). +_workaround_vbo_finish = False + +def create_buffer(size, + target=GL_ARRAY_BUFFER, + usage=GL_DYNAMIC_DRAW, + vbo=True): + '''Create a buffer of vertex data. + + :Parameters: + `size` : int + Size of the buffer, in bytes + `target` : int + OpenGL target buffer + `usage` : int + OpenGL usage constant + `vbo` : bool + True if a `VertexBufferObject` should be created if the driver + supports it; otherwise only a `VertexArray` is created. + + :rtype: `AbstractBuffer` + ''' + from pyglet import gl + if (vbo and + gl_info.have_version(1, 5) and + _enable_vbo and + not gl.current_context._workaround_vbo): + return VertexBufferObject(size, target, usage) + else: + return VertexArray(size) + +def create_mappable_buffer(size, + target=GL_ARRAY_BUFFER, + usage=GL_DYNAMIC_DRAW, + vbo=True): + '''Create a mappable buffer of vertex data. + + :Parameters: + `size` : int + Size of the buffer, in bytes + `target` : int + OpenGL target buffer + `usage` : int + OpenGL usage constant + `vbo` : bool + True if a `VertexBufferObject` should be created if the driver + supports it; otherwise only a `VertexArray` is created. + + :rtype: `AbstractBuffer` with `AbstractMappable` + ''' + from pyglet import gl + if (vbo and + gl_info.have_version(1, 5) and + _enable_vbo and + not gl.current_context._workaround_vbo): + return MappableVertexBufferObject(size, target, usage) + else: + return VertexArray(size) + +class AbstractBuffer(object): + '''Abstract buffer of byte data. + + :Ivariables: + `size` : int + Size of buffer, in bytes + `ptr` : int + Memory offset of the buffer, as used by the ``glVertexPointer`` + family of functions + `target` : int + OpenGL buffer target, for example ``GL_ARRAY_BUFFER`` + `usage` : int + OpenGL buffer usage, for example ``GL_DYNAMIC_DRAW`` + + ''' + + ptr = 0 + size = 0 + + def bind(self): + '''Bind this buffer to its OpenGL target.''' + raise NotImplementedError('abstract') + + def unbind(self): + '''Reset the buffer's OpenGL target.''' + raise NotImplementedError('abstract') + + def set_data(self, data): + '''Set the entire contents of the buffer. + + :Parameters: + `data` : sequence of int or ctypes pointer + The byte array to set. + + ''' + raise NotImplementedError('abstract') + + def set_data_region(self, data, start, length): + '''Set part of the buffer contents. + + :Parameters: + `data` : sequence of int or ctypes pointer + The byte array of data to set + `start` : int + Offset to start replacing data + `length` : int + Length of region to replace + + ''' + raise NotImplementedError('abstract') + + def map(self, invalidate=False): + '''Map the entire buffer into system memory. + + The mapped region must be subsequently unmapped with `unmap` before + performing any other operations on the buffer. + + :Parameters: + `invalidate` : bool + If True, the initial contents of the mapped block need not + reflect the actual contents of the buffer. + + :rtype: ``POINTER(ctypes.c_ubyte)`` + :return: Pointer to the mapped block in memory + ''' + raise NotImplementedError('abstract') + + def unmap(self): + '''Unmap a previously mapped memory block.''' + raise NotImplementedError('abstract') + + def resize(self, size): + '''Resize the buffer to a new size. + + :Parameters: + `size` : int + New size of the buffer, in bytes + + ''' + + def delete(self): + '''Delete this buffer, reducing system resource usage.''' + raise NotImplementedError('abstract') + +class AbstractMappable(object): + def get_region(self, start, size, ptr_type): + '''Map a region of the buffer into a ctypes array of the desired + type. This region does not need to be unmapped, but will become + invalid if the buffer is resized. + + Note that although a pointer type is required, an array is mapped. + For example:: + + get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20)) + + will map bytes 0 to 80 of the buffer to an array of 20 ints. + + Changes to the array may not be recognised until the region's + `AbstractBufferRegion.invalidate` method is called. + + :Parameters: + `start` : int + Offset into the buffer to map from, in bytes + `size` : int + Size of the buffer region to map, in bytes + `ptr_type` : ctypes pointer type + Pointer type describing the array format to create + + :rtype: `AbstractBufferRegion` + ''' + raise NotImplementedError('abstract') + +class VertexArray(AbstractBuffer, AbstractMappable): + '''A ctypes implementation of a vertex array. + + Many of the methods on this class are effectively no-op's, such as `bind`, + `unbind`, `map`, `unmap` and `delete`; they exist in order to present + a consistent interface with `VertexBufferObject`. + + This buffer type is also mappable, and so `get_region` can be used. + ''' + + def __init__(self, size): + self.size = size + + self.array = (ctypes.c_byte * size)() + self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value + + def bind(self): + pass + + def unbind(self): + pass + + def set_data(self, data): + ctypes.memmove(self.ptr, data, self.size) + + def set_data_region(self, data, start, length): + ctypes.memmove(self.ptr + start, data, length) + + def map(self, invalidate=False): + return self.array + + def unmap(self): + pass + + def get_region(self, start, size, ptr_type): + array = ctypes.cast(self.ptr + start, ptr_type).contents + return VertexArrayRegion(array) + + def delete(self): + pass + + def resize(self, size): + array = (ctypes.c_byte * size)() + ctypes.memmove(array, self.array, min(size, self.size)) + self.size = size + self.array = array + self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value + + +class VertexBufferObject(AbstractBuffer): + '''Lightweight representation of an OpenGL VBO. + + The data in the buffer is not replicated in any system memory (unless it + is done so by the video driver). While this can improve memory usage and + possibly performance, updates to the buffer are relatively slow. + + This class does not implement `AbstractMappable`, and so has no + ``get_region`` method. See `MappableVertexBufferObject` for a VBO class + that does implement ``get_region``. + ''' + + def __init__(self, size, target, usage): + self.size = size + self.target = target + self.usage = usage + self._context = pyglet.gl.current_context + + id = GLuint() + glGenBuffers(1, id) + self.id = id.value + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(target, self.id) + glBufferData(target, self.size, None, self.usage) + glPopClientAttrib() + + global _workaround_vbo_finish + if pyglet.gl.current_context._workaround_vbo_finish: + _workaround_vbo_finish = True + + def bind(self): + glBindBuffer(self.target, self.id) + + def unbind(self): + glBindBuffer(self.target, 0) + + def set_data(self, data): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferData(self.target, self.size, data, self.usage) + glPopClientAttrib() + + def set_data_region(self, data, start, length): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferSubData(self.target, start, length, data) + glPopClientAttrib() + + def map(self, invalidate=False): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + if invalidate: + glBufferData(self.target, self.size, None, self.usage) + ptr = ctypes.cast(glMapBuffer(self.target, GL_WRITE_ONLY), + ctypes.POINTER(ctypes.c_byte * self.size)).contents + glPopClientAttrib() + return ptr + + def unmap(self): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glUnmapBuffer(self.target) + glPopClientAttrib() + + def __del__(self): + try: + if self.id is not None: + self._context.delete_buffer(self.id) + except: + pass + + def delete(self): + id = GLuint(self.id) + glDeleteBuffers(1, id) + self.id = None + + def resize(self, size): + # Map, create a copy, then reinitialize. + temp = (ctypes.c_byte * size)() + + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + data = glMapBuffer(self.target, GL_READ_ONLY) + ctypes.memmove(temp, data, min(size, self.size)) + glUnmapBuffer(self.target) + + self.size = size + glBufferData(self.target, self.size, temp, self.usage) + glPopClientAttrib() + +class MappableVertexBufferObject(VertexBufferObject, AbstractMappable): + '''A VBO with system-memory backed store. + + Updates to the data via `set_data`, `set_data_region` and `map` will be + held in local memory until `bind` is called. The advantage is that fewer + OpenGL calls are needed, increasing performance. + + There may also be less performance penalty for resizing this buffer. + + Updates to data via `map` are committed immediately. + ''' + def __init__(self, size, target, usage): + super(MappableVertexBufferObject, self).__init__(size, target, usage) + self.data = (ctypes.c_byte * size)() + self.data_ptr = ctypes.cast(self.data, ctypes.c_void_p).value + self._dirty_min = sys.maxint + self._dirty_max = 0 + + def bind(self): + # Commit pending data + super(MappableVertexBufferObject, self).bind() + size = self._dirty_max - self._dirty_min + if size > 0: + if size == self.size: + glBufferData(self.target, self.size, self.data, self.usage) + else: + glBufferSubData(self.target, self._dirty_min, size, + self.data_ptr + self._dirty_min) + self._dirty_min = sys.maxint + self._dirty_max = 0 + + def set_data(self, data): + super(MappableVertexBufferObject, self).set_data(data) + ctypes.memmove(self.data, data, self.size) + self._dirty_min = 0 + self._dirty_max = self.size + + def set_data_region(self, data, start, length): + ctypes.memmove(self.data_ptr + start, data, length) + self._dirty_min = min(start, self._dirty_min) + self._dirty_max = max(start + length, self._dirty_max) + + def map(self, invalidate=False): + self._dirty_min = 0 + self._dirty_max = self.size + return self.data + + def unmap(self): + pass + + def get_region(self, start, size, ptr_type): + array = ctypes.cast(self.data_ptr + start, ptr_type).contents + return VertexBufferObjectRegion(self, start, start + size, array) + + def resize(self, size): + data = (ctypes.c_byte * size)() + ctypes.memmove(data, self.data, min(size, self.size)) + self.data = data + self.data_ptr = ctypes.cast(self.data, ctypes.c_void_p).value + + self.size = size + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferData(self.target, self.size, self.data, self.usage) + glPopClientAttrib() + + self._dirty_min = sys.maxint + self._dirty_max = 0 + +class AbstractBufferRegion(object): + '''A mapped region of a buffer. + + Buffer regions are obtained using `AbstractMappable.get_region`. + + :Ivariables: + `array` : ctypes array + Array of data, of the type and count requested by ``get_region``. + + ''' + def invalidate(self): + '''Mark this region as changed. + + The buffer may not be updated with the latest contents of the + array until this method is called. (However, it may not be updated + until the next time the buffer is used, for efficiency). + ''' + pass + +class VertexBufferObjectRegion(AbstractBufferRegion): + '''A mapped region of a VBO.''' + def __init__(self, buffer, start, end, array): + self.buffer = buffer + self.start = start + self.end = end + self.array = array + + def invalidate(self): + buffer = self.buffer + buffer._dirty_min = min(buffer._dirty_min, self.start) + buffer._dirty_max = max(buffer._dirty_max, self.end) + +class VertexArrayRegion(AbstractBufferRegion): + '''A mapped region of a vertex array. + + The `invalidate` method is a no-op but is provided in order to present + a consistent interface with `VertexBufferObjectRegion`. + ''' + def __init__(self, array): + self.array = array + +class IndirectArrayRegion(AbstractBufferRegion): + '''A mapped region in which data elements are not necessarily contiguous. + + This region class is used to wrap buffer regions in which the data + must be accessed with some stride. For example, in an interleaved buffer + this region can be used to access a single interleaved component as if the + data was contiguous. + ''' + def __init__(self, region, size, component_count, component_stride): + '''Wrap a buffer region. + + Use the `component_count` and `component_stride` parameters to specify + the data layout of the encapsulated region. For example, if RGBA + data is to be accessed as if it were packed RGB, ``component_count`` + would be set to 3 and ``component_stride`` to 4. If the region + contains 10 RGBA tuples, the ``size`` parameter is ``3 * 10 = 30``. + + :Parameters: + `region` : `AbstractBufferRegion` + The region with interleaved data + `size` : int + The number of elements that this region will provide access to. + `component_count` : int + The number of elements that are contiguous before some must + be skipped. + `component_stride` : int + The number of elements of interleaved data separating + the contiguous sections. + + ''' + self.region = region + self.size = size + self.count = component_count + self.stride = component_stride + self.array = self + + def __repr__(self): + return 'IndirectArrayRegion(size=%d, count=%d, stride=%d)' % ( + self.size, self.count, self.stride) + + def __getitem__(self, index): + count = self.count + if not isinstance(index, slice): + elem = index // count + j = index % count + return self.region.array[elem * self.stride + j] + + start = index.start or 0 + stop = index.stop + step = index.step or 1 + if start < 0: + start = self.size + start + if stop is None: + stop = self.size + elif stop < 0: + stop = self.size + stop + + assert step == 1 or step % count == 0, \ + 'Step must be multiple of component count' + + data_start = (start // count) * self.stride + start % count + data_stop = (stop // count) * self.stride + stop % count + data_step = step * self.stride + + # TODO stepped getitem is probably wrong, see setitem for correct. + value_step = step * count + + # ctypes does not support stepped slicing, so do the work in a list + # and copy it back. + data = self.region.array[:] + value = [0] * ((stop - start) // step) + stride = self.stride + for i in range(count): + value[i::value_step] = \ + data[data_start + i:data_stop + i:data_step] + return value + + def __setitem__(self, index, value): + count = self.count + if not isinstance(index, slice): + elem = index // count + j = index % count + self.region.array[elem * self.stride + j] = value + return + + start = index.start or 0 + stop = index.stop + step = index.step or 1 + if start < 0: + start = self.size + start + if stop is None: + stop = self.size + elif stop < 0: + stop = self.size + stop + + assert step == 1 or step % count == 0, \ + 'Step must be multiple of component count' + + data_start = (start // count) * self.stride + start % count + data_stop = (stop // count) * self.stride + stop % count + + # ctypes does not support stepped slicing, so do the work in a list + # and copy it back. + data = self.region.array[:] + if step == 1: + data_step = self.stride + value_step = count + for i in range(count): + data[data_start + i:data_stop + i:data_step] = \ + value[i::value_step] + else: + data_step = (step // count) * self.stride + data[data_start:data_stop:data_step] = value + self.region.array[:] = data + + def invalidate(self): + self.region.invalidate() + +# ---------------------------------------------------------------------------- +# pyglet +# Copyright (c) 2006-2008 Alex Holkner +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of pyglet nor the names of its +# contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- +# $Id:$ + +'''Manage related vertex attributes within a single vertex domain. + +A vertex "domain" consists of a set of attribute descriptions that together +describe the layout of one or more vertex buffers which are used together to +specify the vertices in a primitive. Additionally, the domain manages the +buffers used to store the data and will resize them as necessary to accomodate +new vertices. + +Domains can optionally be indexed, in which case they also manage a buffer +containing vertex indices. This buffer is grown separately and has no size +relation to the attribute buffers. + +Applications can create vertices (and optionally, indices) within a domain +with the `VertexDomain.create` method. This returns a `VertexList` +representing the list of vertices created. The vertex attribute data within +the group can be modified, and the changes will be made to the underlying +buffers automatically. + +The entire domain can be efficiently drawn in one step with the +`VertexDomain.draw` method, assuming all the vertices comprise primitives of +the same OpenGL primitive mode. +''' + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + + +_usage_format_re = re.compile(r''' + (?P[^/]*) + (/ (?P static|dynamic|stream|none))? +''', re.VERBOSE) + +_gl_usages = { + 'static': GL_STATIC_DRAW, + 'dynamic': GL_DYNAMIC_DRAW, + 'stream': GL_STREAM_DRAW, + 'none': GL_STREAM_DRAW_ARB, # Force no VBO +} + +def _nearest_pow2(v): + # From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + # Credit: Sean Anderson + v -= 1 + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + return v + 1 + +def create_attribute_usage(format): + '''Create an attribute and usage pair from a format string. The + format string is as documented in `pyglet.graphics.vertexattribute`, with + the addition of an optional usage component:: + + usage ::= attribute ( '/' ('static' | 'dynamic' | 'stream' | 'none') )? + + If the usage is not given it defaults to 'dynamic'. The usage corresponds + to the OpenGL VBO usage hint, and for ``static`` also indicates a + preference for interleaved arrays. If ``none`` is specified a buffer + object is not created, and vertex data is stored in system memory. + + Some examples: + + ``v3f/stream`` + 3D vertex position using floats, for stream usage + ``c4b/static`` + 4-byte color attribute, for static usage + + :return: attribute, usage + ''' + match = _usage_format_re.match(format) + attribute_format = match.group('attribute') + attribute = create_attribute(attribute_format) + usage = match.group('usage') + if usage: + vbo = not usage == 'none' + usage = _gl_usages[usage] + else: + usage = GL_DYNAMIC_DRAW + vbo = True + + return (attribute, usage, vbo) + +def create_domain(*attribute_usage_formats): + '''Create a vertex domain covering the given attribute usage formats. + See documentation for `create_attribute_usage` and + `pyglet.graphics.vertexattribute.create_attribute` for the grammar of + these format strings. + + :rtype: `VertexDomain` + ''' + attribute_usages = [create_attribute_usage(f) \ + for f in attribute_usage_formats] + return VertexDomain(attribute_usages) + +def create_indexed_domain(*attribute_usage_formats): + '''Create an indexed vertex domain covering the given attribute usage + formats. See documentation for `create_attribute_usage` and + `pyglet.graphics.vertexattribute.create_attribute` for the grammar of + these format strings. + + :rtype: `VertexDomain` + ''' + attribute_usages = [create_attribute_usage(f) \ + for f in attribute_usage_formats] + return IndexedVertexDomain(attribute_usages) + +class VertexDomain(object): + '''Management of a set of vertex lists. + + Construction of a vertex domain is usually done with the `create_domain` + function. + ''' + _version = 0 + _initial_count = 16 + + def __init__(self, attribute_usages): + self.allocator = Allocator(self._initial_count) + + static_attributes = [] + attributes = [] + self.buffer_attributes = [] # list of (buffer, attributes) + for attribute, usage, vbo in attribute_usages: + if usage == GL_STATIC_DRAW: + # Group attributes for interleaved buffer + static_attributes.append(attribute) + attributes.append(attribute) + else: + # Create non-interleaved buffer + attributes.append(attribute) + attribute.buffer = create_mappable_buffer( + attribute.stride * self.allocator.capacity, + usage=usage, vbo=vbo) + attribute.buffer.element_size = attribute.stride + attribute.buffer.attributes = (attribute,) + self.buffer_attributes.append( + (attribute.buffer, (attribute,))) + + # Create buffer for interleaved data + if static_attributes: + vertexattribute.interleave_attributes(static_attributes) + stride = static_attributes[0].stride + buffer = vertexbuffer.create_mappable_buffer( + stride * self.allocator.capacity, usage=GL_STATIC_DRAW) + buffer.element_size = stride + self.buffer_attributes.append( + (buffer, static_attributes)) + + for attribute in static_attributes: + attribute.buffer = buffer + + # Create named attributes for each attribute + self.attributes = attributes + self.attribute_names = {} + for attribute in attributes: + if isinstance(attribute, GenericAttribute): + index = attribute.index + if 'generic' not in self.attributes: + self.attribute_names['generic'] = {} + assert index not in self.attribute_names['generic'], \ + 'More than one generic attribute with index %d' % index + self.attribute_names['generic'][index] = attribute + else: + name = attribute.plural + assert name not in self.attributes, \ + 'More than one "%s" attribute given' % name + self.attribute_names[name] = attribute + + def __del__(self): + # Break circular refs that Python GC seems to miss even when forced + # collection. + for attribute in self.attributes: + del attribute.buffer + + def _safe_alloc(self, count): + '''Allocate vertices, resizing the buffers if necessary.''' + try: + return self.allocator.alloc(count) + except AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + for buffer, _ in self.buffer_attributes: + buffer.resize(capacity * buffer.element_size) + self.allocator.set_capacity(capacity) + return self.allocator.alloc(count) + + def _safe_realloc(self, start, count, new_count): + '''Reallocate vertices, resizing the buffers if necessary.''' + try: + return self.allocator.realloc(start, count, new_count) + except AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + for buffer, _ in self.buffer_attributes: + buffer.resize(capacity * buffer.element_size) + self.allocator.set_capacity(capacity) + return self.allocator.realloc(start, count, new_count) + + def create(self, count): + '''Create a `VertexList` in this domain. + + :Parameters: + `count` : int + Number of vertices to create. + + :rtype: `VertexList` + ''' + start = self._safe_alloc(count) + return VertexList(self, start, count) + + def draw(self, mode, vertex_list=None): + '''Draw vertices in the domain. + + If `vertex_list` is not specified, all vertices in the domain are + drawn. This is the most efficient way to render primitives. + + If `vertex_list` specifies a `VertexList`, only primitives in that + list will be drawn. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + `vertex_list` : `VertexList` + Vertex list to draw, or ``None`` for all lists in this domain. + + ''' + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + for buffer, attributes in self.buffer_attributes: + buffer.bind() + for attribute in attributes: + attribute.enable() + attribute.set_pointer(attribute.buffer.ptr) + if _workaround_vbo_finish: + glFinish() + + if vertex_list is not None: + glDrawArrays(mode, vertex_list.start, vertex_list.count) + else: + starts, sizes = self.allocator.get_allocated_regions() + primcount = len(starts) + if primcount == 0: + pass + elif primcount == 1: + # Common case + glDrawArrays(mode, starts[0], sizes[0]) + elif gl_info.have_version(1, 4): + starts = (GLint * primcount)(*starts) + sizes = (GLsizei * primcount)(*sizes) + glMultiDrawArrays(mode, starts, sizes, primcount) + else: + for start, size in zip(starts, sizes): + glDrawArrays(mode, start, size) + + for buffer, _ in self.buffer_attributes: + buffer.unbind() + glPopClientAttrib() + + def _is_empty(self): + return not self.allocator.starts + + def __repr__(self): + return '<%s@%x %s>' % (self.__class__.__name__, id(self), + self.allocator) + +class VertexList(object): + '''A list of vertices within a `VertexDomain`. Use + `VertexDomain.create` to construct this list. + ''' + + def __init__(self, domain, start, count): + # TODO make private + self.domain = domain + self.start = start + self.count = count + + def get_size(self): + '''Get the number of vertices in the list. + + :rtype: int + ''' + return self.count + + def get_domain(self): + '''Get the domain this vertex list belongs to. + + :rtype: `VertexDomain` + ''' + return self.domain + + def draw(self, mode): + '''Draw this vertex list in the given OpenGL mode. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + + ''' + self.domain.draw(mode, self) + + def resize(self, count): + '''Resize this group. + + :Parameters: + `count` : int + New number of vertices in the list. + + ''' + new_start = self.domain._safe_realloc(self.start, self.count, count) + if new_start != self.start: + # Copy contents to new location + for attribute in self.domain.attributes: + old = attribute.get_region(attribute.buffer, + self.start, self.count) + new = attribute.get_region(attribute.buffer, + new_start, self.count) + new.array[:] = old.array[:] + new.invalidate() + self.start = new_start + self.count = count + + self._colors_cache_version = None + self._fog_coords_cache_version = None + self._edge_flags_cache_version = None + self._normals_cache_version = None + self._secondary_colors_cache_version = None + self._tex_coords_cache_version = None + self._vertices_cache_version = None + + def delete(self): + '''Delete this group.''' + self.domain.allocator.dealloc(self.start, self.count) + + def migrate(self, domain): + '''Move this group from its current domain and add to the specified + one. Attributes on domains must match. (In practice, used to change + parent state of some vertices). + + :Parameters: + `domain` : `VertexDomain` + Domain to migrate this vertex list to. + + ''' + assert domain.attribute_names.keys() == \ + self.domain.attribute_names.keys(), 'Domain attributes must match.' + + new_start = domain._safe_alloc(self.count) + for key, old_attribute in self.domain.attribute_names.items(): + old = old_attribute.get_region(old_attribute.buffer, + self.start, self.count) + new_attribute = domain.attribute_names[key] + new = new_attribute.get_region(new_attribute.buffer, + new_start, self.count) + new.array[:] = old.array[:] + new.invalidate() + + self.domain.allocator.dealloc(self.start, self.count) + self.domain = domain + self.start = new_start + + self._colors_cache_version = None + self._fog_coords_cache_version = None + self._edge_flags_cache_version = None + self._normals_cache_version = None + self._secondary_colors_cache_version = None + self._tex_coords_cache_version = None + self._vertices_cache_version = None + + def _set_attribute_data(self, i, data): + attribute = self.domain.attributes[i] + # TODO without region + region = attribute.get_region(attribute.buffer, self.start, self.count) + region.array[:] = data + region.invalidate() + + # --- + + def _get_colors(self): + if (self._colors_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['colors'] + self._colors_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._colors_cache_version = domain._version + + region = self._colors_cache + region.invalidate() + return region.array + + def _set_colors(self, data): + self._get_colors()[:] = data + + _colors_cache = None + _colors_cache_version = None + colors = property(_get_colors, _set_colors, + doc='''Array of color data.''') + + # --- + + def _get_fog_coords(self): + if (self._fog_coords_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['fog_coords'] + self._fog_coords_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._fog_coords_cache_version = domain._version + + region = self._fog_coords_cache + region.invalidate() + return region.array + + def _set_fog_coords(self, data): + self._get_fog_coords()[:] = data + + _fog_coords_cache = None + _fog_coords_cache_version = None + fog_coords = property(_get_fog_coords, _set_fog_coords, + doc='''Array of fog coordinate data.''') + + # --- + + def _get_edge_flags(self): + if (self._edge_flags_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['edge_flags'] + self._edge_flags_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._edge_flags_cache_version = domain._version + + region = self._edge_flags_cache + region.invalidate() + return region.array + + def _set_edge_flags(self, data): + self._get_edge_flags()[:] = data + + _edge_flags_cache = None + _edge_flags_cache_version = None + edge_flags = property(_get_edge_flags, _set_edge_flags, + doc='''Array of edge flag data.''') + + # --- + + def _get_normals(self): + if (self._normals_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['normals'] + self._normals_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._normals_cache_version = domain._version + + region = self._normals_cache + region.invalidate() + return region.array + + def _set_normals(self, data): + self._get_normals()[:] = data + + _normals_cache = None + _normals_cache_version = None + normals = property(_get_normals, _set_normals, + doc='''Array of normal vector data.''') + + # --- + + def _get_secondary_colors(self): + if (self._secondary_colors_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['secondary_colors'] + self._secondary_colors_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._secondary_colors_cache_version = domain._version + + region = self._secondary_colors_cache + region.invalidate() + return region.array + + def _set_secondary_colors(self, data): + self._get_secondary_colors()[:] = data + + _secondary_colors_cache = None + _secondary_colors_cache_version = None + secondary_colors = property(_get_secondary_colors, _set_secondary_colors, + doc='''Array of secondary color data.''') + + # --- + + _tex_coords_cache = None + _tex_coords_cache_version = None + + def _get_tex_coords(self): + if (self._tex_coords_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['tex_coords'] + self._tex_coords_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._tex_coords_cache_version = domain._version + + region = self._tex_coords_cache + region.invalidate() + return region.array + + def _set_tex_coords(self, data): + self._get_tex_coords()[:] = data + + tex_coords = property(_get_tex_coords, _set_tex_coords, + doc='''Array of texture coordinate data.''') + + # --- + + _vertices_cache = None + _vertices_cache_version = None + + def _get_vertices(self): + if (self._vertices_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['vertices'] + self._vertices_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._vertices_cache_version = domain._version + + region = self._vertices_cache + region.invalidate() + return region.array + + def _set_vertices(self, data): + self._get_vertices()[:] = data + + vertices = property(_get_vertices, _set_vertices, + doc='''Array of vertex coordinate data.''') + +class IndexedVertexDomain(VertexDomain): + '''Management of a set of indexed vertex lists. + + Construction of an indexed vertex domain is usually done with the + `create_indexed_domain` function. + ''' + _initial_index_count = 16 + + def __init__(self, attribute_usages, index_gl_type=GL_UNSIGNED_INT): + super(IndexedVertexDomain, self).__init__(attribute_usages) + + self.index_allocator = allocation.Allocator(self._initial_index_count) + + self.index_gl_type = index_gl_type + self.index_c_type = vertexattribute._c_types[index_gl_type] + self.index_element_size = ctypes.sizeof(self.index_c_type) + self.index_buffer = vertexbuffer.create_mappable_buffer( + self.index_allocator.capacity * self.index_element_size, + target=GL_ELEMENT_ARRAY_BUFFER) + + def _safe_index_alloc(self, count): + '''Allocate indices, resizing the buffers if necessary.''' + try: + return self.index_allocator.alloc(count) + except allocation.AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + self.index_buffer.resize(capacity * self.index_element_size) + self.index_allocator.set_capacity(capacity) + return self.index_allocator.alloc(count) + + def _safe_index_realloc(self, start, count, new_count): + '''Reallocate indices, resizing the buffers if necessary.''' + try: + return self.index_allocator.realloc(start, count, new_count) + except allocation.AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + self.index_buffer.resize(capacity * self.index_element_size) + self.index_allocator.set_capacity(capacity) + return self.index_allocator.realloc(start, count, new_count) + + def create(self, count, index_count): + '''Create an `IndexedVertexList` in this domain. + + :Parameters: + `count` : int + Number of vertices to create + `index_count` + Number of indices to create + + ''' + start = self._safe_alloc(count) + index_start = self._safe_index_alloc(index_count) + return IndexedVertexList(self, start, count, index_start, index_count) + + def get_index_region(self, start, count): + '''Get a region of the index buffer. + + :Parameters: + `start` : int + Start of the region to map. + `count` : int + Number of indices to map. + + :rtype: Array of int + ''' + byte_start = self.index_element_size * start + byte_count = self.index_element_size * count + ptr_type = ctypes.POINTER(self.index_c_type * count) + return self.index_buffer.get_region(byte_start, byte_count, ptr_type) + + def draw(self, mode, vertex_list=None): + '''Draw vertices in the domain. + + If `vertex_list` is not specified, all vertices in the domain are + drawn. This is the most efficient way to render primitives. + + If `vertex_list` specifies a `VertexList`, only primitives in that + list will be drawn. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + `vertex_list` : `IndexedVertexList` + Vertex list to draw, or ``None`` for all lists in this domain. + + ''' + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + for buffer, attributes in self.buffer_attributes: + buffer.bind() + for attribute in attributes: + attribute.enable() + attribute.set_pointer(attribute.buffer.ptr) + self.index_buffer.bind() + if vertexbuffer._workaround_vbo_finish: + glFinish() + + if vertex_list is not None: + glDrawElements(mode, vertex_list.index_count, self.index_gl_type, + self.index_buffer.ptr + + vertex_list.index_start * self.index_element_size) + else: + starts, sizes = self.index_allocator.get_allocated_regions() + primcount = len(starts) + if primcount == 0: + pass + elif primcount == 1: + # Common case + glDrawElements(mode, sizes[0], self.index_gl_type, + self.index_buffer.ptr + starts[0]) + elif gl_info.have_version(1, 4): + if not isinstance(self.index_buffer, + vertexbuffer.VertexBufferObject): + starts = [s + self.index_buffer.ptr for s in starts] + starts = (GLuint * primcount)(*starts) + sizes = (GLsizei * primcount)(*sizes) + glMultiDrawElements(mode, sizes, self.index_gl_type, starts, + primcount) + else: + for start, size in zip(starts, sizes): + glDrawElements(mode, size, self.index_gl_type, + self.index_buffer.ptr + + start * self.index_element_size) + + self.index_buffer.unbind() + for buffer, _ in self.buffer_attributes: + buffer.unbind() + glPopClientAttrib() + +class IndexedVertexList(VertexList): + '''A list of vertices within an `IndexedVertexDomain` that are indexed. + Use `IndexedVertexDomain.create` to construct this list. + ''' + def __init__(self, domain, start, count, index_start, index_count): + super(IndexedVertexList, self).__init__(domain, start, count) + + self.index_start = index_start + self.index_count = index_count + + def draw(self, mode): + self.domain.draw(mode, self) + + def resize(self, count, index_count): + '''Resize this group. + + :Parameters: + `count` : int + New number of vertices in the list. + `index_count` : int + New number of indices in the list. + + ''' + old_start = self.start + super(IndexedVertexList, self).resize(count) + + # Change indices (because vertices moved) + if old_start != self.start: + diff = self.start - old_start + self.indices[:] = map(lambda i: i + diff, self.indices) + + # Resize indices + new_start = self.domain._safe_index_realloc( + self.index_start, self.index_count, index_count) + if new_start != self.index_start: + old = self.domain.get_index_region( + self.index_start, self.index_count) + new = self.domain.get_index_region( + self.index_start, self.index_count) + new.array[:] = old.array[:] + new.invalidate() + self.index_start = new_start + self.index_count = index_count + self._indices_cache_version = None + + def delete(self): + '''Delete this group.''' + super(IndexedVertexList, self).delete() + self.domain.index_allocator.dealloc(self.index_start, self.index_count) + + def _set_index_data(self, data): + # TODO without region + region = self.domain.get_index_region( + self.index_start, self.index_count) + region.array[:] = data + region.invalidate() + + # --- + + def _get_indices(self): + if self._indices_cache_version != self.domain._version: + domain = self.domain + self._indices_cache = domain.get_index_region( + self.index_start, self.index_count) + self._indices_cache_version = domain._version + + region = self._indices_cache + region.invalidate() + return region.array + + def _set_indices(self, data): + self._get_indices()[:] = data + + _indices_cache = None + _indices_cache_version = None + indices = property(_get_indices, _set_indices, + doc='''Array of index data.''') + diff --git a/lib/batch.pyx b/lib/batch.pyx new file mode 100644 index 0000000..4390f00 --- /dev/null +++ b/lib/batch.pyx @@ -0,0 +1,1573 @@ +############################################################################### +## __init__.py ## +############################################################################### + +import ctypes + +import pyglet +from pyglet.gl import * +import ctypes +import re + +_debug_graphics_batch = pyglet.options['debug_graphics_batch'] + +def _parse_data(data): + '''Given a list of data items, returns (formats, initial_arrays).''' + assert data, 'No attribute formats given' + + # Return tuple (formats, initial_arrays). + formats = [] + initial_arrays = [] + for i, format in enumerate(data): + if isinstance(format, tuple): + format, array = format + initial_arrays.append((i, array)) + formats.append(format) + formats = tuple(formats) + return formats, initial_arrays + +def _get_default_batch(): + shared_object_space = gl.current_context.object_space + try: + return shared_object_space.pyglet_graphics_default_batch + except AttributeError: + shared_object_space.pyglet_graphics_default_batch = Batch() + return shared_object_space.pyglet_graphics_default_batch + +def vertex_list(count, *data): + '''Create a `VertexList` not associated with a batch, group or mode. + + :Parameters: + `count` : int + The number of vertices in the list. + `data` : data items + Attribute formats and initial data for the vertex list. See the + module summary for details. + + :rtype: `VertexList` + ''' + # Note that mode=0 because the default batch is never drawn: vertex lists + # returned from this function are drawn directly by the app. + return _get_default_batch().add(count, 0, None, *data) + +class DomainDraw(object): + def __init__(self, domain, mode, group): + self.domain = domain + self.mode = mode + self.group = group + + def draw(self): + self.group.set_state() + self.domain.draw(self.mode) + self.group.unset_state() + +class Batch(object): + '''Manage a collection of vertex lists for batched rendering. + + Vertex lists are added to a `Batch` using the `add` and `add_indexed` + methods. An optional group can be specified along with the vertex list, + which gives the OpenGL state required for its rendering. Vertex lists + with shared mode and group are allocated into adjacent areas of memory and + sent to the graphics card in a single operation. + + Call `VertexList.delete` to remove a vertex list from the batch. + ''' + def __init__(self): + '''Create a graphics batch.''' + # Mapping to find domain. + # group -> (attributes, mode, indexed) -> domain + self.group_map = {} + + # Mapping of group to list of children. + self.group_children = {} + + # List of top-level groups + self.top_groups = [] + + self._draw_list = [] + self._draw_list_dirty = False + + def add(self, count, mode, group, *data): + '''Add a vertex list to the batch. + + :Parameters: + `count` : int + The number of vertices in the list. + `mode` : int + OpenGL drawing mode enumeration; for example, one of + ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. + See the module summary for additional information. + `group` : `Group` + Group of the vertex list, or ``None`` if no group is required. + `data` : data items + Attribute formats and initial data for the vertex list. See + the module summary for details. + + :rtype: `VertexList` + ''' + formats, initial_arrays = _parse_data(data) + domain = self._get_domain(False, mode, group, formats) + domain.__formats = formats + + # Create vertex list and initialize + vlist = domain.create(count) + for i, array in initial_arrays: + vlist._set_attribute_data(i, array) + + return vlist + + def _get_domain(self, indexed, mode, group, formats): + if group is None: + group = null_group + + # Batch group + if group not in self.group_map: + self._add_group(group) + + domain_map = self.group_map[group] + + # Find domain given formats, indices and mode + key = (formats, mode, indexed) + try: + domain = domain_map[key] + except KeyError: + # Create domain + if indexed: + domain = vertexdomain.create_indexed_domain(*formats) + else: + domain = create_domain(*formats) + domain_map[key] = domain + self._draw_list_dirty = True + + return domain + + def _add_group(self, group): + self.group_map[group] = {} + if group.parent is None: + self.top_groups.append(group) + else: + if group.parent not in self.group_map: + self._add_group(group.parent) + if group.parent not in self.group_children: + self.group_children[group.parent] = [] + self.group_children[group.parent].append(group) + self._draw_list_dirty = True + + def visit(self, group): + draw_list = [] + + # Draw domains using this group + domain_map = self.group_map[group] + for (formats, mode, indexed), domain in list(domain_map.items()): + # Remove unused domains from batch + if domain._is_empty(): + del domain_map[(formats, mode, indexed)] + continue + draw_list.append(DomainDraw(domain, mode, group)) + + # Sort and visit child groups of this group + children = self.group_children.get(group) + if children: + children.sort() + for child in list(children): + draw_list.extend(visit(child)) + + if children or domain_map: + return draw_list + else: + # Remove unused group from batch + del self.group_map[group] + if group.parent: + self.group_children[group.parent].remove(group) + try: + del self.group_children[group] + except KeyError: + pass + try: + self.top_groups.remove(group) + except ValueError: + pass + return [] + + def _update_draw_list(self): + '''Visit group tree in preorder and create a list of bound methods + to call. + ''' + + self._draw_list = [] + + self.top_groups.sort() + for group in list(self.top_groups): + self._draw_list.extend(self.visit(group)) + + self._draw_list_dirty = False + + if _debug_graphics_batch: + self._dump_draw_list() + + def draw(self): + '''Draw the batch. + ''' + if self._draw_list_dirty: + self._update_draw_list() + + for func in self._draw_list: + func.draw() + +class Group(object): + '''Group of common OpenGL state. + + Before a vertex list is rendered, its group's OpenGL state is set; as are + that state's ancestors' states. This can be defined arbitrarily on + subclasses; the default state change has no effect, and groups vertex + lists only in the order in which they are drawn. + ''' + def __init__(self, parent=None): + '''Create a group. + + :Parameters: + `parent` : `Group` + Group to contain this group; its state will be set before this + state's. + + ''' + self.parent = parent + + def set_state(self): + '''Apply the OpenGL state change. + + The default implementation does nothing.''' + pass + + def unset_state(self): + '''Repeal the OpenGL state change. + + The default implementation does nothing.''' + pass + + def set_state_recursive(self): + '''Set this group and its ancestry. + + Call this method if you are using a group in isolation: the + parent groups will be called in top-down order, with this class's + `set` being called last. + ''' + if self.parent: + self.parent.set_state_recursive() + self.set_state() + + def unset_state_recursive(self): + '''Unset this group and its ancestry. + + The inverse of `set_state_recursive`. + ''' + self.unset_state() + if self.parent: + self.parent.unset_state_recursive() + +class NullGroup(Group): + '''The default group class used when ``None`` is given to a batch. + + This implementation has no effect. + ''' + pass + +#: The default group. +#: +#: :type: `Group` +null_group = NullGroup() + + +############################################################################### +## allocation.py ## +############################################################################### + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +# Common cases: +# -regions will be the same size (instances of same object, e.g. sprites) +# -regions will not usually be resized (only exception is text) +# -alignment of 4 vertices (glyphs, sprites, images, ...) +# +# Optimise for: +# -keeping regions adjacent, reduce the number of entries in glMultiDrawArrays +# -finding large blocks of allocated regions quickly (for drawing) +# -finding block of unallocated space is the _uncommon_ case! +# +# Decisions: +# -don't over-allocate regions to any alignment -- this would require more +# work in finding the allocated spaces (for drawing) and would result in +# more entries in glMultiDrawArrays +# -don't move blocks when they truncate themselves. try not to allocate the +# space they freed too soon (they will likely need grow back into it later, +# and growing will usually require a reallocation). +# -allocator does not track individual allocated regions. Trusts caller +# to provide accurate (start, size) tuple, which completely describes +# a region from the allocator's point of view. +# -this means that compacting is probably not feasible, or would be hideously +# expensive + +class AllocatorMemoryException(Exception): + '''The buffer is not large enough to fulfil an allocation. + + Raised by `Allocator` methods when the operation failed due to lack of + buffer space. The buffer should be increased to at least + requested_capacity and then the operation retried (guaranteed to + pass second time). + ''' + + def __init__(self, requested_capacity): + self.requested_capacity = requested_capacity + +class Allocator(object): + '''Buffer space allocation implementation.''' + def __init__(self, capacity): + '''Create an allocator for a buffer of the specified capacity. + + :Parameters: + `capacity` : int + Maximum size of the buffer. + + ''' + self.capacity = capacity + + # Allocated blocks. Start index and size in parallel lists. + # + # # = allocated, - = free + # + # 0 3 5 15 20 24 40 + # |###--##########-----####----------------------| + # + # starts = [0, 5, 20] + # sizes = [3, 10, 4] + # + # To calculate free blocks: + # for i in range(0, len(starts)): + # free_start[i] = starts[i] + sizes[i] + # free_size[i] = starts[i+1] - free_start[i] + # free_size[i+1] = self.capacity - free_start[-1] + + self.starts = [] + self.sizes = [] + + def set_capacity(self, size): + '''Resize the maximum buffer size. + + The capaity cannot be reduced. + + :Parameters: + `size` : int + New maximum size of the buffer. + + ''' + assert size > self.capacity + self.capacity = size + + def alloc(self, size): + '''Allocate memory in the buffer. + + Raises `AllocatorMemoryException` if the allocation cannot be + fulfilled. + + :Parameters: + `size` : int + Size of region to allocate. + + :rtype: int + :return: Starting index of the allocated region. + ''' + assert size > 0 + + # return start + # or raise AllocatorMemoryException + + if not self.starts: + if size <= self.capacity: + self.starts.append(0) + self.sizes.append(size) + return 0 + else: + raise AllocatorMemoryException(size) + + # Allocate in a free space + free_start = self.starts[0] + self.sizes[0] + for i, (alloc_start, alloc_size) in \ + enumerate(zip(self.starts[1:], self.sizes[1:])): + # Danger! + # i is actually index - 1 because of slicing above... + # starts[i] points to the block before this free space + # starts[i+1] points to the block after this free space, and is + # always valid. + free_size = alloc_start - free_start + if free_size == size: + # Merge previous block with this one (removing this free space) + self.sizes[i] += free_size + alloc_size + del self.starts[i+1] + del self.sizes[i+1] + return free_start + elif free_size > size: + # Increase size of previous block to intrude into this free + # space. + self.sizes[i] += size + return free_start + free_start = alloc_start + alloc_size + + # Allocate at end of capacity + free_size = self.capacity - free_start + if free_size >= size: + self.sizes[-1] += size + return free_start + + raise AllocatorMemoryException(self.capacity + size - free_size) + + def realloc(self, start, size, new_size): + '''Reallocate a region of the buffer. + + This is more efficient than separate `dealloc` and `alloc` calls, as + the region can often be resized in-place. + + Raises `AllocatorMemoryException` if the allocation cannot be + fulfilled. + + :Parameters: + `start` : int + Current starting index of the region. + `size` : int + Current size of the region. + `new_size` : int + New size of the region. + + ''' + assert size > 0 and new_size > 0 + + # return start + # or raise AllocatorMemoryException + + # Truncation is the same as deallocating the tail cruft + if new_size < size: + self.dealloc(start + new_size, size - new_size) + return start + + # Find which block it lives in + for i, (alloc_start, alloc_size) in \ + enumerate(zip(*(self.starts, self.sizes))): + p = start - alloc_start + if p >= 0 and size <= alloc_size - p: + break + if not (p >= 0 and size <= alloc_size - p): + print zip(self.starts, self.sizes) + print start, size, new_size + print p, alloc_start, alloc_size + assert p >= 0 and size <= alloc_size - p, 'Region not allocated' + + if size == alloc_size - p: + # Region is at end of block. Find how much free space is after + # it. + is_final_block = i == len(self.starts) - 1 + if not is_final_block: + free_size = self.starts[i + 1] - (start + size) + else: + free_size = self.capacity - (start + size) + + # TODO If region is an entire block being an island in free space, + # can possibly extend in both directions. + + if free_size == new_size - size and not is_final_block: + # Merge block with next (region is expanded in place to + # exactly fill the free space) + self.sizes[i] += free_size + self.sizes[i + 1] + del self.starts[i + 1] + del self.sizes[i + 1] + return start + elif free_size > new_size - size: + # Expand region in place + self.sizes[i] += new_size - size + return start + + # The block must be repositioned. Dealloc then alloc. + + # But don't do this! If alloc fails, we've already silently dealloc'd + # the original block. + # self.dealloc(start, size) + # return self.alloc(new_size) + + # It must be alloc'd first. We're not missing an optimisation + # here, because if freeing the block would've allowed for the block to + # be placed in the resulting free space, one of the above in-place + # checks would've found it. + result = self.alloc(new_size) + self.dealloc(start, size) + return result + + def dealloc(self, start, size): + '''Free a region of the buffer. + + :Parameters: + `start` : int + Starting index of the region. + `size` : int + Size of the region. + + ''' + assert size > 0 + assert self.starts + + # Find which block needs to be split + for i, (alloc_start, alloc_size) in \ + enumerate(zip(*(self.starts, self.sizes))): + p = start - alloc_start + if p >= 0 and size <= alloc_size - p: + break + + # Assert we left via the break + assert p >= 0 and size <= alloc_size - p, 'Region not allocated' + + if p == 0 and size == alloc_size: + # Remove entire block + del self.starts[i] + del self.sizes[i] + elif p == 0: + # Truncate beginning of block + self.starts[i] += size + self.sizes[i] -= size + elif size == alloc_size - p: + # Truncate end of block + self.sizes[i] -= size + else: + # Reduce size of left side, insert block at right side + # $ = dealloc'd block, # = alloc'd region from same block + # + # <------8------> + # <-5-><-6-><-7-> + # 1 2 3 4 + # #####$$$$$##### + # + # 1 = alloc_start + # 2 = start + # 3 = start + size + # 4 = alloc_start + alloc_size + # 5 = start - alloc_start = p + # 6 = size + # 7 = {8} - ({5} + {6}) = alloc_size - (p + size) + # 8 = alloc_size + # + self.sizes[i] = p + self.starts.insert(i + 1, start + size) + self.sizes.insert(i + 1, alloc_size - (p + size)) + + def get_allocated_regions(self): + '''Get a list of (aggregate) allocated regions. + + The result of this method is ``(starts, sizes)``, where ``starts`` is + a list of starting indices of the regions and ``sizes`` their + corresponding lengths. + + :rtype: (list, list) + ''' + # return (starts, sizes); len(starts) == len(sizes) + return (self.starts, self.sizes) + + def get_fragmented_free_size(self): + '''Returns the amount of space unused, not including the final + free block. + + :rtype: int + ''' + if not self.starts: + return 0 + + # Variation of search for free block. + total_free = 0 + free_start = self.starts[0] + self.sizes[0] + for i, (alloc_start, alloc_size) in \ + enumerate(zip(self.starts[1:], self.sizes[1:])): + total_free += alloc_start - free_start + free_start = alloc_start + alloc_size + + return total_free + + def get_free_size(self): + '''Return the amount of space unused. + + :rtype: int + ''' + if not self.starts: + return self.capacity + + free_end = self.capacity - (self.starts[-1] + self.sizes[-1]) + return self.get_fragmented_free_size() + free_end + + def get_usage(self): + '''Return fraction of capacity currently allocated. + + :rtype: float + ''' + return 1. - self.get_free_size() / float(self.capacity) + + def get_fragmentation(self): + '''Return fraction of free space that is not expandable. + + :rtype: float + ''' + free_size = self.get_free_size() + if free_size == 0: + return 0. + return self.get_fragmented_free_size() / float(self.get_free_size()) + + def _is_empty(self): + return not self.starts + + def __str__(self): + return 'allocs=' + repr(zip(self.starts, self.sizes)) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, str(self)) + + +############################################################################### +## vertexattribute.py ## +############################################################################### + +_c_types = { + GL_BYTE: ctypes.c_byte, + GL_UNSIGNED_BYTE: ctypes.c_ubyte, + GL_SHORT: ctypes.c_short, + GL_UNSIGNED_SHORT: ctypes.c_ushort, + GL_INT: ctypes.c_int, + GL_UNSIGNED_INT: ctypes.c_uint, + GL_FLOAT: ctypes.c_float, + GL_DOUBLE: ctypes.c_double, +} + +_gl_types = { + 'b': GL_BYTE, + 'B': GL_UNSIGNED_BYTE, + 's': GL_SHORT, + 'S': GL_UNSIGNED_SHORT, + 'i': GL_INT, + 'I': GL_UNSIGNED_INT, + 'f': GL_FLOAT, + 'd': GL_DOUBLE, +} + +_attribute_format_re = re.compile(r''' + (?P + [cefnstv] | + (?P[0-9]+) g + (?Pn?)) + (?P[1234]) + (?P[bBsSiIfd]) +''', re.VERBOSE) + +_attribute_cache = {} + +def create_attribute(format): + '''Create a vertex attribute description from a format string. + + The initial stride and offset of the attribute will be 0. + + :Parameters: + `format` : str + Attribute format string. See the module summary for details. + + :rtype: `AbstractAttribute` + ''' + try: + cls, args = _attribute_cache[format] + return cls(*args) + except KeyError: + pass + + match = _attribute_format_re.match(format) + assert match, 'Invalid attribute format %r' % format + count = int(match.group('count')) + gl_type = _gl_types[match.group('type')] + generic_index = match.group('generic_index') + if generic_index: + normalized = match.group('generic_normalized') + attr_class = GenericAttribute + args = int(generic_index), normalized, count, gl_type + else: + name = match.group('name') + attr_class = _attribute_classes[name] + if attr_class._fixed_count: + assert count == attr_class._fixed_count, \ + 'Attributes named "%s" must have count of %d' % ( + name, attr_class._fixed_count) + args = (gl_type,) + else: + args = (count, gl_type) + + _attribute_cache[format] = attr_class, args + return attr_class(*args) + +class AbstractAttribute(object): + '''Abstract accessor for an attribute in a mapped buffer. + ''' + + _fixed_count = None + + def __init__(self, count, gl_type): + '''Create the attribute accessor. + + :Parameters: + `count` : int + Number of components in the attribute. + `gl_type` : int + OpenGL type enumerant; for example, ``GL_FLOAT`` + + ''' + assert count in (1, 2, 3, 4), 'Component count out of range' + self.gl_type = gl_type + self.c_type = _c_types[gl_type] + self.count = count + self.align = ctypes.sizeof(self.c_type) + self.size = count * self.align + self.stride = self.size + self.offset = 0 + + def enable(self): + '''Enable the attribute using ``glEnableClientState``.''' + raise NotImplementedError('abstract') + + def set_pointer(self, offset): + '''Setup this attribute to point to the currently bound buffer at + the given offset. + + ``offset`` should be based on the currently bound buffer's ``ptr`` + member. + + :Parameters: + `offset` : int + Pointer offset to the currently bound buffer for this + attribute. + + ''' + raise NotImplementedError('abstract') + + def get_region(self, buffer, start, count): + '''Map a buffer region using this attribute as an accessor. + + The returned region can be modified as if the buffer was a contiguous + array of this attribute (though it may actually be interleaved or + otherwise non-contiguous). + + The returned region consists of a contiguous array of component + data elements. For example, if this attribute uses 3 floats per + vertex, and the `count` parameter is 4, the number of floats mapped + will be ``3 * 4 = 12``. + + :Parameters: + `buffer` : `AbstractMappable` + The buffer to map. + `start` : int + Offset of the first vertex to map. + `count` : int + Number of vertices to map + + :rtype: `AbstractBufferRegion` + ''' + byte_start = self.stride * start + byte_size = self.stride * count + array_count = self.count * count + if self.stride == self.size: + # non-interleaved + ptr_type = ctypes.POINTER(self.c_type * array_count) + return buffer.get_region(byte_start, byte_size, ptr_type) + else: + # interleaved + byte_start += self.offset + byte_size -= self.offset + elem_stride = self.stride // ctypes.sizeof(self.c_type) + elem_offset = self.offset // ctypes.sizeof(self.c_type) + ptr_type = ctypes.POINTER( + self.c_type * (count * elem_stride - elem_offset)) + region = buffer.get_region(byte_start, byte_size, ptr_type) + return vertexbuffer.IndirectArrayRegion( + region, array_count, self.count, elem_stride) + + def set_region(self, buffer, start, count, data): + '''Set the data over a region of the buffer. + + :Parameters: + `buffer` : AbstractMappable` + The buffer to modify. + `start` : int + Offset of the first vertex to set. + `count` : int + Number of vertices to set. + `data` : sequence + Sequence of data components. + + ''' + if self.stride == self.size: + # non-interleaved + byte_start = self.stride * start + byte_size = self.stride * count + array_count = self.count * count + data = (self.c_type * array_count)(*data) + buffer.set_data_region(data, byte_start, byte_size) + else: + # interleaved + region = self.get_region(buffer, start, count) + region[:] = data + +class ColorAttribute(AbstractAttribute): + '''Color vertex attribute.''' + + plural = 'colors' + + def __init__(self, count, gl_type): + assert count in (3, 4), 'Color attributes must have count of 3 or 4' + super(ColorAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_COLOR_ARRAY) + + def set_pointer(self, pointer): + glColorPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class TexCoordAttribute(AbstractAttribute): + '''Texture coordinate attribute.''' + + plural = 'tex_coords' + + def __init__(self, count, gl_type): + assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Texture coord attribute must have non-byte signed type' + super(TexCoordAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_TEXTURE_COORD_ARRAY) + + def set_pointer(self, pointer): + glTexCoordPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class VertexAttribute(AbstractAttribute): + '''Vertex coordinate attribute.''' + + plural = 'vertices' + + def __init__(self, count, gl_type): + assert count > 1, \ + 'Vertex attribute must have count of 2, 3 or 4' + assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \ + 'Vertex attribute must have signed type larger than byte' + super(VertexAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableClientState(GL_VERTEX_ARRAY) + + def set_pointer(self, pointer): + glVertexPointer(self.count, self.gl_type, self.stride, + self.offset + pointer) + +class GenericAttribute(AbstractAttribute): + '''Generic vertex attribute, used by shader programs.''' + + def __init__(self, index, normalized, count, gl_type): + self.normalized = bool(normalized) + self.index = index + super(GenericAttribute, self).__init__(count, gl_type) + + def enable(self): + glEnableVertexAttribArray(self.index) + + def set_pointer(self, pointer): + glVertexAttribPointer(self.index, self.count, self.gl_type, + self.normalized, self.stride, + self.offset + pointer) + +_attribute_classes = { + 'c': ColorAttribute, + 't': TexCoordAttribute, + 'v': VertexAttribute, +} + + +############################################################################### +## vertexbuffer.py ## +############################################################################### + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + +_enable_vbo = pyglet.options['graphics_vbo'] + +# Enable workaround permanently if any VBO is created on a context that has +# this workaround. (On systems with multiple contexts where one is +# unaffected, the workaround will be enabled unconditionally on all of the +# contexts anyway. This is completely unlikely anyway). +_workaround_vbo_finish = False + +def create_buffer(size, + target=GL_ARRAY_BUFFER, + usage=GL_DYNAMIC_DRAW, + vbo=True): + '''Create a buffer of vertex data. + + :Parameters: + `size` : int + Size of the buffer, in bytes + `target` : int + OpenGL target buffer + `usage` : int + OpenGL usage constant + `vbo` : bool + True if a `VertexBufferObject` should be created if the driver + supports it; otherwise only a `VertexArray` is created. + + :rtype: `AbstractBuffer` + ''' + from pyglet import gl + if (vbo and + gl_info.have_version(1, 5) and + _enable_vbo and + not gl.current_context._workaround_vbo): + return VertexBufferObject(size, target, usage) + else: + return VertexArray(size) + +def create_mappable_buffer(size, + target=GL_ARRAY_BUFFER, + usage=GL_DYNAMIC_DRAW, + vbo=True): + '''Create a mappable buffer of vertex data. + + :Parameters: + `size` : int + Size of the buffer, in bytes + `target` : int + OpenGL target buffer + `usage` : int + OpenGL usage constant + `vbo` : bool + True if a `VertexBufferObject` should be created if the driver + supports it; otherwise only a `VertexArray` is created. + + :rtype: `AbstractBuffer` with `AbstractMappable` + ''' + from pyglet import gl + if (vbo and + gl_info.have_version(1, 5) and + _enable_vbo and + not gl.current_context._workaround_vbo): + return MappableVertexBufferObject(size, target, usage) + else: + return VertexArray(size) + +cdef class AbstractBuffer: + '''Abstract buffer of byte data. + + :Ivariables: + `size` : int + Size of buffer, in bytes + `ptr` : int + Memory offset of the buffer, as used by the ``glVertexPointer`` + family of functions + `target` : int + OpenGL buffer target, for example ``GL_ARRAY_BUFFER`` + `usage` : int + OpenGL buffer usage, for example ``GL_DYNAMIC_DRAW`` + + ''' + + ptr = 0 + size = 0 + + def bind(self): + '''Bind this buffer to its OpenGL target.''' + raise NotImplementedError('abstract') + + def unbind(self): + '''Reset the buffer's OpenGL target.''' + raise NotImplementedError('abstract') + + def set_data(self, data): + '''Set the entire contents of the buffer. + + :Parameters: + `data` : sequence of int or ctypes pointer + The byte array to set. + + ''' + raise NotImplementedError('abstract') + + def set_data_region(self, data, start, length): + '''Set part of the buffer contents. + + :Parameters: + `data` : sequence of int or ctypes pointer + The byte array of data to set + `start` : int + Offset to start replacing data + `length` : int + Length of region to replace + + ''' + raise NotImplementedError('abstract') + + def map(self, invalidate=False): + '''Map the entire buffer into system memory. + + The mapped region must be subsequently unmapped with `unmap` before + performing any other operations on the buffer. + + :Parameters: + `invalidate` : bool + If True, the initial contents of the mapped block need not + reflect the actual contents of the buffer. + + :rtype: ``POINTER(ctypes.c_ubyte)`` + :return: Pointer to the mapped block in memory + ''' + raise NotImplementedError('abstract') + + def unmap(self): + '''Unmap a previously mapped memory block.''' + raise NotImplementedError('abstract') + + def resize(self, size): + '''Resize the buffer to a new size. + + :Parameters: + `size` : int + New size of the buffer, in bytes + + ''' + + def delete(self): + '''Delete this buffer, reducing system resource usage.''' + raise NotImplementedError('abstract') + +cdef class VertexArray(AbstractBuffer): + '''A ctypes implementation of a vertex array. + + Many of the methods on this class are effectively no-op's, such as `bind`, + `unbind`, `map`, `unmap` and `delete`; they exist in order to present + a consistent interface with `VertexBufferObject`. + + This buffer type is also mappable, and so `get_region` can be used. + ''' + + cdef int size + cdef int ptr + cdef public int element_size + + def __init__(self, size): + self.size = size + self.array = (ctypes.c_byte * size)() + self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value + + def bind(self): + pass + + def unbind(self): + pass + + def set_data(self, data): + ctypes.memmove(self.ptr, data, self.size) + + def set_data_region(self, data, start, length): + ctypes.memmove(self.ptr + start, data, length) + + def map(self, invalidate=False): + return self.array + + def unmap(self): + pass + + def get_region(self, start, size, ptr_type): + array = ctypes.cast(self.ptr + start, ptr_type).contents + return VertexArrayRegion(array) + + def delete(self): + pass + + def resize(self, size): + array = (ctypes.c_byte * size)() + ctypes.memmove(array, self.array, min(size, self.size)) + self.size = size + self.array = array + self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value + + +class VertexBufferObject(AbstractBuffer): + '''Lightweight representation of an OpenGL VBO. + + The data in the buffer is not replicated in any system memory (unless it + is done so by the video driver). While this can improve memory usage and + possibly performance, updates to the buffer are relatively slow. + + This class does not implement `AbstractMappable`, and so has no + ``get_region`` method. See `MappableVertexBufferObject` for a VBO class + that does implement ``get_region``. + ''' + + def __init__(self, size, target, usage): + self.size = size + self.target = target + self.usage = usage + self._context = pyglet.gl.current_context + + id = GLuint() + glGenBuffers(1, id) + self.id = id.value + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(target, self.id) + glBufferData(target, self.size, None, self.usage) + glPopClientAttrib() + + global _workaround_vbo_finish + if pyglet.gl.current_context._workaround_vbo_finish: + _workaround_vbo_finish = True + + def bind(self): + glBindBuffer(self.target, self.id) + + def unbind(self): + glBindBuffer(self.target, 0) + + def set_data(self, data): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferData(self.target, self.size, data, self.usage) + glPopClientAttrib() + + def set_data_region(self, data, start, length): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + glBufferSubData(self.target, start, length, data) + glPopClientAttrib() + + def map(self, invalidate=False): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + if invalidate: + glBufferData(self.target, self.size, None, self.usage) + ptr = ctypes.cast(glMapBuffer(self.target, GL_WRITE_ONLY), + ctypes.POINTER(ctypes.c_byte * self.size)).contents + glPopClientAttrib() + return ptr + + def unmap(self): + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glUnmapBuffer(self.target) + glPopClientAttrib() + + def __del__(self): + try: + if self.id is not None: + self._context.delete_buffer(self.id) + except: + pass + + def delete(self): + id = GLuint(self.id) + glDeleteBuffers(1, id) + self.id = None + + def resize(self, size): + # Map, create a copy, then reinitialize. + temp = (ctypes.c_byte * size)() + + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + glBindBuffer(self.target, self.id) + data = glMapBuffer(self.target, GL_READ_ONLY) + ctypes.memmove(temp, data, min(size, self.size)) + glUnmapBuffer(self.target) + + self.size = size + glBufferData(self.target, self.size, temp, self.usage) + glPopClientAttrib() + +class AbstractBufferRegion(object): + '''A mapped region of a buffer. + + Buffer regions are obtained using `AbstractMappable.get_region`. + + :Ivariables: + `array` : ctypes array + Array of data, of the type and count requested by ``get_region``. + + ''' + def invalidate(self): + '''Mark this region as changed. + + The buffer may not be updated with the latest contents of the + array until this method is called. (However, it may not be updated + until the next time the buffer is used, for efficiency). + ''' + pass + +class VertexBufferObjectRegion(AbstractBufferRegion): + '''A mapped region of a VBO.''' + def __init__(self, buffer, start, end, array): + self.buffer = buffer + self.start = start + self.end = end + self.array = array + + def invalidate(self): + buffer = self.buffer + buffer._dirty_min = min(buffer._dirty_min, self.start) + buffer._dirty_max = max(buffer._dirty_max, self.end) + +class VertexArrayRegion(AbstractBufferRegion): + '''A mapped region of a vertex array. + + The `invalidate` method is a no-op but is provided in order to present + a consistent interface with `VertexBufferObjectRegion`. + ''' + def __init__(self, array): + self.array = array + +############################################################################### +## vertexdomain.py ## +############################################################################### + + +__docformat__ = 'restructuredtext' +__version__ = '$Id: $' + + +_usage_format_re = re.compile(r''' + (?P[^/]*) + (/ (?P static|dynamic|stream|none))? +''', re.VERBOSE) + +_gl_usages = { + 'static': GL_STATIC_DRAW, + 'dynamic': GL_DYNAMIC_DRAW, + 'stream': GL_STREAM_DRAW, + 'none': GL_STREAM_DRAW_ARB, # Force no VBO +} + +def _nearest_pow2(v): + # From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + # Credit: Sean Anderson + v -= 1 + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + return v + 1 + +def create_attribute_usage(format): + '''Create an attribute and usage pair from a format string. The + format string is as documented in `pyglet.graphics.vertexattribute`, with + the addition of an optional usage component:: + + usage ::= attribute ( '/' ('static' | 'dynamic' | 'stream' | 'none') )? + + If the usage is not given it defaults to 'dynamic'. The usage corresponds + to the OpenGL VBO usage hint, and for ``static`` also indicates a + preference for interleaved arrays. If ``none`` is specified a buffer + object is not created, and vertex data is stored in system memory. + + Some examples: + + ``v3f/stream`` + 3D vertex position using floats, for stream usage + ``c4b/static`` + 4-byte color attribute, for static usage + + :return: attribute, usage + ''' + match = _usage_format_re.match(format) + attribute_format = match.group('attribute') + attribute = create_attribute(attribute_format) + usage = match.group('usage') + if usage: + vbo = not usage == 'none' + usage = _gl_usages[usage] + else: + usage = GL_DYNAMIC_DRAW + vbo = True + + return (attribute, usage, vbo) + +def create_domain(*attribute_usage_formats): + '''Create a vertex domain covering the given attribute usage formats. + See documentation for `create_attribute_usage` and + `pyglet.graphics.vertexattribute.create_attribute` for the grammar of + these format strings. + + :rtype: `VertexDomain` + ''' + attribute_usages = [create_attribute_usage(f) \ + for f in attribute_usage_formats] + return VertexDomain(attribute_usages) + +class VertexDomain(object): + '''Management of a set of vertex lists. + + Construction of a vertex domain is usually done with the `create_domain` + function. + ''' + _version = 0 + _initial_count = 16 + + def __init__(self, attribute_usages): + self.allocator = Allocator(self._initial_count) + + static_attributes = [] + attributes = [] + self.buffer_attributes = [] # list of (buffer, attributes) + for attribute, usage, vbo in attribute_usages: + # Create non-interleaved buffer + attributes.append(attribute) + attribute.buffer = create_mappable_buffer( + attribute.stride * self.allocator.capacity, + usage=usage, vbo=vbo) + attribute.buffer.element_size = attribute.stride + #attribute.buffer.attributes = (attribute,) + self.buffer_attributes.append( + (attribute.buffer, attribute)) + + # Create named attributes for each attribute + self.attributes = attributes + self.attribute_names = {} + for attribute in attributes: + if isinstance(attribute, GenericAttribute): + index = attribute.index + if 'generic' not in self.attributes: + self.attribute_names['generic'] = {} + assert index not in self.attribute_names['generic'], \ + 'More than one generic attribute with index %d' % index + self.attribute_names['generic'][index] = attribute + else: + name = attribute.plural + assert name not in self.attributes, \ + 'More than one "%s" attribute given' % name + self.attribute_names[name] = attribute + + def __del__(self): + # Break circular refs that Python GC seems to miss even when forced + # collection. + for attribute in self.attributes: + del attribute.buffer + + def _safe_alloc(self, count): + '''Allocate vertices, resizing the buffers if necessary.''' + try: + return self.allocator.alloc(count) + except AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + for buffer, _ in self.buffer_attributes: + buffer.resize(capacity * buffer.element_size) + self.allocator.set_capacity(capacity) + return self.allocator.alloc(count) + + def _safe_realloc(self, start, count, new_count): + '''Reallocate vertices, resizing the buffers if necessary.''' + try: + return self.allocator.realloc(start, count, new_count) + except AllocatorMemoryException, e: + capacity = _nearest_pow2(e.requested_capacity) + self._version += 1 + for buffer, _ in self.buffer_attributes: + buffer.resize(capacity * buffer.element_size) + self.allocator.set_capacity(capacity) + return self.allocator.realloc(start, count, new_count) + + def create(self, count): + '''Create a `VertexList` in this domain. + + :Parameters: + `count` : int + Number of vertices to create. + + :rtype: `VertexList` + ''' + start = self._safe_alloc(count) + return VertexList(self, start, count) + + def draw(self, mode): + '''Draw vertices in the domain. + + If `vertex_list` is not specified, all vertices in the domain are + drawn. This is the most efficient way to render primitives. + + If `vertex_list` specifies a `VertexList`, only primitives in that + list will be drawn. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + `vertex_list` : `VertexList` + Vertex list to draw, or ``None`` for all lists in this domain. + + ''' + + starts, sizes = self.allocator.get_allocated_regions() + primcount = len(starts) + + if primcount == 0: + pass + elif primcount == 1: + glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) + for buffer, attribute in self.buffer_attributes: + attribute.enable() + attribute.set_pointer(attribute.buffer.ptr) + if _workaround_vbo_finish: + glFinish() + + # Common case + glDrawArrays(mode, starts[0], sizes[0]) + glPopClientAttrib() + + def _is_empty(self): + return not self.allocator.starts + + def __repr__(self): + return '<%s@%x %s>' % (self.__class__.__name__, id(self), + self.allocator) + +class VertexList(object): + '''A list of vertices within a `VertexDomain`. Use + `VertexDomain.create` to construct this list. + ''' + + def __init__(self, domain, start, count): + # TODO make private + self.domain = domain + self.start = start + self.count = count + + def get_size(self): + '''Get the number of vertices in the list. + + :rtype: int + ''' + return self.count + + def get_domain(self): + '''Get the domain this vertex list belongs to. + + :rtype: `VertexDomain` + ''' + return self.domain + + def draw(self, mode): + '''Draw this vertex list in the given OpenGL mode. + + :Parameters: + `mode` : int + OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc. + + ''' + self.domain.draw(mode, self) + + def resize(self, count): + '''Resize this group. + + :Parameters: + `count` : int + New number of vertices in the list. + + ''' + new_start = self.domain._safe_realloc(self.start, self.count, count) + if new_start != self.start: + # Copy contents to new location + for attribute in self.domain.attributes: + old = attribute.get_region(attribute.buffer, + self.start, self.count) + new = attribute.get_region(attribute.buffer, + new_start, self.count) + new.array[:] = old.array[:] + new.invalidate() + self.start = new_start + self.count = count + + self._colors_cache_version = None + self._fog_coords_cache_version = None + self._edge_flags_cache_version = None + self._normals_cache_version = None + self._secondary_colors_cache_version = None + self._tex_coords_cache_version = None + self._vertices_cache_version = None + + def delete(self): + '''Delete this group.''' + self.domain.allocator.dealloc(self.start, self.count) + + def _set_attribute_data(self, i, data): + attribute = self.domain.attributes[i] + # TODO without region + region = attribute.get_region(attribute.buffer, self.start, self.count) + region.array[:] = data + region.invalidate() + + # --- + + def _get_colors(self): + if (self._colors_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['colors'] + self._colors_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._colors_cache_version = domain._version + + region = self._colors_cache + region.invalidate() + return region.array + + def _set_colors(self, data): + self._get_colors()[:] = data + + _colors_cache = None + _colors_cache_version = None + colors = property(_get_colors, _set_colors, + doc='''Array of color data.''') + + # --- + + _tex_coords_cache = None + _tex_coords_cache_version = None + + def _get_tex_coords(self): + if (self._tex_coords_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['tex_coords'] + self._tex_coords_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._tex_coords_cache_version = domain._version + + region = self._tex_coords_cache + region.invalidate() + return region.array + + def _set_tex_coords(self, data): + self._get_tex_coords()[:] = data + + tex_coords = property(_get_tex_coords, _set_tex_coords, + doc='''Array of texture coordinate data.''') + + # --- + + _vertices_cache = None + _vertices_cache_version = None + + def _get_vertices(self): + if (self._vertices_cache_version != self.domain._version): + domain = self.domain + attribute = domain.attribute_names['vertices'] + self._vertices_cache = attribute.get_region( + attribute.buffer, self.start, self.count) + self._vertices_cache_version = domain._version + + region = self._vertices_cache + region.invalidate() + return region.array + + def _set_vertices(self, data): + self._get_vertices()[:] = data + + vertices = property(_get_vertices, _set_vertices, + doc='''Array of vertex coordinate data.''') diff --git a/lib/blit_into.pyx b/lib/blit_into.pyx new file mode 100644 index 0000000..287923e --- /dev/null +++ b/lib/blit_into.pyx @@ -0,0 +1,28 @@ +cdef extern from "include_gl.h": + ctypedef unsigned int GLenum + ctypedef int GLint + ctypedef unsigned int GLuint + ctypedef int GLsizei + ctypedef void GLvoid + + cdef int GL_RGBA + cdef int GL_UNSIGNED_BYTE + + cdef void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, char *pixels) + cdef void glBindTexture(GLenum target, GLuint texture) + +def render(atlases, objects): + cdef char *data + for i, atlas in enumerate(atlases): + if len(objects[i]) > 0: + glBindTexture(atlas.texture.target, atlas.texture.id) + for obj in objects[i]: + temp = obj.next_frame() + data = temp + tex = obj.tex + glTexSubImage2D(tex.owner.target, + tex.owner.level, + tex.x, tex.y, + tex.width, tex.height, + GL_RGBA, GL_UNSIGNED_BYTE, + data) diff --git a/lib/include_gl.h b/lib/include_gl.h new file mode 100644 index 0000000..172d25a --- /dev/null +++ b/lib/include_gl.h @@ -0,0 +1,11 @@ +#ifdef _MSC_VER +#include +#endif + +#ifdef __APPLE__ +#include +#include +#else +#include +#include +#endif diff --git a/lib/mapset.py b/lib/mapset.py index 6fbbfe2..715d321 100644 --- a/lib/mapset.py +++ b/lib/mapset.py @@ -23,27 +23,24 @@ from lib import h3m import os class OrderedTextureGroup(pyglet.graphics.Group): - def __init__(self, order, texture, parent=None): - super(OrderedTextureGroup, self).__init__(parent) + def __init__(self, order, texture): + super(OrderedTextureGroup, self).__init__() self.texture = texture self.order = order def set_state(self): - pyglet.gl.glEnable(self.texture.target) - pyglet.gl.glBindTexture(self.texture.target, self.texture.id) - + pyglet.gl.glBindTexture(pyglet.gl.GL_TEXTURE_2D, self.texture.id) + def unset_state(self): - pyglet.gl.glDisable(self.texture.target) + pass def __hash__(self): - return hash((self.order, self.texture.target, self.texture.id, self.parent)) + return hash((self.order, self.texture.id)) def __eq__(self, other): return (self.__class__ is other.__class__ and self.order == other.order and - self.texture.target == other.texture.target and - self.texture.id == other.texture.id and - self.parent == self.parent) + self.texture.id == other.texture.id) def __repr__(self): return '%s(order=%d, id=%d)' % (self.__class__.__name__, self.order, self.texture.id) @@ -93,7 +90,7 @@ class Animation(object): class Animation_Object(object): def __init__(self, animation, group=None): - if group: + if group != None: self.texgroup = group else: self.texgroup = animation.texgroup @@ -179,7 +176,7 @@ class MapSet(object): for x, tile in enumerate(line): if tile[0] == -1: #edge if "edg" not in list(tile_textures.keys()): - tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 1000) for i in range(36)] + tile_textures["edg"] = [self.load_map_object('data/advmap_tiles/edg.def/%d.png'%i, 100) for i in range(36)] self.__tiles[x,y] = [tile_textures["edg"][tile[1]]] elif tile[0] == 0: #dirt if "dirttl" not in list(tile_textures.keys()): diff --git a/lib/mapview.py b/lib/mapview.py index b5d225e..cc8ae65 100644 --- a/lib/mapview.py +++ b/lib/mapview.py @@ -131,7 +131,9 @@ class MapView(object): def draw(self): pyglet.gl.glTranslatef(self.view_x+IF_LEFT, self.view_y+IF_BOTTOM, 0) pyglet.gl.glScalef(self.tile_size/32.0, self.tile_size/32.0, 0.0) + pyglet.gl.glEnable(pyglet.gl.GL_TEXTURE_2D) self.batch.draw() + pyglet.gl.glDisable(pyglet.gl.GL_TEXTURE_2D) def _move(self, dx, dy): # new map position diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b72de33 --- /dev/null +++ b/setup.py @@ -0,0 +1,9 @@ +from distutils.core import setup +from distutils.extension import Extension +from Cython.Distutils import build_ext + +setup( + cmdclass = {'build_ext': build_ext}, + ext_modules = [Extension("blit_into", ["lib/blit_into.pyx"]), + Extension("batch", ["lib/batch.pyx"])] +) diff --git a/test-html.py b/test-html.py new file mode 100644 index 0000000..29cdab1 --- /dev/null +++ b/test-html.py @@ -0,0 +1,13 @@ +file = open("test2") +objects = [[] for i in range(255)] +for line in file: + objclass, obj = line.split() + objects[int(objclass)].append(obj) + +file = open("test.html", "w") +for i, objs in enumerate(objects): + if len(objs) == 0: + continue + file.write("

class %d

\n"%i) + for obj in objs: + file.write("\n"%(obj, obj)) diff --git a/test.html b/test.html new file mode 100644 index 0000000..98c1d67 --- /dev/null +++ b/test.html @@ -0,0 +1,1465 @@ +

class 2

+ +

class 4

+ +

class 5

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 6

+ +

class 7

+ +

class 8

+ + + +

class 9

+ + + + + + + + +

class 10

+ + + + + + + + +

class 11

+ +

class 12

+ + + + +

class 13

+ + + +

class 14

+ + + +

class 15

+ +

class 16

+ + + + + + + +

class 17

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 20

+ + +

class 21

+ +

class 22

+ +

class 23

+ +

class 24

+ +

class 25

+ +

class 26

+ +

class 27

+ +

class 28

+ +

class 29

+ +

class 30

+ +

class 31

+ +

class 32

+ +

class 33

+ + +

class 35

+ + +

class 36

+ +

class 37

+ +

class 38

+ +

class 39

+ +

class 41

+ +

class 42

+ +

class 43

+ + + + + + + + +

class 44

+ + + + + + + + +

class 45

+ + + + + + + + +

class 46

+ +

class 47

+ +

class 48

+ +

class 49

+ + + +

class 51

+ +

class 52

+ +

class 53

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 54

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 55

+ +

class 56

+ +

class 57

+ + + + + + + +

class 58

+ + +

class 59

+ +

class 60

+ +

class 61

+ +

class 62

+ +

class 63

+ +

class 64

+ +

class 65

+ +

class 66

+ +

class 67

+ +

class 68

+ +

class 69

+ +

class 71

+ +

class 72

+ +

class 73

+ +

class 74

+ +

class 75

+ +

class 76

+ +

class 77

+ +

class 78

+ +

class 79

+ + + + + + + +

class 80

+ +

class 81

+ +

class 82

+ +

class 83

+ + + +

class 84

+ + + +

class 85

+ + +

class 86

+ +

class 87

+ +

class 88

+ +

class 89

+ +

class 90

+ +

class 91

+ + + + + +

class 92

+ +

class 93

+ +

class 94

+ +

class 95

+ +

class 96

+ +

class 97

+ + +

class 98

+ + + + + + + + + +

class 99

+ + +

class 100

+ +

class 101

+ +

class 102

+ +

class 103

+ +

class 104

+ +

class 105

+ +

class 106

+ +

class 107

+ +

class 108

+ +

class 109

+ + +

class 110

+ +

class 111

+ +

class 112

+ + +

class 113

+ +

class 116

+ + + + + + + + + + + + + + + +

class 117

+ +

class 118

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 119

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 120

+ + + + + + + + + + + + + + + + + + + + + +

class 121

+ + + +

class 124

+ + + + + + + + +

class 125

+ + +

class 126

+ + + + + + + + + + + + + +

class 127

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 128

+ + + +

class 129

+ + + + + + + + + + + + +

class 130

+ +

class 131

+ + + + + +

class 132

+ +

class 133

+ + + + + + + +

class 134

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 135

+ + + + + + + + + + + + + + + + + +

class 136

+ + + + + + + + + + + + + + + + + +

class 137

+ + + + + + + + + + + + + + + + +

class 143

+ + + + + + + + + + + + + + + + +

class 147

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 148

+ + + +

class 149

+ +

class 150

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 151

+ +

class 153

+ + + + + +

class 155

+ + + + + + + + + + + + + + + + + + + + + + + + + +

class 158

+ + + + + +

class 161

+ + + + + + +

class 162

+ +

class 163

+ +

class 164

+ +

class 177

+ +

class 199

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

class 206

+ + + + + + + + + + + + +

class 207

+ + + + + + + + + + + + +

class 208

+ + + + + + + + + + + + +

class 209

+ + + + + + + + + + + + +

class 210

+ + + + + + + + + + + + +

class 211

+ + + + + + + + +

class 212

+ + + + + + + + +

class 213

+ +

class 215

+ +

class 216

+ +

class 217

+ + + + + + + +

class 218

+ + + + + + + + + +

class 219

+ + +

class 220

+ + + + + + + +

class 221

+ +

class 222

+ + + + + + + + +

class 223

+ + + + + + + +

class 224

+ + + + + + + + +

class 225

+ + + + + + + + +

class 226

+ + + + + + + + +

class 227

+ + + + + + + + +

class 228

+ + + + + + + + +

class 229

+ + + + + + + + +

class 230

+ + + + + + + +

class 231

+ + + + + + + + diff --git a/testtest.py b/testtest.py new file mode 100755 index 0000000..1817709 --- /dev/null +++ b/testtest.py @@ -0,0 +1,110 @@ +import time + +import pyglet +from pyglet import graphics +from pyglet import text +from pyglet import event +from pyglet.gl import * + +class SimpleMenu(event.EventDispatcher, graphics.Batch): + def __init__(self, parent, x, y, options): + super(SimpleMenu, self).__init__() + + self.parent = parent + self.x, self.y = x, y + + self.options = [] + y = y + self.width = 0 + for option in options: + l = text.Label(option, x=x, y=y, color=(0, 0, 0, 255), anchor_y='top', batch=self) + self.options.append(l) + self.width = max(self.width, l.content_width) + y -= l.content_height + + self.height = abs(self.y - y) + + # add some padding + self.height += 4 + self.width += 4 + + # adjust menu position to make sure whole menu is visible + if self.x < 0: + self.x = 0 + if self.x + self.width> parent.width: + self.x = parent.width - self.width + if self.y - self.height < 0: + self.y = self.height + if self.y > parent.height: + self.y = parent.height + + # reposition the items + y = self.y - 2 + for option in self.options: + option.x = self.x + 2 + option.y = y + y -= l.content_height + + self.created_time = time.time() + + parent.push_handlers(self) + + def on_mouse_motion(self, x, y, dx, dy): + ret = event.EVENT_UNHANDLED + for option in self.options: + if (option.x < x < option.x + self.width and + option.y - option.content_height < y < option.y): + option.color = (200, 0, 0, 255) + ret = event.EVENT_HANDLED + elif option.color != (0, 0, 0, 255): + option.color = (0, 0, 0, 255) + return ret + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + return self.on_mouse_motion(x, y, dx, dy) + + def on_mouse_press(self, x, y, buttons, modifiers): + # determine action to take + selected_option = None + for option in self.options: + if (option.x < x < option.x + self.width and + option.y - option.content_height < y < option.y): + selected_option = option.text + break + + self.parent.remove_handlers(self) + self.options = None + self.dispatch_event('on_close', selected_option) + return event.EVENT_HANDLED + + def on_mouse_release(self, x, y, button, modifiers): + if time.time() - self.created_time > .5: + return self.on_mouse_press(x, y, button, modifiers) + return event.EVENT_UNHANDLED + + def draw(self): + glColor4f(255, 255, 255, 255) + glRectf(self.x, self.y, self.x + self.width, self.y - self.height) + super(SimpleMenu, self).draw() + +SimpleMenu.register_event_type('on_close') + +if __name__ == '__main__': + win = pyglet.window.Window(height=400) + m = None + + @win.event + def on_draw(): + win.clear() + if m: m.draw() + + m = SimpleMenu(win, 0, 0, ['New', 'Load', 'Save', 'Quit']) + @m.event + def on_close(selected_option): + print selected_option + if selected_option == 'Quit': + pyglet.app.exit() + global m + m = None + + pyglet.app.run()