source: trunk/gltext.py @ 2998

Last change on this file since 2998 was 2889, checked in by vondreele, 6 years ago

a fix to gltext - some bug in pyopengl 3.1.1a1 gives errors but 3.0.2a5 does not when text is displayed on structures. Not a good fix as it leaves a small memory leak!

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