Coverage for trimesh/viewer/windowed.py: 77%

395 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-02 20:54 +0000

1""" 

2windowed.py 

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

4 

5Provides a pyglet- based windowed viewer to preview 

6Trimesh, Scene, PointCloud, and Path objects. 

7 

8Works on all major platforms: Windows, Linux, and OSX. 

9""" 

10 

11import collections 

12 

13import numpy as np 

14import pyglet 

15 

16# pyglet 2.0 is close to a re-write moving from fixed-function 

17# to shaders and we will likely support it by forking an entirely 

18# new viewer `trimesh.viewer.shaders` and then basically keeping 

19# `windowed` around for backwards-compatibility with no changes 

20if int(pyglet.version.split(".")[0]) >= 2: 

21 raise ImportError('`trimesh.viewer.windowed` requires `pip install "pyglet<2"`') 

22 

23from .. import rendering, util 

24from ..transformations import translation_matrix 

25from ..typed import ArrayLike, Callable, Iterable, Number 

26from ..visual import to_rgba 

27from .trackball import Trackball 

28 

29pyglet.options["shadow_window"] = False 

30 

31import pyglet.gl as gl # NOQA 

32 

33# help message for the viewer 

34_HELP_MESSAGE = """ 

35# SceneViewer Controls 

36 

37| Input | Action | 

38|----------------------------|-----------------------------------------------| 

39| `mouse click + drag` | Rotates the view | 

40| `ctl + mouse click + drag` | Pans the view | 

41| `mouse wheel` | Zooms the view | 

42| `z` | Resets to the initial view | 

43| `w` | Toggles wireframe mode | 

44| `c` | Toggles backface culling | 

45| `g` | Toggles an XY grid with Z set to lowest point | 

46| `a` | Toggles an XYZ-RGB axis marker between: off, | 

47| | at world frame, or at every frame and world, | 

48| | and at every frame | 

49| `f` | Toggles between fullscreen and windowed mode | 

50| `h` | Prints this help message | 

51| `m` | Maximizes the window | 

52| `q` | Closes the window | 

53|----------------------------|-----------------------------------------------| 

54""" 

55 

56 

57class SceneViewer(pyglet.window.Window): 

58 def __init__( 

59 self, 

60 scene, 

61 smooth: bool = True, 

62 flags: dict | None = None, 

63 visible: bool = True, 

64 resolution: ArrayLike | None = None, 

65 fullscreen: bool = False, 

66 resizable: bool = True, 

67 start_loop: bool = True, 

68 callback: Callable | None = None, 

69 callback_period: Number | None = None, 

70 caption: str | None = None, 

71 fixed: Iterable | None = None, 

72 offset_lines: bool = True, 

73 line_settings: dict | None = None, 

74 background=None, 

75 window_conf=None, 

76 profile: bool = False, 

77 record: bool = False, 

78 **kwargs, 

79 ): 

80 """ 

81 Create a window that will display a trimesh.Scene object 

82 in an OpenGL context via pyglet. 

83 

84 Parameters 

85 --------------- 

86 scene : trimesh.scene.Scene 

87 Scene with geometry and transforms 

88 smooth 

89 If True try to smooth shade things 

90 flags 

91 If passed apply keys to self.view: 

92 ['cull', 'wireframe', etc] 

93 visible 

94 Display window or not 

95 resolution 

96 Initial resolution of window 

97 fullscreen 

98 Determines whether the window is rendered in fullscreen mode. 

99 resizable 

100 Determines whether the rendered window can be resized by the user. 

101 start_loop 

102 Call pyglet.app.run() at the end of init 

103 callback 

104 A function which can be called periodically to 

105 update things in the scene 

106 callback_period 

107 How often to call the callback, in seconds 

108 caption 

109 Caption for the window title 

110 fixed 

111 List of keys in scene.geometry to skip view 

112 transform on to keep fixed relative to camera 

113 offset_lines 

114 If True, will offset lines slightly so if drawn 

115 coplanar with mesh geometry they will be visible 

116 line_settings 

117 Override default line width and point size with keys 

118 'line_width' and 'point_size' in pixels 

119 background 

120 Color for background 

121 window_conf 

122 Passed to window init 

123 profile 

124 If set will run a `pyinstrument` profile for 

125 every call to `on_draw` and print the output. 

126 record 

127 If True, will save a list of `png` bytes to 

128 a list located in `scene.metadata['recording']` 

129 kwargs 

130 Additional arguments to pass, including 

131 'background' for to set background color 

132 """ 

