init
This commit is contained in:
@@ -0,0 +1,933 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user