diff options
author | Kristoffer Skau <kristoffer.skau@qt.io> | 2022-11-24 14:31:09 +0100 |
---|---|---|
committer | Kristoffer Skau <kristoffer.skau@qt.io> | 2022-12-01 13:42:07 +0100 |
commit | c450f6d21c5153e05bd10afdd54767623cfbe7e8 (patch) | |
tree | e7ce6d73cda2f0f83673cad8b528bb67463e9cdd | |
parent | 9bc74d14f379ad58d7a80d5514a3db5be5012de0 (diff) |
Support stereoscopic rendering with QGraphicsView
This patch adds a manual test and the required work in graphicsview and
qwidget private apis to support stereoscopic rendeing. Basically it
works by doing the drawing in QGraphicsView::paintEvent twice, once for
each buffer. This way the scene items are rendered to both buffers.
There's also an update to resolvement in QOpenGLWidgetPrivate
so that multisampling works correctly.
[ChangeLog][Widgets][QGraphicsView] Added support for
stereoscopic rendering.
Task-number: QTBUG-64587
Change-Id: I20650682daa805b64fe7f0d2ba086917d3f12229
Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io>
-rw-r--r-- | src/openglwidgets/qopenglwidget.cpp | 96 | ||||
-rw-r--r-- | src/openglwidgets/qopenglwidget.h | 1 | ||||
-rw-r--r-- | src/widgets/graphicsview/qgraphicsview.cpp | 253 | ||||
-rw-r--r-- | src/widgets/graphicsview/qgraphicsview_p.h | 2 | ||||
-rw-r--r-- | src/widgets/kernel/qwidget_p.h | 5 | ||||
-rw-r--r-- | tests/manual/stereographicsview/CMakeLists.txt | 32 | ||||
-rw-r--r-- | tests/manual/stereographicsview/main.cpp | 26 | ||||
-rw-r--r-- | tests/manual/stereographicsview/mainwindow.cpp | 79 | ||||
-rw-r--r-- | tests/manual/stereographicsview/mainwindow.h | 30 | ||||
-rw-r--r-- | tests/manual/stereographicsview/mainwindow.ui | 101 | ||||
-rw-r--r-- | tests/manual/stereographicsview/mygraphicsview.cpp | 209 | ||||
-rw-r--r-- | tests/manual/stereographicsview/mygraphicsview.h | 47 | ||||
-rw-r--r-- | tests/manual/stereographicsview/stereographicsview.pro | 13 |
13 files changed, 763 insertions, 131 deletions
diff --git a/src/openglwidgets/qopenglwidget.cpp b/src/openglwidgets/qopenglwidget.cpp index 7a0ac05cee..4d8c181c22 100644 --- a/src/openglwidgets/qopenglwidget.cpp +++ b/src/openglwidgets/qopenglwidget.cpp @@ -552,7 +552,7 @@ public: void destroyFbos(); - void setCurrentTargetBuffer(QOpenGLWidget::TargetBuffer targetBuffer); + bool setCurrentTargetBuffer(QOpenGLWidget::TargetBuffer targetBuffer); QImage grabFramebuffer(QOpenGLWidget::TargetBuffer targetBuffer); QImage grabFramebuffer() override; void beginBackingStorePainting() override { inBackingStorePaint = true; } @@ -560,9 +560,13 @@ public: void beginCompose() override; void endCompose() override; void initializeViewportFramebuffer() override; + bool isStereoEnabled() override; + bool toggleStereoTargetBuffer() override; void resizeViewportFramebuffer() override; void resolveSamples() override; + void resolveSamplesForBuffer(QOpenGLWidget::TargetBuffer targetBuffer); + QOpenGLContext *context = nullptr; QRhiTexture *wrapperTextures[2] = {}; QOpenGLFramebufferObject *fbos[2] = {}; @@ -697,8 +701,6 @@ void QOpenGLWidgetPrivate::reset() void QOpenGLWidgetPrivate::resetRhiDependentResources() { - Q_Q(QOpenGLWidget); - // QRhi resource created from the QRhi. These must be released whenever the // widget gets associated with a different QRhi, even when all OpenGL // contexts share resources. @@ -706,7 +708,7 @@ void QOpenGLWidgetPrivate::resetRhiDependentResources() delete wrapperTextures[0]; wrapperTextures[0] = nullptr; - if (q->format().stereo()) { + if (isStereoEnabled()) { delete wrapperTextures[1]; wrapperTextures[1] = nullptr; } @@ -738,9 +740,9 @@ void QOpenGLWidgetPrivate::recreateFbos() if (samples > 0) resolvedFbos[QOpenGLWidget::LeftBuffer] = new QOpenGLFramebufferObject(deviceSize); - const bool stereoEnabled = q->format().stereo(); + const bool stereo = isStereoEnabled(); - if (stereoEnabled) { + if (stereo) { fbos[QOpenGLWidget::RightBuffer] = new QOpenGLFramebufferObject(deviceSize, format); if (samples > 0) resolvedFbos[QOpenGLWidget::RightBuffer] = new QOpenGLFramebufferObject(deviceSize); @@ -753,11 +755,12 @@ void QOpenGLWidgetPrivate::recreateFbos() context->functions()->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); ensureRhiDependentResources(); - if (stereoEnabled) { + if (stereo) { currentTargetBuffer = QOpenGLWidget::RightBuffer; fbos[currentTargetBuffer]->bind(); context->functions()->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); ensureRhiDependentResources(); + currentTargetBuffer = QOpenGLWidget::LeftBuffer; } flushPending = true; // Make sure the FBO is initialized before use @@ -895,11 +898,17 @@ void QOpenGLWidgetPrivate::initialize() void QOpenGLWidgetPrivate::resolveSamples() { + resolveSamplesForBuffer(QOpenGLWidget::LeftBuffer); + resolveSamplesForBuffer(QOpenGLWidget::RightBuffer); +} + +void QOpenGLWidgetPrivate::resolveSamplesForBuffer(QOpenGLWidget::TargetBuffer targetBuffer) +{ Q_Q(QOpenGLWidget); - if (resolvedFbos[currentTargetBuffer]) { - q->makeCurrent(); - QRect rect(QPoint(0, 0), fbos[currentTargetBuffer]->size()); - QOpenGLFramebufferObject::blitFramebuffer(resolvedFbos[currentTargetBuffer], rect, fbos[currentTargetBuffer], rect); + if (resolvedFbos[targetBuffer]) { + q->makeCurrent(targetBuffer); + QRect rect(QPoint(0, 0), fbos[targetBuffer]->size()); + QOpenGLFramebufferObject::blitFramebuffer(resolvedFbos[targetBuffer], rect, fbos[targetBuffer], rect); flushPending = true; } } @@ -924,8 +933,8 @@ void QOpenGLWidgetPrivate::render() return; } - const bool stereoEnabled = q->format().stereo(); - if (stereoEnabled) { + const bool stereo = isStereoEnabled(); + if (stereo) { static bool warningGiven = false; if (!fbos[QOpenGLWidget::RightBuffer] && !warningGiven) { qWarning("QOpenGLWidget: Stereo is enabled, but no right buffer. Using only left buffer"); @@ -936,7 +945,7 @@ void QOpenGLWidgetPrivate::render() if (updateBehavior == QOpenGLWidget::NoPartialUpdate && hasBeenComposed) { invalidateFbo(); - if (stereoEnabled && fbos[QOpenGLWidget::RightBuffer]) { + if (stereo && fbos[QOpenGLWidget::RightBuffer]) { setCurrentTargetBuffer(QOpenGLWidget::RightBuffer); invalidateFbo(); setCurrentTargetBuffer(QOpenGLWidget::LeftBuffer); @@ -952,7 +961,7 @@ void QOpenGLWidgetPrivate::render() QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = fbos[currentTargetBuffer]->handle(); q->paintGL(); - if (stereoEnabled && fbos[QOpenGLWidget::RightBuffer]) { + if (stereo && fbos[QOpenGLWidget::RightBuffer]) { setCurrentTargetBuffer(QOpenGLWidget::RightBuffer); QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = fbos[currentTargetBuffer]->handle(); q->paintGL(); @@ -1017,7 +1026,7 @@ QImage QOpenGLWidgetPrivate::grabFramebuffer(QOpenGLWidget::TargetBuffer targetB // The second fbo is only created when stereoscopic rendering is enabled // Just use the default one if not. - if (targetBuffer == QOpenGLWidget::RightBuffer && !q->format().stereo()) + if (targetBuffer == QOpenGLWidget::RightBuffer && !isStereoEnabled()) targetBuffer = QOpenGLWidget::LeftBuffer; if (!fbos[targetBuffer]) // could be completely offscreen, without ever getting a resize event @@ -1028,7 +1037,7 @@ QImage QOpenGLWidgetPrivate::grabFramebuffer(QOpenGLWidget::TargetBuffer targetB setCurrentTargetBuffer(targetBuffer); if (resolvedFbos[targetBuffer]) { - resolveSamples(); + resolveSamplesForBuffer(targetBuffer); resolvedFbos[targetBuffer]->bind(); } @@ -1054,6 +1063,22 @@ void QOpenGLWidgetPrivate::initializeViewportFramebuffer() q->setAutoFillBackground(true); } +bool QOpenGLWidgetPrivate::isStereoEnabled() +{ + Q_Q(QOpenGLWidget); + // Note that because this internally might use the requested format, + // then this can return a false positive on hardware where + // steroscopic rendering is not supported. + return q->format().stereo(); +} + +bool QOpenGLWidgetPrivate::toggleStereoTargetBuffer() +{ + return setCurrentTargetBuffer(currentTargetBuffer == QOpenGLWidget::LeftBuffer ? + QOpenGLWidget::RightBuffer : + QOpenGLWidget::LeftBuffer); +} + void QOpenGLWidgetPrivate::resizeViewportFramebuffer() { Q_Q(QOpenGLWidget); @@ -1254,6 +1279,34 @@ void QOpenGLWidget::makeCurrent() } /*! + Prepares for rendering OpenGL content for this widget by making the + context for the passed in buffer current and binding the framebuffer object in that + context. + + \note This only makes sense to call when stereoscopic rendering is enabled. + Nothing will happen if the right buffer is requested when it's disabled. + + It is not necessary to call this function in most cases, because it + is called automatically before invoking paintGL(). + + \since 6.5 + + \sa context(), paintGL(), doneCurrent() + */ +void QOpenGLWidget::makeCurrent(TargetBuffer targetBuffer) +{ + Q_D(QOpenGLWidget); + if (!d->initialized) + return; + + // The FBO for the right buffer is only initialized when stereo is set + if (targetBuffer == TargetBuffer::RightBuffer && !format().stereo()) + return; + + d->setCurrentTargetBuffer(targetBuffer); // calls makeCurrent +} + +/*! Releases the context. It is not necessary to call this function in most cases, since the @@ -1585,11 +1638,18 @@ QPaintEngine *QOpenGLWidget::paintEngine() const return d->paintDevice->paintEngine(); } -void QOpenGLWidgetPrivate::setCurrentTargetBuffer(QOpenGLWidget::TargetBuffer targetBuffer) + +bool QOpenGLWidgetPrivate::setCurrentTargetBuffer(QOpenGLWidget::TargetBuffer targetBuffer) { Q_Q(QOpenGLWidget); + + if (targetBuffer == QOpenGLWidget::RightBuffer && !isStereoEnabled()) + return false; + currentTargetBuffer = targetBuffer; q->makeCurrent(); + + return true; } /*! diff --git a/src/openglwidgets/qopenglwidget.h b/src/openglwidgets/qopenglwidget.h index 84097854e5..847e684cd9 100644 --- a/src/openglwidgets/qopenglwidget.h +++ b/src/openglwidgets/qopenglwidget.h @@ -45,6 +45,7 @@ public: bool isValid() const; void makeCurrent(); + void makeCurrent(TargetBuffer targetBuffer); void doneCurrent(); QOpenGLContext *context() const; diff --git a/src/widgets/graphicsview/qgraphicsview.cpp b/src/widgets/graphicsview/qgraphicsview.cpp index 8e362ce1e0..f9502b938a 100644 --- a/src/widgets/graphicsview/qgraphicsview.cpp +++ b/src/widgets/graphicsview/qgraphicsview.cpp @@ -85,6 +85,16 @@ static const int QGRAPHICSVIEW_PREALLOC_STYLE_OPTIONS = 503; // largest prime < view coordinates and scene coordinates, and to find items on the scene using view coordinates. + When using a QOpenGLWidget as a viewport, stereoscopic rendering is + supported. This is done using the same pattern as QOpenGLWidget::paintGL. + To enable it, enable the QSurfaceFormat::StereoBuffers flag. Because of + how the flag is handled internally, set QSurfaceFormat::StereoBuffers flag + globally before the window is created using QSurfaceFormat::setDefaultFormat(). + If the flag is enabled and there is hardware support for stereoscopic + rendering, then drawBackground() and drawForeground() will be triggered twice + each frame. Call QOpenGLWidget::currentTargetBuffer() to query which buffer + is currently being drawn to. + \image graphicsview-view.png \note Using an OpenGL viewport limits the ability to use QGraphicsProxyWidget. @@ -2724,6 +2734,9 @@ void QGraphicsView::setupViewport(QWidget *widget) widget->setFocusPolicy(Qt::StrongFocus); + if (isGLWidget) + d->stereoEnabled = QWidgetPrivate::get(widget)->isStereoEnabled(); + if (!isGLWidget) { // autoFillBackground enables scroll acceleration. widget->setAutoFillBackground(true); @@ -3425,132 +3438,146 @@ void QGraphicsView::paintEvent(QPaintEvent *event) painter.setWorldTransform(viewportTransform()); const QTransform viewTransform = painter.worldTransform(); - // Draw background - if (d->cacheMode & CacheBackground) { - // Recreate the background pixmap, and flag the whole background as - // exposed. - if (d->mustResizeBackgroundPixmap) { - const qreal dpr = d->viewport->devicePixelRatio(); - d->backgroundPixmap = QPixmap(viewport()->size() * dpr); - d->backgroundPixmap.setDevicePixelRatio(dpr); - QBrush bgBrush = viewport()->palette().brush(viewport()->backgroundRole()); - if (!bgBrush.isOpaque()) - d->backgroundPixmap.fill(Qt::transparent); - QPainter p(&d->backgroundPixmap); - p.fillRect(0, 0, d->backgroundPixmap.width(), d->backgroundPixmap.height(), bgBrush); - d->backgroundPixmapExposed = QRegion(viewport()->rect()); - d->mustResizeBackgroundPixmap = false; - } + const auto actuallyDraw = [&]() { + // Draw background + if (d->cacheMode & CacheBackground) { + // Recreate the background pixmap, and flag the whole background as + // exposed. + if (d->mustResizeBackgroundPixmap) { + const qreal dpr = d->viewport->devicePixelRatio(); + d->backgroundPixmap = QPixmap(viewport()->size() * dpr); + d->backgroundPixmap.setDevicePixelRatio(dpr); + QBrush bgBrush = viewport()->palette().brush(viewport()->backgroundRole()); + if (!bgBrush.isOpaque()) + d->backgroundPixmap.fill(Qt::transparent); + QPainter p(&d->backgroundPixmap); + p.fillRect(0, 0, d->backgroundPixmap.width(), d->backgroundPixmap.height(), bgBrush); + d->backgroundPixmapExposed = QRegion(viewport()->rect()); + d->mustResizeBackgroundPixmap = false; + } - // Redraw exposed areas - if (!d->backgroundPixmapExposed.isEmpty()) { - QPainter backgroundPainter(&d->backgroundPixmap); - backgroundPainter.setClipRegion(d->backgroundPixmapExposed, Qt::ReplaceClip); - if (viewTransformed) - backgroundPainter.setTransform(viewTransform); - QRectF backgroundExposedSceneRect = mapToScene(d->backgroundPixmapExposed.boundingRect()).boundingRect(); - drawBackground(&backgroundPainter, backgroundExposedSceneRect); - d->backgroundPixmapExposed = QRegion(); - } + // Redraw exposed areas + if (!d->backgroundPixmapExposed.isEmpty()) { + QPainter backgroundPainter(&d->backgroundPixmap); + backgroundPainter.setClipRegion(d->backgroundPixmapExposed, Qt::ReplaceClip); + if (viewTransformed) + backgroundPainter.setTransform(viewTransform); + QRectF backgroundExposedSceneRect = mapToScene(d->backgroundPixmapExposed.boundingRect()).boundingRect(); + drawBackground(&backgroundPainter, backgroundExposedSceneRect); + d->backgroundPixmapExposed = QRegion(); + } - // Blit the background from the background pixmap - if (viewTransformed) { - painter.setWorldTransform(QTransform()); - painter.drawPixmap(QPoint(), d->backgroundPixmap); - painter.setWorldTransform(viewTransform); + // Blit the background from the background pixmap + if (viewTransformed) { + painter.setWorldTransform(QTransform()); + painter.drawPixmap(QPoint(), d->backgroundPixmap); + painter.setWorldTransform(viewTransform); + } else { + painter.drawPixmap(QPoint(), d->backgroundPixmap); + } } else { - painter.drawPixmap(QPoint(), d->backgroundPixmap); + if (!(d->optimizationFlags & DontSavePainterState)) + painter.save(); + + drawBackground(&painter, exposedSceneRect); + if (!(d->optimizationFlags & DontSavePainterState)) + painter.restore(); } - } else { - if (!(d->optimizationFlags & DontSavePainterState)) - painter.save(); - drawBackground(&painter, exposedSceneRect); - if (!(d->optimizationFlags & DontSavePainterState)) - painter.restore(); - } - // Items - if (!(d->optimizationFlags & IndirectPainting)) { - const quint32 oldRectAdjust = d->scene->d_func()->rectAdjust; - if (d->optimizationFlags & QGraphicsView::DontAdjustForAntialiasing) - d->scene->d_func()->rectAdjust = 1; - else - d->scene->d_func()->rectAdjust = 2; - d->scene->d_func()->drawItems(&painter, viewTransformed ? &viewTransform : nullptr, - &d->exposedRegion, viewport()); - d->scene->d_func()->rectAdjust = oldRectAdjust; - // Make sure the painter's world transform is restored correctly when - // drawing without painter state protection (DontSavePainterState). - // We only change the worldTransform() so there's no need to do a full-blown - // save() and restore(). Also note that we don't have to do this in case of - // IndirectPainting (the else branch), because in that case we always save() - // and restore() in QGraphicsScene::drawItems(). - if (!d->scene->d_func()->painterStateProtection) - painter.setOpacity(1.0); - painter.setWorldTransform(viewTransform); - } else { - // Make sure we don't have unpolished items before we draw - if (!d->scene->d_func()->unpolishedItems.isEmpty()) - d->scene->d_func()->_q_polishItems(); - // We reset updateAll here (after we've issued polish events) - // so that we can discard update requests coming from polishEvent(). - d->scene->d_func()->updateAll = false; - - // Find all exposed items - bool allItems = false; - QList<QGraphicsItem *> itemList = d->findItems(d->exposedRegion, &allItems, viewTransform); - if (!itemList.isEmpty()) { - // Generate the style options. - const int numItems = itemList.size(); - QGraphicsItem **itemArray = &itemList[0]; // Relies on QList internals, but is perfectly valid. - QStyleOptionGraphicsItem *styleOptionArray = d->allocStyleOptionsArray(numItems); - QTransform transform(Qt::Uninitialized); - for (int i = 0; i < numItems; ++i) { - QGraphicsItem *item = itemArray[i]; - QGraphicsItemPrivate *itemd = item->d_ptr.data(); - itemd->initStyleOption(&styleOptionArray[i], viewTransform, d->exposedRegion, allItems); - // Cache the item's area in view coordinates. - // Note that we have to do this here in case the base class implementation - // (QGraphicsScene::drawItems) is not called. If it is, we'll do this - // operation twice, but that's the price one has to pay for using indirect - // painting :-/. - const QRectF brect = adjustedItemEffectiveBoundingRect(item); - if (!itemd->itemIsUntransformable()) { - transform = item->sceneTransform(); - if (viewTransformed) - transform *= viewTransform; - } else { - transform = item->deviceTransform(viewTransform); + // Items + if (!(d->optimizationFlags & IndirectPainting)) { + const quint32 oldRectAdjust = d->scene->d_func()->rectAdjust; + if (d->optimizationFlags & QGraphicsView::DontAdjustForAntialiasing) + d->scene->d_func()->rectAdjust = 1; + else + d->scene->d_func()->rectAdjust = 2; + d->scene->d_func()->drawItems(&painter, viewTransformed ? &viewTransform : nullptr, + &d->exposedRegion, viewport()); + d->scene->d_func()->rectAdjust = oldRectAdjust; + // Make sure the painter's world transform is restored correctly when + // drawing without painter state protection (DontSavePainterState). + // We only change the worldTransform() so there's no need to do a full-blown + // save() and restore(). Also note that we don't have to do this in case of + // IndirectPainting (the else branch), because in that case we always save() + // and restore() in QGraphicsScene::drawItems(). + if (!d->scene->d_func()->painterStateProtection) + painter.setOpacity(1.0); + painter.setWorldTransform(viewTransform); + } else { + // Make sure we don't have unpolished items before we draw + if (!d->scene->d_func()->unpolishedItems.isEmpty()) + d->scene->d_func()->_q_polishItems(); + // We reset updateAll here (after we've issued polish events) + // so that we can discard update requests coming from polishEvent(). + d->scene->d_func()->updateAll = false; + + // Find all exposed items + bool allItems = false; + QList<QGraphicsItem *> itemList = d->findItems(d->exposedRegion, &allItems, viewTransform); + if (!itemList.isEmpty()) { + // Generate the style options. + const int numItems = itemList.size(); + QGraphicsItem **itemArray = &itemList[0]; // Relies on QList internals, but is perfectly valid. + QStyleOptionGraphicsItem *styleOptionArray = d->allocStyleOptionsArray(numItems); + QTransform transform(Qt::Uninitialized); + for (int i = 0; i < numItems; ++i) { + QGraphicsItem *item = itemArray[i]; + QGraphicsItemPrivate *itemd = item->d_ptr.data(); + itemd->initStyleOption(&styleOptionArray[i], viewTransform, d->exposedRegion, allItems); + // Cache the item's area in view coordinates. + // Note that we have to do this here in case the base class implementation + // (QGraphicsScene::drawItems) is not called. If it is, we'll do this + // operation twice, but that's the price one has to pay for using indirect + // painting :-/. + const QRectF brect = adjustedItemEffectiveBoundingRect(item); + if (!itemd->itemIsUntransformable()) { + transform = item->sceneTransform(); + if (viewTransformed) + transform *= viewTransform; + } else { + transform = item->deviceTransform(viewTransform); + } + itemd->paintedViewBoundingRects.insert(d->viewport, transform.mapRect(brect).toRect()); } - itemd->paintedViewBoundingRects.insert(d->viewport, transform.mapRect(brect).toRect()); + // Draw the items. + drawItems(&painter, numItems, itemArray, styleOptionArray); + d->freeStyleOptionsArray(styleOptionArray); } - // Draw the items. - drawItems(&painter, numItems, itemArray, styleOptionArray); - d->freeStyleOptionsArray(styleOptionArray); } - } - // Foreground - drawForeground(&painter, exposedSceneRect); + // Foreground + drawForeground(&painter, exposedSceneRect); -#if QT_CONFIG(rubberband) - // Rubberband - if (d->rubberBanding && !d->rubberBandRect.isEmpty()) { - painter.restore(); - QStyleOptionRubberBand option; - option.initFrom(viewport()); - option.rect = d->rubberBandRect; - option.shape = QRubberBand::Rectangle; - - QStyleHintReturnMask mask; - if (viewport()->style()->styleHint(QStyle::SH_RubberBand_Mask, &option, viewport(), &mask)) { - // painter clipping for masked rubberbands - painter.setClipRegion(mask.region, Qt::IntersectClip); + #if QT_CONFIG(rubberband) + // Rubberband + if (d->rubberBanding && !d->rubberBandRect.isEmpty()) { + painter.restore(); + QStyleOptionRubberBand option; + option.initFrom(viewport()); + option.rect = d->rubberBandRect; + option.shape = QRubberBand::Rectangle; + + QStyleHintReturnMask mask; + if (viewport()->style()->styleHint(QStyle::SH_RubberBand_Mask, &option, viewport(), &mask)) { + // painter clipping for masked rubberbands + painter.setClipRegion(mask.region, Qt::IntersectClip); + } + + viewport()->style()->drawControl(QStyle::CE_RubberBand, &option, &painter, viewport()); } + #endif + }; - viewport()->style()->drawControl(QStyle::CE_RubberBand, &option, &painter, viewport()); + actuallyDraw(); + + // For stereo we want to draw everything twice, once to each buffer + if (d->stereoEnabled) { + QWidgetPrivate* w = QWidgetPrivate::get(viewport()); + if (w->toggleStereoTargetBuffer()) { + actuallyDraw(); + w->toggleStereoTargetBuffer(); + } } -#endif painter.end(); diff --git a/src/widgets/graphicsview/qgraphicsview_p.h b/src/widgets/graphicsview/qgraphicsview_p.h index f3388764b5..a15defe621 100644 --- a/src/widgets/graphicsview/qgraphicsview_p.h +++ b/src/widgets/graphicsview/qgraphicsview_p.h @@ -100,6 +100,8 @@ public: QGraphicsView::ViewportUpdateMode viewportUpdateMode; QGraphicsView::OptimizationFlags optimizationFlags; + bool stereoEnabled = false; // Set in setupViewport() + QPointer<QGraphicsScene> scene; #if QT_CONFIG(rubberband) QRect rubberBandRect; diff --git a/src/widgets/kernel/qwidget_p.h b/src/widgets/kernel/qwidget_p.h index ea03e9c708..25a9c75701 100644 --- a/src/widgets/kernel/qwidget_p.h +++ b/src/widgets/kernel/qwidget_p.h @@ -621,6 +621,11 @@ public: // Called after each paint event. virtual void resolveSamples() { } + // These two are used in QGraphicsView for supporting stereoscopic rendering with a + // QOpenGLWidget viewport. + virtual bool isStereoEnabled() { return false; } // Called in QGraphicsView::setupViewport + virtual bool toggleStereoTargetBuffer() { return false; } // Called in QGraphicsView::paintEvent + static void setWidgetParentHelper(QObject *widgetAsObject, QObject *newParent); std::string flagsForDumping() const override; diff --git a/tests/manual/stereographicsview/CMakeLists.txt b/tests/manual/stereographicsview/CMakeLists.txt new file mode 100644 index 0000000000..937e10de91 --- /dev/null +++ b/tests/manual/stereographicsview/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from stereographicsview.pro. + +##################################################################### +## stereographicsview Binary: +##################################################################### + +set(CMAKE_AUTOUIC ON) + +qt_internal_add_manual_test(stereographicsview + GUI + SOURCES + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui + mygraphicsview.h + mygraphicsview.cpp + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + Qt::OpenGL + Qt::OpenGLWidgets + Qt::Widgets + Qt::WidgetsPrivate +) + +#### Keys ignored in scope 1:.:.:stereographicsview.pro:<TRUE>: +# TEMPLATE = "app" diff --git a/tests/manual/stereographicsview/main.cpp b/tests/manual/stereographicsview/main.cpp new file mode 100644 index 0000000000..6564e8efc4 --- /dev/null +++ b/tests/manual/stereographicsview/main.cpp @@ -0,0 +1,26 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "mainwindow.h" + +#include <QApplication> +#include <QSurfaceFormat> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + format.setSamples(16); + format.setStereo(true); + QSurfaceFormat::setDefaultFormat(format); + + MainWindow w; + w.show(); + return a.exec(); +} + + +#include "main.moc" diff --git a/tests/manual/stereographicsview/mainwindow.cpp b/tests/manual/stereographicsview/mainwindow.cpp new file mode 100644 index 0000000000..003b4da8c3 --- /dev/null +++ b/tests/manual/stereographicsview/mainwindow.cpp @@ -0,0 +1,79 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "mainwindow.h" +#include "./ui_mainwindow.h" +#include <QPushButton> +#include <QSlider> +#include <QLabel> +#include <QGraphicsEllipseItem> +#include <QRandomGenerator> +#include <QOpenGLWidget> + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + ui->graphicsView->setViewport(new QOpenGLWidget); + ui->graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + QRect rect = ui->graphicsView->rect(); + QGraphicsScene *m_scene = new QGraphicsScene(rect); + ui->graphicsView->setScene(m_scene); + + QSize initialSize(150, 150); + m_ellipse = m_scene->addEllipse( + rect.width() / 2 - initialSize.width() / 2, + rect.height() / 2 - initialSize.height() / 2, + initialSize.width(), + initialSize.height(), + QPen(Qt::magenta, 5), + Qt::blue); + + QPushButton *button = new QPushButton(); + button->setGeometry(QRect(rect.width() / 2 - 75, 100, 150, 50)); + button->setText("Random ellipse color"); + QObject::connect(button, &QPushButton::pressed, [&]() { + m_ellipse->setBrush(QColor::fromRgb(QRandomGenerator::global()->generate())); + m_ellipse->setPen(QPen(QColor::fromRgb(QRandomGenerator::global()->generate()), 5)); + }); + m_scene->addWidget(button); + + m_label = new QLabel(); + m_label->setGeometry(QRect(rect.width() / 2 - 50, rect.height() - 100, 100, 50)); + m_label->setAlignment(Qt::AlignCenter); + m_scene->addWidget(m_label); + + QSlider *slider = new QSlider(Qt::Horizontal); + slider->setGeometry(QRect(rect.width() / 2 - 100, rect.height() - 50, 200, 50)); + slider->setMinimum(10); + slider->setMaximum(300); + slider->setValue(initialSize.width()); + QObject::connect(slider, &QAbstractSlider::valueChanged, [this](int value){ + QRectF rect = m_ellipse->rect(); + rect.setWidth(value); + rect.setHeight(value); + m_ellipse->setRect(rect); + m_label->setText("Ellipse size: " + QString::number(value)); + }); + emit slider->valueChanged(initialSize.width()); + m_scene->addWidget(slider); + + QMenu *screenShotMenu = menuBar()->addMenu("&Screenshot"); + screenShotMenu->addAction("Left buffer", this, [this](){ + ui->graphicsView->saveImage(QOpenGLWidget::LeftBuffer); + }); + + screenShotMenu->addAction("Right buffer", this, [this](){ + ui->graphicsView->saveImage(QOpenGLWidget::RightBuffer); + }); + +} + +MainWindow::~MainWindow() +{ + delete ui; +} + diff --git a/tests/manual/stereographicsview/mainwindow.h b/tests/manual/stereographicsview/mainwindow.h new file mode 100644 index 0000000000..4a65dad68d --- /dev/null +++ b/tests/manual/stereographicsview/mainwindow.h @@ -0,0 +1,30 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QMainWindow> + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class QGraphicsEllipseItem; +class QLabel; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + QGraphicsEllipseItem *m_ellipse; + QLabel *m_label; + + Ui::MainWindow *ui; +}; +#endif // MAINWINDOW_H diff --git a/tests/manual/stereographicsview/mainwindow.ui b/tests/manual/stereographicsview/mainwindow.ui new file mode 100644 index 0000000000..2ebb6b8caa --- /dev/null +++ b/tests/manual/stereographicsview/mainwindow.ui @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1120</width> + <height>695</height> + </rect> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="MyGraphicsView" name="graphicsView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="sizeAdjustPolicy"> + <enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum> + </property> + </widget> + </item> + <item> + <widget class="QFrame" name="frame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>QGraphicsView Stereoscopic Example</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1120</width> + <height>21</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + </widget> + <customwidgets> + <customwidget> + <class>MyGraphicsView</class> + <extends>QGraphicsView</extends> + <header>mygraphicsview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/tests/manual/stereographicsview/mygraphicsview.cpp b/tests/manual/stereographicsview/mygraphicsview.cpp new file mode 100644 index 0000000000..dc6e39f4a0 --- /dev/null +++ b/tests/manual/stereographicsview/mygraphicsview.cpp @@ -0,0 +1,209 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "mygraphicsview.h" +#include <QResizeEvent> +#include <QFileDialog> + +Q_OPENGL_EXPORT QImage qt_gl_read_framebuffer(const QSize &size, bool alpha_format, bool include_alpha); + +MyGraphicsView::MyGraphicsView(QWidget *parent) : + QGraphicsView(parent), + m_indexBuf(QOpenGLBuffer::IndexBuffer) +{ + +} + +MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent) : + QGraphicsView(scene, parent) +{ + +} + +void MyGraphicsView::saveImage(QOpenGLWidget::TargetBuffer targetBuffer) +{ + QOpenGLWidget *w = static_cast<QOpenGLWidget*>(viewport()); + Q_ASSERT(w); + + w->makeCurrent(targetBuffer); + draw(targetBuffer); + + QImage img = qt_gl_read_framebuffer(w->size() * w->devicePixelRatio(), true, true); + + if (img.isNull()) { + qFatal("Failed to grab framebuffer"); + } + + const char *fn = + targetBuffer == QOpenGLWidget::LeftBuffer + ? "leftBuffer.png" : "rightBuffer.png"; + + QFileDialog fd(this); + fd.setAcceptMode(QFileDialog::AcceptSave); + fd.setDefaultSuffix("png"); + fd.selectFile(fn); + if (fd.exec() == QDialog::Accepted) + img.save(fd.selectedFiles().first()); +} + +void MyGraphicsView::drawBackground(QPainter *painter, const QRectF &rect) +{ + if (!m_initialized) + init(); + + QOpenGLWidget *w = static_cast<QOpenGLWidget*>(viewport()); + + painter->beginNativePainting(); + + draw(w->currentTargetBuffer()); + + painter->endNativePainting(); + + + m_yaw += 0.5; + if (m_yaw > 360) + m_yaw = 0; +} + +void MyGraphicsView::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + + resize(event->size().width(), event->size().height()); +} + +void MyGraphicsView::init() +{ + if (m_initialized) + return; + + initializeOpenGLFunctions(); + + initShaders(); + initCube(); + + resize(viewport()->width(), viewport()->height()); + + m_initialized = true; +} + +void MyGraphicsView::initShaders() +{ + static const char *vertexShaderSource = + "#ifdef GL_ES\n" + "// Set default precision to medium\n" + "precision mediump int;\n" + "precision mediump float;\n" + "#endif\n" + "attribute vec4 a_position;\n" + "uniform mat4 u_mvp;\n" + "void main() {\n" + "gl_Position = u_mvp * a_position;\n" + "}\n"; + + static const char *fragmentShaderSource = + "#ifdef GL_ES\n" + "// Set default precision to medium\n" + "precision mediump int;\n" + "precision mediump float;\n" + "#endif\n" + "void main() {\n" + "gl_FragColor = vec4(0.7, 0.1, 0.0, 1.0);\n" + "}\n"; + + if (!m_program.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource)) + close(); + + if (!m_program.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource)) + close(); + + if (!m_program.link()) + close(); + + if (!m_program.bind()) + close(); +} + +void MyGraphicsView::initCube() +{ + m_arrayBuf.create(); + m_indexBuf.create(); + + QVector3D vertices[] = { + QVector3D(-1.0f, -1.0f, 1.0f), + QVector3D( 1.0f, -1.0f, 1.0f), + QVector3D(-1.0f, 1.0f, 1.0f), + QVector3D( 1.0f, 1.0f, 1.0f), + QVector3D( 1.0f, -1.0f, 1.0f), + QVector3D( 1.0f, -1.0f, -1.0f), + QVector3D( 1.0f, 1.0f, 1.0f), + QVector3D( 1.0f, 1.0f, -1.0f), + QVector3D( 1.0f, -1.0f, -1.0f), + QVector3D(-1.0f, -1.0f, -1.0f), + QVector3D( 1.0f, 1.0f, -1.0f), + QVector3D(-1.0f, 1.0f, -1.0f), + QVector3D(-1.0f, -1.0f, -1.0f), + QVector3D(-1.0f, -1.0f, 1.0f), + QVector3D(-1.0f, 1.0f, -1.0f), + QVector3D(-1.0f, 1.0f, 1.0f), + QVector3D(-1.0f, -1.0f, -1.0f), + QVector3D( 1.0f, -1.0f, -1.0f), + QVector3D(-1.0f, -1.0f, 1.0f), + QVector3D( 1.0f, -1.0f, 1.0f), + QVector3D(-1.0f, 1.0f, 1.0f), + QVector3D( 1.0f, 1.0f, 1.0f), + QVector3D(-1.0f, 1.0f, -1.0f), + QVector3D( 1.0f, 1.0f, -1.0f) + }; + + GLushort indices[] = { + 0, 1, 2, 3, 3, + 4, 4, 5, 6, 7, 7, + 8, 8, 9, 10, 11, 11, + 12, 12, 13, 14, 15, 15, + 16, 16, 17, 18, 19, 19, + 20, 20, 21, 22, 23 + }; + + + m_arrayBuf.bind(); + m_arrayBuf.allocate(vertices, 24 * sizeof(QVector3D)); + + m_indexBuf.bind(); + m_indexBuf.allocate(indices, 34 * sizeof(GLushort)); +} + +void MyGraphicsView::draw(QOpenGLWidget::TargetBuffer targetBuffer) +{ + glClearColor(0.0f, 0.7f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + m_program.bind(); + m_arrayBuf.bind(); + m_indexBuf.bind(); + + QMatrix4x4 matrix; + matrix.translate(targetBuffer == QOpenGLWidget::LeftBuffer ? -2.0 : 2.0, + 0.0, + -10.0); + matrix.scale(2.0); + matrix.rotate(QQuaternion::fromEulerAngles(10, m_yaw, 0)); + m_program.setUniformValue("u_mvp", m_projection * matrix); + + int vertexLocation = m_program.attributeLocation("a_position"); + m_program.enableAttributeArray(vertexLocation); + m_program.setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, sizeof(QVector3D)); + + glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr); +} + +void MyGraphicsView::resize(int w, int h) +{ + qreal aspect = qreal(w) / qreal(h ? h : 1); + const qreal zNear = 0.0, zFar = 100.0, fov = 45.0; + m_projection.setToIdentity(); + m_projection.perspective(fov, aspect, zNear, zFar); + + if (m_initialized) + glViewport(0, 0, w, h); +} diff --git a/tests/manual/stereographicsview/mygraphicsview.h b/tests/manual/stereographicsview/mygraphicsview.h new file mode 100644 index 0000000000..03204cb316 --- /dev/null +++ b/tests/manual/stereographicsview/mygraphicsview.h @@ -0,0 +1,47 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef MYGRAPHICSVIEW_H +#define MYGRAPHICSVIEW_H + +#include <QOpenGLFunctions> +#include <QGraphicsView> +#include <QOpenGLShaderProgram> +#include <QOpenGLBuffer> +#include <QMatrix4x4> +#include <QOpenGLWidget> + +class MyGraphicsView : public QGraphicsView, protected QOpenGLFunctions +{ +public: + MyGraphicsView(QWidget *parent = nullptr); + MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr); + + void saveImage(QOpenGLWidget::TargetBuffer targetBuffer); + +protected: + void drawBackground(QPainter *painter, const QRectF &rect) override; + + void resizeEvent(QResizeEvent *event) override; + +private: + void init(); + void initShaders(); + void initCube(); + + void draw(QOpenGLWidget::TargetBuffer targetBuffer); + + void resize(int w, int h); + + + bool m_initialized = false; + + QOpenGLShaderProgram m_program; + QMatrix4x4 m_projection; + QOpenGLBuffer m_arrayBuf; + QOpenGLBuffer m_indexBuf; + + qreal m_yaw = 0; +}; + +#endif // MYGRAPHICSVIEW_H diff --git a/tests/manual/stereographicsview/stereographicsview.pro b/tests/manual/stereographicsview/stereographicsview.pro new file mode 100644 index 0000000000..597166367c --- /dev/null +++ b/tests/manual/stereographicsview/stereographicsview.pro @@ -0,0 +1,13 @@ +QT += widgets widgets-private gui-private core-private + +TARGET = stereographicsview +TEMPLATE = app + +SOURCES += main.cpp \ + mainwindow.cpp \ + mainwindow.h \ + mainwindow.ui \ + mygraphicsview.h \ + mygraphicsview.cpp \ + +HEADERS += openglwidget.h |