source: trunk/gltext.py @ 990

Last change on this file since 990 was 990, checked in by toby, 10 years ago

mac fix for gltext; update docs

File size: 17.1 KB
Line 
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
21import wx
22from OpenGL.GL import *
23
24"""
25Optimize with psyco if possible, this gains us about 50% speed when
26creating our textures in trade for about 4MBytes of additional memory usage for
27psyco. 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
38psyco = None
39         
40class 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.WHITE,
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 and assign a temporary bitmap
150        dc = wx.MemoryDC()
151        dc.SelectObject(wx.EmptyBitmap(100, 100))
152       
153        # set our font
154        dc.SetFont(self._font)
155       
156        # Approximate extend to next power of 2 and create our bitmap
157        # REMARK: You wouldn't believe how much fucking speed this little
158        #         sucker gains compared to sizes not of the power of 2. It's like
159        #         500ms --> 0.5ms (on my ATI-GPU powered Notebook). On Sams nvidia
160        #         machine there don't seem to occur any losses...bad drivers?
161        ow, oh = dc.GetMultiLineTextExtent(self._text)[:2]
162        w, h = self._getUpper2Base(ow), self._getUpper2Base(oh)
163       
164        self._text_size = wx.Size(ow,oh)
165        self._texture_size = wx.Size(w,h)
166        bmp = wx.EmptyBitmap(w,h)
167       
168       
169        #Draw in b/w mode to bmp so we can use it as alpha channel
170        dc.SelectObject(bmp)
171        dc.SetBackground(wx.BLACK_BRUSH)
172        dc.Clear()
173        dc.SetTextForeground(wx.WHITE)
174        x,y = 0,0
175        centered = self.centered
176        for line in self._lines:
177            if not line: line = ' '
178            tw, th = dc.GetTextExtent(line)
179            if centered:
180                x = int(round((w-tw)/2))
181            dc.DrawText(line, x, y)
182            x = 0
183            y += th
184        #Release the dc
185        dc.SelectObject(wx.NullBitmap)
186        del dc
187
188        #Generate a correct RGBA data string from our bmp
189        """
190        NOTE: You could also use wx.AlphaPixelData to access the pixel data
191        in 'bmp' directly, but the iterator given by it is much slower than
192        first converting to an image and using wx.Image.GetData().
193        """
194        img   = wx.ImageFromBitmap(bmp)
195        alpha = img.GetData()
196       
197        if isinstance(self._foreground, wx.Color): 
198            """
199            If we have a static color... 
200            """   
201            r,g,b = self._foreground.Get()
202            color = "%c%c%c" % (chr(r), chr(g), chr(b))
203           
204            data = ''
205            for i in xrange(0, len(alpha)-1, 3):
206                data += color + alpha[i]
207       
208        elif isinstance(self._foreground, wx.Bitmap):
209            """
210            If we have a bitmap...
211            """
212            bg_img    = wx.ImageFromBitmap(self._foreground)
213            bg        = bg_img.GetData()
214            bg_width  = self._foreground.GetWidth()
215            bg_height = self._foreground.GetHeight()
216           
217            data = ''
218
219            for y in xrange(0, h):
220                for x in xrange(0, w):
221                    if (y > (bg_height-1)) or (x > (bg_width-1)):                       
222                        color = "%c%c%c" % (chr(0),chr(0),chr(0))
223                    else:
224                        pos = (x+y*bg_width) * 3
225                        color = bg[pos:pos+3]
226                    data += color + alpha[(x+y*w)*3]
227
228
229        # now convert it to ogl texture
230        self._texture = glGenTextures(1)
231        glBindTexture(GL_TEXTURE_2D, self._texture)
232        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
233        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
234       
235        glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
236        glPixelStorei(GL_UNPACK_ALIGNMENT, 2)
237        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
238   
239    def deleteTexture(self):
240        """
241        Deletes the OpenGL texture object
242        """
243        if self._texture:
244            if glIsTexture(self._texture):
245                glDeleteTextures(self._texture)
246            else:
247                self._texture = None
248
249    def bind(self):
250        """
251        Increase refcount
252        """
253        self._owner_cnt += 1
254   
255    def release(self):
256        """
257        Decrease refcount
258        """
259        self._owner_cnt -= 1
260       
261    def isBound(self):
262        """
263        Return refcount
264        """
265        return self._owner_cnt
266       
267    def __del__(self):
268        """
269        Destructor
270        """
271        self.deleteTexture()
272
273    #---Getters/Setters
274   
275    def getText(self): return self._text
276    def getFont(self): return self._font
277    def getForeground(self): return self._foreground
278    def getCentered(self): return self._centered
279    def getTexture(self): return self._texture
280    def getTexture_size(self): return self._texture_size
281
282    def getOwner_cnt(self): return self._owner_cnt
283    def setOwner_cnt(self, value):
284        self._owner_cnt = value
285       
286    #---Properties
287   
288    text         = property(getText, None, None, "Text of the object")
289    font         = property(getFont, None, None, "Font of the object")
290    foreground   = property(getForeground, None, None, "Color of the text")
291    centered     = property(getCentered, None, None, "Is text centered")
292    owner_cnt    = property(getOwner_cnt, setOwner_cnt, None, "Owner count")
293    texture      = property(getTexture, None, None, "Used texture")
294    texture_size = property(getTexture_size, None, None, "Size of the used texture")       
295               
296
297class Text(object):
298    """
299    A simple class for using System Fonts to display text in
300    an OpenGL scene. The Text adds a global Cache of already
301    created text elements to TextElement's base functionality
302    so you can save some memory and increase speed
303    """
304    _texts         = []    #Global cache for TextElements
305   
306    def __init__(self,
307                 text = 'Text',
308                 font = None,
309                 font_size = 8,
310                 foreground = wx.WHITE,
311                 centered = False):
312        """
313            text (string)           - displayed text
314            font (wx.Font)          - if None, system default font will be used with font_size
315            font_size (int)         - font size in points
316            foreground (wx.Color)   - Color of the text
317                    or (wx.Bitmap)  - Bitmap to overlay the text with
318            centered (bool)         - should the text drawn centered towards position?
319           
320            Initializes the text object
321        """
322        #Init/save variables
323        self._aloc_text = None
324        self._text      = text
325        self._font_size = font_size
326        self._foreground= foreground
327        self._centered  = centered
328       
329        #Check if we are offered a font
330        if not font:
331            #if not use the system default
332            self._font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
333        else: 
334            #save it
335            self._font = font
336           
337        #Bind us to our texture
338        self._initText()
339
340    #---Internal helpers
341
342    def _initText(self):
343        """
344        Initializes/Reinitializes the Text object by binding it
345        to a TextElement suitable for its current settings
346        """
347        #Check if we already bound to a texture
348        if self._aloc_text:
349            #if so release it
350            self._aloc_text.release()
351            if not self._aloc_text.isBound():
352                self._texts.remove(self._aloc_text)
353            self._aloc_text = None
354           
355        #Adjust our font
356        self._font.SetPointSize(self._font_size)
357       
358        #Search for existing element in our global buffer
359        for element in self._texts:
360            if element.text == self._text and\
361              element.font == self._font and\
362              element.foreground == self._foreground and\
363              element.centered == self._centered:
364                # We already exist in global buffer ;-)
365                element.bind()
366                self._aloc_text = element
367                break
368       
369        if not self._aloc_text:
370            # We are not in the global buffer, let's create ourselves
371            aloc_text = self._aloc_text = TextElement(self._text,
372                                                       self._font,
373                                                       self._foreground,
374                                                       self._centered)
375            aloc_text.bind()
376            self._texts.append(aloc_text)
377   
378    def __del__(self):
379        """
380        Destructor
381        """
382        aloc_text = self._aloc_text
383        aloc_text.release()
384        if not aloc_text.isBound():
385            self._texts.remove(aloc_text)
386   
387    #---Functions
388       
389    def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
390        """
391        position (wx.Point)    - x/y Position to draw in scene
392        scale    (float)       - Scale
393        rotation (int)         - Rotation in degree
394       
395        Draws the text to the scene
396        """
397       
398        self._aloc_text.draw_text(position, scale, rotation)
399
400    #---Setter/Getter
401   
402    def getText(self): return self._text
403    def setText(self, value, reinit = True):
404        """
405        value (bool)    - New Text
406        reinit (bool)   - Create a new texture
407       
408        Sets a new text
409        """
410        self._text = value
411        if reinit:
412            self._initText()
413
414    def getFont(self): return self._font
415    def setFont(self, value, reinit = True):
416        """
417        value (bool)    - New Font
418        reinit (bool)   - Create a new texture
419       
420        Sets a new font
421        """
422        self._font = value
423        if reinit:
424            self._initText()
425
426    def getFont_size(self): return self._font_size
427    def setFont_size(self, value, reinit = True):
428        """
429        value (bool)    - New font size
430        reinit (bool)   - Create a new texture
431       
432        Sets a new font size
433        """
434        self._font_size = value
435        if reinit:
436            self._initText()
437
438    def getForeground(self): return self._foreground
439    def setForeground(self, value, reinit = True):
440        """
441        value (bool)    - New centered value
442        reinit (bool)   - Create a new texture
443       
444        Sets a new value for 'centered'
445        """
446        self._foreground = value
447        if reinit:
448            self._initText()
449
450    def getCentered(self): return self._centered
451    def setCentered(self, value, reinit = True):
452        """
453        value (bool)    - New centered value
454        reinit (bool)   - Create a new texture
455       
456        Sets a new value for 'centered'
457        """
458        self._centered = value
459        if reinit:
460            self._initText()
461   
462    def getTexture_size(self):
463        """
464        Returns a texture size tuple
465        """
466        return self._aloc_text.texture_size
467   
468    def getTextElement(self):
469        """
470        Returns the text element bound to the Text class
471        """
472        return self._aloc_text
473   
474    def getTexture(self):
475        """
476        Returns the texture of the bound TextElement
477        """
478        return self._aloc_text.texture
479
480   
481    #---Properties
482   
483    text         = property(getText, setText, None, "Text of the object")
484    font         = property(getFont, setFont, None, "Font of the object")
485    font_size    = property(getFont_size, setFont_size, None, "Font size")
486    foreground   = property(getForeground, setForeground, None, "Color/Overlay bitmap of the text")
487    centered     = property(getCentered, setCentered, None, "Display the text centered")
488    texture_size = property(getTexture_size, None, None, "Size of the used texture")
489    texture      = property(getTexture, None, None, "Texture of bound TextElement")
490    text_element = property(getTextElement,None , None, "TextElement bound to this class")
491
492#Optimize critical functions
493if psyco and not psyco_optimized:
494    psyco.bind(TextElement.createTexture)
495    psyco_optimized = True
Note: See TracBrowser for help on using the repository browser.