File size: 12,022 Bytes
b4c8bc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Punctual light sources as defined by the glTF 2.0 KHR extension at
https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual

Author: Matthew Matl
"""
import abc
import numpy as np
import six

from OpenGL.GL import *

from .utils import format_color_vector
from .texture import Texture
from .constants import SHADOW_TEX_SZ
from .camera import OrthographicCamera, PerspectiveCamera



@six.add_metaclass(abc.ABCMeta)
class Light(object):
    """Base class for all light objects.

    Parameters
    ----------
    color : (3,) float
        RGB value for the light's color in linear space.
    intensity : float
        Brightness of light. The units that this is defined in depend on the
        type of light. Point and spot lights use luminous intensity in candela
        (lm/sr), while directional lights use illuminance in lux (lm/m2).
    name : str, optional
        Name of the light.
    """
    def __init__(self,
                 color=None,
                 intensity=None,
                 name=None):

        if color is None:
            color = np.ones(3)
        if intensity is None:
            intensity = 1.0

        self.name = name
        self.color = color
        self.intensity = intensity
        self._shadow_camera = None
        self._shadow_texture = None

    @property
    def name(self):
        """str : The user-defined name of this object.
        """
        return self._name

    @name.setter
    def name(self, value):
        if value is not None:
            value = str(value)
        self._name = value

    @property
    def color(self):
        """(3,) float : The light's color.
        """
        return self._color

    @color.setter
    def color(self, value):
        self._color = format_color_vector(value, 3)

    @property
    def intensity(self):
        """float : The light's intensity in candela or lux.
        """
        return self._intensity

    @intensity.setter
    def intensity(self, value):
        self._intensity = float(value)

    @property
    def shadow_texture(self):
        """:class:`.Texture` : A texture used to hold shadow maps for this light.
        """
        return self._shadow_texture

    @shadow_texture.setter
    def shadow_texture(self, value):
        if self._shadow_texture is not None:
            if self._shadow_texture._in_context():
                self._shadow_texture.delete()
        self._shadow_texture = value

    @abc.abstractmethod
    def _generate_shadow_texture(self, size=None):
        """Generate a shadow texture for this light.

        Parameters
        ----------
        size : int, optional
            Size of texture map. Must be a positive power of two.
        """
        pass

    @abc.abstractmethod
    def _get_shadow_camera(self, scene_scale):
        """Generate and return a shadow mapping camera for this light.

        Parameters
        ----------
        scene_scale : float
            Length of scene's bounding box diagonal.

        Returns
        -------
        camera : :class:`.Camera`
            The camera used to render shadowmaps for this light.
        """
        pass


class DirectionalLight(Light):
    """Directional lights are light sources that act as though they are
    infinitely far away and emit light in the direction of the local -z axis.
    This light type inherits the orientation of the node that it belongs to;
    position and scale are ignored except for their effect on the inherited
    node orientation. Because it is at an infinite distance, the light is
    not attenuated. Its intensity is defined in lumens per metre squared,
    or lux (lm/m2).

    Parameters
    ----------
    color : (3,) float, optional
        RGB value for the light's color in linear space. Defaults to white
        (i.e. [1.0, 1.0, 1.0]).
    intensity : float, optional
        Brightness of light, in lux (lm/m^2). Defaults to 1.0
    name : str, optional
        Name of the light.
    """

    def __init__(self,
                 color=None,
                 intensity=None,
                 name=None):
        super(DirectionalLight, self).__init__(
            color=color,
            intensity=intensity,
            name=name,
        )

    def _generate_shadow_texture(self, size=None):
        """Generate a shadow texture for this light.

        Parameters
        ----------
        size : int, optional
            Size of texture map. Must be a positive power of two.
        """
        if size is None:
            size = SHADOW_TEX_SZ
        self.shadow_texture = Texture(width=size, height=size,
                                      source_channels='D', data_format=GL_FLOAT)

    def _get_shadow_camera(self, scene_scale):
        """Generate and return a shadow mapping camera for this light.

        Parameters
        ----------
        scene_scale : float
            Length of scene's bounding box diagonal.

        Returns
        -------
        camera : :class:`.Camera`
            The camera used to render shadowmaps for this light.
        """
        return OrthographicCamera(
            znear=0.01 * scene_scale,
            zfar=10 * scene_scale,
            xmag=scene_scale,
            ymag=scene_scale
        )


class PointLight(Light):
    """Point lights emit light in all directions from their position in space;
    rotation and scale are ignored except for their effect on the inherited
    node position. The brightness of the light attenuates in a physically
    correct manner as distance increases from the light's position (i.e.
    brightness goes like the inverse square of the distance). Point light
    intensity is defined in candela, which is lumens per square radian (lm/sr).

    Parameters
    ----------
    color : (3,) float
        RGB value for the light's color in linear space.
    intensity : float
        Brightness of light in candela (lm/sr).
    range : float
        Cutoff distance at which light's intensity may be considered to
        have reached zero. If None, the range is assumed to be infinite.
    name : str, optional
        Name of the light.
    """

    def __init__(self,
                 color=None,
                 intensity=None,
                 range=None,
                 name=None):
        super(PointLight, self).__init__(
            color=color,
            intensity=intensity,
            name=name,
        )
        self.range = range

    @property
    def range(self):
        """float : The cutoff distance for the light.
        """
        return self._range

    @range.setter
    def range(self, value):
        if value is not None:
            value = float(value)
            if value <= 0:
                raise ValueError('Range must be > 0')
            self._range = value
        self._range = value

    def _generate_shadow_texture(self, size=None):
        """Generate a shadow texture for this light.

        Parameters
        ----------
        size : int, optional
            Size of texture map. Must be a positive power of two.
        """
        raise NotImplementedError('Shadows not implemented for point lights')

    def _get_shadow_camera(self, scene_scale):
        """Generate and return a shadow mapping camera for this light.

        Parameters
        ----------
        scene_scale : float
            Length of scene's bounding box diagonal.

        Returns
        -------
        camera : :class:`.Camera`
            The camera used to render shadowmaps for this light.
        """
        raise NotImplementedError('Shadows not implemented for point lights')


class SpotLight(Light):
    """Spot lights emit light in a cone in the direction of the local -z axis.
    The angle and falloff of the cone is defined using two numbers, the
    ``innerConeAngle`` and ``outerConeAngle``.
    As with point lights, the brightness
    also attenuates in a physically correct manner as distance increases from
    the light's position (i.e. brightness goes like the inverse square of the
    distance). Spot light intensity refers to the brightness inside the
    ``innerConeAngle`` (and at the location of the light) and is defined in
    candela, which is lumens per square radian (lm/sr). A spot light's position
    and orientation are inherited from its node transform. Inherited scale does
    not affect cone shape, and is ignored except for its effect on position
    and orientation.

    Parameters
    ----------
    color : (3,) float
        RGB value for the light's color in linear space.
    intensity : float
        Brightness of light in candela (lm/sr).
    range : float
        Cutoff distance at which light's intensity may be considered to
        have reached zero. If None, the range is assumed to be infinite.
    innerConeAngle : float
        Angle, in radians, from centre of spotlight where falloff begins.
        Must be greater than or equal to ``0`` and less
        than ``outerConeAngle``. Defaults to ``0``.
    outerConeAngle : float
        Angle, in radians, from centre of spotlight where falloff ends.
        Must be greater than ``innerConeAngle`` and less than or equal to
        ``PI / 2.0``. Defaults to ``PI / 4.0``.
    name : str, optional
        Name of the light.
    """

    def __init__(self,
                 color=None,
                 intensity=None,
                 range=None,
                 innerConeAngle=0.0,
                 outerConeAngle=(np.pi / 4.0),
                 name=None):
        super(SpotLight, self).__init__(
            name=name,
            color=color,
            intensity=intensity,
        )
        self.outerConeAngle = outerConeAngle
        self.innerConeAngle = innerConeAngle
        self.range = range

    @property
    def innerConeAngle(self):
        """float : The inner cone angle in radians.
        """
        return self._innerConeAngle

    @innerConeAngle.setter
    def innerConeAngle(self, value):
        if value < 0.0 or value > self.outerConeAngle:
            raise ValueError('Invalid value for inner cone angle')
        self._innerConeAngle = float(value)

    @property
    def outerConeAngle(self):
        """float : The outer cone angle in radians.
        """
        return self._outerConeAngle

    @outerConeAngle.setter
    def outerConeAngle(self, value):
        if value < 0.0 or value > np.pi / 2.0 + 1e-9:
            raise ValueError('Invalid value for outer cone angle')
        self._outerConeAngle = float(value)

    @property
    def range(self):
        """float : The cutoff distance for the light.
        """
        return self._range

    @range.setter
    def range(self, value):
        if value is not None:
            value = float(value)
            if value <= 0:
                raise ValueError('Range must be > 0')
            self._range = value
        self._range = value

    def _generate_shadow_texture(self, size=None):
        """Generate a shadow texture for this light.

        Parameters
        ----------
        size : int, optional
            Size of texture map. Must be a positive power of two.
        """
        if size is None:
            size = SHADOW_TEX_SZ
        self.shadow_texture = Texture(width=size, height=size,
                                      source_channels='D', data_format=GL_FLOAT)

    def _get_shadow_camera(self, scene_scale):
        """Generate and return a shadow mapping camera for this light.

        Parameters
        ----------
        scene_scale : float
            Length of scene's bounding box diagonal.

        Returns
        -------
        camera : :class:`.Camera`
            The camera used to render shadowmaps for this light.
        """
        return PerspectiveCamera(
            znear=0.01 * scene_scale,
            zfar=10 * scene_scale,
            yfov=np.clip(2 * self.outerConeAngle + np.pi / 16.0, 0.0, np.pi),
            aspectRatio=1.0
        )


__all__ = ['Light', 'DirectionalLight', 'SpotLight', 'PointLight']