133 self.scene = self._scene = scene 

134 

135 self.callback = callback 

136 self.callback_period = callback_period 

137 self.scene._redraw = self._redraw 

138 self.offset_lines = bool(offset_lines) 

139 self.background = background 

140 # save initial camera transform 

141 self._initial_camera_transform = scene.camera_transform.copy() 

142 

143 # a transform to offset lines slightly to avoid Z-fighting 

144 self._line_offset = translation_matrix( 

145 [0, 0, scene.scale / 1000 if self.offset_lines else 0] 

146 ) 

147 

148 self.reset_view() 

149 self.batch = pyglet.graphics.Batch() 

150 self._smooth = smooth 

151 

152 self._profile = bool(profile) 

153 if self._profile: 

154 from pyinstrument import Profiler 

155 

156 self.Profiler = Profiler 

157 

158 self._record = bool(record) 

159 if self._record: 

160 # will save bytes here 

161 self.scene.metadata["recording"] = [] 

162 

163 # store kwargs 

164 self.kwargs = kwargs 

165 

166 # store a vertexlist for an axis marker 

167 self._axis = None 

168 # store a vertexlist for a grid display 

169 self._grid = None 

170 # store scene geometry as vertex lists 

171 self.vertex_list = {} 

172 # store geometry hashes 

173 self.vertex_list_hash = {} 

174 # store geometry rendering mode 

175 self.vertex_list_mode = {} 

176 # store meshes that don't rotate relative to viewer 

177 self.fixed = fixed 

178 # store a hidden (don't not display) node. 

179 self._nodes_hidden = set() 

180 # name : texture 

181 self.textures = {} 

182 

183 # if resolution isn't defined set a default value 

184 if resolution is None: 

185 resolution = scene.camera.resolution 

186 else: 

187 scene.camera.resolution = resolution 

188 

189 if caption is None: 

190 caption = "Trimesh SceneViewer (`h` for help)" 

191 

192 # set the default line settings to a fraction 

193 # of our resolution so the points aren't tiny 

194 scale = max(resolution) 

195 self.line_settings = {"point_size": scale / 200, "line_width": scale / 400} 

196 # if we've been passed line settings override the default 

197 if line_settings is not None: 

198 self.line_settings.update(line_settings) 

199 

200 # no window conf was passed so try to get the best looking one 

201 if window_conf is None: 

202 try: 

203 # try enabling antialiasing 

204 # if you have a graphics card this will probably work 

205 conf = gl.Config( 

206 sample_buffers=1, samples=4, depth_size=24, double_buffer=True 

207 ) 

208 super().__init__( 

209 config=conf, 

210 visible=visible, 

211 fullscreen=fullscreen, 

212 resizable=resizable, 

213 width=resolution[0], 

214 height=resolution[1], 

215 caption=caption, 

216 ) 

217 except pyglet.window.NoSuchConfigException: 

218 conf = gl.Config(double_buffer=True) 

219 super().__init__( 

220 config=conf, 

221 fullscreen=fullscreen, 

222 resizable=resizable, 

223 visible=visible, 

224 width=resolution[0], 

225 height=resolution[1], 

226 caption=caption, 

227 ) 

228 else: 

229 # window config was manually passed 

230 super().__init__( 

231 config=window_conf, 

232 fullscreen=fullscreen, 

233 resizable=resizable, 

234 visible=visible, 

235 width=resolution[0], 

236 height=resolution[1], 

237 caption=caption, 

238 ) 

239 

240 # add scene geometry to viewer geometry 

241 self._update_vertex_list() 

242 

243 # call after geometry is added 

244 self.init_gl() 

245 self.set_size(*resolution) 

246 if flags is not None: 

247 self.reset_view(flags=flags) 

248 self.update_flags() 

249 

250 # someone has passed a callback to be called periodically 

251 if self.callback is not None: 

252 # if no callback period is specified set it to default 

253 if callback_period is None: 

254 # 30 times per second 

255 callback_period = 1.0 / 30.0 

