pyrender-maintained 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyrender/font.py ADDED
@@ -0,0 +1,272 @@
1
+ """Font texture loader and processor.
2
+
3
+ Author: Matthew Matl
4
+ """
5
+ import freetype
6
+ import numpy as np
7
+ import os
8
+
9
+ import OpenGL
10
+ from OpenGL.GL import *
11
+
12
+ from .constants import TextAlign, FLOAT_SZ
13
+ from .texture import Texture
14
+ from .sampler import Sampler
15
+
16
+
17
+ class FontCache(object):
18
+ """A cache for fonts.
19
+ """
20
+
21
+ def __init__(self, font_dir=None):
22
+ self._font_cache = {}
23
+ self.font_dir = font_dir
24
+ if self.font_dir is None:
25
+ base_dir, _ = os.path.split(os.path.realpath(__file__))
26
+ self.font_dir = os.path.join(base_dir, 'fonts')
27
+
28
+ def get_font(self, font_name, font_pt):
29
+ # If it's a file, load it directly, else, try to load from font dir.
30
+ if os.path.isfile(font_name):
31
+ font_filename = font_name
32
+ _, font_name = os.path.split(font_name)
33
+ font_name, _ = os.path.split(font_name)
34
+ else:
35
+ font_filename = os.path.join(self.font_dir, font_name) + '.ttf'
36
+
37
+ cid = OpenGL.contextdata.getContext()
38
+ key = (cid, font_name, int(font_pt))
39
+
40
+ if key not in self._font_cache:
41
+ self._font_cache[key] = Font(font_filename, font_pt)
42
+ return self._font_cache[key]
43
+
44
+ def clear(self):
45
+ for key in self._font_cache:
46
+ self._font_cache[key].delete()
47
+ self._font_cache = {}
48
+
49
+
50
+ class Character(object):
51
+ """A single character, with its texture and attributes.
52
+ """
53
+
54
+ def __init__(self, texture, size, bearing, advance):
55
+ self.texture = texture
56
+ self.size = size
57
+ self.bearing = bearing
58
+ self.advance = advance
59
+
60
+
61
+ class Font(object):
62
+ """A font object.
63
+
64
+ Parameters
65
+ ----------
66
+ font_file : str
67
+ The file to load the font from.
68
+ font_pt : int
69
+ The height of the font in pixels.
70
+ """
71
+
72
+ def __init__(self, font_file, font_pt=40):
73
+ self.font_file = font_file
74
+ self.font_pt = int(font_pt)
75
+ self._face = freetype.Face(font_file)
76
+ self._face.set_pixel_sizes(0, font_pt)
77
+ self._character_map = {}
78
+
79
+ for i in range(0, 128):
80
+
81
+ # Generate texture
82
+ face = self._face
83
+ face.load_char(chr(i))
84
+ buf = face.glyph.bitmap.buffer
85
+ src = (np.array(buf) / 255.0).astype(np.float32)
86
+ src = src.reshape((face.glyph.bitmap.rows,
87
+ face.glyph.bitmap.width))
88
+ tex = Texture(
89
+ sampler=Sampler(
90
+ magFilter=GL_LINEAR,
91
+ minFilter=GL_LINEAR,
92
+ wrapS=GL_CLAMP_TO_EDGE,
93
+ wrapT=GL_CLAMP_TO_EDGE
94
+ ),
95
+ source=src,
96
+ source_channels='R',
97
+ )
98
+ character = Character(
99
+ texture=tex,
100
+ size=np.array([face.glyph.bitmap.width,
101
+ face.glyph.bitmap.rows]),
102
+ bearing=np.array([face.glyph.bitmap_left,
103
+ face.glyph.bitmap_top]),
104
+ advance=face.glyph.advance.x
105
+ )
106
+ self._character_map[chr(i)] = character
107
+
108
+ self._vbo = None
109
+ self._vao = None
110
+
111
+ @property
112
+ def font_file(self):
113
+ """str : The file the font was loaded from.
114
+ """
115
+ return self._font_file
116
+
117
+ @font_file.setter
118
+ def font_file(self, value):
119
+ self._font_file = value
120
+
121
+ @property
122
+ def font_pt(self):
123
+ """int : The height of the font in pixels.
124
+ """
125
+ return self._font_pt
126
+
127
+ @font_pt.setter
128
+ def font_pt(self, value):
129
+ self._font_pt = int(value)
130
+
131
+ def _add_to_context(self):
132
+
133
+ self._vao = glGenVertexArrays(1)
134
+ glBindVertexArray(self._vao)
135
+ self._vbo = glGenBuffers(1)
136
+ glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
137
+ glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, None, GL_DYNAMIC_DRAW)
138
+ glEnableVertexAttribArray(0)
139
+ glVertexAttribPointer(
140
+ 0, 4, GL_FLOAT, GL_FALSE, 4 * FLOAT_SZ, ctypes.c_void_p(0)
141
+ )
142
+ glBindVertexArray(0)
143
+
144
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
145
+ for c in self._character_map:
146
+ ch = self._character_map[c]
147
+ if not ch.texture._in_context():
148
+ ch.texture._add_to_context()
149
+
150
+ def _remove_from_context(self):
151
+ for c in self._character_map:
152
+ ch = self._character_map[c]
153
+ ch.texture.delete()
154
+ if self._vao is not None:
155
+ glDeleteVertexArrays(1, [self._vao])
156
+ glDeleteBuffers(1, [self._vbo])
157
+ self._vao = None
158
+ self._vbo = None
159
+
160
+ def _in_context(self):
161
+ return self._vao is not None
162
+
163
+ def _bind(self):
164
+ glBindVertexArray(self._vao)
165
+
166
+ def _unbind(self):
167
+ glBindVertexArray(0)
168
+
169
+ def delete(self):
170
+ self._unbind()
171
+ self._remove_from_context()
172
+
173
+ def render_string(self, text, x, y, scale=1.0,
174
+ align=TextAlign.BOTTOM_LEFT):
175
+ """Render a string to the current view buffer.
176
+
177
+ Note
178
+ ----
179
+ Assumes correct shader program already bound w/ uniforms set.
180
+
181
+ Parameters
182
+ ----------
183
+ text : str
184
+ The text to render.
185
+ x : int
186
+ Horizontal pixel location of text.
187
+ y : int
188
+ Vertical pixel location of text.
189
+ scale : int
190
+ Scaling factor for text.
191
+ align : int
192
+ One of the TextAlign options which specifies where the ``x``
193
+ and ``y`` parameters lie on the text. For example,
194
+ :attr:`.TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate
195
+ the position of the bottom-left corner of the textbox.
196
+ """
197
+ glActiveTexture(GL_TEXTURE0)
198
+ glEnable(GL_BLEND)
199
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
200
+ glDisable(GL_DEPTH_TEST)
201
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
202
+ self._bind()
203
+
204
+ # Determine width and height of text relative to x, y
205
+ width = 0.0
206
+ height = 0.0
207
+ for c in text:
208
+ ch = self._character_map[c]
209
+ height = max(height, ch.bearing[1] * scale)
210
+ width += (ch.advance >> 6) * scale
211
+
212
+ # Determine offsets based on alignments
213
+ xoff = 0
214
+ yoff = 0
215
+ if align == TextAlign.BOTTOM_RIGHT:
216
+ xoff = -width
217
+ elif align == TextAlign.BOTTOM_CENTER:
218
+ xoff = -width / 2.0
219
+ elif align == TextAlign.TOP_LEFT:
220
+ yoff = -height
221
+ elif align == TextAlign.TOP_RIGHT:
222
+ yoff = -height
223
+ xoff = -width
224
+ elif align == TextAlign.TOP_CENTER:
225
+ yoff = -height
226
+ xoff = -width / 2.0
227
+ elif align == TextAlign.CENTER:
228
+ xoff = -width / 2.0
229
+ yoff = -height / 2.0
230
+ elif align == TextAlign.CENTER_LEFT:
231
+ yoff = -height / 2.0
232
+ elif align == TextAlign.CENTER_RIGHT:
233
+ xoff = -width
234
+ yoff = -height / 2.0
235
+
236
+ x += xoff
237
+ y += yoff
238
+
239
+ ch = None
240
+ for c in text:
241
+ ch = self._character_map[c]
242
+ xpos = x + ch.bearing[0] * scale
243
+ ypos = y - (ch.size[1] - ch.bearing[1]) * scale
244
+ w = ch.size[0] * scale
245
+ h = ch.size[1] * scale
246
+
247
+ vertices = np.array([
248
+ [xpos, ypos, 0.0, 0.0],
249
+ [xpos + w, ypos, 1.0, 0.0],
250
+ [xpos + w, ypos + h, 1.0, 1.0],
251
+ [xpos + w, ypos + h, 1.0, 1.0],
252
+ [xpos, ypos + h, 0.0, 1.0],
253
+ [xpos, ypos, 0.0, 0.0],
254
+ ], dtype=np.float32)
255
+
256
+ ch.texture._bind()
257
+
258
+ glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
259
+ glBufferData(
260
+ GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW
261
+ )
262
+ # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken
263
+ # glBufferSubData(
264
+ # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ,
265
+ # np.ascontiguousarray(vertices.flatten)
266
+ # )
267
+ glDrawArrays(GL_TRIANGLES, 0, 6)
268
+ x += (ch.advance >> 6) * scale
269
+
270
+ self._unbind()
271
+ if ch:
272
+ ch.texture._unbind()
pyrender/light.py ADDED
@@ -0,0 +1,382 @@
1
+ """Punctual light sources as defined by the glTF 2.0 KHR extension at
2
+ https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
3
+
4
+ Author: Matthew Matl
5
+ """
6
+ import abc
7
+ import numpy as np
8
+
9
+ from OpenGL.GL import *
10
+
11
+ from .utils import format_color_vector
12
+ from .texture import Texture
13
+ from .constants import SHADOW_TEX_SZ
14
+ from .camera import OrthographicCamera, PerspectiveCamera
15
+
16
+
17
+ class Light(abc.ABC):
18
+ """Base class for all light objects.
19
+
20
+ Parameters
21
+ ----------
22
+ color : (3,) float
23
+ RGB value for the light's color in linear space.
24
+ intensity : float
25
+ Brightness of light. The units that this is defined in depend on the
26
+ type of light. Point and spot lights use luminous intensity in candela
27
+ (lm/sr), while directional lights use illuminance in lux (lm/m2).
28
+ name : str, optional
29
+ Name of the light.
30
+ """
31
+ def __init__(self,
32
+ color=None,
33
+ intensity=None,
34
+ name=None):
35
+
36
+ if color is None:
37
+ color = np.ones(3)
38
+ if intensity is None:
39
+ intensity = 1.0
40
+
41
+ self.name = name
42
+ self.color = color
43
+ self.intensity = intensity
44
+ self._shadow_camera = None
45
+ self._shadow_texture = None
46
+
47
+ @property
48
+ def name(self):
49
+ """str : The user-defined name of this object.
50
+ """
51
+ return self._name
52
+
53
+ @name.setter
54
+ def name(self, value):
55
+ if value is not None:
56
+ value = str(value)
57
+ self._name = value
58
+
59
+ @property
60
+ def color(self):
61
+ """(3,) float : The light's color.
62
+ """
63
+ return self._color
64
+
65
+ @color.setter
66
+ def color(self, value):
67
+ self._color = format_color_vector(value, 3)
68
+
69
+ @property
70
+ def intensity(self):
71
+ """float : The light's intensity in candela or lux.
72
+ """
73
+ return self._intensity
74
+
75
+ @intensity.setter
76
+ def intensity(self, value):
77
+ self._intensity = float(value)
78
+
79
+ @property
80
+ def shadow_texture(self):
81
+ """:class:`.Texture` : A texture used to hold shadow maps for this light.
82
+ """
83
+ return self._shadow_texture
84
+
85
+ @shadow_texture.setter
86
+ def shadow_texture(self, value):
87
+ if self._shadow_texture is not None:
88
+ if self._shadow_texture._in_context():
89
+ self._shadow_texture.delete()
90
+ self._shadow_texture = value
91
+
92
+ @abc.abstractmethod
93
+ def _generate_shadow_texture(self, size=None):
94
+ """Generate a shadow texture for this light.
95
+
96
+ Parameters
97
+ ----------
98
+ size : int, optional
99
+ Size of texture map. Must be a positive power of two.
100
+ """
101
+ pass
102
+
103
+ @abc.abstractmethod
104
+ def _get_shadow_camera(self, scene_scale):
105
+ """Generate and return a shadow mapping camera for this light.
106
+
107
+ Parameters
108
+ ----------
109
+ scene_scale : float
110
+ Length of scene's bounding box diagonal.
111
+
112
+ Returns
113
+ -------
114
+ camera : :class:`.Camera`
115
+ The camera used to render shadowmaps for this light.
116
+ """
117
+ pass
118
+
119
+
120
+ class DirectionalLight(Light):
121
+ """Directional lights are light sources that act as though they are
122
+ infinitely far away and emit light in the direction of the local -z axis.
123
+ This light type inherits the orientation of the node that it belongs to;
124
+ position and scale are ignored except for their effect on the inherited
125
+ node orientation. Because it is at an infinite distance, the light is
126
+ not attenuated. Its intensity is defined in lumens per metre squared,
127
+ or lux (lm/m2).
128
+
129
+ Parameters
130
+ ----------
131
+ color : (3,) float, optional
132
+ RGB value for the light's color in linear space. Defaults to white
133
+ (i.e. [1.0, 1.0, 1.0]).
134
+ intensity : float, optional
135
+ Brightness of light, in lux (lm/m^2). Defaults to 1.0
136
+ name : str, optional
137
+ Name of the light.
138
+ """
139
+
140
+ def __init__(self,
141
+ color=None,
142
+ intensity=None,
143
+ name=None):
144
+ super(DirectionalLight, self).__init__(
145
+ color=color,
146
+ intensity=intensity,
147
+ name=name,
148
+ )
149
+
150
+ def _generate_shadow_texture(self, size=None):
151
+ """Generate a shadow texture for this light.
152
+
153
+ Parameters
154
+ ----------
155
+ size : int, optional
156
+ Size of texture map. Must be a positive power of two.
157
+ """
158
+ if size is None:
159
+ size = SHADOW_TEX_SZ
160
+ self.shadow_texture = Texture(width=size, height=size,
161
+ source_channels='D', data_format=GL_FLOAT)
162
+
163
+ def _get_shadow_camera(self, scene_scale):
164
+ """Generate and return a shadow mapping camera for this light.
165
+
166
+ Parameters
167
+ ----------
168
+ scene_scale : float
169
+ Length of scene's bounding box diagonal.
170
+
171
+ Returns
172
+ -------
173
+ camera : :class:`.Camera`
174
+ The camera used to render shadowmaps for this light.
175
+ """
176
+ return OrthographicCamera(
177
+ znear=0.01 * scene_scale,
178
+ zfar=10 * scene_scale,
179
+ xmag=scene_scale,
180
+ ymag=scene_scale
181
+ )
182
+
183
+
184
+ class PointLight(Light):
185
+ """Point lights emit light in all directions from their position in space;
186
+ rotation and scale are ignored except for their effect on the inherited
187
+ node position. The brightness of the light attenuates in a physically
188
+ correct manner as distance increases from the light's position (i.e.
189
+ brightness goes like the inverse square of the distance). Point light
190
+ intensity is defined in candela, which is lumens per square radian (lm/sr).
191
+
192
+ Parameters
193
+ ----------
194
+ color : (3,) float
195
+ RGB value for the light's color in linear space.
196
+ intensity : float
197
+ Brightness of light in candela (lm/sr).
198
+ range : float
199
+ Cutoff distance at which light's intensity may be considered to
200
+ have reached zero. If None, the range is assumed to be infinite.
201
+ name : str, optional
202
+ Name of the light.
203
+ """
204
+
205
+ def __init__(self,
206
+ color=None,
207
+ intensity=None,
208
+ range=None,
209
+ name=None):
210
+ super(PointLight, self).__init__(
211
+ color=color,
212
+ intensity=intensity,
213
+ name=name,
214
+ )
215
+ self.range = range
216
+
217
+ @property
218
+ def range(self):
219
+ """float : The cutoff distance for the light.
220
+ """
221
+ return self._range
222
+
223
+ @range.setter
224
+ def range(self, value):
225
+ if value is not None:
226
+ value = float(value)
227
+ if value <= 0:
228
+ raise ValueError('Range must be > 0')
229
+ self._range = value
230
+ self._range = value
231
+
232
+ def _generate_shadow_texture(self, size=None):
233
+ """Generate a shadow texture for this light.
234
+
235
+ Parameters
236
+ ----------
237
+ size : int, optional
238
+ Size of texture map. Must be a positive power of two.
239
+ """
240
+ raise NotImplementedError('Shadows not implemented for point lights')
241
+
242
+ def _get_shadow_camera(self, scene_scale):
243
+ """Generate and return a shadow mapping camera for this light.
244
+
245
+ Parameters
246
+ ----------
247
+ scene_scale : float
248
+ Length of scene's bounding box diagonal.
249
+
250
+ Returns
251
+ -------
252
+ camera : :class:`.Camera`
253
+ The camera used to render shadowmaps for this light.
254
+ """
255
+ raise NotImplementedError('Shadows not implemented for point lights')
256
+
257
+
258
+ class SpotLight(Light):
259
+ """Spot lights emit light in a cone in the direction of the local -z axis.
260
+ The angle and falloff of the cone is defined using two numbers, the
261
+ ``innerConeAngle`` and ``outerConeAngle``.
262
+ As with point lights, the brightness
263
+ also attenuates in a physically correct manner as distance increases from
264
+ the light's position (i.e. brightness goes like the inverse square of the
265
+ distance). Spot light intensity refers to the brightness inside the
266
+ ``innerConeAngle`` (and at the location of the light) and is defined in
267
+ candela, which is lumens per square radian (lm/sr). A spot light's position
268
+ and orientation are inherited from its node transform. Inherited scale does
269
+ not affect cone shape, and is ignored except for its effect on position
270
+ and orientation.
271
+
272
+ Parameters
273
+ ----------
274
+ color : (3,) float
275
+ RGB value for the light's color in linear space.
276
+ intensity : float
277
+ Brightness of light in candela (lm/sr).
278
+ range : float
279
+ Cutoff distance at which light's intensity may be considered to
280
+ have reached zero. If None, the range is assumed to be infinite.
281
+ innerConeAngle : float
282
+ Angle, in radians, from centre of spotlight where falloff begins.
283
+ Must be greater than or equal to ``0`` and less
284
+ than ``outerConeAngle``. Defaults to ``0``.
285
+ outerConeAngle : float
286
+ Angle, in radians, from centre of spotlight where falloff ends.
287
+ Must be greater than ``innerConeAngle`` and less than or equal to
288
+ ``PI / 2.0``. Defaults to ``PI / 4.0``.
289
+ name : str, optional
290
+ Name of the light.
291
+ """
292
+
293
+ def __init__(self,
294
+ color=None,
295
+ intensity=None,
296
+ range=None,
297
+ innerConeAngle=0.0,
298
+ outerConeAngle=(np.pi / 4.0),
299
+ name=None):
300
+ super(SpotLight, self).__init__(
301
+ name=name,
302
+ color=color,
303
+ intensity=intensity,
304
+ )
305
+ self.outerConeAngle = outerConeAngle
306
+ self.innerConeAngle = innerConeAngle
307
+ self.range = range
308
+
309
+ @property
310
+ def innerConeAngle(self):
311
+ """float : The inner cone angle in radians.
312
+ """
313
+ return self._innerConeAngle
314
+
315
+ @innerConeAngle.setter
316
+ def innerConeAngle(self, value):
317
+ if value < 0.0 or value > self.outerConeAngle:
318
+ raise ValueError('Invalid value for inner cone angle')
319
+ self._innerConeAngle = float(value)
320
+
321
+ @property
322
+ def outerConeAngle(self):
323
+ """float : The outer cone angle in radians.
324
+ """
325
+ return self._outerConeAngle
326
+
327
+ @outerConeAngle.setter
328
+ def outerConeAngle(self, value):
329
+ if value < 0.0 or value > np.pi / 2.0 + 1e-9:
330
+ raise ValueError('Invalid value for outer cone angle')
331
+ self._outerConeAngle = float(value)
332
+
333
+ @property
334
+ def range(self):
335
+ """float : The cutoff distance for the light.
336
+ """
337
+ return self._range
338
+
339
+ @range.setter
340
+ def range(self, value):
341
+ if value is not None:
342
+ value = float(value)
343
+ if value <= 0:
344
+ raise ValueError('Range must be > 0')
345
+ self._range = value
346
+ self._range = value
347
+
348
+ def _generate_shadow_texture(self, size=None):
349
+ """Generate a shadow texture for this light.
350
+
351
+ Parameters
352
+ ----------
353
+ size : int, optional
354
+ Size of texture map. Must be a positive power of two.
355
+ """
356
+ if size is None:
357
+ size = SHADOW_TEX_SZ
358
+ self.shadow_texture = Texture(width=size, height=size,
359
+ source_channels='D', data_format=GL_FLOAT)
360
+
361
+ def _get_shadow_camera(self, scene_scale):
362
+ """Generate and return a shadow mapping camera for this light.
363
+
364
+ Parameters
365
+ ----------
366
+ scene_scale : float
367
+ Length of scene's bounding box diagonal.
368
+
369
+ Returns
370
+ -------
371
+ camera : :class:`.Camera`
372
+ The camera used to render shadowmaps for this light.
373
+ """
374
+ return PerspectiveCamera(
375
+ znear=0.01 * scene_scale,
376
+ zfar=10 * scene_scale,
377
+ yfov=np.clip(2 * self.outerConeAngle + np.pi / 16.0, 0.0, np.pi),
378
+ aspectRatio=1.0
379
+ )
380
+
381
+
382
+ __all__ = ['Light', 'DirectionalLight', 'SpotLight', 'PointLight']