Coverage for trimesh/scene/lighting.py: 84%
76 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2lighting.py
3--------------
5Hold basic information about lights.
7Forked from the light model in `pyrender`:
8https://github.com/mmatl/pyrender
9"""
11import numpy as np
13from .. import transformations, util, visual
14from ..typed import NDArray, float64
16# default light color
17_DEFAULT_RGBA = np.array([60, 60, 60, 255], dtype=np.uint8)
20class Light(util.ABC):
21 """
22 Base class for all light objects.
24 Attributes
25 ----------
26 name : str, optional
27 Name of the light.
28 color : (4,) uint8
29 RGBA value for the light's color in linear space.
30 intensity : float
31 Brightness of light. The units that this is defined in depend
32 on the type of light: point and spot lights use luminous intensity
33 in candela (lm/sr) while directional lights use illuminance
34 in lux (lm/m2).
35 radius : float
36 Cutoff distance at which light's intensity may be considered to
37 have reached zero. Supported only for point and spot lights
38 Must be > 0.0
39 If None, the radius is assumed to be infinite.
40 """
42 def __init__(self, name=None, color=None, intensity=None, radius=None):
43 if name is None:
44 # if name is not passed, make it something unique
45 self.name = f"light_{util.unique_id(6).upper()}"
46 else:
47 # otherwise assign it
48 self.name = name
50 self.color = color
51 self.intensity = intensity
52 self.radius = radius
54 @property
55 def color(self):
56 return self._color
58 @color.setter
59 def color(self, value):
60 if value is None:
61 self._color = _DEFAULT_RGBA
62 else:
63 value = visual.to_rgba(value)
64 if len(value.shape) == 2:
65 value = value[0]
66 if value.shape != (4,):
67 raise ValueError("couldn't convert color to RGBA!")
68 # uint8 RGB color
69 self._color = value
71 @property
72 def intensity(self):
73 return self._intensity
75 @intensity.setter
76 def intensity(self, value):
77 if value is not None:
78 self._intensity = float(value)
79 else:
80 self._intensity = 1.0
82 @property
83 def radius(self):
84 return self._radius
86 @radius.setter
87 def radius(self, value):
88 if value is None or value < 0.0:
89 self._radius = value
90 else:
91 self._radius = float(value)
94class DirectionalLight(Light):
95 """
96 Directional lights are light sources that act as though they are
97 infinitely far away and emit light in the direction of the local -z axis.
98 This light type inherits the orientation of the node that it belongs to;
99 position and scale are ignored except for their effect on the inherited
100 node orientation. Because it is at an infinite distance, the light is
101 not attenuated. Its intensity is defined in lumens per metre squared,
102 or lux (lm/m2).
104 Attributes
105 ----------
106 name : str, optional
107 Name of the light.
108 color : (4,) unit8
109 RGBA value for the light's color in linear space.
110 intensity : float
111 Brightness of light. The units that this is defined in depend
112 on the type of light.
113 point and spot lights use luminous intensity in candela (lm/sr),
114 while directional lights use illuminance in lux (lm/m2).
115 radius : float
116 Cutoff distance at which light's intensity may be considered to
117 have reached zero. Supported only for point and spot lights, must be > 0.
118 If None, the radius is assumed to be infinite.
119 """
121 def __init__(self, name=None, color=None, intensity=None, radius=None):
122 super().__init__(name=name, color=color, intensity=intensity, radius=radius)
125class PointLight(Light):
126 """
127 Point lights emit light in all directions from their position in space;
128 rotation and scale are ignored except for their effect on the inherited
129 node position. The brightness of the light attenuates in a physically
130 correct manner as distance increases from the light's position (i.e.
131 brightness goes like the inverse square of the distance). Point light
132 intensity is defined in candela, which is lumens per square radian (lm/sr).
134 Attributes
135 ----------
136 name : str, optional
137 Name of the light.
138 color : (4,) uint8
139 RGBA value for the light's color in linear space.
140 intensity : float
141 Brightness of light. The units that this is defined in depend
142 on the type of light.
143 point and spot lights use luminous intensity in candela (lm/sr),
144 while directional lights use illuminance in lux (lm/m2).
145 radius : float
146 Cutoff distance at which light's intensity may be considered to
147 have reached zero. Supported only for point and spot lights, must be > 0.
148 If None, the radius is assumed to be infinite.
149 """
151 def __init__(self, name=None, color=None, intensity=None, radius=None):
152 super().__init__(name=name, color=color, intensity=intensity, radius=radius)
155class SpotLight(Light):
156 """
157 Spot lights emit light in a cone in the direction of the local -z axis.
158 The angle and falloff of the cone is defined using two numbers, the
159 `innerConeAngle` and `outerConeAngle`. As with point lights, the brightness
160 also attenuates in a physically correct manner as distance increases from
161 the light's position (i.e. brightness goes like the inverse square of the
162 distance). Spot light intensity refers to the brightness inside the
163 `innerConeAngle` (and at the location of the light) and is defined in
164 candela, which is lumens per square radian (lm/sr). A spot light's position
165 and orientation are inherited from its node transform. Inherited scale does
166 not affect cone shape, and is ignored except for its effect on position
167 and orientation.
169 Attributes
170 ----------
171 name : str, optional
172 Name of the light.
173 color : (4,) uint8
174 RGBA value for the light's color in linear space.
175 intensity : float
176 Brightness of light. The units that this is defined in depend
177 on the type of light.
178 point and spot lights use luminous intensity in candela (lm/sr),
179 while directional lights use illuminance in lux (lm/m2).
180 radius : float
181 Cutoff distance at which light's intensity may be considered to
182 have reached zero. Supported only for point and spot lights, must be > 0.
183 If None, the radius is assumed to be infinite.
184 innerConeAngle : float
185 Angle, in radians, from centre of spotlight where falloff begins.
186 Must be greater than or equal to `0` and less than `outerConeAngle`.
187 outerConeAngle : float
188 Angle, in radians, from centre of spotlight where falloff ends.
189 Must be greater than `innerConeAngle` and less than or equal to `PI / 2.0`.
190 """
192 def __init__(
193 self,
194 name=None,
195 color=None,
196 intensity=None,
197 radius=None,
198 innerConeAngle=0.0,
199 outerConeAngle=np.pi / 4.0,
200 ):
201 super().__init__(name=name, color=color, intensity=intensity, radius=radius)
202 self.outerConeAngle = outerConeAngle
203 self.innerConeAngle = innerConeAngle
205 @property
206 def innerConeAngle(self):
207 return self._innerConeAngle
209 @innerConeAngle.setter
210 def innerConeAngle(self, value):
211 if value < 0.0 or value > self.outerConeAngle:
212 raise ValueError("Invalid value for inner cone angle")
213 self._innerConeAngle = float(value)
215 @property
216 def outerConeAngle(self):
217 return self._outerConeAngle
219 @outerConeAngle.setter
220 def outerConeAngle(self, value):
221 if value < 0.0 or value > np.pi / 2.0 + 1e-9:
222 raise ValueError("Invalid value for outer cone angle")
223 self._outerConeAngle = float(value)
226def autolight(scene) -> tuple[list[Light], list[NDArray[float64]]]:
227 """
228 Generate a list of lights for a scene that looks decent.
230 Parameters
231 --------------
232 scene : trimesh.Scene
233 Scene with geometry
235 Returns
236 --------------
237 lights : [Light]
238 List of light objects
239 transforms : (len(lights), 4, 4) float
240 Transformation matrices for light positions.
241 """
243 # start with empty lights and transforms
244 lights = []
245 transforms = []
247 # if there are no objects in the scene don't crash
248 bounds = scene.bounds
249 if bounds is not None:
250 # create two translation matrices for bounds corners
251 transforms.extend(transformations.translation_matrix(b) for b in bounds)
252 lights.extend(PointLight() for _ in range(2))
254 return lights, transforms