256 # set up a do-nothing periodic task which will 

257 # trigger `self.on_draw` every `callback_period` 

258 # seconds if someone has passed a callback 

259 pyglet.clock.schedule_interval(lambda x: x, callback_period) 

260 if start_loop: 

261 pyglet.app.run() 

262 

263 def _redraw(self): 

264 self.on_draw() 

265 

266 def _update_vertex_list(self): 

267 # update vertex_list if needed 

268 for name, geom in self.scene.geometry.items(): 

269 if geom.is_empty: 

270 continue 

271 if _geometry_hash(geom) == self.vertex_list_hash.get(name): 

272 continue 

273 self.add_geometry(name=name, geometry=geom, smooth=bool(self._smooth)) 

274 

275 def _update_meshes(self): 

276 # call the callback if specified 

277 if self.callback is not None: 

278 self.callback(self.scene) 

279 self._update_vertex_list() 

280 self._update_perspective(self.width, self.height) 

281 

282 def add_geometry(self, name, geometry, **kwargs): 

283 """ 

284 Add a geometry to the viewer. 

285 

286 Parameters 

287 -------------- 

288 name : hashable 

289 Name that references geometry 

290 geometry : Trimesh, Path2D, Path3D, PointCloud 

291 Geometry to display in the viewer window 

292 kwargs ** 

293 Passed to rendering.convert_to_vertexlist 

294 """ 

295 try: 

296 # convert geometry to constructor args 

297 args = rendering.convert_to_vertexlist(geometry, **kwargs) 

298 except BaseException: 

299 util.log.warning(f"failed to add geometry `{name}`", exc_info=True) 

300 return 

301 

302 # delete old vertex list with the same name 

303 if name in self.vertex_list: 

304 self.vertex_list[name].delete() 

305 

306 # create the indexed vertex list 

307 self.vertex_list[name] = self.batch.add_indexed(*args) 

308 # save the hash of the geometry 

309 self.vertex_list_hash[name] = _geometry_hash(geometry) 

310 # save the rendering mode from the constructor args 

311 self.vertex_list_mode[name] = args[1] 

312 

313 # get the visual if the element has it 

314 visual = getattr(geometry, "visual", None) 

315 if hasattr(visual, "uv") and hasattr(visual, "material"): 

316 try: 

317 tex = rendering.material_to_texture(visual.material) 

318 if tex is not None: 

319 self.textures[name] = tex 

320 except BaseException: 

321 util.log.warning("failed to load texture", exc_info=True) 

322 

323 def cleanup_geometries(self): 

324 """ 

325 Remove any stored vertex lists that no longer 

326 exist in the scene. 

327 """ 

328 # shorthand to scene graph 

329 graph = self.scene.graph 

330 # which parts of the graph still have geometry 

331 geom_keep = {graph[node][1] for node in graph.nodes_geometry} 

332 # which geometries no longer need to be kept 

333 geom_delete = [geom for geom in self.vertex_list if geom not in geom_keep] 

334 for geom in geom_delete: 

335 # delete the vertex list 

336 if geom in self.vertex_list: 

337 self.vertex_list[geom].delete() 

338 # remove stored vertex references 

339 self.vertex_list.pop(geom, None) 

340 self.vertex_list_hash.pop(geom, None) 

341 self.vertex_list_mode.pop(geom, None) 

342 self.textures.pop(geom, None) 

343 

344 def unhide_geometry(self, node): 

345 """ 

346 If a node is hidden remove the flag and show the 

347 geometry on the next draw. 

348 

349 Parameters 

350 ------------- 

351 node : str 

352 Node to display 

353 """ 

354 self._nodes_hidden.discard(node) 

355 

356 def hide_geometry(self, node): 

357 """ 

358 Don't display the geometry contained at a node on 

359 the next draw. 

360 

361 Parameters 

362 ------------- 

363 node : str 

364 Node to not display 

365 """ 

366 self._nodes_hidden.add(node) 

367 

368 def reset_view(self, flags=None): 

369 """ 

370 Set view to the default view. 

371 

372 Parameters 

373 -------------- 

374 flags : None or dict 

375 If any view key passed override the default 

376 e.g. {'cull': False} 

377 """ 

