This repository has been archived by the owner on Nov 13, 2023. It is now read-only.
/
texture.py
444 lines (374 loc) · 14.7 KB
/
texture.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
'''
Texture: abstraction to handle GL texture, and region
'''
__all__ = ('Texture', 'TextureRegion')
import os
import re
from array import array
from pymt import pymt_logger
import OpenGL
from OpenGL.GL import GL_RGBA, GL_UNSIGNED_BYTE, GL_TEXTURE_MIN_FILTER, \
GL_TEXTURE_MAG_FILTER, GL_TEXTURE_WRAP_T, GL_TEXTURE_WRAP_S, \
GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_RECTANGLE_ARB, \
GL_CLAMP_TO_EDGE, GL_LINEAR_MIPMAP_LINEAR, GL_GENERATE_MIPMAP, \
GL_TRUE, GL_LINEAR, GL_UNPACK_ALIGNMENT, GL_BGR, GL_BGRA, GL_RGB, \
glEnable, glDisable, glBindTexture, glTexParameteri, glTexImage2D, \
glTexSubImage2D, glFlush, glGenTextures, glDeleteTextures, \
GLubyte, glPixelStorei, GL_LUMINANCE
from OpenGL.GL.NV.texture_rectangle import glInitTextureRectangleNV
from OpenGL.GL.ARB.texture_rectangle import glInitTextureRectangleARB
from OpenGL.extensions import hasGLExtension
# for a specific bug in 3.0.0, about deletion of framebuffer.
# same hack as FBO :(
OpenGLversion = tuple(int(re.match('^(\d+)', i).groups()[0]) \
for i in OpenGL.__version__.split('.'))
if OpenGLversion < (3, 0, 1):
try:
import numpy
have_numpy = True
except Exception:
have_numpy = False
def _nearest_pow2(v):
# From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
# Credit: Sean Anderson
v -= 1
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return v + 1
def _is_pow2(v):
# http://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2
return (v & (v - 1)) == 0
#
# Releasing texture through GC is problematic
# GC can happen in a middle of glBegin/glEnd
# So, to prevent that, call the _texture_release
# at flip time.
_texture_release_list = []
def _texture_release(*largs):
global _texture_release_list
for texture_id in _texture_release_list:
# try/except are here to prevent an error like this :
# Exception TypeError: "'NoneType' object is not callable"
# in <bound method Texture.__del__ of <pymt.texture.Texture
# object at 0x3a1acb0>> ignored
#
# It occured only when leaving the application.
# So, maybe numpy or pyopengl is unloaded, and have weird things happen.
#
try:
if OpenGLversion < (3, 0, 1) and have_numpy:
glDeleteTextures(numpy.array(texture_id))
else:
glDeleteTextures(texture_id)
except:
pass
# let the list to 0
_texture_release_list = []
class Texture(object):
'''Handle a OpenGL texture. This class can be used to create simple texture
or complex texture based on ImageData.'''
__slots__ = ('tex_coords', '_width', '_height', '_target', '_id', '_mipmap',
'_gl_wrap', '_gl_min_filter', '_gl_mag_filter', '_rectangle')
_has_bgr = None
_has_bgr_tested = False
_has_texture_nv = None
_has_texture_arb = None
def __init__(self, width, height, target, texid, mipmap=False, rectangle=False):
self.tex_coords = (0., 0., 1., 0., 1., 1., 0., 1.)
self._width = width
self._height = height
self._target = target
self._id = texid
self._mipmap = mipmap
self._gl_wrap = None
self._gl_min_filter = None
self._gl_mag_filter = None
self._rectangle = rectangle
def __del__(self):
# Add texture deletion outside GC call.
# This case happen if some texture have been not deleted
# before application exit...
if _texture_release_list is not None:
_texture_release_list.append(self.id)
@property
def mipmap(self):
'''Return True if the texture have mipmap enabled (readonly)'''
return self._mipmap
@property
def rectangle(self):
'''Return True if the texture is a rectangle texture (readonly)'''
return self._rectangle
@property
def id(self):
'''Return the OpenGL ID of the texture (readonly)'''
return self._id
@property
def target(self):
'''Return the OpenGL target of the texture (readonly)'''
return self._target
@property
def width(self):
'''Return the width of the texture (readonly)'''
return self._width
@property
def height(self):
'''Return the height of the texture (readonly)'''
return self._height
def flip_vertical(self):
'''Flip tex_coords for vertical displaying'''
a, b, c, d, e, f, g, h = self.tex_coords
self.tex_coords = (g, h, e, f, c, d, a, b)
def get_region(self, x, y, width, height):
'''Return a part of the texture, from (x,y) with (width,height)
dimensions'''
return TextureRegion(x, y, width, height, self)
def bind(self):
'''Bind the texture to current opengl state'''
glBindTexture(self.target, self.id)
def enable(self):
'''Do the appropriate glEnable()'''
glEnable(self.target)
def disable(self):
'''Do the appropriate glDisable()'''
glDisable(self.target)
def _get_min_filter(self):
return self._gl_min_filter
def _set_min_filter(self, x):
if x == self._gl_min_filter:
return
self.bind()
glTexParameteri(self.target, GL_TEXTURE_MIN_FILTER, x)
self._gl_min_filter = x
min_filter = property(_get_min_filter, _set_min_filter,
doc='''Get/set the GL_TEXTURE_MIN_FILTER property''')
def _get_mag_filter(self):
return self._gl_mag_filter
def _set_mag_filter(self, x):
if x == self._gl_mag_filter:
return
self.bind()
glTexParameteri(self.target, GL_TEXTURE_MAG_FILTER, x)
self._gl_mag_filter = x
mag_filter = property(_get_mag_filter, _set_mag_filter,
doc='''Get/set the GL_TEXTURE_MAG_FILTER property''')
def _get_wrap(self):
return self._gl_wrap
def _set_wrap(self, wrap):
if wrap == self._gl_wrap:
return
self.bind()
glTexParameteri(self.target, GL_TEXTURE_WRAP_S, wrap)
glTexParameteri(self.target, GL_TEXTURE_WRAP_T, wrap)
wrap = property(_get_wrap, _set_wrap,
doc='''Get/set the GL_TEXTURE_WRAP_S,T property''')
@staticmethod
def create(width, height, format=GL_RGBA, rectangle=False, mipmap=False):
'''Create a texture based on size.'''
target = GL_TEXTURE_2D
if rectangle:
if _is_pow2(width) and _is_pow2(height):
rectangle = False
else:
rectangle = False
try:
if Texture._has_texture_nv is None:
Texture._has_texture_nv = glInitTextureRectangleNV()
if Texture._has_texture_nv:
target = GL_TEXTURE_RECTANGLE_NV
rectangle = True
except Exception:
pass
try:
if Texture._has_texture_arb is None:
Texture._has_texture_arb = glInitTextureRectangleARB()
if not rectangle and Texture._has_texture_arb:
target = GL_TEXTURE_RECTANGLE_ARB
rectangle = True
except Exception:
pass
if not rectangle:
pymt_logger.debug(
'Texture: Missing support for rectangular texture')
else:
# Can't do mipmap with rectangle texture
mipmap = False
if rectangle:
texture_width = width
texture_height = height
else:
texture_width = _nearest_pow2(width)
texture_height = _nearest_pow2(height)
texid = glGenTextures(1)
texture = Texture(texture_width, texture_height, target, texid,
mipmap=mipmap)
texture.bind()
texture.wrap = GL_CLAMP_TO_EDGE
if mipmap:
texture.min_filter = GL_LINEAR_MIPMAP_LINEAR
#texture.mag_filter = GL_LINEAR_MIPMAP_LINEAR
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE)
else:
texture.min_filter = GL_LINEAR
texture.mag_filter = GL_LINEAR
if not Texture.is_gl_format_supported(format):
format = Texture.convert_gl_format(format)
data = (GLubyte * texture_width * texture_height *
Texture.gl_format_size(format))()
glTexImage2D(target, 0, format, texture_width, texture_height, 0,
format, GL_UNSIGNED_BYTE, data)
if rectangle:
texture.tex_coords = \
(0., 0., width, 0., width, height, 0., height)
glFlush()
if texture_width == width and texture_height == height:
return texture
return texture.get_region(0, 0, width, height)
@staticmethod
def create_from_data(im, rectangle=True, mipmap=False):
'''Create a texture from an ImageData class'''
format = Texture.mode_to_gl_format(im.mode)
texture = Texture.create(im.width, im.height,
format, rectangle=rectangle,
mipmap=mipmap)
if texture is None:
return None
texture.blit_data(im)
return texture
def blit_data(self, im, pos=(0, 0)):
'''Replace a whole texture with a image data'''
self.blit_buffer(im.data, size=(im.width, im.height),
mode=im.mode, pos=pos)
def blit_buffer(self, buffer, size=None, mode='RGB', format=None,
pos=(0, 0), buffertype=GL_UNSIGNED_BYTE):
'''Blit a buffer into a texture.
:Parameters:
`buffer` : str
Image data
`size` : tuple, default to texture size
Size of the image (width, height)
`mode` : str, default to 'RGB'
Image mode, can be one of RGB, RGBA, BGR, BGRA
`format` : glconst, default to None
if format is passed, it will be used instead of mode
`pos` : tuple, default to (0, 0)
Position to blit in the texture
`buffertype` : glglconst, default to GL_UNSIGNED_BYTE
Type of the data buffer
'''
if size is None:
size = self.size
if format is None:
format = self.mode_to_gl_format(mode)
target = self.target
glBindTexture(target, self.id)
glEnable(target)
# activate 1 alignement, of window failed on updating weird size
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
# need conversion ?
pdata, format = self._convert_buffer(buffer, format)
# transfer the new part of texture
glTexSubImage2D(target, 0, pos[0], pos[1],
size[0], size[1], format,
buffertype, pdata)
glFlush()
glDisable(target)
@staticmethod
def has_bgr():
if not Texture._has_bgr_tested:
pymt_logger.warning('Texture: BGR/BGRA format is not supported by'
'your graphic card')
pymt_logger.warning('Texture: Software conversion will be done to'
'RGB/RGBA')
Texture._has_bgr = hasGLExtension('GL_EXT_bgra')
Texture._has_bgr_tested = True
return Texture._has_bgr
@staticmethod
def is_gl_format_supported(format):
if format in (GL_BGR, GL_BGRA):
return not Texture.has_bgr()
return True
@staticmethod
def convert_gl_format(format):
if format == GL_BGR:
return GL_RGB
elif format == GL_BGRA:
return GL_RGBA
return format
def _convert_buffer(self, data, format):
# check if format is supported by user
ret_format = format
ret_buffer = data
# BGR / BGRA conversion not supported by hardware ?
if not Texture.is_gl_format_supported(format):
if format == GL_BGR:
ret_format = GL_RGB
a = array('b', data)
a[0::3], a[2::3] = a[2::3], a[0::3]
ret_buffer = a.tostring()
elif format == GL_BGRA:
ret_format = GL_RGBA
a = array('b', data)
a[0::4], a[2::4] = a[2::4], a[0::4]
ret_buffer = a.tostring()
else:
pymt_logger.critical('Texture: non implemented'
'%s texture conversion' % str(format))
raise Exception('Unimplemented texture conversion for %s' %
str(format))
return ret_buffer, ret_format
@property
def size(self):
return (self.width, self.height)
@staticmethod
def mode_to_gl_format(format):
if format == 'RGBA':
return GL_RGBA
elif format == 'BGRA':
return GL_BGRA
elif format == 'BGR':
return GL_BGR
else:
return GL_RGB
@staticmethod
def gl_format_size(format):
if format in (GL_RGB, GL_BGR):
return 3
elif format in (GL_RGBA, GL_BGRA):
return 4
elif format in (GL_LUMINANCE, ):
return 1
raise Exception('Unsupported format size <%s>' % str(format))
def __str__(self):
return '<Texture size=(%d, %d)>' % self.size
class TextureRegion(Texture):
'''Handle a region of a Texture class. Useful for non power-of-2
texture handling.'''
__slots__ = ('x', 'y', 'owner')
def __init__(self, x, y, width, height, origin):
super(TextureRegion, self).__init__(
width, height, origin.target, origin.id)
self.x = x
self.y = y
self.owner = origin
# recalculate texture coordinate
origin_u1 = origin.tex_coords[0]
origin_v1 = origin.tex_coords[1]
origin_u2 = origin.tex_coords[2]
origin_v2 = origin.tex_coords[5]
scale_u = origin_u2 - origin_u1
scale_v = origin_v2 - origin_v1
u1 = x / float(origin.width) * scale_u + origin_u1
v1 = y / float(origin.height) * scale_v + origin_v1
u2 = (x + width) / float(origin.width) * scale_u + origin_u1
v2 = (y + height) / float(origin.height) * scale_v + origin_v1
self.tex_coords = (u1, v1, u2, v1, u2, v2, u1, v2)
def __del__(self):
# don't use self of owner !
pass
if 'PYMT_DOC' not in os.environ:
from pymt.clock import getClock
# install tick to release texture every 200ms
getClock().schedule_interval(_texture_release, 0.2)