/*========================================================================= Program: Visualization Toolkit Module: QVTKOpenGLWidget.cxx Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen All rights reserved. See Copyright.txt or http://www.kitware.com/Copyright.htm for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notice for more information. =========================================================================*/ #include "QVTKOpenGLWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "QVTKInteractor.h" #include "QVTKInteractorAdapter.h" #include "vtkCommand.h" #include "vtkGenericOpenGLRenderWindow.h" #include "vtkInteractorStyleTrackballCamera.h" #include "vtkNew.h" #include "vtkObjectFactory.h" // #define DEBUG_QVTKOPENGL_WIDGET #ifdef DEBUG_QVTKOPENGL_WIDGET #define vtkQVTKOpenGLWidgetDebugMacro(msg) \ cout << this << ": " msg << endl; \ if (this->Logger) \ { \ this->Logger->logMessage( \ QOpenGLDebugMessage::createApplicationMessage(QStringLiteral("QVTKOpenGLWidget::" msg))); \ } #else #define vtkQVTKOpenGLWidgetDebugMacro(x) #endif class QVTKOpenGLWidgetObserver : public vtkCommand { public: static QVTKOpenGLWidgetObserver* New() { return new QVTKOpenGLWidgetObserver(); } vtkTypeMacro(QVTKOpenGLWidgetObserver, vtkCommand); void SetTarget(QVTKOpenGLWidget* target) { this->Target = target; } void Execute(vtkObject*, unsigned long eventId, void* callData) override { if (this->Target) { switch (eventId) { case vtkCommand::WindowMakeCurrentEvent: { // We do not call QOpenGLWidget::makeCurrent() as that also makes the // frame buffer object used by QOpenGLWidget active. This can have // unintended side effects when MakeCurrent gets called in a // render-pass, for example. We should only be making the context // active. To do that, we use this trick. We rely on the // QOpenGLContext have been called makeCurrent() previously so we // can get to the surface that was used to do that. We simply // reactivate on that surface. QOpenGLContext* ctxt = this->Target->context(); QSurface* surface = ctxt? ctxt->surface() : nullptr; if (surface) { ctxt->makeCurrent(surface); } Q_ASSERT(ctxt == nullptr || surface != nullptr); } break; case vtkCommand::WindowIsCurrentEvent: { bool& cstatus = *reinterpret_cast(callData); cstatus = (QOpenGLContext::currentContext() == this->Target->context()); } break; case vtkCommand::WindowFrameEvent: this->Target->windowFrameEventCallback(); break; case vtkCommand::StartEvent: VTK_FALLTHROUGH; case vtkCommand::StartPickEvent: this->Target->startEventCallback(); break; } } } protected: QVTKOpenGLWidgetObserver() {} ~QVTKOpenGLWidgetObserver() override {} QPointer Target; }; //----------------------------------------------------------------------------- QVTKOpenGLWidget::QVTKOpenGLWidget(QWidget* parentWdg, Qt::WindowFlags f) : Superclass(parentWdg, f) , InteractorAdaptor(nullptr) , EnableHiDPI(false) , OriginalDPI(0) , FBO(nullptr) , InPaintGL(false) , DoVTKRenderInPaintGL(false) , Logger(nullptr) { this->Observer->SetTarget(this); // default to strong focus this->setFocusPolicy(Qt::StrongFocus); this->setUpdateBehavior(QOpenGLWidget::PartialUpdate); this->InteractorAdaptor = new QVTKInteractorAdapter(this); this->InteractorAdaptor->SetDevicePixelRatio(this->devicePixelRatio()); this->setMouseTracking(true); // QOpenGLWidget::resized() is triggered when the default FBO in QOpenGLWidget is recreated. // We use the same signal to recreate our FBO. this->connect(this, SIGNAL(resized()), SLOT(recreateFBO())); } //----------------------------------------------------------------------------- QVTKOpenGLWidget::~QVTKOpenGLWidget() { vtkQVTKOpenGLWidgetDebugMacro("~QVTKOpenGLWidget"); // essential to cleanup context so that the render window finalizes and // releases any graphics resources it may have allocated. this->cleanupContext(); this->SetRenderWindow(static_cast(nullptr)); this->Observer->SetTarget(nullptr); delete this->InteractorAdaptor; delete this->Logger; } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::SetRenderWindow(vtkRenderWindow* win) { vtkGenericOpenGLRenderWindow* gwin = vtkGenericOpenGLRenderWindow::SafeDownCast(win); this->SetRenderWindow(gwin); if (gwin == nullptr && win != nullptr) { qDebug() << "QVTKOpenGLWidget requires a `vtkGenericOpenGLRenderWindow`. `" << win->GetClassName() << "` is not supported."; } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::SetRenderWindow(vtkGenericOpenGLRenderWindow* win) { if (this->RenderWindow == win) { return; } if (this->RenderWindow) { this->RenderWindow->RemoveObserver(this->Observer); this->RenderWindow->SetReadyForRendering(false); } this->RenderWindow = win; this->requireRenderWindowInitialization(); if (this->RenderWindow) { // set this to 0 to reinitialize it before setting the RenderWindow DPI this->OriginalDPI = 0; // if an interactor wasn't provided, we'll make one by default if (!this->RenderWindow->GetInteractor()) { // create a default interactor vtkNew iren; // iren->SetUseTDx(this->UseTDx); this->RenderWindow->SetInteractor(iren); iren->Initialize(); // now set the default style vtkNew style; iren->SetInteractorStyle(style); } this->RenderWindow->AddObserver(vtkCommand::WindowMakeCurrentEvent, this->Observer); this->RenderWindow->AddObserver(vtkCommand::WindowIsCurrentEvent, this->Observer); this->RenderWindow->AddObserver(vtkCommand::WindowFrameEvent, this->Observer); this->RenderWindow->AddObserver(vtkCommand::StartEvent, this->Observer); this->RenderWindow->AddObserver(vtkCommand::StartPickEvent, this->Observer); if (this->FBO) { this->makeCurrent(); this->recreateFBO(); } } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::startEventCallback() { vtkQVTKOpenGLWidgetDebugMacro("startEventCallback"); this->makeCurrent(); if (this->FBO) { // ensure that before vtkRenderWindow starts to render, we activate the FBO // to render into. VTK code can be a bit lax with it. This just ensures that // we have the FBO activated. this->FBO->bind(); } } //----------------------------------------------------------------------------- vtkRenderWindow* QVTKOpenGLWidget::GetRenderWindow() { return this->RenderWindow; } //----------------------------------------------------------------------------- QVTKInteractor* QVTKOpenGLWidget::GetInteractor() { return QVTKInteractor::SafeDownCast(this->GetRenderWindow()->GetInteractor()); } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::copyFromFormat(const QSurfaceFormat& format, vtkRenderWindow* win) { if (vtkOpenGLRenderWindow* oglWin = vtkOpenGLRenderWindow::SafeDownCast(win)) { oglWin->SetStereoCapableWindow(format.stereo() ? 1 : 0); // samples may not be correct if format is obtained from // QOpenGLWidget::format() after the context is created. That's because // QOpenGLWidget always created context to samples=0. oglWin->SetMultiSamples(format.samples()); oglWin->SetStencilCapable(format.stencilBufferSize() > 0); } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::copyToFormat(vtkRenderWindow* win, QSurfaceFormat& format) { if (vtkOpenGLRenderWindow* oglWin = vtkOpenGLRenderWindow::SafeDownCast(win)) { format.setStereo(oglWin->GetStereoCapableWindow()); format.setSamples(oglWin->GetMultiSamples()); format.setStencilBufferSize(oglWin->GetStencilCapable() ? 8 : 0); } } //----------------------------------------------------------------------------- QSurfaceFormat QVTKOpenGLWidget::defaultFormat() { QSurfaceFormat fmt; fmt.setRenderableType(QSurfaceFormat::OpenGL); fmt.setVersion(3, 2); fmt.setProfile(QSurfaceFormat::CoreProfile); fmt.setSwapBehavior(QSurfaceFormat::DoubleBuffer); fmt.setRedBufferSize(1); fmt.setGreenBufferSize(1); fmt.setBlueBufferSize(1); fmt.setDepthBufferSize(1); fmt.setStencilBufferSize(0); fmt.setAlphaBufferSize(1); fmt.setStereo(false); fmt.setSamples(vtkOpenGLRenderWindow::GetGlobalMaximumNumberOfMultiSamples()); #ifdef DEBUG_QVTKOPENGL_WIDGET fmt.setOption(QSurfaceFormat::DebugContext); #endif return fmt; } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::setEnableHiDPI(bool enable) { this->EnableHiDPI = enable; if (this->RenderWindow) { if (this->OriginalDPI == 0) { this->OriginalDPI = this->RenderWindow->GetDPI(); } if (this->EnableHiDPI) { this->RenderWindow->SetDPI(this->OriginalDPI * this->devicePixelRatio()); } else { this->RenderWindow->SetDPI(this->OriginalDPI); } } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::recreateFBO() { vtkQVTKOpenGLWidgetDebugMacro("recreateFBO"); delete this->FBO; this->FBO = nullptr; if (!this->RenderWindow) { return; } // Since QVTKOpenGLWidget::initializeGL() cannot set multi-samples // state on the RenderWindow correctly, we do it here. QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); GLint samples; f->glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES, &samples); // Some graphics drivers report the number of samples as 1 when // multisampling is off. Set the number of samples to 0 in this case. samples = samples > 1 ? samples : 0; this->RenderWindow->SetMultiSamples(static_cast(samples)); QOpenGLFramebufferObjectFormat format; format.setAttachment(QOpenGLFramebufferObject::Depth); format.setSamples(samples); const int devicePixelRatio_ = this->devicePixelRatio(); const QSize widgetSize = this->size(); const QSize deviceSize = widgetSize * devicePixelRatio_; // This is as good an opportunity as any to communicate size to the render // window. this->InteractorAdaptor->SetDevicePixelRatio(devicePixelRatio_); if (vtkRenderWindowInteractor* iren = this->RenderWindow->GetInteractor()) { iren->SetSize(deviceSize.width(), deviceSize.height()); } this->RenderWindow->SetSize(deviceSize.width(), deviceSize.height()); this->RenderWindow->SetPosition(this->x() * devicePixelRatio_, this->y() * devicePixelRatio_); // Set screen size on render window. const QRect screenGeometry = QApplication::desktop()->screenGeometry(this); this->RenderWindow->SetScreenSize(screenGeometry.width(), screenGeometry.height()); this->FBO = new QOpenGLFramebufferObject(deviceSize, format); this->FBO->bind(); this->RenderWindow->SetForceMaximumHardwareLineWidth(1); this->RenderWindow->SetReadyForRendering(true); this->RenderWindow->InitializeFromCurrentContext(); this->setEnableHiDPI(this->EnableHiDPI); // Since the context or frame buffer was recreated, if a paintGL call ensues, // we need to ensure we're requesting VTK to render. this->DoVTKRenderInPaintGL = true; // Clear to ensure that an uninitialized framebuffer is never displayed. f->glDisable(GL_SCISSOR_TEST); f->glClearColor(0.0f, 0.0f, 0.0f, 1.0f); f->glClear(GL_COLOR_BUFFER_BIT); } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::initializeGL() { this->Superclass::initializeGL(); #ifdef DEBUG_QVTKOPENGL_WIDGET delete this->Logger; this->Logger = new QOpenGLDebugLogger(this); this->Logger->initialize(); // initializes in the current context. #endif vtkQVTKOpenGLWidgetDebugMacro("initializeGL"); if (this->RenderWindow) { // use QSurfaceFormat for the widget, update ivars on the vtkRenderWindow. QVTKOpenGLWidget::copyFromFormat(this->format(), this->RenderWindow); // When a QOpenGLWidget is told to use a QSurfaceFormat with samples > 0, // QOpenGLWidget doesn't actually create a context with multi-samples and // internally changes the QSurfaceFormat to be samples=0. Thus, we can't // rely on the QSurfaceFormat to indicate to us if multisampling is being // used. We should use glGetRenderbufferParameteriv(..) to get // GL_RENDERBUFFER_SAMPLES to determine the samples used. This is done by // in recreateFBO(). } this->connect( this->context(), SIGNAL(aboutToBeDestroyed()), SLOT(cleanupContext()), Qt::UniqueConnection); this->requireRenderWindowInitialization(); } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::requireRenderWindowInitialization() { if (this->RenderWindow) { this->RenderWindow->SetReadyForRendering(false); } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::resizeGL(int w, int h) { vtkQVTKOpenGLWidgetDebugMacro("resizeGL"); this->Superclass::resizeGL(w, h); } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::paintGL() { if (this->InPaintGL) { return; } if (!this->RenderWindow) { return; } if (!this->FBO || this->FBO->handle() != this->RenderWindow->GetDefaultFrameBufferId()) { this->recreateFBO(); } QScopedValueRollback var(this->InPaintGL, true); this->Superclass::paintGL(); if (this->DoVTKRenderInPaintGL && !this->renderVTK()) { vtkQVTKOpenGLWidgetDebugMacro("paintGL:skipped-renderVTK"); // This should be very rare, but it's conceivable that subclasses of // QVTKOpenGLWidget are simply not ready to do a // render on VTK render window when widget is being painted. // Leave the buffer unchanged. return; } // We just did a render, if we needed it. Turn the flag off. this->DoVTKRenderInPaintGL = false; // If render was triggered by above calls, that may change the current context // due to things like progress events triggering updates on other widgets // (e.g. progress bar). Hence we need to make sure to call makeCurrent() // before proceeding with blit-ing. this->makeCurrent(); // blit from this->FBO to QOpenGLWidget's FBO. vtkQVTKOpenGLWidgetDebugMacro("paintGL::blit-to-defaultFBO"); QOpenGLFunctions_3_2_Core* f = QOpenGLContext::currentContext()->versionFunctions(); if (f) { f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, this->defaultFramebufferObject()); f->glDrawBuffer(GL_COLOR_ATTACHMENT0); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, this->FBO->handle()); f->glReadBuffer(GL_COLOR_ATTACHMENT0); f->glDisable(GL_SCISSOR_TEST); // Scissor affects glBindFramebuffer. f->glBlitFramebuffer(0, 0, this->RenderWindow->GetSize()[0], this->RenderWindow->GetSize()[1], 0, 0, this->RenderWindow->GetSize()[0], this->RenderWindow->GetSize()[1], GL_COLOR_BUFFER_BIT, GL_NEAREST); // now clear alpha otherwise we end up blending the rendering with // background windows in certain cases. It happens on OsX // (if QSurfaceFormat::alphaBufferSize() > 0) or when using Mesa on Linux // (see paraview/paraview#17159). GLboolean colorMask[4]; f->glGetBooleanv(GL_COLOR_WRITEMASK, colorMask); f->glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE); GLfloat clearColor[4]; f->glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColor); f->glClearColor(0.0f, 0.0f, 0.0f, 1.0f); f->glClear(GL_COLOR_BUFFER_BIT); f->glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]); f->glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::cleanupContext() { // logger gets uninitialized when this gets called. We get errors from // QOpenGLDebugLogger if logMessage is called here. So we just destroy the // logger. delete this->Logger; this->Logger = nullptr; vtkQVTKOpenGLWidgetDebugMacro("cleanupContext"); // QOpenGLWidget says when this slot is called, the context may not be current // and hence is a good practice to make it so. this->makeCurrent(); if (this->RenderWindow) { if (this->FBO) { this->FBO->bind(); } this->RenderWindow->Finalize(); this->RenderWindow->SetReadyForRendering(false); } delete this->FBO; this->FBO = nullptr; this->requireRenderWindowInitialization(); } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::windowFrameEventCallback() { Q_ASSERT(this->RenderWindow); vtkQVTKOpenGLWidgetDebugMacro("frame"); if (!this->InPaintGL) { // Handing vtkOpenGLRenderWindow::Frame is tricky. VTK code traditionally // calls `Frame` to indicate that VTK is done rendering 1 frame. Now, when // that happens, should we tell Qt to update the widget -- that's the // question? In general, yes, but sometimes VTK does things in the // background i.e. back buffer without wanting to update the front buffer // e.g. when making selections. In that case, we don't want to update Qt // widget either, since whatever it did was not meant to be visible. // To handle that, we check if vtkOpenGLRenderWindow::SwapBuffers is true, // and request an update only when it is. if (this->RenderWindow->GetSwapBuffers() || this->RenderWindow->GetDoubleBuffer() == 0) { // Means that the vtkRenderWindow rendered outside a paintGL call. That can // happen when application code call vtkRenderWindow::Render() directly, // instead of calling QVTKOpenGLWidget::update() or letting Qt update the // widget. In that case, since QOpenGLWidget rendering into an offscreen // FBO, the result still needs to be composed by Qt widget stack. We request // that using `update()`. vtkQVTKOpenGLWidgetDebugMacro("update"); this->update(); this->DoVTKRenderInPaintGL = false; } else { vtkQVTKOpenGLWidgetDebugMacro("buffer bad -- do not show"); // Since this->FBO right now is garbage, if paint event is received before // a Render request is made on the render window, we will have to Render // explicitly. this->DoVTKRenderInPaintGL = true; } } } //----------------------------------------------------------------------------- bool QVTKOpenGLWidget::renderVTK() { vtkQVTKOpenGLWidgetDebugMacro("renderVTK"); Q_ASSERT(this->FBO); Q_ASSERT(this->FBO->handle() == this->RenderWindow->GetDefaultFrameBufferId()); // Bind the FBO we'll be rendering into. This may not be needed, since VTK will // bind it anyways, but we'll be extra cautious. this->FBO->bind(); vtkRenderWindowInteractor* iren = this->RenderWindow ? this->RenderWindow->GetInteractor() : nullptr; if (iren) { iren->Render(); } else if (this->RenderWindow) { this->RenderWindow->Render(); } else { // no render window set, just fill with white. QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); f->glClearColor(1.0f, 1.0f, 1.0f, 1.0f); f->glClear(GL_COLOR_BUFFER_BIT); } return true; } //----------------------------------------------------------------------------- bool QVTKOpenGLWidget::event(QEvent* evt) { switch (evt->type()) { case QEvent::MouseMove: case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: case QEvent::MouseButtonDblClick: // skip events that are explicitly handled by overrides to avoid duplicate // calls to InteractorAdaptor->ProcessEvent(). break; case QEvent::Resize: // we don't let QVTKInteractorAdapter process resize since we handle it // in this->recreateFBO(). break; default: if (this->RenderWindow && this->RenderWindow->GetInteractor()) { this->InteractorAdaptor->ProcessEvent(evt, this->RenderWindow->GetInteractor()); } } return this->Superclass::event(evt); } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::mousePressEvent(QMouseEvent* event) { emit mouseEvent(event); if (this->RenderWindow && this->RenderWindow->GetInteractor()) { this->InteractorAdaptor->ProcessEvent(event, this->RenderWindow->GetInteractor()); } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::mouseMoveEvent(QMouseEvent* event) { emit mouseEvent(event); if (this->RenderWindow && this->RenderWindow->GetInteractor()) { this->InteractorAdaptor->ProcessEvent(event, this->RenderWindow->GetInteractor()); } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::mouseReleaseEvent(QMouseEvent* event) { emit mouseEvent(event); if (this->RenderWindow && this->RenderWindow->GetInteractor()) { this->InteractorAdaptor->ProcessEvent(event, this->RenderWindow->GetInteractor()); } } //----------------------------------------------------------------------------- void QVTKOpenGLWidget::mouseDoubleClickEvent(QMouseEvent* event) { emit mouseEvent(event); if (this->RenderWindow && this->RenderWindow->GetInteractor()) { this->InteractorAdaptor->ProcessEvent(event, this->RenderWindow->GetInteractor()); } }