wxpython/opengl easy text to texture class - gltext.py

Description: gltext utilizes wxpython to enable the user to easily display system fonts in an pyopengl environment.
Raw download: gltext.py
Jump to: gltext.py

gltext.py (Raw)

  1 #!/usr/bin/env python
  2 # -*- coding: utf-8
  3 #
  4 #    Provides some text display functions for wx + ogl
  5 #    Copyright (C) 2007 Christian Brugger, Stefan Hacker
  6 #
  7 #    This program is free software; you can redistribute it and/or modify
  8 #    it under the terms of the GNU General Public License as published by
  9 #    the Free Software Foundation; either version 2 of the License, or
 10 #    (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 along
 18 #    with this program; if not, write to the Free Software Foundation, Inc.,
 19 #    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 20 
 21 import wx
 22 from OpenGL.GL import *
 23 
 24 """
 25 Optimize with psyco if possible, this gains us about 50% speed when
 26 creating our textures in trade for about 4MBytes of additional memory usage for
 27 psyco. If you don't like loosing the memory you have to turn the lines following
 28 "enable psyco" into a comment while uncommenting the line after "Disable psyco".
 29 """
 30 #Try to enable psyco
 31 try:
 32     import psyco
 33     psyco_optimized = False
 34 except ImportError:
 35     psyco = None
 36     
 37 #Disable psyco
 38 #psyco = None
 39           
 40 class TextElement(object):
 41     """
 42     A simple class for using system Fonts to display
 43     text in an OpenGL scene
 44     """
 45     def __init__(self,
 46                  text = '',
 47                  font = None,
 48                  foreground = wx.BLACK,
 49                  centered = False):
 50         """
 51         text (String)         - Text
 52         font (wx.Font)        - Font to draw with (None = System default)
 53         foreground (wx.Color) - Color of the text
 54                 or (wx.Bitmap)- Bitmap to overlay the text with
 55         centered (bool)       - Center the text
 56         
 57         Initializes the TextElement
 58         """
 59         # save given variables
 60         self._text        = text
 61         self._lines       = text.split('\n')
 62         self._font        = font
 63         self._foreground  = foreground
 64         self._centered    = centered
 65         
 66         # init own variables
 67         self._owner_cnt   = 0        #refcounter
 68         self._texture     = None     #OpenGL texture ID
 69         self._text_size   = None     #x/y size tuple of the text
 70         self._texture_size= None     #x/y Texture size tuple
 71         
 72         # create Texture
 73         self.createTexture()
 74         
 75 
 76     #---Internal helpers
 77     
 78     def _getUpper2Base(self, value):
 79         """
 80         Returns the lowest value with the power of
 81         2 greater than 'value' (2^n>value)
 82         """
 83         base2 = 1
 84         while base2 < value:
 85             base2 *= 2
 86         return base2
 87         
 88     #---Functions
 89     
 90     def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
 91         """
 92         position (wx.Point)    - x/y Position to draw in scene
 93         scale    (float)       - Scale
 94         rotation (int)         - Rotation in degree
 95         
 96         Draws the text to the scene
 97         """
 98         #Enable necessary functions
 99         glColor(1,1,1,1)
