Coverage for trimesh/viewer/windowed.py: 73%
390 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"""
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 # create the indexed vertex list
303 self.vertex_list[name] = self.batch.add_indexed(*args)
304 # save the hash of the geometry
305 self.vertex_list_hash[name] = _geometry_hash(geometry)
306 # save the rendering mode from the constructor args
307 self.vertex_list_mode[name] = args[1]
309 # get the visual if the element has it
310 visual = getattr(geometry, "visual", None)
311 if hasattr(visual, "uv") and hasattr(visual, "material"):
312 try:
313 tex = rendering.material_to_texture(visual.material)
314 if tex is not None:
315 self.textures[name] = tex
316 except BaseException:
317 util.log.warning("failed to load texture", exc_info=True)
319 def cleanup_geometries(self):
320 """
321 Remove any stored vertex lists that no longer
322 exist in the scene.
323 """
324 # shorthand to scene graph
325 graph = self.scene.graph
326 # which parts of the graph still have geometry
327 geom_keep = {graph[node][1] for node in graph.nodes_geometry}
328 # which geometries no longer need to be kept
329 geom_delete = [geom for geom in self.vertex_list if geom not in geom_keep]
330 for geom in geom_delete:
331 # remove stored vertex references
332 self.vertex_list.pop(geom, None)
333 self.vertex_list_hash.pop(geom, None)
334 self.vertex_list_mode.pop(geom, None)
335 self.textures.pop(geom, None)
337 def unhide_geometry(self, node):
338 """
339 If a node is hidden remove the flag and show the
340 geometry on the next draw.
342 Parameters
343 -------------
344 node : str
345 Node to display
346 """
347 self._nodes_hidden.discard(node)
349 def hide_geometry(self, node):
350 """
351 Don't display the geometry contained at a node on
352 the next draw.
354 Parameters
355 -------------
356 node : str
357 Node to not display
358 """
359 self._nodes_hidden.add(node)
361 def reset_view(self, flags=None):
362 """
363 Set view to the default view.
365 Parameters
366 --------------
367 flags : None or dict
368 If any view key passed override the default
369 e.g. {'cull': False}
370 """
371 self.view = {
372 "cull": True,
373 "axis": False,
374 "grid": False,
375 "fullscreen": False,
376 "wireframe": False,
377 "ball": Trackball(
378 pose=self._initial_camera_transform,
379 size=self.scene.camera.resolution,
380 scale=self.scene.scale,
381 target=self.scene.centroid,
382 ),
383 }
384 try:
385 # if any flags are passed override defaults
386 if isinstance(flags, dict):
387 for k, v in flags.items():
388 if k in self.view:
389 self.view[k] = v
390 self.update_flags()
391 except BaseException:
392 pass
394 def init_gl(self):
395 """
396 Perform the magic incantations to create an
397 OpenGL scene using pyglet.
398 """
400 # if user passed a background color use it
401 if self.background is None:
402 # default background color is white
403 background = np.ones(4)
404 else:
405 # convert to (4,) uint8 RGBA
406 background = to_rgba(self.background)
407 # convert to 0.0-1.0 float
408 background = background.astype(np.float64) / 255.0
410 self._gl_set_background(background)
411 # use camera setting for depth
412 self._gl_enable_depth(self.scene.camera)
413 self._gl_enable_color_material()
414 self._gl_enable_blending()
415 self._gl_enable_smooth_lines(**self.line_settings)
416 self._gl_enable_lighting(self.scene)
418 @staticmethod
419 def _gl_set_background(background):
420 gl.glClearColor(*background)
422 @staticmethod
423 def _gl_unset_background():
424 gl.glClearColor(*[0, 0, 0, 0])
426 @staticmethod
427 def _gl_enable_depth(camera):
428 """
429 Enable depth test in OpenGL using distances
430 from `scene.camera`.
431 """
432 gl.glClearDepth(1.0)
433 gl.glEnable(gl.GL_DEPTH_TEST)
434 gl.glDepthFunc(gl.GL_LEQUAL)
436 gl.glEnable(gl.GL_DEPTH_TEST)
437 gl.glEnable(gl.GL_CULL_FACE)
439 @staticmethod
440 def _gl_enable_color_material():
441 # do some openGL things
442 gl.glColorMaterial(gl.GL_FRONT_AND_BACK, gl.GL_AMBIENT_AND_DIFFUSE)
443 gl.glEnable(gl.GL_COLOR_MATERIAL)
444 gl.glShadeModel(gl.GL_SMOOTH)
446 gl.glMaterialfv(
447 gl.GL_FRONT,
448 gl.GL_AMBIENT,
449 rendering.vector_to_gl(0.192250, 0.192250, 0.192250),
450 )
451 gl.glMaterialfv(
452 gl.GL_FRONT,
453 gl.GL_DIFFUSE,
454 rendering.vector_to_gl(0.507540, 0.507540, 0.507540),
455 )
456 gl.glMaterialfv(
457 gl.GL_FRONT,
458 gl.GL_SPECULAR,
459 rendering.vector_to_gl(0.5082730, 0.5082730, 0.5082730),
460 )
462 gl.glMaterialf(gl.GL_FRONT, gl.GL_SHININESS, 0.4 * 128.0)
464 @staticmethod
465 def _gl_enable_blending():
466 # enable blending for transparency
467 gl.glEnable(gl.GL_BLEND)
468 gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
470 @staticmethod
471 def _gl_enable_smooth_lines(line_width=4, point_size=4):
472 # make the lines from Path3D objects less ugly
473 gl.glEnable(gl.GL_LINE_SMOOTH)
474 gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
475 # set the width of lines to 4 pixels
476 gl.glLineWidth(line_width)
477 # set PointCloud markers to 4 pixels in size
478 gl.glPointSize(point_size)
480 @staticmethod
481 def _gl_enable_lighting(scene):
482 """
483 Take the lights defined in scene.lights and
484 apply them as openGL lights.
485 """
486 gl.glEnable(gl.GL_LIGHTING)
487 # opengl only supports 7 lights?
488 for i, light in enumerate(scene.lights[:7]):
489 # the index of which light we have
490 lightN = getattr(gl, f"GL_LIGHT{i}")
492 # get the transform for the light by name
493 matrix = scene.graph.get(light.name)[0]
495 # convert light object to glLightfv calls
496 multiargs = rendering.light_to_gl(
497 light=light, transform=matrix, lightN=lightN
498 )
500 # enable the light in question
501 gl.glEnable(lightN)
502 # run the glLightfv calls
503 for args in multiargs:
504 gl.glLightfv(*args)
506 def toggle_culling(self):
507 """
508 Toggle back face culling.
510 It is on by default but if you are dealing with
511 non- watertight meshes you probably want to be able
512 to see the back sides.
513 """
514 self.view["cull"] = not self.view["cull"]
515 self.update_flags()
517 def toggle_wireframe(self):
518 """
519 Toggle wireframe mode
521 Good for looking inside meshes, off by default.
522 """
523 self.view["wireframe"] = not self.view["wireframe"]
524 self.update_flags()
526 def toggle_fullscreen(self):
527 """
528 Toggle between fullscreen and windowed mode.
529 """
530 self.view["fullscreen"] = not self.view["fullscreen"]
531 self.update_flags()
533 def toggle_axis(self):
534 """
535 Toggle a rendered XYZ/RGB axis marker:
536 off, world frame, every frame
537 """
538 # cycle through three axis states
539 states = [False, "world", "all", "without_world"]
540 # the state after toggling
541 index = (states.index(self.view["axis"]) + 1) % len(states)
542 # update state to next index
543 self.view["axis"] = states[index]
544 # perform gl actions
545 self.update_flags()
547 def toggle_grid(self):
548 """
549 Toggle a rendered grid.
550 """
551 # update state to next index
552 self.view["grid"] = not self.view["grid"]
553 # perform gl actions
554 self.update_flags()
556 def update_flags(self):
557 """
558 Check the view flags, and call required GL functions.
559 """
560 # view mode, filled vs wirefrom
561 if self.view["wireframe"]:
562 gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
563 else:
564 gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
566 # set fullscreen or windowed
567 self.set_fullscreen(fullscreen=self.view["fullscreen"])
569 # backface culling on or off
570 if self.view["cull"]:
571 gl.glEnable(gl.GL_CULL_FACE)
572 else:
573 gl.glDisable(gl.GL_CULL_FACE)
575 # case where we WANT an axis and NO vertexlist
576 # is stored internally
577 if self.view["axis"] and self._axis is None:
578 from .. import creation
580 # create an axis marker sized relative to the scene
581 axis = creation.axis(origin_size=self.scene.scale / 100)
582 # create ordered args for a vertex list
583 args = rendering.mesh_to_vertexlist(axis)
584 # store the axis as a reference
585 self._axis = self.batch.add_indexed(*args)
586 # case where we DON'T want an axis but a vertexlist
587 # IS stored internally
588 elif not self.view["axis"] and self._axis is not None:
589 # remove the axis from the rendering batch
590 self._axis.delete()
591 # set the reference to None
592 self._axis = None
594 if self.view["grid"] and self._grid is None:
595 try:
596 # create a grid marker
597 from ..path.creation import grid
599 bounds = self.scene.bounds
600 center = bounds.mean(axis=0)
601 # set the grid to the lowest Z position
602 # also offset by the scale to avoid interference
603 center[2] = bounds[0][2] - (np.ptp(bounds[:, 2]) / 100)
604 # choose the side length by maximum XY length
605 side = np.ptp(bounds, axis=0)[:2].max()
606 # create an axis marker sized relative to the scene
607 grid_mesh = grid(side=side, count=4, transform=translation_matrix(center))
608 # convert the path to vertexlist args
609 args = rendering.convert_to_vertexlist(grid_mesh)
610 # create ordered args for a vertex list
611 self._grid = self.batch.add_indexed(*args)
612 except BaseException:
613 util.log.warning("failed to create grid!", exc_info=True)
614 elif not self.view["grid"] and self._grid is not None:
615 self._grid.delete()
616 self._grid = None
618 def _update_perspective(self, width, height):
619 try:
620 # for high DPI screens viewport size
621 # will be different then the passed size
622 width, height = self.get_viewport_size()
623 except BaseException:
624 # older versions of pyglet may not have this
625 pass
627 # set the new viewport size
628 gl.glViewport(0, 0, width, height)
629 gl.glMatrixMode(gl.GL_PROJECTION)
630 gl.glLoadIdentity()
632 # get field of view and Z range from camera
633 camera = self.scene.camera
635 # set perspective from camera data
636 gl.gluPerspective(
637 camera.fov[1], width / float(height), camera.z_near, camera.z_far
638 )
639 gl.glMatrixMode(gl.GL_MODELVIEW)
641 return width, height
643 def on_resize(self, width, height):
644 """
645 Handle resized windows.
646 """
647 width, height = self._update_perspective(width, height)
648 self.scene.camera.resolution = (width, height)
649 self.view["ball"].resize(self.scene.camera.resolution)
650 self.scene.camera_transform = self.view["ball"].pose
652 def on_mouse_press(self, x, y, buttons, modifiers):
653 """
654 Set the start point of the drag.
655 """
656 self.view["ball"].set_state(Trackball.STATE_ROTATE)
657 if buttons == pyglet.window.mouse.LEFT:
658 ctrl = modifiers & pyglet.window.key.MOD_CTRL
659 shift = modifiers & pyglet.window.key.MOD_SHIFT
660 if ctrl and shift:
661 self.view["ball"].set_state(Trackball.STATE_ZOOM)
662 elif shift:
663 self.view["ball"].set_state(Trackball.STATE_ROLL)
664 elif ctrl:
665 self.view["ball"].set_state(Trackball.STATE_PAN)
666 elif buttons == pyglet.window.mouse.MIDDLE:
667 self.view["ball"].set_state(Trackball.STATE_PAN)
668 elif buttons == pyglet.window.mouse.RIGHT:
669 self.view["ball"].set_state(Trackball.STATE_ZOOM)
671 self.view["ball"].down(np.array([x, y]))
672 self.scene.camera_transform = self.view["ball"].pose
674 def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
675 """
676 Pan or rotate the view.
677 """
678 self.view["ball"].drag(np.array([x, y]))
679 self.scene.camera_transform = self.view["ball"].pose
681 def on_mouse_scroll(self, x, y, dx, dy):
682 """
683 Zoom the view.
684 """
685 self.view["ball"].scroll(dy)
686 self.scene.camera_transform = self.view["ball"].pose
688 def on_key_press(self, symbol, modifiers):
689 """
690 Call appropriate functions given key presses.
691 """
692 magnitude = 10
693 if symbol == pyglet.window.key.W:
694 self.toggle_wireframe()
695 elif symbol == pyglet.window.key.Z:
696 self.reset_view()
697 elif symbol == pyglet.window.key.C:
698 self.toggle_culling()
699 elif symbol == pyglet.window.key.A:
700 self.toggle_axis()
701 elif symbol == pyglet.window.key.G:
702 self.toggle_grid()
703 elif symbol == pyglet.window.key.Q:
704 self.on_close()
705 elif symbol == pyglet.window.key.M:
706 self.maximize()
707 elif symbol == pyglet.window.key.F:
708 self.toggle_fullscreen()
709 elif symbol == pyglet.window.key.H:
710 print(_HELP_MESSAGE) # noqa: T201
712 if symbol in [
713 pyglet.window.key.LEFT,
714 pyglet.window.key.RIGHT,
715 pyglet.window.key.DOWN,
716 pyglet.window.key.UP,
717 ]:
718 self.view["ball"].down([0, 0])
719 if symbol == pyglet.window.key.LEFT:
720 self.view["ball"].drag([-magnitude, 0])
721 elif symbol == pyglet.window.key.RIGHT:
722 self.view["ball"].drag([magnitude, 0])
723 elif symbol == pyglet.window.key.DOWN:
724 self.view["ball"].drag([0, -magnitude])
725 elif symbol == pyglet.window.key.UP:
726 self.view["ball"].drag([0, magnitude])
727 self.scene.camera_transform = self.view["ball"].pose
729 def on_draw(self):
730 """
731 Run the actual draw calls.
732 """
734 if self._profile:
735 profiler = self.Profiler()
736 profiler.start()
738 self._update_meshes()
739 gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
740 gl.glLoadIdentity()
742 # pull the new camera transform from the scene
743 transform_camera = np.linalg.inv(self.scene.camera_transform)
745 # apply the camera transform to the matrix stack
746 gl.glMultMatrixf(rendering.matrix_to_gl(transform_camera))
748 # we want to render fully opaque objects first,
749 # followed by objects which have transparency
750 node_names = collections.deque(self.scene.graph.nodes_geometry)
751 # how many nodes did we start with
752 count_original = len(node_names)
753 count = -1
755 # if we are rendering an axis marker at the world
756 if self._axis and not self.view["axis"] == "without_world":
757 # we stored it as a vertex list
758 self._axis.draw(mode=gl.GL_TRIANGLES)
759 if self._grid:
760 self._grid.draw(mode=gl.GL_LINES)
762 # save a reference outside of the loop
763 geometry = self.scene.geometry
764 graph = self.scene.graph
766 while len(node_names) > 0:
767 count += 1
768 current_node = node_names.popleft()
770 if current_node in self._nodes_hidden:
771 continue
773 # get the transform from world to geometry and mesh name
774 transform, geometry_name = graph.get(current_node)
775 # if no geometry at this frame continue without rendering
776 if geometry_name is None or geometry_name not in self.vertex_list_mode:
777 continue
779 # if a geometry is marked as fixed apply the inverse view transform
780 if self.fixed is not None and geometry_name in self.fixed:
781 # remove altered camera transform from fixed geometry
782 transform_fix = np.linalg.inv(
783 np.dot(self._initial_camera_transform, transform_camera)
784 )
785 # apply the transform so the fixed geometry doesn't move
786 transform = np.dot(transform, transform_fix)
788 # get a reference to the mesh so we can check transparency
789 mesh = geometry[geometry_name]
790 if mesh.is_empty:
791 continue
792 # get the GL mode of the current geometry
793 mode = self.vertex_list_mode[geometry_name]
795 # if you draw a coplanar line with a triangle it will z-fight
796 # the best way to do this is probably a shader but this works fine
797 if mode == gl.GL_LINES:
798 # apply the offset in camera space
799 transform = util.multi_dot(
800 [
801 transform,
802 np.linalg.inv(transform_camera),
803 self._line_offset,
804 transform_camera,
805 ]
806 )
808 # add a new matrix to the model stack
809 gl.glPushMatrix()
810 # transform by the nodes transform
811 gl.glMultMatrixf(rendering.matrix_to_gl(transform))
813 # draw an axis marker for each mesh frame
814 if self.view["axis"] == "all":
815 self._axis.draw(mode=gl.GL_TRIANGLES)
816 elif self.view["axis"] == "without_world":
817 if not util.allclose(transform, np.eye(4), atol=1e-5):
818 self._axis.draw(mode=gl.GL_TRIANGLES)
820 # transparent things must be drawn last
821 if (
822 hasattr(mesh, "visual")
823 and hasattr(mesh.visual, "transparency")
824 and mesh.visual.transparency
825 ):
826 # put the current item onto the back of the queue
827 if count < count_original:
828 # add the node to be drawn last
829 node_names.append(current_node)
830 # pop the matrix stack for now
831 gl.glPopMatrix()
832 # come back to this mesh later
833 continue
835 # if we have texture enable the target texture
836 texture = None
837 if geometry_name in self.textures:
838 texture = self.textures[geometry_name]
839 gl.glEnable(texture.target)
840 gl.glBindTexture(texture.target, texture.id)
842 # draw the mesh with its transform applied
843 self.vertex_list[geometry_name].draw(mode=mode)
844 # pop the matrix stack as we drew what we needed to draw
845 gl.glPopMatrix()
847 # disable texture after using
848 if texture is not None:
849 gl.glDisable(texture.target)
851 if self._profile:
852 profiler.stop()
853 util.log.debug(profiler.output_text(unicode=True, color=True))
855 def flip(self):
856 super().flip()
857 if self._record:
858 # will save a PNG-encoded bytes
859 img = self.save_image(util.BytesIO())
860 # seek start of file-like object
861 img.seek(0)
862 # save the bytes from the file object
863 self.scene.metadata["recording"].append(img.read())
865 def save_image(self, file_obj):
866 """
867 Save the current color buffer to a file object
868 in PNG format.
870 Parameters
871 -------------
872 file_obj: file name, or file- like object
873 """
874 manager = pyglet.image.get_buffer_manager()
875 colorbuffer = manager.get_color_buffer()
876 # if passed a string save by name
877 if hasattr(file_obj, "write"):
878 colorbuffer.save(file=file_obj)
879 else:
880 colorbuffer.save(filename=file_obj)
881 return file_obj
884def _geometry_hash(geometry):
885 """
886 Get a hash for a geometry object
888 Parameters
889 ------------
890 geometry : object
892 Returns
893 ------------
894 hash : str
895 """
896 h = str(hash(geometry))
897 if hasattr(geometry, "visual"):
898 # if visual properties are defined
899 h += str(hash(geometry.visual))
901 return h
904def render_scene(
905 scene, resolution=None, visible=True, fullscreen=False, resizable=True, **kwargs
906):
907 """
908 Render a preview of a scene to a PNG. Note that
909 whether this works or not highly variable based on
910 platform and graphics driver.
912 Parameters
913 ------------
914 scene : trimesh.Scene
915 Geometry to be rendered
916 resolution : (2,) int or None
917 Resolution in pixels or set from scene.camera
918 visible : bool
919 Show a window during rendering. Note that MANY
920 platforms refuse to render with hidden windows
921 and will likely return a blank image; this is a
922 platform issue and cannot be fixed in Python.
923 fullscreen : bool
924 Determines whether the window is rendered in fullscreen mode.
925 Defaults to False (windowed).
926 resizable : bool
927 Determines whether the rendered window can be resized by the user.
928 Defaults to True (resizable).
929 kwargs : **
930 Passed to SceneViewer
932 Returns
933 ---------
934 render : bytes
935 Image in PNG format
936 """
937 window = SceneViewer(
938 scene,
939 start_loop=False,
940 visible=visible,
941 resolution=resolution,
942 fullscreen=fullscreen,
943 resizable=resizable,
944 **kwargs,
945 )
947 from ..util import BytesIO
949 # need to run loop twice to display anything
950 for save in [False, False, True]:
951 pyglet.clock.tick()
952 window.switch_to()
953 window.dispatch_events()
954 window.dispatch_event("on_draw")
955 window.flip()
956 if save:
957 # save the color buffer data to memory
958 file_obj = BytesIO()
959 window.save_image(file_obj)
960 file_obj.seek(0)
961 render = file_obj.read()
962 window.close()
964 return render