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

1""" 

2lighting.py 

3-------------- 

4 

5Hold basic information about lights. 

6 

7Forked from the light model in `pyrender`: 

8https://github.com/mmatl/pyrender 

9""" 

10 

11import numpy as np 

12 

13from .. import transformations, util, visual 

14from ..typed import NDArray, float64 

15 

16# default light color 

17_DEFAULT_RGBA = np.array([60, 60, 60, 255], dtype=np.uint8) 

18 

19 

20class Light(util.ABC): 

21 """ 

22 Base class for all light objects. 

23 

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 """ 

41 

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 

49 

50 self.color = color 

51 self.intensity = intensity 

52 self.radius = radius 

53 

54 @property 

55 def color(self): 

56 return self._color 

57 

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 

70 

71 @property 

72 def intensity(self): 

73 return self._intensity 

74 

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 

81 

82 @property 

83 def radius(self): 

84 return self._radius 

85 

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) 

92 

93 

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). 

103 

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 """ 

120 

121 def __init__(self, name=None, color=None, intensity=None, radius=None): 

122 super().__init__(name=name, color=color, intensity=intensity, radius=radius) 

123 

124 

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). 

133 

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 """ 

150 

151 def __init__(self, name=None, color=None, intensity=None, radius=None): 

152 super().__init__(name=name, color=color, intensity=intensity, radius=radius) 

153 

154 

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. 

168 

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 """ 

191 

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 

204 

205 @property 

206 def innerConeAngle(self): 

207 return self._innerConeAngle 

208 

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) 

214 

215 @property 

216 def outerConeAngle(self): 

217 return self._outerConeAngle 

218 

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) 

224 

225 

226def autolight(scene) -> tuple[list[Light], list[NDArray[float64]]]: 

227 """ 

228 Generate a list of lights for a scene that looks decent. 

229 

230 Parameters 

231 -------------- 

232 scene : trimesh.Scene 

233 Scene with geometry 

234 

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 """ 

242 

243 # start with empty lights and transforms 

244 lights = [] 

245 transforms = [] 

246 

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)) 

253 

254 return lights, transforms