-
-
Save BigRoy/5ac50208969fdc69a722d66874faf8a2 to your computer and use it in GitHub Desktop.
""" | |
MIT License | |
Copyright (c) 2019 Roy Nieterau | |
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. | |
""" | |
import sys | |
from PySide import QtGui, QtCore | |
from pxr import Usd, UsdUtils, Sdf | |
from pxr.Usdviewq.stageView import StageView | |
app = QtGui.QApplication([]) | |
class Widget(QtGui.QWidget): | |
def __init__(self, stage=None): | |
super(Widget, self).__init__() | |
self.model = StageView.DefaultDataModel() | |
self.view = StageView(dataModel=self.model, | |
printTiming=True) | |
layout = QtGui.QVBoxLayout(self) | |
layout.addWidget(self.view) | |
layout.setContentsMargins(0, 0, 0, 0) | |
if stage: | |
self.setStage(stage) | |
def setStage(self, stage): | |
self.model.stage = stage | |
def closeEvent(self, event): | |
# Ensure to close the renderer to avoid GlfPostPendingGLErrors | |
self.view.closeRenderer() | |
# Load kitchen set | |
path = r"path/to/Kitchen_set/Kitchen_set.usd" | |
with Usd.StageCacheContext(UsdUtils.StageCache.Get()): | |
stage = Usd.Stage.Open(path) | |
window = Widget(stage) | |
window.setWindowTitle("USD Viewer") | |
window.resize(QtCore.QSize(750, 750)) | |
window.show() | |
# Make camera fit the loaded geometry | |
window.view.updateView(resetCam=True, forceComputeBBox=True) | |
app.exec_() |
Playing through time of the USD stage in the StageView embedded in your custom Qt interface
Playing through time might not seem obvious on how to do, because it involves setting up your own QTimer
to continuously trigger the viewport updates and actually advance the time. Just setting the model.playing = True
does not end up actually playing the scene. It's only telling the StageView.DefaultDataModel
that it should be representing the data in a way that's optimized for playback.
Similarly, during playback you'll need to trigger the update of the StageView
manually, preferably with view.updateForPlayback()
as opposed to view.updateView()
since that is also more optimized for playback. Plus it'll then also consider the playback settings, like not showing bounding boxes or selections during playback by default, dependent on ViewSettings
Since you advance the frames manually you'll also have to make sure you run at the right FPS and not faster. Here's what USD view implements to play the frames in a speed that matches the FPS.
Example Qt USD view widget with timeline
Here's an updated example of the above Gist snippet that also implements a very simple Timeline widget to start playing your loaded Alembics or USD files using the Hydra viewer right in your own Qt interface. Note that this example does not implement the "correct FPS speed" as described in the link above.
"""
MIT License
Copyright (c) 2019 Roy Nieterau
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.
"""
import sys
from PySide import QtGui, QtCore
from pxr import Usd
from pxr.Usdviewq.stageView import StageView
class QJumpSlider(QtGui.QSlider):
"""QSlider that jumps to exactly where you click on it.
This can also be done using QProxyStyle however that is unavailable
in PySide, PyQt4 and early releases of PySide2 (e.g. Maya 2019) as
such we implement it in a slightly less clean way.
See: https://stackoverflow.com/a/26281608/1838864
"""
def __init__(self, parent = None):
super(QJumpSlider, self).__init__(parent)
def mousePressEvent(self, event):
#Jump to click position
self.setValue(QtGui.QStyle.sliderValueFromPosition(self.minimum(),
self.maximum(),
event.x(),
self.width()))
def mouseMoveEvent(self, event):
#Jump to pointer position while moving
self.setValue(QtGui.QStyle.sliderValueFromPosition(self.minimum(),
self.maximum(),
event.x(),
self.width()))
class TimelineWidget(QtGui.QWidget):
"""Timeline widget
The timeline plays throught time using QTimer and
will try to match the FPS based on time spent between
each frame.
"""
# todo: Allow auto stop on __del__ or cleanup to kill timers
frameChanged = QtCore.Signal(int, bool)
playbackStopped = QtCore.Signal()
playbackStarted = QtCore.Signal()
def __init__(self, parent=None):
super(TimelineWidget, self).__init__(parent=parent)
# Don't take up more space in height than needed
self.setSizePolicy(QtGui.QSizePolicy.Preferred,
QtGui.QSizePolicy.Fixed)
self.slider = QJumpSlider(QtCore.Qt.Horizontal)
self.slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #999999;
background-color: #BBBBBB;
margin: 0px 0;
}
QSlider::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
border: 1px solid #5c5c5c;
width: 15px;
border-radius: 3px;
}
""")
# A bit of a random min/max
# todo: replace this with sys.minint or alike
RANGE = 1e6
self.start = QtGui.QSpinBox()
self.start.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons)
self.start.setMinimum(-RANGE)
self.start.setMaximum(RANGE)
self.start.setKeyboardTracking(False)
self.end = QtGui.QSpinBox()
self.end.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons)
self.end.setMinimum(-RANGE)
self.end.setMaximum(RANGE)
self.end.setKeyboardTracking(False)
self.frame = QtGui.QSpinBox()
self.frame.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons)
self.frame.setMinimum(-RANGE)
self.frame.setMaximum(RANGE)
self.frame.setKeyboardTracking(False)
self.playButton = QtGui.QPushButton("Play")
layout = QtGui.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
layout.addWidget(self.start)
layout.addWidget(self.slider)
layout.addWidget(self.end)
layout.addWidget(self.frame)
layout.addWidget(self.playButton)
# Timeout interval in ms. We set it to 0 so it runs as fast as
# possible. In advanceFrameForPlayback we use the sleep() call
# to slow down rendering to self.framesPerSecond fps.
self._timer = QtCore.QTimer(self)
self._timer.setInterval(0)
self._timer.timeout.connect(self._advanceFrameForPlayback)
self.playButton.clicked.connect(self.toggle_play)
self.slider.valueChanged.connect(self.frame.setValue)
self.frame.valueChanged.connect(self._frameChanged)
self.start.valueChanged.connect(self.slider.setMinimum)
self.end.valueChanged.connect(self.slider.setMaximum)
def setStartFrame(self, start):
self.start.setValue(start)
def setEndFrame(self, end):
self.end.setValue(end)
@property
def playing(self):
return self._timer.isActive()
@playing.setter
def playing(self, state):
if self.playing == state:
# Do nothing
return
# Change play/stop button based on new state
self.playButton.setText("Stop" if state else "Play")
if state:
self._timer.start()
self.playbackStarted.emit()
# Set focus to the slider as it helps
# key shortcuts to be registered on
# the widgets we actually want it.
self.slider.setFocus()
else:
self._timer.stop()
self.playbackStopped.emit()
def toggle_play(self):
# Toggle play state
self.playing = not self.playing
def _advanceFrameForPlayback(self):
# This should actually make sure that the playback speed
# matches the FPS of the scene. Currently it will advance
# as fast as possible. As such a very light scene will run
# super fast. See `_advanceFrameForPlayback` in USD view
# on how they manage the playback speed. That code is in:
# pxr/usdImaging/lib/usdviewq/appController.py
frame = self.frame.value()
frame += 1
# Loop around
if frame >= self.slider.maximum():
frame = self.slider.minimum()
self.slider.setValue(frame)
def _frameChanged(self, frame):
"""Trigger a frame change callback together with whether it's currently playing."""
if self.slider.value() != frame:
# Whenever a manual frame was entered
# in the frame lineedit then the slider
# would not have updated along.
self.slider.blockSignals(True)
self.slider.setValue(True)
self.slider.blockSignals(False)
self.frameChanged.emit(frame, self.playing)
class Widget(QtGui.QWidget):
def __init__(self, stage=None):
super(Widget, self).__init__()
self.model = StageView.DefaultDataModel()
self.model.viewSettings.showHUD = False
self.view = StageView(dataModel=self.model,
printTiming=True)
self.timeline = TimelineWidget()
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.timeline)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
self.timeline.frameChanged.connect(self.on_frame_changed)
self.timeline.playbackStarted.connect(self.on_playback_started)
self.timeline.playbackStopped.connect(self.on_playback_stopped)
if stage:
self.setStage(stage)
def setStage(self, stage):
self.model.stage = stage
# Set the model to the earliest time so that for animated meshes
# like Alembicit will be able to display the geometry
# see: https://github.com/PixarAnimationStudios/USD/issues/1022
earliest = Usd.TimeCode.EarliestTime()
self.model.currentFrame = Usd.TimeCode(earliest)
# Show the timeline and set it to frame range of the animation
# if the loaded stage has an authored time code.
if stage.HasAuthoredTimeCodeRange():
self.timeline.setVisible(True)
self.timeline.setStartFrame(stage.GetStartTimeCode())
self.timeline.setEndFrame(stage.GetEndTimeCode())
else:
self.timeline.setVisible(False)
def closeEvent(self, event):
# Stop timeline so it stops its QTimer
self.timeline.playing = False
# Ensure to close the renderer to avoid GlfPostPendingGLErrors
self.view.closeRenderer()
def on_frame_changed(self, value, playback):
self.model.currentFrame = Usd.TimeCode(value)
if playback:
self.view.updateForPlayback()
else:
self.view.updateView()
def on_playback_stopped(self):
self.model.playing = False
self.view.updateView()
def on_playback_started(self):
self.model.playing = True
self.view.updateForPlayback()
def keyPressEvent(self, event):
# Implement some shortcuts for the widget
# todo: move this code
key = event.key()
if key == QtCore.Qt.Key_Space:
self.timeline.toggle_play()
elif key == QtCore.Qt.Key_F:
# Reframe the objects
self.view.updateView(resetCam=True,
forceComputeBBox=True)
# Load kitchen set
path = r"path/to/Kitchen_set/Kitchen_set.usd"
stage = Usd.Stage.Open(path,
Usd.Stage.LoadAll)
app = QtGui.QApplication([])
window = Widget(stage)
window.setWindowTitle("USD Viewer")
window.show()
# Make camera fit the loaded geometry
window.view.updateView(resetCam=True, forceComputeBBox=True)
window.resize(QtCore.QSize(750, 750))
app.exec_()
Note: if you actually run this with the Kitchen_set.usd
the timeline will default to invisible as the set has no animation range whatsoever. Loading up a .usd
with timesamples or an .abc
file directly would show the timeline.
Original snippet of this gist updated to PySide2 by Thorsten Kaufmann (originally posted on Academy Software Foundation slack channel)
import sys
from PySide2 import QtGui, QtCore, QtWidgets
from pxr import Usd, UsdUtils, Sdf
from pxr.Usdviewq.stageView import StageView
app = QtWidgets.QApplication([])
class Widget(QtWidgets.QWidget):
def __init__(self, stage=None):
super(Widget, self).__init__()
self.model = StageView.DefaultDataModel()
self.view = StageView(dataModel=self.model)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.view)
layout.setContentsMargins(0, 0, 0, 0)
if stage:
self.setStage(stage)
def setStage(self, stage):
self.model.stage = stage
def closeEvent(self, event):
# Ensure to close the renderer to avoid GlfPostPendingGLErrors
self.view.closeRenderer()
path = r"<path_to_usd>"
with Usd.StageCacheContext(UsdUtils.StageCache.Get()):
stage = Usd.Stage.Open(path)
window = Widget(stage)
window.setWindowTitle("USD Viewer")
window.resize(QtCore.QSize(750, 750))
window.show()
# Make camera fit the loaded geometry
window.view.updateView(resetCam=True, forceComputeBBox=True)
app.exec_()
Hello @BigRoy, Thank you so much for providing this example code. It helped me get started making a PySide6 USD Hydra project.
You can find it as a test project here:
https://github.com/natestrong/hydra-usd-pyside-viewer
Force a current frame for time dependent loaded USD stages to ensure geometry gets displayed
When loading an Alembic file or any format that only has time-dependent input data make sure to set the time to read from in USD data model, otherwise it might fail to display the geometry as described in this USD issue.
The easiest fix is for example to set the timecode to "EarliestTime":
As a reference, this is where
usdview
is applying the currentFrame