yet more refinements to the fancy PyQt integration example + necessary code adaptations
parent
fd7400593a
commit
96832ab70d
224
doc/python.tex
224
doc/python.tex
|
@ -567,46 +567,162 @@ code that updates the status and progress bar for more detail.
|
||||||
import mitsuba, multiprocessing, sys, time
|
import mitsuba, multiprocessing, sys, time
|
||||||
|
|
||||||
from mitsuba.core import Scheduler, PluginManager, Thread, Vector, Point2i, \
|
from mitsuba.core import Scheduler, PluginManager, Thread, Vector, Point2i, \
|
||||||
LocalWorker, Properties, Bitmap, Spectrum, Appender, EWarn, Transform, FileStream
|
Vector2i, LocalWorker, Properties, Bitmap, Spectrum, Appender, EWarn, \
|
||||||
|
Transform, FileStream
|
||||||
from mitsuba.render import RenderQueue, RenderJob, Scene, RenderListener
|
from mitsuba.render import RenderQueue, RenderJob, Scene, RenderListener
|
||||||
|
from PyQt4.QtCore import Qt, QPoint, QSize, QRect, pyqtSignal
|
||||||
|
from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage, \
|
||||||
|
QProgressBar, QWidget, QSizePolicy
|
||||||
|
|
||||||
from PyQt4.QtCore import Qt, QPoint, pyqtSignal
|
Signal = pyqtSignal
|
||||||
from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage, QProgressBar
|
|
||||||
|
|
||||||
class MitsubaView(QMainWindow):
|
class MitsubaRenderBuffer(RenderListener):
|
||||||
viewUpdated = pyqtSignal()
|
"""
|
||||||
renderProgress = pyqtSignal(int)
|
Implements the Mitsuba callback interface to capture notifications about
|
||||||
renderingCompleted = pyqtSignal(bool)
|
rendering progress. Partially completed image blocks are efficiently
|
||||||
|
tonemapped into a local 8-bit Mitsuba Bitmap instance and exposed as a QImage.
|
||||||
|
"""
|
||||||
|
RENDERING_FINISHED = 0
|
||||||
|
RENDERING_CANCELLED = 1
|
||||||
|
RENDERING_UPDATED = 2
|
||||||
|
|
||||||
|
def __init__(self, queue, callback):
|
||||||
|
super(MitsubaRenderBuffer, self).__init__()
|
||||||
|
self.bitmap = self.qimage = None
|
||||||
|
self.callback = callback
|
||||||
|
self.time = 0
|
||||||
|
self.size = Vector2i(0, 0)
|
||||||
|
queue.registerListener(self)
|
||||||
|
|
||||||
|
def workBeginEvent(self, job, wu, thr):
|
||||||
|
""" Callback: a worker thread started rendering an image block.
|
||||||
|
Draw a rectangle to highlight this """
|
||||||
|
_ = self._get_film_ensure_initialized(job)
|
||||||
|
self.bitmap.drawRect(wu.getOffset(), wu.getSize(), Spectrum(0.8))
|
||||||
|
self._potentially_send_update()
|
||||||
|
|
||||||
|
def workEndEvent(self, job, wr):
|
||||||
|
""" Callback: a worker thread finished rendering an image block.
|
||||||
|
Tonemap the associated pixels and store them in 'self.bitmap' """
|
||||||
|
film = self._get_film_ensure_initialized(job)
|
||||||
|
film.develop(wr.getOffset(), wr.getSize(), wr.getOffset(), self.bitmap)
|
||||||
|
self._potentially_send_update()
|
||||||
|
|
||||||
|
def refreshEvent(self, job):
|
||||||
|
""" Callback: the entire image changed (some rendering techniques
|
||||||
|
do this occasionally). Hence, tonemap the full film. """
|
||||||
|
film = self._get_film_ensure_initialized(job)
|
||||||
|
film.develop(Point2i(0), self.size, Point2i(0), self.bitmap)
|
||||||
|
self._potentially_send_update(force=True)
|
||||||
|
|
||||||
|
def finishJobEvent(self, job, cancelled):
|
||||||
|
""" Callback: the rendering job has finished or was cancelled.
|
||||||
|
Re-develop the image once more for the final result. """
|
||||||
|
film = self._get_film_ensure_initialized(job)
|
||||||
|
film.develop(Point2i(0), self.size, Point2i(0), self.bitmap)
|
||||||
|
self.callback(MitsubaRenderBuffer.RENDERING_CANCELLED if cancelled
|
||||||
|
else MitsubaRenderBuffer.RENDERING_FINISHED)
|
||||||
|
|
||||||
|
def _get_film_ensure_initialized(self, job):
|
||||||
|
""" Ensure that all internal data structure are set up to deal
|
||||||
|
with the given rendering job """
|
||||||
|
film = job.getScene().getFilm()
|
||||||
|
size = film.getSize()
|
||||||
|
|
||||||
|
if self.size != size:
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
# Round the buffer size to the next power of 4 to ensure 32-bit
|
||||||
|
# aligned scanlines in the underlying buffer. This is needed so
|
||||||
|
# that QtGui.QImage and mitsuba.Bitmap have exactly the same
|
||||||
|
# in-memory representation.
|
||||||
|
bufsize = Vector2i((size.x + 3) // 4 * 4, (size.y + 3) // 4 * 4)
|
||||||
|
|
||||||
|
# Create an 8-bit Mitsuba bitmap that will store tonemapped pixels
|
||||||
|
self.bitmap = Bitmap(Bitmap.ERGB, Bitmap.EUInt8, bufsize)
|
||||||
|
self.bitmap.clear()
|
||||||
|
|
||||||
|
# Create a QImage that is backed by the Mitsuba Bitmap instance
|
||||||
|
# (i.e. without doing unnecessary bitmap copy operations)
|
||||||
|
self.qimage = QImage(self.bitmap.getNativeBuffer(), self.size.x,
|
||||||
|
self.size.y, QImage.Format_RGB888)
|
||||||
|
return film
|
||||||
|
|
||||||
|
def _potentially_send_update(self, force = False):
|
||||||
|
""" Send an update request to any attached widgets, but not too often """
|
||||||
|
now = time.time()
|
||||||
|
if now - self.time > .25 or force:
|
||||||
|
self.time = now
|
||||||
|
self.callback(MitsubaRenderBuffer.RENDERING_UPDATED)
|
||||||
|
|
||||||
|
class RenderWidget(QWidget):
|
||||||
|
""" This simple widget attaches itself to a Mitsuba RenderQueue instance
|
||||||
|
and displays the progress of everything that's being rendered """
|
||||||
|
renderingUpdated = Signal(int)
|
||||||
|
|
||||||
|
def __init__(self, parent, queue):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.buffer = MitsubaRenderBuffer(queue, self.renderingUpdated.emit)
|
||||||
|
# Need a queued conn. to avoid threading issues between Qt and Mitsuba
|
||||||
|
self.renderingUpdated.connect(self._handle_update, Qt.QueuedConnection)
|
||||||
|
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return QSize(self.buffer.size.x, self.buffer.size.y)
|
||||||
|
|
||||||
|
def _handle_update(self, event):
|
||||||
|
image = self.buffer.qimage
|
||||||
|
# Detect when an image of different resolution is being rendered
|
||||||
|
if image.width() < self.width() or image.height() < self.height():
|
||||||
|
self.updateGeometry()
|
||||||
|
self.repaint()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
""" When there is more space then necessary, display the image centered
|
||||||
|
on a black background, surrounded by a light gray border """
|
||||||
|
QWidget.paintEvent(self, event)
|
||||||
|
qp = QPainter(self)
|
||||||
|
qp.fillRect(self.rect(), Qt.black)
|
||||||
|
|
||||||
|
image = self.buffer.qimage
|
||||||
|
if image is not None:
|
||||||
|
offset = QPoint((self.width() - image.width()) / 2,
|
||||||
|
(self.height() - image.height()) / 2)
|
||||||
|
qp.setPen(Qt.lightGray)
|
||||||
|
qp.drawRect(QRect(offset - QPoint(1, 1), image.size() + QSize(1, 1)))
|
||||||
|
qp.drawImage(offset, image)
|
||||||
|
qp.end()
|
||||||
|
|
||||||
|
class MitsubaDemo(QMainWindow):
|
||||||
|
renderProgress = Signal(int)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MitsubaView, self).__init__()
|
super(MitsubaDemo, self).__init__()
|
||||||
|
|
||||||
# Initialize Mitsuba
|
# Initialize Mitsuba
|
||||||
self.initializeMitsuba()
|
self.initializeMitsuba()
|
||||||
self.job = self.createRenderJob()
|
self.job = self.createRenderJob()
|
||||||
|
|
||||||
# Initialize the user interface
|
# Initialize the user interface
|
||||||
self.setWindowTitle('Mitsuba/Qt demo')
|
|
||||||
status = self.statusBar()
|
status = self.statusBar()
|
||||||
|
self.rwidget = RenderWidget(self, self.queue)
|
||||||
|
progress = QProgressBar(status)
|
||||||
status.setContentsMargins(0,0,5,0)
|
status.setContentsMargins(0,0,5,0)
|
||||||
self.progress = QProgressBar(status)
|
status.addPermanentWidget(progress)
|
||||||
status.addPermanentWidget(self.progress)
|
|
||||||
status.setSizeGripEnabled(False)
|
status.setSizeGripEnabled(False)
|
||||||
self.setFixedSize(self.qimage.width(), self.qimage.height() +
|
self.setWindowTitle('Mitsuba/Qt demo')
|
||||||
self.progress.height()*1.5)
|
self.setCentralWidget(self.rwidget)
|
||||||
|
|
||||||
def handleRenderingCompleted(cancelled):
|
# Hide the scroll bar once the rendering is done
|
||||||
status.showMessage("Rendering finished.")
|
def renderingUpdated(event):
|
||||||
self.progress.setVisible(False)
|
if event == MitsubaRenderBuffer.RENDERING_FINISHED:
|
||||||
|
status.showMessage("Done.")
|
||||||
|
progress.hide()
|
||||||
|
|
||||||
if not cancelled:
|
self.renderProgress.connect(progress.setValue, Qt.QueuedConnection)
|
||||||
outFile = FileStream("rendering.png", FileStream.ETruncReadWrite)
|
self.rwidget.renderingUpdated.connect(renderingUpdated,
|
||||||
self.bitmap.write(Bitmap.EPNG, outFile)
|
|
||||||
outFile.close()
|
|
||||||
|
|
||||||
self.viewUpdated.connect(self.repaint, Qt.QueuedConnection)
|
|
||||||
self.renderProgress.connect(self.progress.setValue, Qt.QueuedConnection)
|
|
||||||
self.renderingCompleted.connect(handleRenderingCompleted,
|
|
||||||
Qt.QueuedConnection)
|
Qt.QueuedConnection)
|
||||||
|
|
||||||
# Start the rendering process
|
# Start the rendering process
|
||||||
status.showMessage("Rendering ..")
|
status.showMessage("Rendering ..")
|
||||||
self.job.start()
|
self.job.start()
|
||||||
|
@ -621,47 +737,20 @@ class MitsubaView(QMainWindow):
|
||||||
self.queue = RenderQueue()
|
self.queue = RenderQueue()
|
||||||
# Get a reference to the plugin manager
|
# Get a reference to the plugin manager
|
||||||
self.pmgr = PluginManager.getInstance()
|
self.pmgr = PluginManager.getInstance()
|
||||||
# Appender to process log and progress messages within Python
|
|
||||||
|
# Process Mitsuba log and progress messages within Python
|
||||||
class CustomAppender(Appender):
|
class CustomAppender(Appender):
|
||||||
def append(self2, logLevel, message):
|
def append(self2, logLevel, message):
|
||||||
print(message)
|
print(message)
|
||||||
def logProgress(self2, progress, name, formatted, eta):
|
def logProgress(self2, progress, name, formatted, eta):
|
||||||
|
# Asynchronously notify the main thread
|
||||||
self.renderProgress.emit(progress)
|
self.renderProgress.emit(progress)
|
||||||
|
|
||||||
logger = Thread.getThread().getLogger()
|
logger = Thread.getThread().getLogger()
|
||||||
logger.setLogLevel(EWarn)
|
logger.setLogLevel(EWarn) # Display warning & error messages
|
||||||
logger.clearAppenders()
|
logger.clearAppenders()
|
||||||
logger.addAppender(CustomAppender())
|
logger.addAppender(CustomAppender())
|
||||||
|
|
||||||
# Listener to update bitmap subregions when blocks finish rendering
|
|
||||||
class CustomListener(RenderListener):
|
|
||||||
def __init__(self):
|
|
||||||
super(CustomListener, self).__init__()
|
|
||||||
self.time = 0
|
|
||||||
def workBeginEvent(self2, job, wu, thr):
|
|
||||||
self.bitmap.drawRect(wu.getOffset(), wu.getSize(), Spectrum(1.0))
|
|
||||||
now = time.time()
|
|
||||||
if now - self2.time > .25:
|
|
||||||
self.viewUpdated.emit()
|
|
||||||
self2.time = now
|
|
||||||
def workEndEvent(self2, job, wr):
|
|
||||||
self.film.develop(wr.getOffset(), wr.getSize(),
|
|
||||||
wr.getOffset(), self.bitmap)
|
|
||||||
now = time.time()
|
|
||||||
if now - self2.time > .25:
|
|
||||||
self.viewUpdated.emit()
|
|
||||||
self2.time = now
|
|
||||||
def refreshEvent(self2, job):
|
|
||||||
self.film.develop(Point2i(0), self.bitmap.getSize(),
|
|
||||||
Point2i(0), self.bitmap)
|
|
||||||
self.viewUpdated.emit()
|
|
||||||
def finishJobEvent(self2, job, cancelled):
|
|
||||||
self2.refreshEvent(job)
|
|
||||||
self.renderingCompleted.emit(cancelled)
|
|
||||||
|
|
||||||
# Create a custom listener and register it with the image queue
|
|
||||||
self.queue.registerListener(CustomListener())
|
|
||||||
|
|
||||||
def closeEvent(self, e):
|
def closeEvent(self, e):
|
||||||
self.job.cancel()
|
self.job.cancel()
|
||||||
self.queue.join()
|
self.queue.join()
|
||||||
|
@ -686,38 +775,21 @@ class MitsubaView(QMainWindow):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.film = self.scene.getFilm()
|
|
||||||
size = self.film.getSize()
|
|
||||||
|
|
||||||
# Mitsuba Bitmap that will store pixels of the developed film
|
|
||||||
self.bitmap = Bitmap(Bitmap.ERGB, Bitmap.EUInt8, size)
|
|
||||||
self.bitmap.clear()
|
|
||||||
|
|
||||||
# Create a Qt Image that directly points into the contents of self.bitmap
|
|
||||||
self.qimage = QImage(self.bitmap.getNativeBuffer(),
|
|
||||||
size.x, size.y, QImage.Format_RGB888)
|
|
||||||
|
|
||||||
return RenderJob('rjob', self.scene, self.queue)
|
return RenderJob('rjob', self.scene, self.queue)
|
||||||
|
|
||||||
def keyPressEvent(self, e):
|
def keyPressEvent(self, e):
|
||||||
if e.key() == Qt.Key_Escape:
|
if e.key() == Qt.Key_Escape:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
super(MitsubaView, self).paintEvent(event)
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.drawImage(QPoint(0, 0), self.qimage)
|
|
||||||
painter.end()
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import signal
|
import signal
|
||||||
# Stop the program upon Ctrl-C (SIGINT)
|
# Stop the program upon Ctrl-C (SIGINT)
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
view = MitsubaView()
|
demo = MitsubaDemo()
|
||||||
view.show()
|
demo.show()
|
||||||
view.raise_()
|
demo.raise_()
|
||||||
retval = app.exec_()
|
retval = app.exec_()
|
||||||
sys.exit(retval)
|
sys.exit(retval)
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,12 @@ public:
|
||||||
*/
|
*/
|
||||||
inline bool isInteractive() const { return m_interactive; }
|
inline bool isInteractive() const { return m_interactive; }
|
||||||
|
|
||||||
|
/// Get a pointer to the underlying scene
|
||||||
|
inline Scene *getScene() { return m_scene.get(); }
|
||||||
|
|
||||||
|
/// Get a pointer to the underlying scene (const version)
|
||||||
|
inline const Scene *getScene() const { return m_scene.get(); }
|
||||||
|
|
||||||
MTS_DECLARE_CLASS()
|
MTS_DECLARE_CLASS()
|
||||||
protected:
|
protected:
|
||||||
/// Virtual destructor
|
/// Virtual destructor
|
||||||
|
|
|
@ -373,7 +373,7 @@ public:
|
||||||
uint8_t *targetData = target->getUInt8Data()
|
uint8_t *targetData = target->getUInt8Data()
|
||||||
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
|
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
|
||||||
|
|
||||||
if (size.x == m_cropSize.x) {
|
if (size.x == m_cropSize.x && target->getWidth() == m_storage->getWidth()) {
|
||||||
/* Develop a connected part of the underlying buffer */
|
/* Develop a connected part of the underlying buffer */
|
||||||
cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
|
cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
|
||||||
target->getPixelFormat(), target->getGamma(), targetData,
|
target->getPixelFormat(), target->getGamma(), targetData,
|
||||||
|
|
|
@ -273,7 +273,7 @@ public:
|
||||||
uint8_t *targetData = target->getUInt8Data()
|
uint8_t *targetData = target->getUInt8Data()
|
||||||
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
|
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
|
||||||
|
|
||||||
if (size.x == m_cropSize.x) {
|
if (size.x == m_cropSize.x && target->getWidth() == m_storage->getWidth()) {
|
||||||
/* Develop a connected part of the underlying buffer */
|
/* Develop a connected part of the underlying buffer */
|
||||||
cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
|
cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
|
||||||
target->getPixelFormat(), target->getGamma(), targetData,
|
target->getPixelFormat(), target->getGamma(), targetData,
|
||||||
|
|
|
@ -214,7 +214,7 @@ public:
|
||||||
uint8_t *targetData = target->getUInt8Data()
|
uint8_t *targetData = target->getUInt8Data()
|
||||||
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
|
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
|
||||||
|
|
||||||
if (size.x == m_cropSize.x) {
|
if (size.x == m_cropSize.x && target->getWidth() == m_storage->getWidth()) {
|
||||||
/* Develop a connected part of the underlying buffer */
|
/* Develop a connected part of the underlying buffer */
|
||||||
cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
|
cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
|
||||||
target->getPixelFormat(), target->getGamma(), targetData,
|
target->getPixelFormat(), target->getGamma(), targetData,
|
||||||
|
|
|
@ -334,11 +334,13 @@ void export_render() {
|
||||||
.def("loadScene", &loadScene2, BP_RETURN_VALUE)
|
.def("loadScene", &loadScene2, BP_RETURN_VALUE)
|
||||||
.staticmethod("loadScene");
|
.staticmethod("loadScene");
|
||||||
|
|
||||||
|
Scene *(RenderJob::*renderJob_getScene)(void) = &RenderJob::getScene;
|
||||||
BP_CLASS(RenderJob, Thread, (bp::init<const std::string &, Scene *, RenderQueue *>()))
|
BP_CLASS(RenderJob, Thread, (bp::init<const std::string &, Scene *, RenderQueue *>()))
|
||||||
.def(bp::init<const std::string &, Scene *, RenderQueue *, int, bp::optional<int, int> >())
|
.def(bp::init<const std::string &, Scene *, RenderQueue *, int, bp::optional<int, int> >())
|
||||||
.def("flush", &RenderJob::flush)
|
.def("flush", &RenderJob::flush)
|
||||||
.def("cancel", renderJob_cancel)
|
.def("cancel", renderJob_cancel)
|
||||||
.def("wait", &RenderJob::wait);
|
.def("wait", &RenderJob::wait)
|
||||||
|
.def("getScene", renderJob_getScene, BP_RETURN_VALUE);
|
||||||
|
|
||||||
BP_CLASS(RenderQueue, Object, bp::init<>())
|
BP_CLASS(RenderQueue, Object, bp::init<>())
|
||||||
.def("getJobCount", &RenderQueue::getJobCount)
|
.def("getJobCount", &RenderQueue::getJobCount)
|
||||||
|
|
Loading…
Reference in New Issue