Coverage for trimesh/rendering.py: 87%

125 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1""" 

2rendering.py 

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

4 

5Functions to convert trimesh objects to pyglet/opengl objects. 

6""" 

7 

8import numpy as np 

9 

10from . import util 

11 

12# avoid importing pyglet or pyglet.gl 

13# as pyglet does things on import 

14GL_POINTS, GL_LINES, GL_TRIANGLES = (0, 1, 4) 

15 

16 

17def convert_to_vertexlist(geometry, **kwargs): 

18 """ 

19 Try to convert various geometry objects to the constructor 

20 args for a pyglet indexed vertex list. 

21 

22 Parameters 

23 ------------ 

24 obj : Trimesh, Path2D, Path3D, (n,2) float, (n,3) float 

25 Object to render 

26 

27 Returns 

28 ------------ 

29 args : tuple 

30 Args to be passed to pyglet indexed vertex list 

31 constructor. 

32 """ 

33 if util.is_instance_named(geometry, "Trimesh"): 

34 return mesh_to_vertexlist(geometry, **kwargs) 

35 elif util.is_instance_named(geometry, "Path"): 

36 # works for Path3D and Path2D 

37 # both of which inherit from Path 

38 return path_to_vertexlist(geometry, **kwargs) 

39 elif util.is_instance_named(geometry, "PointCloud"): 

40 # pointcloud objects contain colors 

41 return points_to_vertexlist(geometry.vertices, colors=geometry.colors, **kwargs) 

42 elif util.is_instance_named(geometry, "ndarray"): 

43 # (n,2) or (n,3) points 

44 return points_to_vertexlist(geometry, **kwargs) 

45 elif util.is_instance_named(geometry, "VoxelGrid"): 

46 # for voxels view them as a bunch of boxes 

47 return mesh_to_vertexlist(geometry.as_boxes(**kwargs), **kwargs) 

48 else: 

49 raise ValueError("Geometry passed is not a viewable type!") 

50 

51 

52def mesh_to_vertexlist(mesh, group=None, smooth=True, smooth_threshold=60000): 

53 """ 

54 Convert a Trimesh object to arguments for an 

55 indexed vertex list constructor. 

56 

57 Parameters 

58 ------------- 

59 mesh : trimesh.Trimesh 

60 Mesh to be rendered 

61 group : str 

62 Rendering group for the vertex list 

63 smooth : bool 

64 Should we try to smooth shade the mesh 

65 smooth_threshold : int 

66 Maximum number of faces to smooth shade 

67 

68 Returns 

69 -------------- 

70 args : (7,) tuple 

71 Args for vertex list constructor 

72 

73 """ 

74 # nominally support 2D vertices 

75 if len(mesh.vertices.shape) == 2 and mesh.vertices.shape[1] == 2: 

76 vertices = np.column_stack((mesh.vertices, np.zeros(len(mesh.vertices)))) 

77 else: 

78 vertices = mesh.vertices 

79 

80 if hasattr(mesh.visual, "uv"): 

81 # if the mesh has texture defined pass it to pyglet 

82 vertex_count = len(vertices) 

83 normals = mesh.vertex_normals 

84 faces = mesh.faces 

85 

86 # get the per-vertex UV coordinates 

87 uv = mesh.visual.uv 

88 

89 # shortcut for the material 

90 material = mesh.visual.material 

91 if hasattr(material, "image"): 

92 # does the material actually have an image specified 

93 no_image = material.image is None 

94 elif hasattr(material, "baseColorTexture"): 

95 no_image = material.baseColorTexture is None 

96 else: 

97 no_image = True 

98 

99 # didn't get valid texture so skip it 

100 if uv is None or no_image or len(uv) != vertex_count: 

101 # if no UV coordinates on material, just set face colors 

102 # to the diffuse color of the material 

103 color_gl = colors_to_gl(material.main_color, vertex_count) 

104 else: 

105 # if someone passed (n, 3) UVR cut it off here 

106 if uv.shape[1] > 2: 

107 uv = uv[:, :2] 

108 # texcoord as (2,) float 

109 color_gl = ("t2f/static", uv.astype(np.float64).reshape(-1).tolist()) 

110 

111 elif smooth and len(mesh.faces) < smooth_threshold: 

112 # if we have a small number of faces and colors defined 

113 # smooth the mesh by merging vertices of faces below 

114 # the threshold angle 

115 smooth = mesh.smooth_shaded 

116 vertices = smooth.vertices 

117 vertex_count = len(vertices) 

118 normals = smooth.vertex_normals 

119 faces = smooth.faces 

120 color_gl = colors_to_gl(smooth.visual.vertex_colors, vertex_count) 

121 else: 

122 # we don't have textures or want to smooth so 

123 # send a polygon soup of disconnected triangles to opengl 

124 vertex_count = len(mesh.faces) * 3 

