Module Texture
[hide private]
[frames] | no frames]

Source Code for Module Texture

  1  #####################################################################
 
  2  # -*- coding: iso-8859-1 -*-                                        #
 
  3  #                                                                   #
 
  4  # Frets on Fire                                                     #
 
  5  # Copyright (C) 2006 Sami Kyöstilä                                  #
 
  6  #                                                                   #
 
  7  # This program is free software; you can redistribute it and/or     #
 
  8  # modify it under the terms of the GNU General Public License       #
 
  9  # as published by the Free Software Foundation; either version 2    #
 
 10  # of the License, or (at your option) any later version.            #
 
 11  #                                                                   #
 
 12  # This program is distributed in the hope that it will be useful,   #
 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of    #
 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the     #
 
 15  # GNU General Public License for more details.                      #
 
 16  #                                                                   #
 
 17  # You should have received a copy of the GNU General Public License #
 
 18  # along with this program; if not, write to the Free Software       #
 
 19  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,        #
 
 20  # MA  02110-1301, USA.                                              #
 
 21  #####################################################################
 
 22  
 
 23  from __future__ import division 
 24  
 
 25  import Log 
 26  import Config 
 27  import Image 
 28  import pygame 
 29  import StringIO 
 30  import PngImagePlugin 
 31  from OpenGL.GL import * 
 32  from OpenGL.GLU import * 
 33  from Queue import Queue, Empty 
 34  
 
 35  Config.define("opengl", "supportfbo", bool, False) 
 36  
 
 37  try: 
 38    from glew import * 
 39  except ImportError: 
 40    #Log.warn("GLEWpy not found -> Emulating Render to texture functionality.")
 
 41    pass 
 42  
 
