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
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-02 20:54 +0000
1"""
2windowed.py
3---------------
5Provides a pyglet- based windowed viewer to preview
6Trimesh, Scene, PointCloud, and Path objects.
8Works on all major platforms: Windows, Linux, and OSX.
9"""
11import collections
13import numpy as np
14import pyglet
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"`')
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
29pyglet.options["shadow_window"] = False
31import pyglet.gl as gl # NOQA
33# help message for the viewer
34_HELP_MESSAGE = """
35# SceneViewer Controls
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"""
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.
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
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()
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 )
148 self.reset_view()
149 self.batch = pyglet.graphics.Batch()
150 self._smooth = smooth
152 self._profile = bool(profile)
153 if self._profile:
154 from pyinstrument import Profiler
156 self.Profiler = Profiler
158 self._record = bool(record)
159 if self._record:
160 # will save bytes here
161 self.scene.metadata["recording"] = []
163 # store kwargs
164 self.kwargs = kwargs
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 = {}
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
189 if caption is None:
190 caption = "Trimesh SceneViewer (`h` for help)"
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)
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 )
240 # add scene geometry to viewer geometry
241 self._update_vertex_list()
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()
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()
263 def _redraw(self):
264 self.on_draw()
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))
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)
282 def add_geometry(self, name, geometry, **kwargs):
283 """
284 Add a geometry to the viewer.
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
302 # delete old vertex list with the same name
303 if name in self.vertex_list:
304 self.vertex_list[name].delete()
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]
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)
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)
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.
349 Parameters
350 -------------
351 node : str
352 Node to display
353 """
354 self._nodes_hidden.discard(node)
356 def hide_geometry(self, node):
357 """
358 Don't display the geometry contained at a node on
359 the next draw.
361 Parameters
362 -------------
363 node : str
364 Node to not display
365 """
366 self._nodes_hidden.add(node)
368 def reset_view(self, flags=None):
369 """
370 Set view to the default view.
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
402 def init_gl(self):
403 """
404 Perform the magic incantations to create an
405 OpenGL scene using pyglet.
406 """
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
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)
426 @staticmethod
427 def _gl_set_background(background):
428 gl.glClearColor(*background)
430 @staticmethod
431 def _gl_unset_background():
432 gl.glClearColor(*[0, 0, 0, 0])
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)
444 gl.glEnable(gl.GL_DEPTH_TEST)
445 gl.glEnable(gl.GL_CULL_FACE)
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)
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 )
470 gl.glMaterialf(gl.GL_FRONT, gl.GL_SHININESS, 0.4 * 128.0)
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)
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)
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}")
500 # get the transform for the light by name
501 matrix = scene.graph.get(light.name)[0]
503 # convert light object to glLightfv calls
504 multiargs = rendering.light_to_gl(
505 light=light, transform=matrix, lightN=lightN
506 )
508 # enable the light in question
509 gl.glEnable(lightN)
510 # run the glLightfv calls
511 for args in multiargs:
512 gl.glLightfv(*args)
514 def toggle_culling(self):
515 """
516 Toggle back face culling.
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()
525 def toggle_wireframe(self):
526 """
527 Toggle wireframe mode
529 Good for looking inside meshes, off by default.
530 """
531 self.view["wireframe"] = not self.view["wireframe"]
532 self.update_flags()
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()
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()
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()
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)
574 # set fullscreen or windowed
575 self.set_fullscreen(fullscreen=self.view["fullscreen"])
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)
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
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
602 if self.view["grid"] and self._grid is None:
603 try:
604 # create a grid marker
605 from ..path.creation import grid
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
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
635 # set the new viewport size
636 gl.glViewport(0, 0, width, height)
637 gl.glMatrixMode(gl.GL_PROJECTION)
638 gl.glLoadIdentity()
640 # get field of view and Z range from camera
641 camera = self.scene.camera
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)
649 return width, height
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
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)
679 self.view["ball"].down(np.array([x, y]))
680 self.scene.camera_transform = self.view["ball"].pose
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
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
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
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
737 def on_draw(self):
738 """
739 Run the actual draw calls.
740 """
742 if self._profile:
743 profiler = self.Profiler()
744 profiler.start()
746 self._update_meshes()
747 gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
748 gl.glLoadIdentity()
750 # pull the new camera transform from the scene
751 transform_camera = np.linalg.inv(self.scene.camera_transform)
753 # apply the camera transform to the matrix stack
754 gl.glMultMatrixf(rendering.matrix_to_gl(transform_camera))
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
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)
770 # save a reference outside of the loop
771 geometry = self.scene.geometry
772 graph = self.scene.graph
774 while len(node_names) > 0:
775 count += 1
776 current_node = node_names.popleft()
778 if current_node in self._nodes_hidden:
779 continue
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
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)
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]
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 )
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))
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)
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
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)
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()
855 # disable texture after using
856 if texture is not None:
857 gl.glDisable(texture.target)
859 if self._profile:
860 profiler.stop()
861 util.log.debug(profiler.output_text(unicode=True, color=True))
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())
873 def save_image(self, file_obj):
874 """
875 Save the current color buffer to a file object
876 in PNG format.
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
892def _geometry_hash(geometry):
893 """
894 Get a hash for a geometry object
896 Parameters
897 ------------
898 geometry : object
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))
909 return h
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.
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
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 )
955 from ..util import BytesIO
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()
972 return render