diff --git a/doc/python.tex b/doc/python.tex index a4088418..2f0e4c56 100644 --- a/doc/python.tex +++ b/doc/python.tex @@ -567,46 +567,162 @@ code that updates the status and progress bar for more detail. import mitsuba, multiprocessing, sys, time 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 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 -from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage, QProgressBar +Signal = pyqtSignal -class MitsubaView(QMainWindow): - viewUpdated = pyqtSignal() - renderProgress = pyqtSignal(int) - renderingCompleted = pyqtSignal(bool) +class MitsubaRenderBuffer(RenderListener): + """ + Implements the Mitsuba callback interface to capture notifications about + 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): - super(MitsubaView, self).__init__() + super(MitsubaDemo, self).__init__() + # Initialize Mitsuba self.initializeMitsuba() self.job = self.createRenderJob() + # Initialize the user interface - self.setWindowTitle('Mitsuba/Qt demo') status = self.statusBar() + self.rwidget = RenderWidget(self, self.queue) + progress = QProgressBar(status) status.setContentsMargins(0,0,5,0) - self.progress = QProgressBar(status) - status.addPermanentWidget(self.progress) + status.addPermanentWidget(progress) status.setSizeGripEnabled(False) - self.setFixedSize(self.qimage.width(), self.qimage.height() + - self.progress.height()*1.5) + self.setWindowTitle('Mitsuba/Qt demo') + self.setCentralWidget(self.rwidget) - def handleRenderingCompleted(cancelled): - status.showMessage("Rendering finished.") - self.progress.setVisible(False) + # Hide the scroll bar once the rendering is done + def renderingUpdated(event): + if event == MitsubaRenderBuffer.RENDERING_FINISHED: + status.showMessage("Done.") + progress.hide() - if not cancelled: - outFile = FileStream("rendering.png", FileStream.ETruncReadWrite) - 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, + self.renderProgress.connect(progress.setValue, Qt.QueuedConnection) + self.rwidget.renderingUpdated.connect(renderingUpdated, Qt.QueuedConnection) + # Start the rendering process status.showMessage("Rendering ..") self.job.start() @@ -621,47 +737,20 @@ class MitsubaView(QMainWindow): self.queue = RenderQueue() # Get a reference to the plugin manager self.pmgr = PluginManager.getInstance() - # Appender to process log and progress messages within Python + + # Process Mitsuba log and progress messages within Python class CustomAppender(Appender): def append(self2, logLevel, message): print(message) def logProgress(self2, progress, name, formatted, eta): + # Asynchronously notify the main thread self.renderProgress.emit(progress) logger = Thread.getThread().getLogger() - logger.setLogLevel(EWarn) + logger.setLogLevel(EWarn) # Display warning & error messages logger.clearAppenders() 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): self.job.cancel() 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) def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: 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(): import signal # Stop the program upon Ctrl-C (SIGINT) signal.signal(signal.SIGINT, signal.SIG_DFL) app = QApplication(sys.argv) - view = MitsubaView() - view.show() - view.raise_() + demo = MitsubaDemo() + demo.show() + demo.raise_() retval = app.exec_() sys.exit(retval) diff --git a/include/mitsuba/render/renderjob.h b/include/mitsuba/render/renderjob.h index edc4b7f0..bd908607 100644 --- a/include/mitsuba/render/renderjob.h +++ b/include/mitsuba/render/renderjob.h @@ -92,6 +92,12 @@ public: */ 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() protected: /// Virtual destructor diff --git a/src/films/hdrfilm.cpp b/src/films/hdrfilm.cpp index b87bb103..efa6af26 100644 --- a/src/films/hdrfilm.cpp +++ b/src/films/hdrfilm.cpp @@ -373,7 +373,7 @@ public: uint8_t *targetData = target->getUInt8Data() + (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 */ cvt->convert(source->getPixelFormat(), 1.0f, sourceData, target->getPixelFormat(), target->getGamma(), targetData, diff --git a/src/films/ldrfilm.cpp b/src/films/ldrfilm.cpp index fb485e60..3333af57 100644 --- a/src/films/ldrfilm.cpp +++ b/src/films/ldrfilm.cpp @@ -273,7 +273,7 @@ public: uint8_t *targetData = target->getUInt8Data() + (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 */ cvt->convert(source->getPixelFormat(), 1.0f, sourceData, target->getPixelFormat(), target->getGamma(), targetData, diff --git a/src/films/mfilm.cpp b/src/films/mfilm.cpp index 67d399bf..adf9832d 100644 --- a/src/films/mfilm.cpp +++ b/src/films/mfilm.cpp @@ -214,7 +214,7 @@ public: uint8_t *targetData = target->getUInt8Data() + (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 */ cvt->convert(source->getPixelFormat(), 1.0f, sourceData, target->getPixelFormat(), target->getGamma(), targetData, diff --git a/src/libpython/render.cpp b/src/libpython/render.cpp index 6b37611b..dc11b1e9 100644 --- a/src/libpython/render.cpp +++ b/src/libpython/render.cpp @@ -334,11 +334,13 @@ void export_render() { .def("loadScene", &loadScene2, BP_RETURN_VALUE) .staticmethod("loadScene"); + Scene *(RenderJob::*renderJob_getScene)(void) = &RenderJob::getScene; BP_CLASS(RenderJob, Thread, (bp::init())) .def(bp::init >()) .def("flush", &RenderJob::flush) .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<>()) .def("getJobCount", &RenderQueue::getJobCount)