125 normals = np.tile(mesh.face_normals, (1, 3)) 

126 vertices = vertices[mesh.faces] 

127 faces = np.arange(vertex_count, dtype=np.int64) 

128 colors = np.tile(mesh.visual.face_colors, (1, 3)).reshape((-1, 4)) 

129 color_gl = colors_to_gl(colors, vertex_count) 

130 

131 # create the ordered tuple for pyglet, use like: 

132 # `batch.add_indexed(*args)` 

133 args = ( 

134 vertex_count, # number of vertices 

135 GL_TRIANGLES, # mode 

136 group, # group 

137 faces.reshape(-1).tolist(), # indices 

138 ("v3f/static", vertices.reshape(-1).tolist()), 

139 ("n3f/static", normals.reshape(-1).tolist()), 

140 color_gl, 

141 ) 

142 

143 return args 

144 

145 

146def path_to_vertexlist(path, group=None, **kwargs): 

147 """ 

148 Convert a Path3D object to arguments for a 

149 pyglet indexed vertex list constructor. 

150 

151 Parameters 

152 ------------- 

153 path : trimesh.path.Path3D object 

154 Mesh to be rendered 

155 group : str 

156 Rendering group for the vertex list 

157 

158 Returns 

159 -------------- 

160 args : (7,) tuple 

161 Args for vertex list constructor 

162 """ 

163 # avoid cache check inside tight loop 

164 vertices = path.vertices 

165 

166 # get (n, 2, (2|3)) lines 

167 stacked = [util.stack_lines(e.discrete(vertices)) for e in path.entities] 

168 lines = util.vstack_empty(stacked) 

169 count = len(lines) 

170 

171 # stack zeros for 2D lines 

172 if util.is_shape(vertices, (-1, 2)): 

173 lines = lines.reshape((-1, 2)) 

174 lines = np.column_stack((lines, np.zeros(len(lines)))) 

175 # index for GL is one per point 

176 index = np.arange(count).tolist() 

177 # convert from entity color to the color of 

178 # each vertex in the line segments 

179 colors = path.colors 

180 if colors is not None: 

181 colors = np.vstack( 

182 [ 

183 (np.ones((len(s), 4)) * c).astype(np.uint8) 

184 for s, c in zip(stacked, path.colors) 

185 ] 

186 ) 

187 # convert to gl-friendly colors 

188 gl_colors = colors_to_gl(colors, count=count) 

189 

190 # collect args for vertexlist constructor 

191 args = ( 

192 count, # number of lines 

193 GL_LINES, # mode 

194 group, # group 

195 index, # indices 

196 ("v3f/static", lines.reshape(-1)), 

197 gl_colors, 

198 ) 

199 return args 

200 

201 

202def points_to_vertexlist(points, colors=None, group=None, **kwargs): 

203 """ 

204 Convert a numpy array of 3D points to args for 

205 a vertex list constructor. 

206 

207 Parameters 

208 ------------- 

209 points : (n, 3) float 

210 Points to be rendered 

211 colors : (n, 3) or (n, 4) float 

212 Colors for each point 

213 group : str 

214 Rendering group for the vertex list 

215 

216 Returns 

217 -------------- 

218 args : (7,) tuple 

219 Args for vertex list constructor 

220 """ 

221 points = np.asanyarray(points, dtype=np.float64) 

222 

223 if util.is_shape(points, (-1, 2)): 

224 points = np.column_stack((points, np.zeros(len(points)))) 

225 elif not util.is_shape(points, (-1, 3)): 

226 raise ValueError("Pointcloud must be (n,3)!") 

227 

228 index = np.arange(len(points)).tolist() 

229 

230 args = ( 

231 len(points), # number of vertices 

232 GL_POINTS, # mode 

233 group, # group 

234 index, # indices 

235 ("v3f/static", points.reshape(-1)), 

236 colors_to_gl(colors, len(points)), 

237 ) 

238 return args 

239 

240 

241def colors_to_gl(colors, count): 

242 """ 

243 Given a list of colors (or None) return a GL-acceptable 

244 list of colors. 

245 

246 Parameters 

247 ------------ 

248 colors: (count, (3 or 4)) float 

249 Input colors as an array 

250 

251 Returns 

252 --------- 

253 colors_type : str 

254 Color type 

255 colors_gl : (count,) list 

256 Colors to pass to pyglet 

257 """ 

258 

259 colors = np.asanyarray(colors) 

260 count = int(count) 

261 # get the GL kind of color we have 

262 colors_dtypes = {"f": "f", "i": "B", "u": "B"} 

263 

264 if colors.dtype.kind in colors_dtypes: 

265 dtype = colors_dtypes[colors.dtype.kind] 

266 else: 

267 dtype = None 

268 

269 if dtype is not None and util.is_shape(colors, (count, (3, 4))): 

270 # save the shape and dtype for opengl color string 

