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