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

Created on Tue Jul 15 15:39:58 2014 using pygments.