##################################################################### # -*- coding: iso-8859-1 -*- # # # # Frets on Fire # # Copyright (C) 2006 Sami Kyöstilä # # # # 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 2 # # 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, write to the Free Software # # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # # MA 02110-1301, USA. # ##################################################################### from __future__ import division import Log import Config from PIL import Image import pygame import StringIO from OpenGL.GL import * from OpenGL.GLU import * class TextureException(Exception): pass # A queue containing (function, args) pairs that clean up deleted OpenGL handles. # The functions are called in the main OpenGL thread. #stump: Replaced queue with list to avoid deadlocking under # certain conditions due to a bug in Python. cleanupQueue = [] class Texture: """Represents an OpenGL texture, optionally loaded from disk in any format supported by PIL""" def __init__(self, name = None, target = GL_TEXTURE_2D): # Delete all pending textures try: func, args = cleanupQueue[0] del cleanupQueue[0] func(*args) except IndexError: pass except Exception, e: #MFH - to catch "did you call glewInit?" crashes Log.error("Texture.py texture deletion exception: %s" % e) self.texture = glGenTextures(1) self.texEnv = GL_MODULATE self.glTarget = target self.framebuffer = None self.setDefaults() self.name = name if name: self.loadFile(name) def loadFile(self, name): """Load the texture from disk, using PIL to open the file""" self.loadImage(Image.open(name)) self.name = name def loadImage(self, image): """Load the texture from a PIL image""" image = image.transpose(Image.FLIP_TOP_BOTTOM) if image.mode == "RGBA": string = image.tostring('raw', 'RGBA', 0, -1) self.loadRaw(image.size, string, GL_RGBA, 4) elif image.mode == "RGB": string = image.tostring('raw', 'RGB', 0, -1) self.loadRaw(image.size, string, GL_RGB, 3) elif image.mode == "L": string = image.tostring('raw', 'L', 0, -1) self.loadRaw(image.size, string, GL_LUMINANCE, 1) else: try: image = image.convert('RGB') Log.warn("Unsupported image mode '%s' converted to 'RGB'. May have unexpected results." % image.mode) string = image.tostring('raw', 'RGB', 0, -1) self.loadRaw(image.size, string, GL_RGB, 3) except: raise TextureException("Unsupported image mode '%s'" % image.mode) def nextPowerOfTwo(self, n): m = 1 while m < n: m <<= 1 return m def loadSurface(self, surface, monochrome = False, alphaChannel = False): """Load the texture from a pygame surface""" # make it a power of two self.pixelSize = w, h = surface.get_size() w2, h2 = [self.nextPowerOfTwo(x) for x in [w, h]] if w != w2 or h != h2: s = pygame.Surface((w2, h2), pygame.SRCALPHA, 32) s.blit(surface, (0, h2 - h)) surface = s if monochrome: # pygame doesn't support monochrome, so the fastest way # appears to be using PIL to do the conversion. string = pygame.image.tostring(surface, "RGB") image = Image.fromstring("RGB", surface.get_size(), string).convert("L") string = image.tostring('raw', 'L', 0, -1) self.loadRaw(surface.get_size(), string, GL_LUMINANCE, GL_INTENSITY8) else: if alphaChannel: string = pygame.image.tostring(surface, "RGBA", True) self.loadRaw(surface.get_size(), string, GL_RGBA, 4) else: string = pygame.image.tostring(surface, "RGB", True) self.loadRaw(surface.get_size(), string, GL_RGB, 3) self.size = (w / w2, h / h2) def loadSubsurface(self, surface, position = (0, 0), monochrome = False, alphaChannel = False): """Load the texture from a pygame surface""" if monochrome: # pygame doesn't support monochrome, so the fastest way # appears to be using PIL to do the conversion. string = pygame.image.tostring(surface, "RGB") image = Image.fromstring("RGB", surface.get_size(), string).convert("L") string = image.tostring('raw', 'L', 0, -1) self.loadSubRaw(surface.get_size(), position, string, GL_INTENSITY8) else: if alphaChannel: string = pygame.image.tostring(surface, "RGBA", True) self.loadSubRaw(surface.get_size(), position, string, GL_RGBA) else: string = pygame.image.tostring(surface, "RGB", True) self.loadSubRaw(surface.get_size(), position, string, GL_RGB) def loadRaw(self, size, string, format, components): """Load a raw image from the given string. 'format' is a constant such as GL_RGB or GL_RGBA that can be passed to gluBuild2DMipmaps. """ self.pixelSize = size self.size = (1.0, 1.0) self.format = format self.components = components (w, h) = size Texture.bind(self) glPixelStorei(GL_UNPACK_ALIGNMENT, 1) gluBuild2DMipmaps(self.glTarget, components, w, h, format, GL_UNSIGNED_BYTE, string) def loadSubRaw(self, size, position, string, format): Texture.bind(self) glPixelStorei(GL_UNPACK_ALIGNMENT, 1) glTexSubImage2D(self.glTarget, 0, position[0], position[1], size[0], size[1], format, GL_UNSIGNED_BYTE, string) def loadEmpty(self, size, format): self.pixelSize = size self.size = (1.0, 1.0) self.format = format Texture.bind(self) glTexImage2D(GL_TEXTURE_2D, 0, format, size[0], size[1], 0, format, GL_UNSIGNED_BYTE, "\x00" * (size[0] * size[1] * 4)) def setDefaults(self): """Set the default OpenGL options for this texture""" self.setRepeat() self.setFilter() glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) def setRepeat(self, u=GL_REPEAT, v=GL_REPEAT): Texture.bind(self) glTexParameteri(self.glTarget, GL_TEXTURE_WRAP_S, u) glTexParameteri(self.glTarget, GL_TEXTURE_WRAP_T, v) def setFilter(self, min=GL_LINEAR_MIPMAP_LINEAR, mag=GL_LINEAR): Texture.bind(self) glTexParameteri(self.glTarget, GL_TEXTURE_MIN_FILTER, min) glTexParameteri(self.glTarget, GL_TEXTURE_MAG_FILTER, mag) def __del__(self): # Queue this texture to be deleted later try: cleanupQueue.append((glDeleteTextures, [self.texture])) except NameError: pass def bind(self, glTarget = None): """Bind this texture to self.glTarget in the current OpenGL context""" if not glTarget: glTarget = self.glTarget glBindTexture(glTarget, self.texture) glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, self.texEnv) # # Texture atlas # TEXTURE_ATLAS_SIZE = 1024 class TextureAtlasFullException(Exception): pass class TextureAtlas(object): def __init__(self, size = TEXTURE_ATLAS_SIZE): self.texture = Texture() self.cursor = (0, 0) self.rowHeight = 0 self.surfaceCount = 0 self.texture.loadEmpty((size, size), GL_RGBA) def add(self, surface, margin = 0): w, h = surface.get_size() x, y = self.cursor w += margin h += margin if w > self.texture.pixelSize[0] or h > self.texture.pixelSize[1]: raise ValueError("Surface is too big to fit into atlas.") if x + w >= self.texture.pixelSize[0]: x = 0 y += self.rowHeight self.rowHeight = 0 if y + h >= self.texture.pixelSize[1]: Log.debug("Texture atlas %s full after %d surfaces." % (self.texture.pixelSize, self.surfaceCount)) raise TextureAtlasFullException() self.texture.loadSubsurface(surface, position = (x, y), alphaChannel = True) self.surfaceCount += 1 self.rowHeight = max(self.rowHeight, h) self.cursor = (x + w, y + h) # Return the coordinates for the uploaded texture patch w -= margin h -= margin return x / float(self.texture.pixelSize[0]), y / float(self.texture.pixelSize[1]), \ (x + w) / float(self.texture.pixelSize[0]), (y + h) / float(self.texture.pixelSize[1]) def bind(self): self.texture.bind()