init
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
viewer
|
||||
-------------
|
||||
|
||||
View meshes and scenes via pyglet or inline HTML.
|
||||
"""
|
||||
|
||||
from .. import exceptions
|
||||
from .notebook import (
|
||||
in_notebook,
|
||||
scene_to_html,
|
||||
scene_to_mo_notebook,
|
||||
scene_to_notebook,
|
||||
)
|
||||
|
||||
try:
|
||||
# try importing windowed which will fail
|
||||
# if we can't create an openGL context
|
||||
from .windowed import SceneViewer, render_scene
|
||||
except BaseException as E:
|
||||
# if windowed failed to import only raise
|
||||
# the exception if someone tries to use them
|
||||
SceneViewer = exceptions.ExceptionWrapper(E)
|
||||
render_scene = exceptions.ExceptionWrapper(E)
|
||||
|
||||
|
||||
# this is only standard library imports
|
||||
|
||||
# explicitly list imports in __all__
|
||||
# as otherwise flake8 gets mad
|
||||
__all__ = [
|
||||
"SceneViewer",
|
||||
"SceneWidget",
|
||||
"in_notebook",
|
||||
"render_scene",
|
||||
"scene_to_html",
|
||||
"scene_to_mo_notebook",
|
||||
"scene_to_notebook",
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
notebook.py
|
||||
-------------
|
||||
|
||||
Render trimesh.Scene objects in HTML
|
||||
and jupyter and marimo notebooks using three.js
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import Literal
|
||||
|
||||
# for our template
|
||||
from .. import resources, util
|
||||
|
||||
|
||||
def scene_to_html(scene, escape_quotes: bool = False) -> str:
|
||||
"""
|
||||
Return HTML that will render the scene using
|
||||
GLTF/GLB encoded to base64 loaded by three.js
|
||||
|
||||
Parameters
|
||||
--------------
|
||||
scene : trimesh.Scene
|
||||
Source geometry
|
||||
escape_quotes
|
||||
If true, replaces quotes '"' with '"' so that the
|
||||
HTML is valid inside a `srcdoc` property.
|
||||
|
||||
Returns
|
||||
--------------
|
||||
html : str
|
||||
HTML containing embedded geometry
|
||||
"""
|
||||
# fetch HTML template from ZIP archive
|
||||
# it is bundling all of three.js so compression is nice
|
||||
base = (
|
||||
util.decompress(resources.get_bytes("templates/viewer.zip"), file_type="zip")[
|
||||
"viewer.html.template"
|
||||
]
|
||||
.read()
|
||||
.decode("utf-8")
|
||||
)
|
||||
# make sure scene has camera populated before export
|
||||
_ = scene.camera
|
||||
# get export as bytes
|
||||
data = scene.export(file_type="glb")
|
||||
# encode as base64 string
|
||||
encoded = base64.b64encode(data).decode("utf-8")
|
||||
# replace keyword with our scene data
|
||||
html = base.replace("$B64GLTF", encoded)
|
||||
|
||||
if escape_quotes:
|
||||
return html.replace('"', """)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def scene_to_notebook(scene, height=500, **kwargs):
|
||||
"""
|
||||
Convert a scene to HTML containing embedded geometry
|
||||
and a three.js viewer that will display nicely in
|
||||
an IPython/Jupyter notebook.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
scene : trimesh.Scene
|
||||
Source geometry
|
||||
|
||||
Returns
|
||||
-------------
|
||||
html : IPython.display.HTML
|
||||
Object containing rendered scene
|
||||
"""
|
||||
# keep as soft dependency
|
||||
from IPython import display
|
||||
|
||||
# convert scene to a full HTML page
|
||||
as_html = scene_to_html(scene=scene, escape_quotes=True)
|
||||
|
||||
# escape the quotes in the HTML
|
||||
srcdoc = as_html
|
||||
# embed this puppy as the srcdoc attr of an IFframe
|
||||
# I tried this a dozen ways and this is the only one that works
|
||||
# display.IFrame/display.Javascript really, really don't work
|
||||
# div is to avoid IPython's pointless hardcoded warning
|
||||
embedded = display.HTML(
|
||||
" ".join(
|
||||
[
|
||||
'<div><iframe srcdoc="{srcdoc}"',
|
||||
'width="100%" height="{height}px"',
|
||||
'style="border:none;"></iframe></div>',
|
||||
]
|
||||
).format(srcdoc=srcdoc, height=height)
|
||||
)
|
||||
return embedded
|
||||
|
||||
|
||||
def scene_to_mo_notebook(scene, height=500, **kwargs):
|
||||
"""
|
||||
Convert a scene to HTML containing embedded geometry
|
||||
and a three.js viewer that will display nicely in
|
||||
an Marimo notebook.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
scene : trimesh.Scene
|
||||
Source geometry
|
||||
|
||||
Returns
|
||||
-------------
|
||||
html : mo.Html
|
||||
Object containing rendered scene
|
||||
"""
|
||||
# keep as soft dependency
|
||||
import marimo as mo
|
||||
|
||||
# convert scene to a full HTML page
|
||||
srcdoc = scene_to_html(scene=scene, escape_quotes=True)
|
||||
|
||||
# Embed as srcdoc attr of IFrame, using mo.iframe
|
||||
# turns out displaying an empty image. Likely
|
||||
# similar to display.IFrame
|
||||
embedded = mo.Html(
|
||||
" ".join(
|
||||
[
|
||||
'<div><iframe srcdoc="{srcdoc}"',
|
||||
'width="100%" height="{height}px"',
|
||||
'style="border:none;"></iframe></div>',
|
||||
]
|
||||
).format(srcdoc=srcdoc, height=height)
|
||||
)
|
||||
|
||||
return embedded
|
||||
|
||||
|
||||
def in_notebook() -> Literal["jupyter", "marimo", False]:
|
||||
"""
|
||||
Check to see if we are in a Jypyter or Marimo notebook.
|
||||
|
||||
Returns
|
||||
-----------
|
||||
in_notebook
|
||||
Returns the type of notebook we're in or False if it
|
||||
is running as terminal application.
|
||||
"""
|
||||
try:
|
||||
# function returns IPython context, but only in IPython
|
||||
ipy = get_ipython() # NOQA
|
||||
# we only want to render rich output in notebooks
|
||||
# in terminals we definitely do not want to output HTML
|
||||
name = str(ipy.__class__).lower()
|
||||
terminal = "terminal" in name
|
||||
|
||||
# spyder uses ZMQshell, and can appear to be a notebook
|
||||
spyder = "_" in os.environ and "spyder" in os.environ["_"]
|
||||
|
||||
# assume we are in a notebook if we are not in
|
||||
# a terminal and we haven't been run by spyder
|
||||
if (not terminal) and (not spyder):
|
||||
return "jupyter"
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
try:
|
||||
import marimo as mo
|
||||
|
||||
if mo.running_in_notebook():
|
||||
return "marimo"
|
||||
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,240 @@
|
||||
# Copied from
|
||||
# https://github.com/mmatl/pyrender/blob/master/pyrender/trackball.py
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2019 Matthew Matl
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""Trackball class for 3D manipulation of viewpoints."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import transformations
|
||||
|
||||
|
||||
class Trackball:
|
||||
"""A trackball class for creating camera transforms from mouse movements."""
|
||||
|
||||
STATE_ROTATE = 0
|
||||
STATE_PAN = 1
|
||||
STATE_ROLL = 2
|
||||
STATE_ZOOM = 3
|
||||
|
||||
def __init__(self, pose, size, scale, target=None):
|
||||
"""Initialize a trackball with an initial camera-to-world pose
|
||||
and the given parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pose : [4,4]
|
||||
An initial camera-to-world pose for the trackball.
|
||||
|
||||
size : (float, float)
|
||||
The width and height of the camera image in pixels.
|
||||
|
||||
scale : float
|
||||
The diagonal of the scene's bounding box --
|
||||
used for ensuring translation motions are sufficiently
|
||||
fast for differently-sized scenes.
|
||||
|
||||
target : (3,) float
|
||||
The center of the scene in world coordinates.
|
||||
The trackball will revolve around this point.
|
||||
"""
|
||||
self._size = np.array(size)
|
||||
self._scale = float(scale)
|
||||
|
||||
self._pose = pose
|
||||
self._n_pose = pose
|
||||
|
||||
if target is None:
|
||||
self._target = np.array([0.0, 0.0, 0.0])
|
||||
self._n_target = np.array([0.0, 0.0, 0.0])
|
||||
else:
|
||||
self._target = target
|
||||
self._n_target = target
|
||||
|
||||
self._state = Trackball.STATE_ROTATE
|
||||
|
||||
@property
|
||||
def pose(self):
|
||||
"""autolab_core.RigidTransform : The current camera-to-world pose."""
|
||||
return self._n_pose
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set the state of the trackball in order to change the effect of
|
||||
dragging motions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : int
|
||||
One of Trackball.STATE_ROTATE, Trackball.STATE_PAN,
|
||||
Trackball.STATE_ROLL, and Trackball.STATE_ZOOM.
|
||||
"""
|
||||
self._state = state
|
||||
|
||||
def resize(self, size):
|
||||
"""Resize the window.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size : (float, float)
|
||||
The new width and height of the camera image in pixels.
|
||||
"""
|
||||
self._size = np.array(size)
|
||||
|
||||
def down(self, point):
|
||||
"""Record an initial mouse press at a given point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point : (2,) int
|
||||
The x and y pixel coordinates of the mouse press.
|
||||
"""
|
||||
self._pdown = np.array(point, dtype=np.float32)
|
||||
self._pose = self._n_pose
|
||||
self._target = self._n_target
|
||||
|
||||
def drag(self, point):
|
||||
"""Update the tracball during a drag.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point : (2,) int
|
||||
The current x and y pixel coordinates of the mouse during a drag.
|
||||
This will compute a movement for the trackball with the relative
|
||||
motion between this point and the one marked by down().
|
||||
"""
|
||||
point = np.array(point, dtype=np.float32)
|
||||
# get the "down" point defaulting to current point making
|
||||
# this a no-op if the "down" event didn't trigger for some reason
|
||||
dx, dy = point - getattr(self, "_pdown", point)
|
||||
mindim = 0.3 * np.min(self._size)
|
||||
|
||||
target = self._target
|
||||
x_axis = self._pose[:3, 0].flatten()
|
||||
y_axis = self._pose[:3, 1].flatten()
|
||||
z_axis = self._pose[:3, 2].flatten()
|
||||
eye = self._pose[:3, 3].flatten()
|
||||
|
||||
# Interpret drag as a rotation
|
||||
if self._state == Trackball.STATE_ROTATE:
|
||||
x_angle = -dx / mindim
|
||||
x_rot_mat = transformations.rotation_matrix(x_angle, y_axis, target)
|
||||
|
||||
y_angle = dy / mindim
|
||||
y_rot_mat = transformations.rotation_matrix(y_angle, x_axis, target)
|
||||
|
||||
self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose))
|
||||
|
||||
# Interpret drag as a roll about the camera axis
|
||||
elif self._state == Trackball.STATE_ROLL:
|
||||
center = self._size / 2.0
|
||||
v_init = self._pdown - center
|
||||
v_curr = point - center
|
||||
v_init = v_init / np.linalg.norm(v_init)
|
||||
v_curr = v_curr / np.linalg.norm(v_curr)
|
||||
|
||||
theta = -np.arctan2(v_curr[1], v_curr[0]) + np.arctan2(v_init[1], v_init[0])
|
||||
|
||||
rot_mat = transformations.rotation_matrix(theta, z_axis, target)
|
||||
|
||||
self._n_pose = rot_mat.dot(self._pose)
|
||||
|
||||
# Interpret drag as a camera pan in view plane
|
||||
elif self._state == Trackball.STATE_PAN:
|
||||
dx = -dx / (5.0 * mindim) * self._scale
|
||||
dy = -dy / (5.0 * mindim) * self._scale
|
||||
|
||||
translation = dx * x_axis + dy * y_axis
|
||||
self._n_target = self._target + translation
|
||||
t_tf = np.eye(4)
|
||||
t_tf[:3, 3] = translation
|
||||
self._n_pose = t_tf.dot(self._pose)
|
||||
|
||||
# Interpret drag as a zoom motion
|
||||
elif self._state == Trackball.STATE_ZOOM:
|
||||
radius = np.linalg.norm(eye - target)
|
||||
ratio = 0.0
|
||||
if dy > 0:
|
||||
ratio = np.exp(abs(dy) / (0.5 * self._size[1])) - 1.0
|
||||
elif dy < 0:
|
||||
ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1])))
|
||||
translation = -np.sign(dy) * ratio * radius * z_axis
|
||||
t_tf = np.eye(4)
|
||||
t_tf[:3, 3] = translation
|
||||
self._n_pose = t_tf.dot(self._pose)
|
||||
|
||||
def scroll(self, clicks):
|
||||
"""Zoom using a mouse scroll wheel motion.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
clicks : int
|
||||
The number of clicks. Positive numbers indicate forward wheel
|
||||
movement.
|
||||
"""
|
||||
target = self._target
|
||||
ratio = 0.90
|
||||
|
||||
mult = 1.0
|
||||
if clicks > 0:
|
||||
mult = ratio**clicks
|
||||
elif clicks < 0:
|
||||
mult = (1.0 / ratio) ** abs(clicks)
|
||||
|
||||
z_axis = self._n_pose[:3, 2].flatten()
|
||||
eye = self._n_pose[:3, 3].flatten()
|
||||
radius = np.linalg.norm(eye - target)
|
||||
translation = (mult * radius - radius) * z_axis
|
||||
t_tf = np.eye(4)
|
||||
t_tf[:3, 3] = translation
|
||||
self._n_pose = t_tf.dot(self._n_pose)
|
||||
|
||||
z_axis = self._pose[:3, 2].flatten()
|
||||
eye = self._pose[:3, 3].flatten()
|
||||
radius = np.linalg.norm(eye - target)
|
||||
translation = (mult * radius - radius) * z_axis
|
||||
t_tf = np.eye(4)
|
||||
t_tf[:3, 3] = translation
|
||||
self._pose = t_tf.dot(self._pose)
|
||||
|
||||
def rotate(self, azimuth, axis=None):
|
||||
"""Rotate the trackball about the "Up" axis by azimuth radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
azimuth : float
|
||||
The number of radians to rotate.
|
||||
"""
|
||||
target = self._target
|
||||
|
||||
y_axis = self._n_pose[:3, 1].flatten()
|
||||
if axis is not None:
|
||||
y_axis = axis
|
||||
x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
|
||||
self._n_pose = x_rot_mat.dot(self._n_pose)
|
||||
|
||||
y_axis = self._pose[:3, 1].flatten()
|
||||
if axis is not None:
|
||||
y_axis = axis
|
||||
x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
|
||||
self._pose = x_rot_mat.dot(self._pose)
|
||||
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
widget.py
|
||||
-------------
|
||||
|
||||
A widget which can visualize trimesh.Scene objects in a glooey window.
|
||||
|
||||
Check out an example in `examples/widget.py`
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import glooey
|
||||
import numpy as np
|
||||
import pyglet
|
||||
from pyglet import gl
|
||||
|
||||
from trimesh import rendering
|
||||
from trimesh.viewer.trackball import Trackball
|
||||
from trimesh.viewer.windowed import SceneViewer, _geometry_hash
|
||||
|
||||
warnings.warn(
|
||||
"`trimesh.viewer.widget` is deprecated and will "
|
||||
+ "be removed in January 2026, please vendor `widget.py` "
|
||||
+ "into your own project. It will be moved to the `examples` "
|
||||
+ "of trimesh and will no longer be importable!",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
class SceneGroup(pyglet.graphics.Group):
|
||||
def __init__(
|
||||
self,
|
||||
rect,
|
||||
scene,
|
||||
background=None,
|
||||
pixel_per_point=(1, 1),
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.rect = rect
|
||||
self.scene = scene
|
||||
|
||||
if background is None:
|
||||
background = [0.99, 0.99, 0.99, 1.0]
|
||||
self._background = background
|
||||
|
||||
self._pixel_per_point = pixel_per_point
|
||||
|
||||
def _set_view(self):
|
||||
left = int(self._pixel_per_point[0] * self.rect.left)
|
||||
bottom = int(self._pixel_per_point[1] * self.rect.bottom)
|
||||
width = int(self._pixel_per_point[0] * self.rect.width)
|
||||
height = int(self._pixel_per_point[1] * self.rect.height)
|
||||
|
||||
gl.glPushAttrib(gl.GL_ENABLE_BIT)
|
||||
gl.glEnable(gl.GL_SCISSOR_TEST)
|
||||
gl.glScissor(left, bottom, width, height)
|
||||
|
||||
self._mode = (gl.GLint)()
|
||||
gl.glGetIntegerv(gl.GL_MATRIX_MODE, self._mode)
|
||||
self._viewport = (gl.GLint * 4)()
|
||||
gl.glGetIntegerv(gl.GL_VIEWPORT, self._viewport)
|
||||
|
||||
gl.glViewport(left, bottom, width, height)
|
||||
gl.glMatrixMode(gl.GL_PROJECTION)
|
||||
gl.glPushMatrix()
|
||||
gl.glLoadIdentity()
|
||||
near = 0.01
|
||||
far = 1000.0
|
||||
gl.gluPerspective(self.scene.camera.fov[1], width / height, near, far)
|
||||
gl.glMatrixMode(gl.GL_MODELVIEW)
|
||||
|
||||
def _unset_view(self):
|
||||
gl.glMatrixMode(gl.GL_PROJECTION)
|
||||
gl.glPopMatrix()
|
||||
gl.glMatrixMode(self._mode.value)
|
||||
gl.glViewport(
|
||||
self._viewport[0],
|
||||
self._viewport[1],
|
||||
self._viewport[2],
|
||||
self._viewport[3],
|
||||
)
|
||||
|
||||
gl.glPopAttrib()
|
||||
|
||||
def set_state(self):
|
||||
self._set_view()
|
||||
|
||||
SceneViewer._gl_set_background(self._background)
|
||||
SceneViewer._gl_enable_depth(self.scene.camera)
|
||||
SceneViewer._gl_enable_color_material()
|
||||
SceneViewer._gl_enable_blending()
|
||||
SceneViewer._gl_enable_smooth_lines()
|
||||
SceneViewer._gl_enable_lighting(self.scene)
|
||||
|
||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
gl.glPushMatrix()
|
||||
gl.glLoadIdentity()
|
||||
gl.glMultMatrixf(
|
||||
rendering.matrix_to_gl(np.linalg.inv(self.scene.camera_transform))
|
||||
)
|
||||
|
||||
def unset_state(self):
|
||||
gl.glPopMatrix()
|
||||
|
||||
SceneViewer._gl_unset_background()
|
||||
|
||||
self._unset_view()
|
||||
|
||||
|
||||
class MeshGroup(pyglet.graphics.Group):
|
||||
def __init__(self, transform=None, texture=None, parent=None):
|
||||
super().__init__(parent)
|
||||
if transform is None:
|
||||
transform = np.eye(4)
|
||||
self.transform = transform
|
||||
self.texture = texture
|
||||
|
||||
def set_state(self):
|
||||
gl.glPushMatrix()
|
||||
gl.glMultMatrixf(rendering.matrix_to_gl(self.transform))
|
||||
|
||||
if self.texture:
|
||||
gl.glEnable(self.texture.target)
|
||||
gl.glBindTexture(self.texture.target, self.texture.id)
|
||||
|
||||
def unset_state(self):
|
||||
if self.texture:
|
||||
gl.glDisable(self.texture.target)
|
||||
|
||||
gl.glPopMatrix()
|
||||
|
||||
|
||||
class SceneWidget(glooey.Widget):
|
||||
def __init__(self, scene, **kwargs):
|
||||
super().__init__()
|
||||
self.scene = scene
|
||||
self._scene_group = None
|
||||
|
||||
# key is node_name
|
||||
self.mesh_group = {}
|
||||
|
||||
# key is geometry_name
|
||||
self.vertex_list = {}
|
||||
self.vertex_list_hash = {}
|
||||
self.textures = {}
|
||||
|
||||
self._initial_camera_transform = self.scene.camera_transform.copy()
|
||||
self.reset_view()
|
||||
|
||||
self._background = kwargs.pop("background", None)
|
||||
self._smooth = kwargs.pop("smooth", True)
|
||||
if kwargs:
|
||||
raise TypeError(f"unexpected kwargs: {kwargs}")
|
||||
|
||||
@property
|
||||
def scene_group(self):
|
||||
if self._scene_group is None:
|
||||
pixel_per_point = np.array(self.window.get_viewport_size()) / np.array(
|
||||
self.window.get_size()
|
||||
)
|
||||
self._scene_group = SceneGroup(
|
||||
rect=self.rect,
|
||||
scene=self.scene,
|
||||
background=self._background,
|
||||
pixel_per_point=pixel_per_point,
|
||||
parent=self.group,
|
||||
)
|
||||
return self._scene_group
|
||||
|
||||
def clear(self):
|
||||
self._scene_group = None
|
||||
self.mesh_group = {}
|
||||
while self.vertex_list:
|
||||
_, vertex = self.vertex_list.popitem()
|
||||
vertex.delete()
|
||||
self.vertex_list_hash = {}
|
||||
self.textures = {}
|
||||
|
||||
def reset_view(self):
|
||||
self.view = {
|
||||
"ball": Trackball(
|
||||
pose=self._initial_camera_transform,
|
||||
size=self.scene.camera.resolution,
|
||||
scale=self.scene.scale,
|
||||
target=self.scene.centroid,
|
||||
)
|
||||
}
|
||||
self.scene.camera_transform = self.view["ball"].pose
|
||||
|
||||
def do_claim(self):
|
||||
return 0, 0
|
||||
|
||||
def do_regroup(self):
|
||||
if not self.vertex_list:
|
||||
return
|
||||
|
||||
node_names = self.scene.graph.nodes_geometry
|
||||
for node_name in node_names:
|
||||
transform, geometry_name = self.scene.graph[node_name]
|
||||
if geometry_name not in self.vertex_list:
|
||||
continue
|
||||
vertex_list = self.vertex_list[geometry_name]
|
||||
|
||||
if node_name in self.mesh_group:
|
||||
mesh_group = self.mesh_group[node_name]
|
||||
else:
|
||||
mesh_group = MeshGroup(
|
||||
transform=transform,
|
||||
texture=self.textures.get(geometry_name),
|
||||
parent=self.scene_group,
|
||||
)
|
||||
self.mesh_group[node_name] = mesh_group
|
||||
self.batch.migrate(vertex_list, gl.GL_TRIANGLES, mesh_group, self.batch)
|
||||
|
||||
def do_draw(self):
|
||||
resolution = (self.rect.width, self.rect.height)
|
||||
if not (resolution == self.scene.camera.resolution).all():
|
||||
self.scene.camera.resolution = resolution
|
||||
|
||||
node_names = self.scene.graph.nodes_geometry
|
||||
for node_name in node_names:
|
||||
transform, geometry_name = self.scene.graph[node_name]
|
||||
geometry = self.scene.geometry[geometry_name]
|
||||
self._update_node(node_name, geometry_name, geometry, transform)
|
||||
|
||||
def do_undraw(self):
|
||||
if not self.vertex_list:
|
||||
return
|
||||
for vertex_list in self.vertex_list.values():
|
||||
vertex_list.delete()
|
||||
self._scene_group = None
|
||||
self.mesh_group = {}
|
||||
self.vertex_list = {}
|
||||
self.vertex_list_hash = {}
|
||||
self.textures = {}
|
||||
|
||||
def on_mouse_press(self, x, y, buttons, modifiers):
|
||||
SceneViewer.on_mouse_press(self, x, y, buttons, modifiers)
|
||||
self._draw()
|
||||
|
||||
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
||||
# detect a drag across widgets
|
||||
x_prev = x - dx
|
||||
y_prev = y - dy
|
||||
left, bottom = self.rect.left, self.rect.bottom
|
||||
width, height = self.rect.width, self.rect.height
|
||||
if not (left < x_prev <= left + width) or not (
|
||||
bottom < y_prev <= bottom + height
|
||||
):
|
||||
self.view["ball"].down(np.array([x, y]))
|
||||
|
||||
SceneViewer.on_mouse_drag(self, x, y, dx, dy, buttons, modifiers)
|
||||
self._draw()
|
||||
|
||||
def on_mouse_scroll(self, x, y, dx, dy):
|
||||
SceneViewer.on_mouse_scroll(self, x, y, dx, dy)
|
||||
self._draw()
|
||||
|
||||
def _update_node(self, node_name, geometry_name, geometry, transform):
|
||||
geometry_hash_new = _geometry_hash(geometry)
|
||||
if self.vertex_list_hash.get(geometry_name) != geometry_hash_new:
|
||||
# if geometry has texture defined convert it to opengl form
|
||||
if hasattr(geometry, "visual") and hasattr(geometry.visual, "material"):
|
||||
tex = rendering.material_to_texture(geometry.visual.material)
|
||||
if tex is not None:
|
||||
self.textures[geometry_name] = tex
|
||||
|
||||
if node_name in self.mesh_group:
|
||||
mesh_group = self.mesh_group[node_name]
|
||||
mesh_group.transform = transform
|
||||
mesh_group.texture = self.textures.get(geometry_name)
|
||||
else:
|
||||
mesh_group = MeshGroup(
|
||||
transform=transform,
|
||||
texture=self.textures.get(geometry_name),
|
||||
parent=self.scene_group,
|
||||
)
|
||||
self.mesh_group[node_name] = mesh_group
|
||||
|
||||
if self.vertex_list_hash.get(geometry_name) != geometry_hash_new:
|
||||
if geometry_name in self.vertex_list:
|
||||
self.vertex_list[geometry_name].delete()
|
||||
|
||||
# convert geometry to constructor args
|
||||
args = rendering.convert_to_vertexlist(
|
||||
geometry, group=mesh_group, smooth=self._smooth
|
||||
)
|
||||
# create the indexed vertex list
|
||||
self.vertex_list[geometry_name] = self.batch.add_indexed(*args)
|
||||
# save the MD5 of the geometry
|
||||
self.vertex_list_hash[geometry_name] = geometry_hash_new
|
||||
@@ -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