378 self.view = { 

379 "cull": True, 

380 "axis": False, 

381 "grid": False, 

382 "fullscreen": False, 

383 "wireframe": False, 

384 "ball": Trackball( 

385 pose=self._initial_camera_transform, 

386 size=self.scene.camera.resolution, 

387 scale=self.scene.scale, 

388 target=self.scene.centroid, 

389 ), 

390 } 

391 try: 

392 # if any flags are passed override defaults 

393 if isinstance(flags, dict): 

394 for k, v in flags.items(): 

395 if k in self.view: 

396 self.view[k] = v 

397 self.update_flags() 

398 except BaseException: 

399 pass 

400 self.scene.camera_transform = self.view["ball"].pose 

401 

402 def init_gl(self): 

403 """ 

404 Perform the magic incantations to create an 

405 OpenGL scene using pyglet. 

406 """ 

407 

408 # if user passed a background color use it 

409 if self.background is None: 

410 # default background color is white 

411 background = np.ones(4) 

412 else: 

413 # convert to (4,) uint8 RGBA 

414 background = to_rgba(self.background) 

415 # convert to 0.0-1.0 float 

416 background = background.astype(np.float64) / 255.0 

417 

418 self._gl_set_background(background) 

419 # use camera setting for depth 

420 self._gl_enable_depth(self.scene.camera) 

421 self._gl_enable_color_material() 

422 self._gl_enable_blending() 

423 self._gl_enable_smooth_lines(**self.line_settings) 

424 self._gl_enable_lighting(self.scene) 

425 

426 @staticmethod 

427 def _gl_set_background(background): 

428 gl.glClearColor(*background) 

429 

430 @staticmethod 

431 def _gl_unset_background(): 

432 gl.glClearColor(*[0, 0, 0, 0]) 

433 

434 @staticmethod 

435 def _gl_enable_depth(camera): 

436 """ 

437 Enable depth test in OpenGL using distances 

438 from `scene.camera`. 

439 """ 

440 gl.glClearDepth(1.0) 

441 gl.glEnable(gl.GL_DEPTH_TEST) 

442 gl.glDepthFunc(gl.GL_LEQUAL) 

443 

444 gl.glEnable(gl.GL_DEPTH_TEST) 

445 gl.glEnable(gl.GL_CULL_FACE) 

446 

447 @staticmethod 

448 def _gl_enable_color_material(): 

449 # do some openGL things 

450 gl.glColorMaterial(gl.GL_FRONT_AND_BACK, gl.GL_AMBIENT_AND_DIFFUSE) 

451 gl.glEnable(gl.GL_COLOR_MATERIAL) 

452 gl.glShadeModel(gl.GL_SMOOTH) 

453 

454 gl.glMaterialfv( 

455 gl.GL_FRONT, 

456 gl.GL_AMBIENT, 

457 rendering.vector_to_gl(0.192250, 0.192250, 0.192250), 

458 ) 

459 gl.glMaterialfv( 

460 gl.GL_FRONT, 

461 gl.GL_DIFFUSE, 

462 rendering.vector_to_gl(0.507540, 0.507540, 0.507540), 

463 ) 

464 gl.glMaterialfv( 

465 gl.GL_FRONT, 

466 gl.GL_SPECULAR, 

467 rendering.vector_to_gl(0.5082730, 0.5082730, 0.5082730), 

468 ) 

469 

470 gl.glMaterialf(gl.GL_FRONT, gl.GL_SHININESS, 0.4 * 128.0) 

471 

472 @staticmethod 

473 def _gl_enable_blending(): 

474 # enable blending for transparency 

475 gl.glEnable(gl.GL_BLEND) 

476 gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 

477 

478 @staticmethod 

479 def _gl_enable_smooth_lines(line_width=4, point_size=4): 

480 # make the lines from Path3D objects less ugly 

481 gl.glEnable(gl.GL_LINE_SMOOTH) 

482 gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) 

483 # set the width of lines to 4 pixels 

484 gl.glLineWidth(line_width) 

485 # set PointCloud markers to 4 pixels in size 

486 gl.glPointSize(point_size) 

487 

488 @staticmethod 

489 def _gl_enable_lighting(scene): 

490 """ 

491 Take the lights defined in scene.lights and 

492 apply them as openGL lights. 

493 """ 

494 gl.glEnable(gl.GL_LIGHTING) 

