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