100         glEnable(GL_TEXTURE_2D)     
101         glEnable(GL_ALPHA_TEST)       #Enable alpha test
102         glAlphaFunc(GL_GREATER, 0)
103         glEnable(GL_BLEND)            #Enable blending
104         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
105         #Bind texture
106         glBindTexture(GL_TEXTURE_2D, self._texture)
107         
108         ow, oh = self._text_size
109         w , h  = self._texture_size
110         #Perform transformations
111         glPushMatrix()
112         glTranslated(position.x, position.y, 0)
113         glRotate(-rotation, 0, 0, 1)
114         glScaled(scale, scale, scale)
115         if self._centered:
116             glTranslate(-w/2, -oh/2, 0)
117         #Draw vertices
118         glBegin(GL_QUADS)
119         glTexCoord2f(0,0); glVertex2f(0,0)
120         glTexCoord2f(0,1); glVertex2f(0,h)
121         glTexCoord2f(1,1); glVertex2f(w,h)
122         glTexCoord2f(1,0); glVertex2f(w,0)
123         glEnd()
124         glPopMatrix()
125         
126         #Disable features
127         glDisable(GL_BLEND)
128         glDisable(GL_ALPHA_TEST)
129         glDisable(GL_TEXTURE_2D)
130         
131     def createTexture(self):
132         """
133         Creates a texture from the settings saved in TextElement, to be able to use normal
134         system fonts conviently a wx.MemoryDC is used to draw on a wx.Bitmap. As wxwidgets 
135         device contexts don't support alpha at all it is necessary to apply a little hack
136         to preserve antialiasing without sticking to a fixed background color:
137         
138         We draw the bmp in b/w mode so we can use its data as a alpha channel for a solid
139         color bitmap which after GL_ALPHA_TEST and GL_BLEND will show a nicely antialiased
140         text on any surface.
141         
142         To access the raw pixel data the bmp gets converted to a wx.Image. Now we just have
143         to merge our foreground color with the alpha data we just created and push it all
144         into a OpenGL texture and we are DONE *inhalesdelpy*
145         
146         DRAWBACK of the whole conversion thing is a really long time for creating the
147         texture. If you see any optimizations that could save time PLEASE CREATE A PATCH!!!
148         """
149         # get a memory dc
150         dc = wx.MemoryDC()
151         
152         # set our font
153         dc.SetFont(self._font)
154         
155         # Approximate extend to next power of 2 and create our bitmap
156         # REMARK: You wouldn't believe how much fucking speed this little
157         #         sucker gains compared to sizes not of the power of 2. It's like
158         #         500ms --> 0.5ms (on my ATI-GPU powered Notebook). On Sams nvidia
159         #         machine there don't seem to occur any losses...bad drivers?
160         ow, oh = dc.GetMultiLineTextExtent(self._text)[:2]
161         w, h = self._getUpper2Base(ow), self._getUpper2Base(oh)
162         
163         self._text_size = wx.Size(ow,oh)
164         self._texture_size = wx.Size(w,h)
165         bmp = wx.EmptyBitmap(w,h)
166         
167         
168         #Draw in b/w mode to bmp so we can use it as alpha channel
169         dc.SelectObject(bmp)
170         dc.SetBackground(wx.BLACK_BRUSH)
171         dc.Clear()
172         dc.SetTextForeground(wx.WHITE)
173         x,y = 0,0
174         centered = self.centered
175         for line in self._lines:
176             if not line: line = ' '
177             tw, th = dc.GetTextExtent(line)
178             if centered:
179                 x = int(round((w-tw)/2))
180             dc.DrawText(line, x, y)
181             x = 0
182             y += th
183         #Release the dc
184         dc.SelectObject(wx.NullBitmap)
185         del dc
186 
187         #Generate a correct RGBA data string from our bmp 
188         """
189         NOTE: You could also use wx.AlphaPixelData to access the pixel data
190         in 'bmp' directly, but the iterator given by it is much slower than
191         first converting to an image and using wx.Image.GetData().
192         """
193         img   = wx.ImageFromBitmap(bmp)
194         alpha = img.GetData()
195         
196         if isinstance(self._foreground, wx.Color):  
197             """
198             If we have a static color...  
199             """    
200             r,g,b = self._foreground.Get()
201             color = "%c%c%c" % (chr(r), chr(g), chr(b))
202             
203             data = ''
204             for i in xrange(0, len(alpha)-1, 3):
205                 data += color + alpha[i]
206         
207         elif isinstance(self._foreground, wx.Bitmap):
208             """
209             If we have a bitmap...
210             """
211             bg_img    = wx.ImageFromBitmap(self._foreground)
212             bg        = bg_img.GetData()
213             bg_width  = self._foreground.GetWidth()
214             bg_height = self._foreground.GetHeight()
215             
216             data = ''
217 
218             for y in xrange(0, h):
219                 for x in xrange(0, w):
220                     if (y > (bg_height-1)) or (x > (bg_width-1)):                       
221                         color = "%c%c%c" % (chr(0),chr(0),chr(0))
222                     else:
223                         pos = (x+y*bg_width) * 3
224                         color = bg[pos:pos+3]
225                     data += color + alpha[(x+y*w)*3]
226 
227 
228         # now convert it to ogl texture
229         self._texture = glGenTextures(1)
230         glBindTexture(GL_TEXTURE_2D, self._texture)
231         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
232         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
233         
234         glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
235         glPixelStorei(GL_UNPACK_ALIGNMENT, 2)
236         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
237     
238     def deleteTexture(self):
239         """
240         Deletes the OpenGL texture object
241         """
242         if self._texture:
243             if glIsTexture(self._texture):
244                 glDeleteTextures(self._texture)
245             else:
246                 self._texture = None
247 
248     def bind(self):
249         """
250         Increase refcount
251         """
252         self._owner_cnt += 1
253     
254     def release(self):
255         """
256         Decrease refcount
257         """
258         self._owner_cnt -= 1
259         
260     def isBound(self):
261         """
262         Return refcount
263         """
264         return self._owner_cnt
265         
266     def __del__(self):
267         """
268         Destructor
269         """
270         self.deleteTexture()
271 
272     #---Getters/Setters
273     
274     def getText(self): return self._text
275     def getFont(self): return self._font
276     def getForeground(self): return self._foreground
277     def getCentered(self): return self._centered
278     def getTexture(self): return self._texture
279     def getTexture_size(self): return self._texture_size
280 
281     def getOwner_cnt(self): return self._owner_cnt
282     def setOwner_cnt(self, value):
283         self._owner_cnt = value
284         
285     #---Properties
286     
287     text         = property(getText, None, None, "Text of the object")
288     font         = property(getFont, None, None, "Font of the object")
289     foreground   = property(getForeground, None, None, "Color of the text")
290     centered     = property(getCentered, None, None, "Is text centered")
291     owner_cnt    = property(getOwner_cnt, setOwner_cnt, None, "Owner count")
292     texture      = property(getTexture, None, None, "Used texture")
293     texture_size = property(getTexture_size, None, None, "Size of the used texture")       
294                
295 
296 class Text(object):
297     """
298     A simple class for using System Fonts to display text in
299     an OpenGL scene. The Text adds a global Cache of already
300     created text elements to TextElement's base functionality
301     so you can save some memory and increase speed
302     """
303     _texts         = []    #Global cache for TextElements
304     
305     def __init__(self,
306                  text = 'Text',
307                  font = None,
308                  font_size = 8,
309                  foreground = wx.BLACK,
310                  centered = False):
311         """
312             text (string)           - displayed text
313             font (wx.Font)          - if None, system default font will be used with font_size
314             font_size (int)         - font size in points
315             foreground (wx.Color)   - Color of the text
316                     or (wx.Bitmap)  - Bitmap to overlay the text with
317             centered (bool)         - should the text drawn centered towards position?
318             
319             Initializes the text object
320         """
321         #Init/save variables
322         self._aloc_text = None
323         self._text      = text
324         self._font_size = font_size
325         self._foreground= foreground
326         self._centered  = centered
327         
328         #Check if we are offered a font
329         if not font:
330             #if not use the system default
331             self._font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
332         else: 
333             #save it
334             self._font = font
335             
336         #Bind us to our texture
337         self._initText()
338 
339     #---Internal helpers
340 
341     def _initText(self):
342         """
343         Initializes/Reinitializes the Text object by binding it
344         to a TextElement suitable for its current settings
345         """
346         #Check if we already bound to a texture
347         if self._aloc_text:
348             #if so release it
349             self._aloc_text.release()
350             if not self._aloc_text.isBound():
351                 self._texts.remove(self._aloc_text)
352             self._aloc_text = None
353             
354         #Adjust our font
355         self._font.SetPointSize(self._font_size)
356         
357         #Search for existing element in our global buffer
358         for element in self._texts:
359             if element.text == self._text and\
360               element.font == self._font and\
361               element.foreground == self._foreground and\
362               element.centered == self._centered:
363                 # We already exist in global buffer ;-)
364                 element.bind()
365                 self._aloc_text = element
366                 break
367         
368         if not self._aloc_text:
369             # We are not in the global buffer, let's create ourselves
370             aloc_text = self._aloc_text = TextElement(self._text,
371                                                        self._font,
372                                                        self._foreground,
373                                                        self._centered)
374             aloc_text.bind()
375             self._texts.append(aloc_text)
376     
377     def __del__(self):
378         """
379         Destructor
380         """
381         aloc_text = self._aloc_text
382         aloc_text.release()
383         if not aloc_text.isBound():
384             self._texts.remove(aloc_text)
385     
386     #---Functions
387         
388     def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
389         """
390         position (wx.Point)    - x/y Position to draw in scene
391         scale    (float)       - Scale
392         rotation (int)         - Rotation in degree
393         
394         Draws the text to the scene
395         """
396         
397         self._aloc_text.draw_text(position, scale, rotation)
398 
399     #---Setter/Getter
400     
401     def getText(self): return self._text
402     def setText(self, value, reinit = True):
403         """
404         value (bool)    - New Text
405         reinit (bool)   - Create a new texture
406         
407         Sets a new text
408         """
409         self._text = value
410         if reinit:
411             self._initText()
412 
413     def getFont(self): return self._font
414     def setFont(self, value, reinit = True):
415         """
416         value (bool)    - New Font
417         reinit (bool)   - Create a new texture
418         
419         Sets a new font
420         """
421         self._font = value
422         if reinit:
423             self._initText()
424 
425     def getFont_size(self): return self._font_size
426     def setFont_size(self, value, reinit = True):
427         """
428         value (bool)    - New font size
429         reinit (bool)   - Create a new texture
430         
431         Sets a new font size
432         """
433         self._font_size = value
434         if reinit:
435             self._initText()
436 
437     def getForeground(self): return self._foreground
438     def setForeground(self, value, reinit = True):
439         """
440         value (bool)    - New centered value
441         reinit (bool)   - Create a new texture
442         
443         Sets a new value for 'centered'
444         """
445         self._foreground = value
446         if reinit:
447             self._initText()
448 
449     def getCentered(self): return self._centered
450     def setCentered(self, value, reinit = True):
451         """
452         value (bool)    - New centered value
453         reinit (bool)   - Create a new texture
454         
455         Sets a new value for 'centered'
456         """
457         self._centered = value
458         if reinit:
459             self._initText()
460     
461     def getTexture_size(self):
462         """
463         Returns a texture size tuple
464         """
465         return self._aloc_text.texture_size
466     
467     def getTextElement(self):
468         """
469         Returns the text element bound to the Text class
470         """
471         return self._aloc_text
472     
473     def getTexture(self):
474         """
475         Returns the texture of the bound TextElement
476         """
477         return self._aloc_text.texture
478 
479     
480     #---Properties
481     
482     text         = property(getText, setText, None, "Text of the object")
483     font         = property(getFont, setFont, None, "Font of the object")
484     font_size    = property(getFont_size, setFont_size, None, "Font size")
485     foreground   = property(getForeground, setForeground, None, "Color/Overlay bitmap of the text")
486     centered     = property(getCentered, setCentered, None, "Display the text centered")
487     texture_size = property(getTexture_size, None, None, "Size of the used texture")
488     texture      = property(getTexture, None, None, "Texture of bound TextElement")
489     text_element = property(getTextElement,None , None, "TextElement bound to this class")
490 
491 #Optimize critical functions
492 if psyco and not psyco_optimized:
493     psyco.bind(TextElement.createTexture)
494     psyco_optimized = True

Created on Wed Aug 29 14:52:02 2007 using pygments.