495 # opengl only supports 7 lights? 

496 for i, light in enumerate(scene.lights[:7]): 

497 # the index of which light we have 

498 lightN = getattr(gl, f"GL_LIGHT{i}") 

499 

500 # get the transform for the light by name 

501 matrix = scene.graph.get(light.name)[0] 

502 

503 # convert light object to glLightfv calls 

504 multiargs = rendering.light_to_gl( 

505 light=light, transform=matrix, lightN=lightN 

506 ) 

507 

508 # enable the light in question 

509 gl.glEnable(lightN) 

510 # run the glLightfv calls 

511 for args in multiargs: 

512 gl.glLightfv(*args) 

513 

514 def toggle_culling(self): 

515 """ 

516 Toggle back face culling. 

517 

518 It is on by default but if you are dealing with 

519 non- watertight meshes you probably want to be able 

520 to see the back sides. 

521 """ 

522 self.view["cull"] = not self.view["cull"] 

523 self.update_flags() 

524 

525 def toggle_wireframe(self): 

526 """ 

527 Toggle wireframe mode 

528 

529 Good for looking inside meshes, off by default. 

530 """ 

531 self.view["wireframe"] = not self.view["wireframe"] 

532 self.update_flags() 

533 

534 def toggle_fullscreen(self): 

535 """ 

536 Toggle between fullscreen and windowed mode. 

537 """ 

538 self.view["fullscreen"] = not self.view["fullscreen"] 

539 self.update_flags() 

540 

541 def toggle_axis(self): 

542 """ 

543 Toggle a rendered XYZ/RGB axis marker: 

544 off, world frame, every frame 

545 """ 

546 # cycle through three axis states 

547 states = [False, "world", "all", "without_world"] 

548 # the state after toggling 

549 index = (states.index(self.view["axis"]) + 1) % len(states) 

550 # update state to next index 

551 self.view["axis"] = states[index] 

552 # perform gl actions 

553 self.update_flags() 

554 

555 def toggle_grid(self): 

556 """ 

557 Toggle a rendered grid. 

558 """ 

559 # update state to next index 

560 self.view["grid"] = not self.view["grid"] 

561 # perform gl actions 

562 self.update_flags() 

563 

564 def update_flags(self): 

565 """ 

566 Check the view flags, and call required GL functions. 

567 """ 

568 # view mode, filled vs wirefrom 

569 if self.view["wireframe"]: 

570 gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE) 

571 else: 

572 gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL) 

573 

574 # set fullscreen or windowed 

575 self.set_fullscreen(fullscreen=self.view["fullscreen"]) 

576 

577 # backface culling on or off 

578 if self.view["cull"]: 

579 gl.glEnable(gl.GL_CULL_FACE) 

580 else: 

581 gl.glDisable(gl.GL_CULL_FACE) 

582 

583 # case where we WANT an axis and NO vertexlist 

584 # is stored internally 

585 if self.view["axis"] and self._axis is None: 

586 from .. import creation 

587 

588 # create an axis marker sized relative to the scene 

589 axis = creation.axis(origin_size=self.scene.scale / 100) 

590 # create ordered args for a vertex list 

591 args = rendering.mesh_to_vertexlist(axis) 

592 # store the axis as a reference 

593 self._axis = self.batch.add_indexed(*args) 

594 # case where we DON'T want an axis but a vertexlist 

595 # IS stored internally 

596 elif not self.view["axis"] and self._axis is not None: 

597 # remove the axis from the rendering batch 

598 self._axis.delete() 

599 # set the reference to None 

600 self._axis = None 

601 

602 if self.view["grid"] and self._grid is None: 

603 try: 

604 # create a grid marker 

605 from ..path.creation import grid 

606 

607 bounds = self.scene.bounds 

608 center = bounds.mean(axis=0) 

609 # set the grid to the lowest Z position 

610 # also offset by the scale to avoid interference 

611 center[2] = bounds[0][2] - (np.ptp(bounds[:, 2]) / 100) 

612 # choose the side length by maximum XY length 

613 side = np.ptp(bounds, axis=0)[:2].max() 

614 # create an axis marker sized relative to the scene 

615 grid_mesh = grid(side=side, count=4, transform=translation_matrix(center)) 

616 # convert the path to vertexlist args 

