Files
geMoldInsight/venv/lib/python3.11/site-packages/trimesh/viewer/windowed.py
T
2026-02-12 23:27:48 +08:00

934 lines
31 KiB
Python

"""
windowed.py
---------------
Provides a pyglet- based windowed viewer to preview
Trimesh, Scene, PointCloud, and Path objects.
Works on all major platforms: Windows, Linux, and OSX.
"""
import collections
import numpy as np
import pyglet
# pyglet 2.0 is close to a re-write moving from fixed-function
# to shaders and we will likely support it by forking an entirely
# new viewer `trimesh.viewer.shaders` and then basically keeping
# `windowed` around for backwards-compatibility with no changes
if int(pyglet.version.split(".")[0]) >= 2:
raise ImportError('`trimesh.viewer.windowed` requires `pip install "pyglet<2"`')
from .. import rendering, util
from ..transformations import translation_matrix
from ..visual import to_rgba
from .trackball import Trackball
pyglet.options["shadow_window"] = False
import pyglet.gl as gl # NOQA
# smooth only when fewer faces than this
_SMOOTH_MAX_FACES = 100000
class SceneViewer(pyglet.window.Window):
def __init__(
self,
scene,
smooth=True,
flags=None,
visible=True,
resolution=None,
fullscreen=False,
resizable=True,
start_loop=True,
callback=None,
callback_period=None,
caption=None,
fixed=None,
offset_lines=True,
line_settings=None,
background=None,
window_conf=None,
profile=False,
record=False,
**kwargs,
):
"""
Create a window that will display a trimesh.Scene object
in an OpenGL context via pyglet.
Parameters
---------------
scene : trimesh.scene.Scene
Scene with geometry and transforms
smooth : bool
If True try to smooth shade things
flags : dict
If passed apply keys to self.view:
['cull', 'wireframe', etc]
visible : bool
Display window or not
resolution : (2,) int
Initial resolution of window
fullscreen : bool
Determines whether the window is rendered in fullscreen mode.
resizable : bool
Determines whether the rendered window can be resized by the user.
start_loop : bool
Call pyglet.app.run() at the end of init
callback : function
A function which can be called periodically to
update things in the scene
callback_period : float
How often to call the callback, in seconds
fixed : None or iterable
List of keys in scene.geometry to skip view
transform on to keep fixed relative to camera
offset_lines : bool
If True, will offset lines slightly so if drawn
coplanar with mesh geometry they will be visible
background : None or (4,) uint8
Color for background
window_conf : None, or gl.Config
Passed to window init
profile : bool
If set will run a `pyinstrument` profile for
every call to `on_draw` and print the output.
record : bool
If True, will save a list of `png` bytes to
a list located in `scene.metadata['recording']`
kwargs : dict
Additional arguments to pass, including
'background' for to set background color
"""
self.scene = self._scene = scene
self.callback = callback
self.callback_period = callback_period
self.scene._redraw = self._redraw
self.offset_lines = bool(offset_lines)
self.background = background
# save initial camera transform
self._initial_camera_transform = scene.camera_transform.copy()
# a transform to offset lines slightly to avoid Z-fighting
self._line_offset = translation_matrix(
[0, 0, scene.scale / 1000 if self.offset_lines else 0]
)
self.reset_view()
self.batch = pyglet.graphics.Batch()
self._smooth = smooth
self._profile = bool(profile)
if self._profile:
from pyinstrument import Profiler
self.Profiler = Profiler
self._record = bool(record)
if self._record:
# will save bytes here
self.scene.metadata["recording"] = []
# store kwargs
self.kwargs = kwargs
# store a vertexlist for an axis marker
self._axis = None
# store a vertexlist for a grid display
self._grid = None
# store scene geometry as vertex lists
self.vertex_list = {}
# store geometry hashes
self.vertex_list_hash = {}
# store geometry rendering mode
self.vertex_list_mode = {}
# store meshes that don't rotate relative to viewer
self.fixed = fixed
# store a hidden (don't not display) node.
self._nodes_hidden = set()
# name : texture
self.textures = {}
# if resolution isn't defined set a default value
if resolution is None:
resolution = scene.camera.resolution
else:
scene.camera.resolution = resolution
# set the default line settings to a fraction
# of our resolution so the points aren't tiny
scale = max(resolution)
self.line_settings = {"point_size": scale / 200, "line_width": scale / 400}
# if we've been passed line settings override the default
if line_settings is not None:
self.line_settings.update(line_settings)
# no window conf was passed so try to get the best looking one
if window_conf is None:
try:
# try enabling antialiasing
# if you have a graphics card this will probably work
conf = gl.Config(
sample_buffers=1, samples=4, depth_size=24, double_buffer=True
)
super().__init__(
config=conf,
visible=visible,
fullscreen=fullscreen,
resizable=resizable,
width=resolution[0],
height=resolution[1],
caption=caption,
)
except pyglet.window.NoSuchConfigException:
conf = gl.Config(double_buffer=True)
super().__init__(
config=conf,
fullscreen=fullscreen,
resizable=resizable,
visible=visible,
width=resolution[0],
height=resolution[1],
caption=caption,
)
else:
# window config was manually passed
super().__init__(
config=window_conf,
fullscreen=fullscreen,
resizable=resizable,
visible=visible,
width=resolution[0],
height=resolution[1],
caption=caption,
)
# add scene geometry to viewer geometry
self._update_vertex_list()
# call after geometry is added
self.init_gl()
self.set_size(*resolution)
if flags is not None:
self.reset_view(flags=flags)
self.update_flags()
# someone has passed a callback to be called periodically
if self.callback is not None:
# if no callback period is specified set it to default
if callback_period is None:
# 30 times per second
callback_period = 1.0 / 30.0
# set up a do-nothing periodic task which will
# trigger `self.on_draw` every `callback_period`
# seconds if someone has passed a callback
pyglet.clock.schedule_interval(lambda x: x, callback_period)
if start_loop:
pyglet.app.run()
def _redraw(self):
self.on_draw()
def _update_vertex_list(self):
# update vertex_list if needed
for name, geom in self.scene.geometry.items():
if geom.is_empty:
continue
if _geometry_hash(geom) == self.vertex_list_hash.get(name):
continue
self.add_geometry(name=name, geometry=geom, smooth=bool(self._smooth))
def _update_meshes(self):
# call the callback if specified
if self.callback is not None:
self.callback(self.scene)
self._update_vertex_list()
self._update_perspective(self.width, self.height)
def add_geometry(self, name, geometry, **kwargs):
"""
Add a geometry to the viewer.
Parameters
--------------
name : hashable
Name that references geometry
geometry : Trimesh, Path2D, Path3D, PointCloud
Geometry to display in the viewer window
kwargs **
Passed to rendering.convert_to_vertexlist
"""
try:
# convert geometry to constructor args
args = rendering.convert_to_vertexlist(geometry, **kwargs)
except BaseException:
util.log.warning(f"failed to add geometry `{name}`", exc_info=True)
return
# create the indexed vertex list
self.vertex_list[name] = self.batch.add_indexed(*args)
# save the hash of the geometry
self.vertex_list_hash[name] = _geometry_hash(geometry)
# save the rendering mode from the constructor args
self.vertex_list_mode[name] = args[1]
# get the visual if the element has it
visual = getattr(geometry, "visual", None)
if hasattr(visual, "uv") and hasattr(visual, "material"):
try:
tex = rendering.material_to_texture(visual.material)
if tex is not None:
self.textures[name] = tex
except BaseException:
util.log.warning("failed to load texture", exc_info=True)
def cleanup_geometries(self):
"""
Remove any stored vertex lists that no longer
exist in the scene.
"""
# shorthand to scene graph
graph = self.scene.graph
# which parts of the graph still have geometry
geom_keep = {graph[node][1] for node in graph.nodes_geometry}
# which geometries no longer need to be kept
geom_delete = [geom for geom in self.vertex_list if geom not in geom_keep]
for geom in geom_delete:
# remove stored vertex references
self.vertex_list.pop(geom, None)
self.vertex_list_hash.pop(geom, None)
self.vertex_list_mode.pop(geom, None)
self.textures.pop(geom, None)
def unhide_geometry(self, node):
"""
If a node is hidden remove the flag and show the
geometry on the next draw.
Parameters
-------------
node : str
Node to display
"""
self._nodes_hidden.discard(node)
def hide_geometry(self, node):
"""
Don't display the geometry contained at a node on
the next draw.
Parameters
-------------
node : str
Node to not display
"""
self._nodes_hidden.add(node)
def reset_view(self, flags=None):
"""
Set view to the default view.
Parameters
--------------
flags : None or dict
If any view key passed override the default
e.g. {'cull': False}
"""
self.view = {
"cull": True,
"axis": False,
"grid": False,
"fullscreen": False,
"wireframe": False,
"ball": Trackball(
pose=self._initial_camera_transform,
size=self.scene.camera.resolution,
scale=self.scene.scale,
target=self.scene.centroid,
),
}
try:
# if any flags are passed override defaults
if isinstance(flags, dict):
for k, v in flags.items():
if k in self.view:
self.view[k] = v
self.update_flags()
except BaseException:
pass
def init_gl(self):
"""
Perform the magic incantations to create an
OpenGL scene using pyglet.
"""
# if user passed a background color use it
if self.background is None:
# default background color is white
background = np.ones(4)
else:
# convert to (4,) uint8 RGBA
background = to_rgba(self.background)
# convert to 0.0-1.0 float
background = background.astype(np.float64) / 255.0
self._gl_set_background(background)
# use camera setting for depth
self._gl_enable_depth(self.scene.camera)
self._gl_enable_color_material()
self._gl_enable_blending()
self._gl_enable_smooth_lines(**self.line_settings)
self._gl_enable_lighting(self.scene)
@staticmethod
def _gl_set_background(background):
gl.glClearColor(*background)
@staticmethod
def _gl_unset_background():
gl.glClearColor(*[0, 0, 0, 0])
@staticmethod
def _gl_enable_depth(camera):
"""
Enable depth test in OpenGL using distances
from `scene.camera`.
"""
gl.glClearDepth(1.0)
gl.glEnable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LEQUAL)
gl.glEnable(gl.GL_DEPTH_TEST)
gl.glEnable(gl.GL_CULL_FACE)
@staticmethod
def _gl_enable_color_material():
# do some openGL things
gl.glColorMaterial(gl.GL_FRONT_AND_BACK, gl.GL_AMBIENT_AND_DIFFUSE)
gl.glEnable(gl.GL_COLOR_MATERIAL)
gl.glShadeModel(gl.GL_SMOOTH)
gl.glMaterialfv(
gl.GL_FRONT,
gl.GL_AMBIENT,
rendering.vector_to_gl(0.192250, 0.192250, 0.192250),
)
gl.glMaterialfv(
gl.GL_FRONT,
gl.GL_DIFFUSE,
rendering.vector_to_gl(0.507540, 0.507540, 0.507540),
)
gl.glMaterialfv(
gl.GL_FRONT,
gl.GL_SPECULAR,
rendering.vector_to_gl(0.5082730, 0.5082730, 0.5082730),
)
gl.glMaterialf(gl.GL_FRONT, gl.GL_SHININESS, 0.4 * 128.0)
@staticmethod
def _gl_enable_blending():
# enable blending for transparency
gl.glEnable(gl.GL_BLEND)
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
@staticmethod
def _gl_enable_smooth_lines(line_width=4, point_size=4):
# make the lines from Path3D objects less ugly
gl.glEnable(gl.GL_LINE_SMOOTH)
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
# set the width of lines to 4 pixels
gl.glLineWidth(line_width)
# set PointCloud markers to 4 pixels in size
gl.glPointSize(point_size)
@staticmethod
def _gl_enable_lighting(scene):
"""
Take the lights defined in scene.lights and
apply them as openGL lights.
"""
gl.glEnable(gl.GL_LIGHTING)
# opengl only supports 7 lights?
for i, light in enumerate(scene.lights[:7]):
# the index of which light we have
lightN = eval(f"gl.GL_LIGHT{i}")
# get the transform for the light by name
matrix = scene.graph.get(light.name)[0]
# convert light object to glLightfv calls
multiargs = rendering.light_to_gl(
light=light, transform=matrix, lightN=lightN
)
# enable the light in question
gl.glEnable(lightN)
# run the glLightfv calls
for args in multiargs:
gl.glLightfv(*args)
def toggle_culling(self):
"""
Toggle back face culling.
It is on by default but if you are dealing with
non- watertight meshes you probably want to be able
to see the back sides.
"""
self.view["cull"] = not self.view["cull"]
self.update_flags()
def toggle_wireframe(self):
"""
Toggle wireframe mode
Good for looking inside meshes, off by default.
"""
self.view["wireframe"] = not self.view["wireframe"]
self.update_flags()
def toggle_fullscreen(self):
"""
Toggle between fullscreen and windowed mode.
"""
self.view["fullscreen"] = not self.view["fullscreen"]
self.update_flags()
def toggle_axis(self):
"""
Toggle a rendered XYZ/RGB axis marker:
off, world frame, every frame
"""
# cycle through three axis states
states = [False, "world", "all", "without_world"]
# the state after toggling
index = (states.index(self.view["axis"]) + 1) % len(states)
# update state to next index
self.view["axis"] = states[index]
# perform gl actions
self.update_flags()
def toggle_grid(self):
"""
Toggle a rendered grid.
"""
# update state to next index
self.view["grid"] = not self.view["grid"]
# perform gl actions
self.update_flags()
def update_flags(self):
"""
Check the view flags, and call required GL functions.
"""
# view mode, filled vs wirefrom
if self.view["wireframe"]:
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
else:
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
# set fullscreen or windowed
self.set_fullscreen(fullscreen=self.view["fullscreen"])
# backface culling on or off
if self.view["cull"]:
gl.glEnable(gl.GL_CULL_FACE)
else:
gl.glDisable(gl.GL_CULL_FACE)
# case where we WANT an axis and NO vertexlist
# is stored internally
if self.view["axis"] and self._axis is None:
from .. import creation
# create an axis marker sized relative to the scene
axis = creation.axis(origin_size=self.scene.scale / 100)
# create ordered args for a vertex list
args = rendering.mesh_to_vertexlist(axis)
# store the axis as a reference
self._axis = self.batch.add_indexed(*args)
# case where we DON'T want an axis but a vertexlist
# IS stored internally
elif not self.view["axis"] and self._axis is not None:
# remove the axis from the rendering batch
self._axis.delete()
# set the reference to None
self._axis = None
if self.view["grid"] and self._grid is None:
try:
# create a grid marker
from ..path.creation import grid
bounds = self.scene.bounds
center = bounds.mean(axis=0)
# set the grid to the lowest Z position
# also offset by the scale to avoid interference
center[2] = bounds[0][2] - (np.ptp(bounds[:, 2]) / 100)
# choose the side length by maximum XY length
side = np.ptp(bounds, axis=0)[:2].max()
# create an axis marker sized relative to the scene
grid_mesh = grid(side=side, count=4, transform=translation_matrix(center))
# convert the path to vertexlist args
args = rendering.convert_to_vertexlist(grid_mesh)
# create ordered args for a vertex list
self._grid = self.batch.add_indexed(*args)
except BaseException:
util.log.warning("failed to create grid!", exc_info=True)
elif not self.view["grid"] and self._grid is not None:
self._grid.delete()
self._grid = None
def _update_perspective(self, width, height):
try:
# for high DPI screens viewport size
# will be different then the passed size
width, height = self.get_viewport_size()
except BaseException:
# older versions of pyglet may not have this
pass
# set the new viewport size
gl.glViewport(0, 0, width, height)
gl.glMatrixMode(gl.GL_PROJECTION)
gl.glLoadIdentity()
# get field of view and Z range from camera
camera = self.scene.camera
# set perspective from camera data
gl.gluPerspective(
camera.fov[1], width / float(height), camera.z_near, camera.z_far
)
gl.glMatrixMode(gl.GL_MODELVIEW)
return width, height
def on_resize(self, width, height):
"""
Handle resized windows.
"""
width, height = self._update_perspective(width, height)
self.scene.camera.resolution = (width, height)
self.view["ball"].resize(self.scene.camera.resolution)
self.scene.camera_transform = self.view["ball"].pose
def on_mouse_press(self, x, y, buttons, modifiers):
"""
Set the start point of the drag.
"""
self.view["ball"].set_state(Trackball.STATE_ROTATE)
if buttons == pyglet.window.mouse.LEFT:
ctrl = modifiers & pyglet.window.key.MOD_CTRL
shift = modifiers & pyglet.window.key.MOD_SHIFT
if ctrl and shift:
self.view["ball"].set_state(Trackball.STATE_ZOOM)
elif shift:
self.view["ball"].set_state(Trackball.STATE_ROLL)
elif ctrl:
self.view["ball"].set_state(Trackball.STATE_PAN)
elif buttons == pyglet.window.mouse.MIDDLE:
self.view["ball"].set_state(Trackball.STATE_PAN)
elif buttons == pyglet.window.mouse.RIGHT:
self.view["ball"].set_state(Trackball.STATE_ZOOM)
self.view["ball"].down(np.array([x, y]))
self.scene.camera_transform = self.view["ball"].pose
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
"""
Pan or rotate the view.
"""
self.view["ball"].drag(np.array([x, y]))
self.scene.camera_transform = self.view["ball"].pose
def on_mouse_scroll(self, x, y, dx, dy):
"""
Zoom the view.
"""
self.view["ball"].scroll(dy)
self.scene.camera_transform = self.view["ball"].pose
def on_key_press(self, symbol, modifiers):
"""
Call appropriate functions given key presses.
"""
magnitude = 10
if symbol == pyglet.window.key.W:
self.toggle_wireframe()
elif symbol == pyglet.window.key.Z:
self.reset_view()
elif symbol == pyglet.window.key.C:
self.toggle_culling()
elif symbol == pyglet.window.key.A:
self.toggle_axis()
elif symbol == pyglet.window.key.G:
self.toggle_grid()
elif symbol == pyglet.window.key.Q:
self.on_close()
elif symbol == pyglet.window.key.M:
self.maximize()
elif symbol == pyglet.window.key.F:
self.toggle_fullscreen()
if symbol in [
pyglet.window.key.LEFT,
pyglet.window.key.RIGHT,
pyglet.window.key.DOWN,
pyglet.window.key.UP,
]:
self.view["ball"].down([0, 0])
if symbol == pyglet.window.key.LEFT:
self.view["ball"].drag([-magnitude, 0])
elif symbol == pyglet.window.key.RIGHT:
self.view["ball"].drag([magnitude, 0])
elif symbol == pyglet.window.key.DOWN:
self.view["ball"].drag([0, -magnitude])
elif symbol == pyglet.window.key.UP:
self.view["ball"].drag([0, magnitude])
self.scene.camera_transform = self.view["ball"].pose
def on_draw(self):
"""
Run the actual draw calls.
"""
if self._profile:
profiler = self.Profiler()
profiler.start()
self._update_meshes()
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
gl.glLoadIdentity()
# pull the new camera transform from the scene
transform_camera = np.linalg.inv(self.scene.camera_transform)
# apply the camera transform to the matrix stack
gl.glMultMatrixf(rendering.matrix_to_gl(transform_camera))
# we want to render fully opaque objects first,
# followed by objects which have transparency
node_names = collections.deque(self.scene.graph.nodes_geometry)
# how many nodes did we start with
count_original = len(node_names)
count = -1
# if we are rendering an axis marker at the world
if self._axis and not self.view["axis"] == "without_world":
# we stored it as a vertex list
self._axis.draw(mode=gl.GL_TRIANGLES)
if self._grid:
self._grid.draw(mode=gl.GL_LINES)
# save a reference outside of the loop
geometry = self.scene.geometry
graph = self.scene.graph
while len(node_names) > 0:
count += 1
current_node = node_names.popleft()
if current_node in self._nodes_hidden:
continue
# get the transform from world to geometry and mesh name
transform, geometry_name = graph.get(current_node)
# if no geometry at this frame continue without rendering
if geometry_name is None or geometry_name not in self.vertex_list_mode:
continue
# if a geometry is marked as fixed apply the inverse view transform
if self.fixed is not None and geometry_name in self.fixed:
# remove altered camera transform from fixed geometry
transform_fix = np.linalg.inv(
np.dot(self._initial_camera_transform, transform_camera)
)
# apply the transform so the fixed geometry doesn't move
transform = np.dot(transform, transform_fix)
# get a reference to the mesh so we can check transparency
mesh = geometry[geometry_name]
if mesh.is_empty:
continue
# get the GL mode of the current geometry
mode = self.vertex_list_mode[geometry_name]
# if you draw a coplanar line with a triangle it will z-fight
# the best way to do this is probably a shader but this works fine
if mode == gl.GL_LINES:
# apply the offset in camera space
transform = util.multi_dot(
[
transform,
np.linalg.inv(transform_camera),
self._line_offset,
transform_camera,
]
)
# add a new matrix to the model stack
gl.glPushMatrix()
# transform by the nodes transform
gl.glMultMatrixf(rendering.matrix_to_gl(transform))
# draw an axis marker for each mesh frame
if self.view["axis"] == "all":
self._axis.draw(mode=gl.GL_TRIANGLES)
elif self.view["axis"] == "without_world":
if not util.allclose(transform, np.eye(4), atol=1e-5):
self._axis.draw(mode=gl.GL_TRIANGLES)
# transparent things must be drawn last
if (
hasattr(mesh, "visual")
and hasattr(mesh.visual, "transparency")
and mesh.visual.transparency
):
# put the current item onto the back of the queue
if count < count_original:
# add the node to be drawn last
node_names.append(current_node)
# pop the matrix stack for now
gl.glPopMatrix()
# come back to this mesh later
continue
# if we have texture enable the target texture
texture = None
if geometry_name in self.textures:
texture = self.textures[geometry_name]
gl.glEnable(texture.target)
gl.glBindTexture(texture.target, texture.id)
# draw the mesh with its transform applied
self.vertex_list[geometry_name].draw(mode=mode)
# pop the matrix stack as we drew what we needed to draw
gl.glPopMatrix()
# disable texture after using
if texture is not None:
gl.glDisable(texture.target)
if self._profile:
profiler.stop()
util.log.debug(profiler.output_text(unicode=True, color=True))
def flip(self):
super().flip()
if self._record:
# will save a PNG-encoded bytes
img = self.save_image(util.BytesIO())
# seek start of file-like object
img.seek(0)
# save the bytes from the file object
self.scene.metadata["recording"].append(img.read())
def save_image(self, file_obj):
"""
Save the current color buffer to a file object
in PNG format.
Parameters
-------------
file_obj: file name, or file- like object
"""
manager = pyglet.image.get_buffer_manager()
colorbuffer = manager.get_color_buffer()
# if passed a string save by name
if hasattr(file_obj, "write"):
colorbuffer.save(file=file_obj)
else:
colorbuffer.save(filename=file_obj)
return file_obj
def _geometry_hash(geometry):
"""
Get a hash for a geometry object
Parameters
------------
geometry : object
Returns
------------
hash : str
"""
h = str(hash(geometry))
if hasattr(geometry, "visual"):
# if visual properties are defined
h += str(hash(geometry.visual))
return h
def render_scene(
scene, resolution=None, visible=True, fullscreen=False, resizable=True, **kwargs
):
"""
Render a preview of a scene to a PNG. Note that
whether this works or not highly variable based on
platform and graphics driver.
Parameters
------------
scene : trimesh.Scene
Geometry to be rendered
resolution : (2,) int or None
Resolution in pixels or set from scene.camera
visible : bool
Show a window during rendering. Note that MANY
platforms refuse to render with hidden windows
and will likely return a blank image; this is a
platform issue and cannot be fixed in Python.
fullscreen : bool
Determines whether the window is rendered in fullscreen mode.
Defaults to False (windowed).
resizable : bool
Determines whether the rendered window can be resized by the user.
Defaults to True (resizable).
kwargs : **
Passed to SceneViewer
Returns
---------
render : bytes
Image in PNG format
"""
window = SceneViewer(
scene,
start_loop=False,
visible=visible,
resolution=resolution,
fullscreen=fullscreen,
resizable=resizable,
**kwargs,
)
from ..util import BytesIO
# need to run loop twice to display anything
for save in [False, False, True]:
pyglet.clock.tick()
window.switch_to()
window.dispatch_events()
window.dispatch_event("on_draw")
window.flip()
if save:
# save the color buffer data to memory
file_obj = BytesIO()
window.save_image(file_obj)
file_obj.seek(0)
render = file_obj.read()
window.close()
return render