271 colors_type = f"c{colors.shape[1]}{dtype}/static" 

272 # reshape the 2D array into a 1D one and then convert to a python list 

273 gl_colors = colors.reshape(-1).tolist() 

274 elif dtype is not None and colors.shape in [(3,), (4,)]: 

275 # we've been passed a single color so tile them 

276 gl_colors = ( 

277 (np.ones((count, colors.size), dtype=colors.dtype) * colors) 

278 .reshape(-1) 

279 .tolist() 

280 ) 

281 # we know we're tiling 

282 colors_type = f"c{colors.size}{dtype}/static" 

283 else: 

284 # case where colors are wrong shape 

285 # use black as the default color 

286 gl_colors = np.tile([0.0, 0.0, 0.0], (count, 1)).reshape(-1).tolist() 

287 # we're returning RGB float colors 

288 colors_type = "c3f/static" 

289 

290 return colors_type, gl_colors 

291 

292 

293def material_to_texture(material, upsize=True): 

294 """ 

295 Convert a trimesh.visual.texture.Material object into 

296 a pyglet-compatible texture object. 

297 

298 Parameters 

299 -------------- 

300 material : trimesh.visual.texture.Material 

301 Material to be converted 

302 upsize: bool 

303 If True, will upscale textures to their nearest power 

304 of two resolution to avoid weirdness 

305 

306 Returns 

307 --------------- 

308 texture : pyglet.image.Texture 

309 Texture loaded into pyglet form 

310 """ 

311 import pyglet 

312 

313 # try to extract a PIL image from material 

314 if hasattr(material, "image"): 

315 img = material.image 

316 elif hasattr(material, "baseColorTexture"): 

317 img = material.baseColorTexture 

318 else: 

319 return None 

320 

321 # if no images in texture return now 

322 if img is None: 

323 return None 

324 

325 # if we're not powers of two upsize 

326 if upsize: 

327 from .visual.texture import power_resize 

328 

329 img = power_resize(img) 

330 

331 # use a PNG export to exchange into pyglet 

332 # probably a way to do this with a PIL converter 

333 with util.BytesIO() as f: 

334 # export PIL image as PNG 

335 img.save(f, format="png") 

336 f.seek(0) 

337 # filename used for format guess 

338 gl_image = pyglet.image.load(filename=".png", file=f) 

339 

340 # turn image into pyglet texture 

341 texture = gl_image.get_texture() 

342 

343 return texture 

344 

345 

346def matrix_to_gl(matrix): 

347 """ 

348 Convert a numpy row-major homogeneous transformation matrix 

349 to a flat column-major GLfloat transformation. 

350 

351 Parameters 

352 ------------- 

353 matrix : (4,4) float 

354 Row-major homogeneous transform 

355 

356 Returns 

357 ------------- 

358 glmatrix : (16,) gl.GLfloat 

359 Transform in pyglet format 

360 """ 

361 from pyglet import gl 

362 

363 # convert to GLfloat, switch to column major and flatten to (16,) 

364 return (gl.GLfloat * 16)(*np.array(matrix, dtype=np.float32).T.ravel()) 

365 

366 

367def vector_to_gl(array, *args): 

368 """ 

369 Convert an array and an optional set of args into a 

370 flat vector of gl.GLfloat 

371 """ 

372 from pyglet import gl 

373 

374 array = np.array(array) 

375 if len(args) > 0: 

376 array = np.append(array, args) 

377 vector = (gl.GLfloat * len(array))(*array) 

378 return vector 

379 

380 

381def light_to_gl(light, transform, lightN): 

382 """ 

383 Convert trimesh.scene.lighting.Light objects into 

384 args for gl.glLightFv calls 

385 

386 Parameters 

387 -------------- 

388 light : trimesh.scene.lighting.Light 

389 Light object to be converted to GL 

390 transform : (4, 4) float 

391 Transformation matrix of light 

392 lightN : int 

393 Result of gl.GL_LIGHT0, gl.GL_LIGHT1, etc 

394 

395 Returns 

396 -------------- 

397 multiarg : [tuple] 

398 List of args to pass to gl.glLightFv eg: 

399 [gl.glLightfb(*a) for a in multiarg] 

400 """ 

401 from pyglet import gl 

402 

403 # convert color to opengl 

404 gl_color = vector_to_gl(light.color.astype(np.float64) / 255.0) 

405 assert len(gl_color) == 4 

406 

407 # cartesian translation from matrix 

408 gl_position = vector_to_gl(transform[:3, 3]) 

409 

410 # create the different position and color arguments 

411 args = [ 

412 (lightN, gl.GL_POSITION, gl_position), 

413 (lightN, gl.GL_SPECULAR, gl_color), 

414 (lightN, gl.GL_DIFFUSE, gl_color), 

415 (lightN, gl.GL_AMBIENT, gl_color), 

416 ] 

417 return args