617 args = rendering.convert_to_vertexlist(grid_mesh) 

618 # create ordered args for a vertex list 

619 self._grid = self.batch.add_indexed(*args) 

620 except BaseException: 

621 util.log.warning("failed to create grid!", exc_info=True) 

622 elif not self.view["grid"] and self._grid is not None: 

623 self._grid.delete() 

624 self._grid = None 

625 

626 def _update_perspective(self, width, height): 

627 try: 

628 # for high DPI screens viewport size 

629 # will be different then the passed size 

630 width, height = self.get_viewport_size() 

631 except BaseException: 

632 # older versions of pyglet may not have this 

633 pass 

634 

635 # set the new viewport size 

636 gl.glViewport(0, 0, width, height) 

637 gl.glMatrixMode(gl.GL_PROJECTION) 

638 gl.glLoadIdentity() 

639 

640 # get field of view and Z range from camera 

641 camera = self.scene.camera 

642 

643 # set perspective from camera data 

644 gl.gluPerspective( 

645 camera.fov[1], width / float(height), camera.z_near, camera.z_far 

646 ) 

647 gl.glMatrixMode(gl.GL_MODELVIEW) 

648 

649 return width, height 

650 

651 def on_resize(self, width, height): 

652 """ 

653 Handle resized windows. 

654 """ 

655 width, height = self._update_perspective(width, height) 

656 self.scene.camera.resolution = (width, height) 

657 self.view["ball"].resize(self.scene.camera.resolution) 

658 self.scene.camera_transform = self.view["ball"].pose 

659 

660 def on_mouse_press(self, x, y, buttons, modifiers): 

661 """ 

662 Set the start point of the drag. 

663 """ 

664 self.view["ball"].set_state(Trackball.STATE_ROTATE) 

665 if buttons == pyglet.window.mouse.LEFT: 

666 ctrl = modifiers & pyglet.window.key.MOD_CTRL 

667 shift = modifiers & pyglet.window.key.MOD_SHIFT 

668 if ctrl and shift: 

669 self.view["ball"].set_state(Trackball.STATE_ZOOM) 

670 elif shift: 

671 self.view["ball"].set_state(Trackball.STATE_ROLL) 

672 elif ctrl: 

673 self.view["ball"].set_state(Trackball.STATE_PAN) 

674 elif buttons == pyglet.window.mouse.MIDDLE: 

675 self.view["ball"].set_state(Trackball.STATE_PAN) 

676 elif buttons == pyglet.window.mouse.RIGHT: 

677 self.view["ball"].set_state(Trackball.STATE_ZOOM) 

678 

679 self.view["ball"].down(np.array([x, y])) 

680 self.scene.camera_transform = self.view["ball"].pose 

681 

682 def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): 

683 """ 

684 Pan or rotate the view. 

685 """ 

686 self.view["ball"].drag(np.array([x, y])) 

687 self.scene.camera_transform = self.view["ball"].pose 

688 

689 def on_mouse_scroll(self, x, y, dx, dy): 

690 """ 

691 Zoom the view. 

692 """ 

693 self.view["ball"].scroll(dy) 

694 self.scene.camera_transform = self.view["ball"].pose 

695 

696 def on_key_press(self, symbol, modifiers): 

697 """ 

698 Call appropriate functions given key presses. 

699 """ 

700 magnitude = 10 

701 if symbol == pyglet.window.key.W: 

702 self.toggle_wireframe() 

703 elif symbol == pyglet.window.key.Z: 

704 self.reset_view() 

705 elif symbol == pyglet.window.key.C: 

706 self.toggle_culling() 

707 elif symbol == pyglet.window.key.A: 

708 self.toggle_axis() 

709 elif symbol == pyglet.window.key.G: 

710 self.toggle_grid() 

711 elif symbol == pyglet.window.key.Q: 

712 self.on_close() 

713 elif symbol == pyglet.window.key.M: 

714 self.maximize() 

715 elif symbol == pyglet.window.key.F: 

716 self.toggle_fullscreen() 

717 elif symbol == pyglet.window.key.H: 

718 print(_HELP_MESSAGE) # noqa: T201 

719 