43 -class TextureException(Exception):
44 pass
45 46 # A queue containing (function, args) pairs that clean up deleted OpenGL handles. 47 # The functions are called in the main OpenGL thread. 48 cleanupQueue = Queue() 49
50 -class Framebuffer:
51 fboSupported = None 52
53 - def __init__(self, texture, width, height, generateMipmap = False):
54 self.emulated = not self._fboSupported() 55 self.size = (width, height) 56 self.colorbuf = texture 57 self.generateMipmap = generateMipmap 58 self.fb = 0 59 self.depthbuf = 0 60 self.stencilbuf = 0 61 62 if self.emulated: 63 if (width & (width - 1)) or (height & (height - 1)): 64 raise TextureException("Only power of two render target textures are supported when frame buffer objects support is missing.") 65 else: 66 self.fb = glGenFramebuffersEXT(1)[0] 67 self.depthbuf = glGenRenderbuffersEXT(1)[0] 68 self.stencilbuf = glGenRenderbuffersEXT(1)[0] 69 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self.fb) 70 self._checkError() 71 72 glBindTexture(GL_TEXTURE_2D, self.colorbuf) 73 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) 74 # PyOpenGL does not support NULL textures, so we must make a temporary buffer here 75 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, 76 GL_RGBA, GL_UNSIGNED_BYTE, "\x00" * (width * height * 4)) 77 self._checkError() 78 79 if self.emulated: 80 return 81 82 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self.fb) 83 84 try: 85 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, 86 GL_COLOR_ATTACHMENT0_EXT, 87 GL_TEXTURE_2D, self.colorbuf, 0) 88 self._checkError() 89 90 # On current NVIDIA hardware, the stencil buffer must be packed 91 # with the depth buffer (GL_NV_packed_depth_stencil) instead of 92 # separate binding, so we must check for that extension here 93 if glewGetExtension("GL_NV_packed_depth_stencil"): 94 GL_DEPTH_STENCIL_EXT = 0x84F9 95 96 glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, self.depthbuf) 97 glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, 98 GL_DEPTH_STENCIL_EXT, width, height) 99 glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, 100 GL_DEPTH_ATTACHMENT_EXT, 101 GL_RENDERBUFFER_EXT, self.depthbuf) 102 self._checkError() 103 else: 104 glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, self.depthbuf) 105 glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, 106 GL_DEPTH_COMPONENT24, width, height) 107 glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, 108 GL_DEPTH_ATTACHMENT_EXT, 109 GL_RENDERBUFFER_EXT, self.depthbuf) 110 self._checkError() 111 glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, self.stencilbuf) 112 glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, 113 GL_STENCIL_INDEX_EXT, width, height) 114 glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, 115 GL_STENCIL_ATTACHMENT_EXT, 116 GL_RENDERBUFFER_EXT, self.stencilbuf) 117 self._checkError() 118 finally: 119 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)
120
121 - def __del__(self):
122 # Queue the buffers to be deleted later 123 try: 124 cleanupQueue.put((glDeleteBuffers, [3, [self.depthbuf, self.stencilbuf, self.fb]])) 125 except NameError: 126 pass
127
128 - def _fboSupported(self):
129 if Framebuffer.fboSupported is not None: 130 return Framebuffer.fboSupported 131 Framebuffer.fboSupported = False 132 133 if not Config.get("opengl", "supportfbo"): 134 Log.warn("Frame buffer object support disabled in configuration.") 135 return False 136 137 if not "glewGetExtension" in globals(): 138 Log.warn("GLEWpy not found, so render to texture functionality disabled.") 139 return False 140 141 glewInit() 142 143 if not glewGetExtension("GL_EXT_framebuffer_object"): 144 Log.warn("No support for framebuffer objects, so render to texture functionality disabled.") 145 return False 146 147 if glGetString(GL_VENDOR) == "ATI Technologies Inc.": 148 Log.warn("Frame buffer object support disabled until ATI learns to make proper OpenGL drivers (no stencil support).") 149 return False 150 151 Framebuffer.fboSupported = True 152 return True
153
154 - def _checkError(self):
155 pass
156 # No glGetError() anymore... 157 #err = glGetError() 158 #if (err != GL_NO_ERROR): 159 # raise TextureException(gluErrorString(err)) 160
161 - def setAsRenderTarget(self):
162 if not self.emulated: 163 glBindTexture(GL_TEXTURE_2D, 0) 164 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self.fb) 165 self._checkError()
166
167 - def resetDefaultRenderTarget(self):
168 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) 169 if not self.emulated: 170 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0) 171 glBindTexture(GL_TEXTURE_2D, self.colorbuf) 172 if self.generateMipmap: 173 glGenerateMipmapEXT(GL_TEXTURE_2D) 174 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) 175 else: 176 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) 177 else: 178 glBindTexture(GL_TEXTURE_2D, self.colorbuf) 179 glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, self.size[0], self.size[1]) 180 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
181
182 -class Texture:
183 """Represents an OpenGL texture, optionally loaded from disk in any format supported by PIL""" 184
185 - def __init__(self, name = None, target = GL_TEXTURE_2D):
186 # Delete all pending textures 187 try: 188 func, args = cleanupQueue.get_nowait() 189 func(*args) 190 except Empty: 191 pass 192 193 self.texture = glGenTextures(1) 194 self.texEnv = GL_MODULATE 195 self.glTarget = target 196 self.framebuffer = None 197 198 self.setDefaults() 199 self.name = name 200 201 if name: 202 self.loadFile(name)
203
204 - def loadFile(self, name):
205 """Load the texture from disk, using PIL to open the file""" 206 self.loadImage(Image.open(name)) 207 self.name = name
208
209 - def loadImage(self, image):
210 """Load the texture from a PIL image""" 211 image = image.transpose(Image.FLIP_TOP_BOTTOM) 212 if image.mode == "RGBA": 213 string = image.tostring('raw', 'RGBA', 0, -1) 214 self.loadRaw(image.size, string, GL_RGBA, 4) 215 elif image.mode == "RGB": 216 string = image.tostring('raw', 'RGB', 0, -1) 217 self.loadRaw(image.size, string, GL_RGB, 3) 218 elif image.mode == "L": 219 string = image.tostring('raw', 'L', 0, -1) 220 self.loadRaw(image.size, string, GL_LUMINANCE, 1) 221 else: 222 raise TextureException("Unsupported image mode '%s'" % image.mode)
223
224 - def prepareRenderTarget(self, width, height, generateMipmap = True):
225 self.framebuffer = Framebuffer(self.texture, width, height, generateMipmap) 226 self.pixelSize = (width, height) 227 self.size = (1.0, 1.0)
228
229 - def setAsRenderTarget(self):
230 assert self.framebuffer 231 self.framebuffer.setAsRenderTarget()
232
233 - def resetDefaultRenderTarget(self):
234 assert self.framebuffer 235 self.framebuffer.resetDefaultRenderTarget()
236
237 - def nextPowerOfTwo(self, n):
238 m = 1 239 while m < n: 240 m <<= 1 241 return m
242
243 - def loadSurface(self, surface, monochrome = False, alphaChannel = False):
244 """Load the texture from a pygame surface""" 245 246 # make it a power of two 247 self.pixelSize = w, h = surface.get_size() 248 w2, h2 = [self.nextPowerOfTwo(x) for x in [w, h]] 249 if w != w2 or h != h2: 250 s = pygame.Surface((w2, h2), pygame.SRCALPHA, 32) 251 s.blit(surface, (0, h2 - h)) 252 surface = s 253 254 if monochrome: 255 # pygame doesn't support monochrome, so the fastest way 256 # appears to be using PIL to do the conversion. 257 string = pygame.image.tostring(surface, "RGB") 258 image = Image.fromstring("RGB", surface.get_size(), string).convert("L") 259 string = image.tostring('raw', 'L', 0, -1) 260 self.loadRaw(surface.get_size(), string, GL_LUMINANCE, GL_INTENSITY8) 261 else: 262 if alphaChannel: 263 string = pygame.image.tostring(surface, "RGBA", True) 264 self.loadRaw(surface.get_size(), string, GL_RGBA, 4) 265 else: 266 string = pygame.image.tostring(surface, "RGB", True) 267 self.loadRaw(surface.get_size(), string, GL_RGB, 3) 268 self.size = (w / w2, h / h2)
269
270 - def loadSubsurface(self, surface, position = (0, 0), monochrome = False, alphaChannel = False):
271 """Load the texture from a pygame surface""" 272 273 if monochrome: 274 # pygame doesn't support monochrome, so the fastest way 275 # appears to be using PIL to do the conversion. 276 string = pygame.image.tostring(surface, "RGB") 277 image = Image.fromstring("RGB", surface.get_size(), string).convert("L") 278 string = image.tostring('raw', 'L', 0, -1) 279 self.loadSubRaw(surface.get_size(), position, string, GL_INTENSITY8) 280 else: 281 if alphaChannel: 282 string = pygame.image.tostring(surface, "RGBA", True) 283 self.loadSubRaw(surface.get_size(), position, string, GL_RGBA) 284 else: 285 string = pygame.image.tostring(surface, "RGB", True) 286 self.loadSubRaw(surface.get_size(), position, string, GL_RGB)
287
288 - def loadRaw(self, size, string, format, components):
289 """Load a raw image from the given string. 'format' is a constant such as 290 GL_RGB or GL_RGBA that can be passed to gluBuild2DMipmaps. 291 """ 292 self.pixelSize = size 293 self.size = (1.0, 1.0) 294 self.format = format 295 self.components = components 296 (w, h) = size 297 Texture.bind(self) 298 glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 299 gluBuild2DMipmaps(self.glTarget, components, w, h, format, GL_UNSIGNED_BYTE, string)
300
301 - def loadSubRaw(self, size, position, string, format):
302 Texture.bind(self) 303 glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 304 glTexSubImage2D(self.glTarget, 0, position[0], position[1], size[0], size[1], format, GL_UNSIGNED_BYTE, string)
305
306 - def loadEmpty(self, size, format):
307 self.pixelSize = size 308 self.size = (1.0, 1.0) 309 self.format = format 310 Texture.bind(self) 311 glTexImage2D(GL_TEXTURE_2D, 0, format, size[0], size[1], 0, 312 format, GL_UNSIGNED_BYTE, "\x00" * (size[0] * size[1] * 4))
313
314 - def setDefaults(self):
315 """Set the default OpenGL options for this texture""" 316 self.setRepeat() 317 self.setFilter() 318 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
319
320 - def setRepeat(self, u=GL_REPEAT, v=GL_REPEAT):
321 Texture.bind(self) 322 glTexParameteri(self.glTarget, GL_TEXTURE_WRAP_S, u) 323 glTexParameteri(self.glTarget, GL_TEXTURE_WRAP_T, v)
324
325 - def setFilter(self, min=GL_LINEAR_MIPMAP_LINEAR, mag=GL_LINEAR):
326 Texture.bind(self) 327 glTexParameteri(self.glTarget, GL_TEXTURE_MIN_FILTER, min) 328 glTexParameteri(self.glTarget, GL_TEXTURE_MAG_FILTER, mag)
329
330 - def __del__(self):
331 # Queue this texture to be deleted later 332 try: 333 cleanupQueue.put((glDeleteTextures, [self.texture])) 334 except NameError: 335 pass
336
337 - def bind(self, glTarget = None):
338 """Bind this texture to self.glTarget in the current OpenGL context""" 339 if not glTarget: 340 glTarget = self.glTarget 341 glBindTexture(glTarget, self.texture) 342 glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, self.texEnv)
343 344 # 345 # Texture atlas 346 # 347 TEXTURE_ATLAS_SIZE = 1024 348
349 -class TextureAtlasFullException(Exception):
350 pass
351
352 -class TextureAtlas(object):
353 - def __init__(self, size = TEXTURE_ATLAS_SIZE):
354 self.texture = Texture() 355 self.cursor = (0, 0) 356 self.rowHeight = 0 357 self.surfaceCount = 0 358 self.texture.loadEmpty((size, size), GL_RGBA)
359
360 - def add(self, surface, margin = 0):
361 w, h = surface.get_size() 362 x, y = self.cursor 363 364 w += margin 365 h += margin 366 367 if w > self.texture.pixelSize[0] or h > self.texture.pixelSize[1]: 368 raise ValueError("Surface is too big to fit into atlas.") 369 370 if x + w >= self.texture.pixelSize[0]: 371 x = 0 372 y += self.rowHeight 373 self.rowHeight = 0 374 375 if y + h >= self.texture.pixelSize[1]: 376 Log.debug("Texture atlas %s full after %d surfaces." % (self.texture.pixelSize, self.surfaceCount)) 377 raise TextureAtlasFullException() 378 379 self.texture.loadSubsurface(surface, position = (x, y), alphaChannel = True) 380 381 self.surfaceCount += 1 382 self.rowHeight = max(self.rowHeight, h) 383 self.cursor = (x + w, y + h) 384 385 # Return the coordinates for the uploaded texture patch 386 w -= margin 387 h -= margin 388 return x / float(self.texture.pixelSize[0]), y / float(self.texture.pixelSize[1]), \ 389 (x + w) / float(self.texture.pixelSize[0]), (y + h) / float(self.texture.pixelSize[1])
390
391 - def bind(self):
392 self.texture.bind()
393