720 if symbol in [ 

721 pyglet.window.key.LEFT, 

722 pyglet.window.key.RIGHT, 

723 pyglet.window.key.DOWN, 

724 pyglet.window.key.UP, 

725 ]: 

726 self.view["ball"].down([0, 0]) 

727 if symbol == pyglet.window.key.LEFT: 

728 self.view["ball"].drag([-magnitude, 0]) 

729 elif symbol == pyglet.window.key.RIGHT: 

730 self.view["ball"].drag([magnitude, 0]) 

731 elif symbol == pyglet.window.key.DOWN: 

732 self.view["ball"].drag([0, -magnitude]) 

733 elif symbol == pyglet.window.key.UP: 

734 self.view["ball"].drag([0, magnitude]) 

735 self.scene.camera_transform = self.view["ball"].pose 

736 

737 def on_draw(self): 

738 """ 

739 Run the actual draw calls. 

740 """ 

741 

742 if self._profile: 

743 profiler = self.Profiler() 

744 profiler.start() 

745 

746 self._update_meshes() 

747 gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) 

748 gl.glLoadIdentity() 

749 

750 # pull the new camera transform from the scene 

751 transform_camera = np.linalg.inv(self.scene.camera_transform) 

752 

753 # apply the camera transform to the matrix stack 

754 gl.glMultMatrixf(rendering.matrix_to_gl(transform_camera)) 

755 

756 # we want to render fully opaque objects first, 

757 # followed by objects which have transparency 

758 node_names = collections.deque(self.scene.graph.nodes_geometry) 

759 # how many nodes did we start with 

760 count_original = len(node_names) 

761 count = -1 

762 

763 # if we are rendering an axis marker at the world 

764 if self._axis and not self.view["axis"] == "without_world": 

765 # we stored it as a vertex list 

766 self._axis.draw(mode=gl.GL_TRIANGLES) 

767 if self._grid: 

768 self._grid.draw(mode=gl.GL_LINES) 

769 

770 # save a reference outside of the loop 

771 geometry = self.scene.geometry 

772 graph = self.scene.graph 

773 

774 while len(node_names) > 0: 

775 count += 1 

776 current_node = node_names.popleft() 

777 

778 if current_node in self._nodes_hidden: 

779 continue 

780 

781 # get the transform from world to geometry and mesh name 

782 transform, geometry_name = graph.get(current_node) 

783 # if no geometry at this frame continue without rendering 

784 if geometry_name is None or geometry_name not in self.vertex_list_mode: 

785 continue 

786 

787 # if a geometry is marked as fixed apply the inverse view transform 

788 if self.fixed is not None and geometry_name in self.fixed: 

789 # remove altered camera transform from fixed geometry 

790 transform_fix = np.linalg.inv( 

791 np.dot(self._initial_camera_transform, transform_camera) 

792 ) 

793 # apply the transform so the fixed geometry doesn't move 

794 transform = np.dot(transform, transform_fix) 

795 

796 # get a reference to the mesh so we can check transparency 

797 mesh = geometry[geometry_name] 

798 if mesh.is_empty: 

799 continue 

800 # get the GL mode of the current geometry 

801 mode = self.vertex_list_mode[geometry_name] 

802 

803 # if you draw a coplanar line with a triangle it will z-fight 

804 # the best way to do this is probably a shader but this works fine 

805 if mode == gl.GL_LINES: 

806 # apply the offset in camera space 

807 transform = util.multi_dot( 

808 [ 

809 transform, 

810 np.linalg.inv(transform_camera), 

811 self._line_offset, 

812 transform_camera, 

813 ] 

814 ) 

815 

816 # add a new matrix to the model stack 

817 gl.glPushMatrix() 

818 # transform by the nodes transform 

819 gl.glMultMatrixf(rendering.matrix_to_gl(transform)) 

820 

821 # draw an axis marker for each mesh frame 

822 if self.view["axis"] == "all": 

823 self._axis.draw(mode=gl.GL_TRIANGLES) 

824 elif self.view["axis"] == "without_world": 

825 if not util.allclose(transform, np.eye(4), atol=1e-5): 

826 self._axis.draw(mode=gl.GL_TRIANGLES) 

827 

828 # transparent things must be drawn last 

829 if ( 

830 hasattr(mesh, "visual") 

831 and hasattr(mesh.visual, "transparency") 

832 and mesh.visual.transparency 

833 ): 

834 # put the current item onto the back of the queue 

835 if count < count_original: 

836 # add the node to be drawn last 

837 node_names.append(current_node) 

838 # pop the matrix stack for now 

839 gl.glPopMatrix() 

840 # come back to this mesh later 

841 continue 

842 

843 # if we have texture enable the target texture 

844 texture = None 

845 if geometry_name in self.textures: 

846 texture = self.textures[geometry_name] 

847 gl.glEnable(texture.target) 

848 gl.glBindTexture(texture.target, texture.id) 

849 

850 # draw the mesh with its transform applied 

851 self.vertex_list[geometry_name].draw(mode=mode) 

852 # pop the matrix stack as we drew what we needed to draw 

853 gl.glPopMatrix() 

854 

855 # disable texture after using 

856 if texture is not None: 

857 gl.glDisable(texture.target) 

858 

859 if self._profile: 

860 profiler.stop() 

861 util.log.debug(profiler.output_text(unicode=True, color=True)) 

862 

863 def flip(self): 

864 super().flip() 

865 if self._record: 

866 # will save a PNG-encoded bytes 

867 img = self.save_image(util.BytesIO()) 

868 # seek start of file-like object 

869 img.seek(0) 

870 # save the bytes from the file object 

871 self.scene.metadata["recording"].append(img.read()) 

872 

873 def save_image(self, file_obj): 

874 """ 

875 Save the current color buffer to a file object 

876 in PNG format. 

877 

878 Parameters 

879 ------------- 

880 file_obj: file name, or file- like object 

881 """ 

882 manager = pyglet.image.get_buffer_manager() 

883 colorbuffer = manager.get_color_buffer() 

884 # if passed a string save by name 

885 if hasattr(file_obj, "write"): 

886 colorbuffer.save(file=file_obj) 

887 else: 

888 colorbuffer.save(filename=file_obj) 

889 return file_obj 

890 

891 

892def _geometry_hash(geometry): 

893 """ 

894 Get a hash for a geometry object 

895 

896 Parameters 

897 ------------ 

898 geometry : object 

899 

900 Returns 

901 ------------ 

902 hash : str 

903 """ 

904 h = str(hash(geometry)) 

905 if hasattr(geometry, "visual"): 

906 # if visual properties are defined 

907 h += str(hash(geometry.visual)) 

908 

909 return h 

910 

911 

912def render_scene( 

913 scene, resolution=None, visible=True, fullscreen=False, resizable=True, **kwargs 

914): 

915 """ 

916 Render a preview of a scene to a PNG. Note that 

917 whether this works or not highly variable based on 

918 platform and graphics driver. 

919 

920 Parameters 

921 ------------ 

922 scene : trimesh.Scene 

923 Geometry to be rendered 

924 resolution : (2,) int or None 

925 Resolution in pixels or set from scene.camera 

926 visible : bool 

927 Show a window during rendering. Note that MANY 

928 platforms refuse to render with hidden windows 

929 and will likely return a blank image; this is a 

930 platform issue and cannot be fixed in Python. 

931 fullscreen : bool 

932 Determines whether the window is rendered in fullscreen mode. 

933 Defaults to False (windowed). 

934 resizable : bool 

935 Determines whether the rendered window can be resized by the user. 

936 Defaults to True (resizable). 

937 kwargs : ** 

938 Passed to SceneViewer 

939 

940 Returns 

941 --------- 

942 render : bytes 

943 Image in PNG format 

944 """ 

945 window = SceneViewer( 

946 scene, 

947 start_loop=False, 

948 visible=visible, 

949 resolution=resolution, 

950 fullscreen=fullscreen, 

951 resizable=resizable, 

952 **kwargs, 

953 ) 

954 

955 from ..util import BytesIO 

956 

957 # need to run loop twice to display anything 

958 for save in [False, False, True]: 

959 pyglet.clock.tick() 

960 window.switch_to() 

961 window.dispatch_events() 

962 window.dispatch_event("on_draw") 

963 window.flip() 

964 if save: 

965 # save the color buffer data to memory 

966 file_obj = BytesIO() 

967 window.save_image(file_obj) 

968 file_obj.seek(0) 

969 render = file_obj.read() 

970 window.close() 

971 

972 return render