Add support for stereoscopic content in QRhi::OpenGLES2

Setting the flag QSurfaceFormat::StereoBuffers does not actually do
anything, because we do not utilize the extra buffers provided. We need
to expose setting the correct buffers using glDrawBuffers between draw
calls.

Change-Id: I6a5110405e621030ac3a2886fa83df0cfe928723
Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io>
This commit is contained in:
Kristoffer Skau 2022-10-25 22:30:15 +02:00
parent 516871d3e5
commit c9ad5ad3b7
11 changed files with 516 additions and 12 deletions

View File

@ -4859,6 +4859,35 @@ QRhiResource::Type QRhiSwapChain::resourceType() const
\note the value must not be cached and reused between frames
*/
/*!
\enum QRhiSwapChain::StereoTargetBuffer
Selects the backbuffer to use with a stereoscopic swapchain.
\value LeftBuffer
\value RightBuffer
*/
/*!
\return a render target that can be used with beginPass() in order to
render to the swapchain's left or right backbuffer. This overload should be
used only with stereoscopic rendering, that is, when the associated QWindow
is backed by two color buffers, one for each eye, instead of just one.
When stereoscopic rendering is not supported, the return value will be
null. For the time being the only backend and 3D API where traditional
stereoscopic rendering is supported is OpenGL (excluding OpenGL ES), in
combination with \l QSurfaceFormat::StereoBuffers, assuming it is supported
by the graphics and display driver stack at run time. All other backends
are going to return null from this overload.
\note the value must not be cached and reused between frames
*/
QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer targetBuffer)
{
Q_UNUSED(targetBuffer);
return nullptr;
}
/*!
\fn bool QRhiSwapChain::createOrResize()

View File

@ -1379,6 +1379,11 @@ public:
HDR10
};
enum StereoTargetBuffer {
LeftBuffer,
RightBuffer
};
QRhiResource::Type resourceType() const override;
QWindow *window() const { return m_window; }
@ -1403,6 +1408,7 @@ public:
virtual QRhiCommandBuffer *currentFrameCommandBuffer() = 0;
virtual QRhiRenderTarget *currentFrameRenderTarget() = 0;
virtual QRhiRenderTarget *currentFrameRenderTarget(StereoTargetBuffer targetBuffer);
virtual QSize surfacePixelSize() = 0;
virtual bool isFormatSupported(Format f) = 0;
virtual QRhiRenderPassDescriptor *newCompatibleRenderPassDescriptor() = 0;

View File

@ -435,6 +435,14 @@ QT_BEGIN_NAMESPACE
#define GL_GEOMETRY_SHADER 0x8DD9
#endif
#ifndef GL_BACK_LEFT
#define GL_BACK_LEFT 0x0402
#endif
#ifndef GL_BACK_RIGHT
#define GL_BACK_RIGHT 0x0403
#endif
/*!
Constructs a new QRhiGles2InitParams.
@ -2997,24 +3005,31 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
cmd.args.bindShaderResources.dynamicOffsetCount);
break;
case QGles2CommandBuffer::Command::BindFramebuffer:
{
QVarLengthArray<GLenum, 8> bufs;
if (cmd.args.bindFramebuffer.fbo) {
f->glBindFramebuffer(GL_FRAMEBUFFER, cmd.args.bindFramebuffer.fbo);
const int colorAttCount = cmd.args.bindFramebuffer.colorAttCount;
bufs.append(colorAttCount > 0 ? GL_COLOR_ATTACHMENT0 : GL_NONE);
if (caps.maxDrawBuffers > 1) {
const int colorAttCount = cmd.args.bindFramebuffer.colorAttCount;
QVarLengthArray<GLenum, 8> bufs;
for (int i = 0; i < colorAttCount; ++i)
for (int i = 1; i < colorAttCount; ++i)
bufs.append(GL_COLOR_ATTACHMENT0 + uint(i));
f->glDrawBuffers(colorAttCount, bufs.constData());
}
} else {
f->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject());
if (cmd.args.bindFramebuffer.stereo && cmd.args.bindFramebuffer.stereoTarget == QRhiSwapChain::RightBuffer)
bufs.append(GL_BACK_RIGHT);
else
bufs.append(caps.gles ? GL_BACK : GL_BACK_LEFT);
}
f->glDrawBuffers(bufs.count(), bufs.constData());
if (caps.srgbCapableDefaultFramebuffer) {
if (cmd.args.bindFramebuffer.srgb)
f->glEnable(GL_FRAMEBUFFER_SRGB);
else
f->glDisable(GL_FRAMEBUFFER_SRGB);
}
}
break;
case QGles2CommandBuffer::Command::Clear:
f->glDisable(GL_SCISSOR_TEST);
@ -3980,6 +3995,9 @@ QGles2RenderTargetData *QRhiGles2::enqueueBindFramebuffer(QRhiRenderTarget *rt,
*wantsDsClear = doClearBuffers;
fbCmd.args.bindFramebuffer.fbo = 0;
fbCmd.args.bindFramebuffer.colorAttCount = 1;
fbCmd.args.bindFramebuffer.stereo = rtD->stereoTarget.has_value();
if (fbCmd.args.bindFramebuffer.stereo)
fbCmd.args.bindFramebuffer.stereoTarget = rtD->stereoTarget.value();
break;
case QRhiResource::TextureRenderTarget:
{
@ -3991,6 +4009,7 @@ QGles2RenderTargetData *QRhiGles2::enqueueBindFramebuffer(QRhiRenderTarget *rt,
*wantsDsClear = !rtTex->m_flags.testFlag(QRhiTextureRenderTarget::PreserveDepthStencilContents);
fbCmd.args.bindFramebuffer.fbo = rtTex->framebuffer;
fbCmd.args.bindFramebuffer.colorAttCount = rtD->colorAttCount;
fbCmd.args.bindFramebuffer.stereo = false;
for (auto it = rtTex->m_desc.cbeginColorAttachments(), itEnd = rtTex->m_desc.cendColorAttachments();
it != itEnd; ++it)
@ -5753,6 +5772,8 @@ void QGles2CommandBuffer::destroy()
QGles2SwapChain::QGles2SwapChain(QRhiImplementation *rhi)
: QRhiSwapChain(rhi),
rt(rhi, this),
rtLeft(rhi, this),
rtRight(rhi, this),
cb(rhi)
{
}
@ -5779,6 +5800,16 @@ QRhiRenderTarget *QGles2SwapChain::currentFrameRenderTarget()
return &rt;
}
QRhiRenderTarget *QGles2SwapChain::currentFrameRenderTarget(StereoTargetBuffer targetBuffer)
{
if (targetBuffer == LeftBuffer)
return rtLeft.d.isValid() ? &rtLeft : &rt;
else if (targetBuffer == RightBuffer)
return rtRight.d.isValid() ? &rtRight : &rt;
else
Q_UNREACHABLE_RETURN(nullptr);
}
QSize QGles2SwapChain::surfacePixelSize()
{
Q_ASSERT(m_window);
@ -5795,6 +5826,18 @@ QRhiRenderPassDescriptor *QGles2SwapChain::newCompatibleRenderPassDescriptor()
return new QGles2RenderPassDescriptor(m_rhi);
}
void QGles2SwapChain::initSwapChainRenderTarget(QGles2SwapChainRenderTarget *rt)
{
rt->setRenderPassDescriptor(m_renderPassDesc); // for the public getter in QRhiRenderTarget
rt->d.rp = QRHI_RES(QGles2RenderPassDescriptor, m_renderPassDesc);
rt->d.pixelSize = pixelSize;
rt->d.dpr = float(m_window->devicePixelRatio());
rt->d.sampleCount = qBound(1, m_sampleCount, 64);
rt->d.colorAttCount = 1;
rt->d.dsAttCount = m_depthStencil ? 1 : 0;
rt->d.srgbUpdateAndBlend = m_flags.testFlag(QRhiSwapChain::sRGB);
}
bool QGles2SwapChain::createOrResize()
{
// can be called multiple times due to window resizes
@ -5813,14 +5856,14 @@ bool QGles2SwapChain::createOrResize()
m_depthStencil->create();
}
rt.setRenderPassDescriptor(m_renderPassDesc); // for the public getter in QRhiRenderTarget
rt.d.rp = QRHI_RES(QGles2RenderPassDescriptor, m_renderPassDesc);
rt.d.pixelSize = pixelSize;
rt.d.dpr = float(m_window->devicePixelRatio());
rt.d.sampleCount = qBound(1, m_sampleCount, 64);
rt.d.colorAttCount = 1;
rt.d.dsAttCount = m_depthStencil ? 1 : 0;
rt.d.srgbUpdateAndBlend = m_flags.testFlag(QRhiSwapChain::sRGB);
initSwapChainRenderTarget(&rt);
if (m_window->format().stereo()) {
initSwapChainRenderTarget(&rtLeft);
rtLeft.d.stereoTarget = QRhiSwapChain::LeftBuffer;
initSwapChainRenderTarget(&rtRight);
rtRight.d.stereoTarget = QRhiSwapChain::RightBuffer;
}
frameCount = 0;

View File

@ -23,6 +23,7 @@
#include <QWindow>
#include <QPointer>
#include <QtCore/private/qduplicatetracker_p.h>
#include <optional>
QT_BEGIN_NAMESPACE
@ -173,6 +174,8 @@ struct QGles2RenderTargetData
{
QGles2RenderTargetData(QRhiImplementation *) { }
bool isValid() const { return rp != nullptr; }
QGles2RenderPassDescriptor *rp = nullptr;
QSize pixelSize;
float dpr = 1;
@ -181,6 +184,7 @@ struct QGles2RenderTargetData
int dsAttCount = 0;
bool srgbUpdateAndBlend = false;
QRhiRenderTargetAttachmentTracker::ResIdList currentResIdList;
std::optional<QRhiSwapChain::StereoTargetBuffer> stereoTarget;
};
struct QGles2SwapChainRenderTarget : public QRhiSwapChainRenderTarget
@ -399,6 +403,8 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer
GLuint fbo;
bool srgb;
int colorAttCount;
bool stereo;
QRhiSwapChain::StereoTargetBuffer stereoTarget;
} bindFramebuffer;
struct {
GLenum target;
@ -690,6 +696,7 @@ struct QGles2SwapChain : public QRhiSwapChain
QRhiCommandBuffer *currentFrameCommandBuffer() override;
QRhiRenderTarget *currentFrameRenderTarget() override;
QRhiRenderTarget *currentFrameRenderTarget(StereoTargetBuffer targetBuffer) override;
QSize surfacePixelSize() override;
bool isFormatSupported(Format f) override;
@ -697,9 +704,13 @@ struct QGles2SwapChain : public QRhiSwapChain
QRhiRenderPassDescriptor *newCompatibleRenderPassDescriptor() override;
bool createOrResize() override;
void initSwapChainRenderTarget(QGles2SwapChainRenderTarget *rt);
QSurface *surface = nullptr;
QSize pixelSize;
QGles2SwapChainRenderTarget rt;
QGles2SwapChainRenderTarget rtLeft;
QGles2SwapChainRenderTarget rtRight;
QGles2CommandBuffer cb;
int frameCount = 0;
};

View File

@ -30,6 +30,7 @@ add_subdirectory(polygonmode)
add_subdirectory(tessellation)
add_subdirectory(geometryshader)
add_subdirectory(stenciloutline)
add_subdirectory(stereo)
if(QT_FEATURE_widgets)
add_subdirectory(rhiwidget)
endif()

View File

@ -0,0 +1,40 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
# Generated from stereo.pro.
#####################################################################
## stereo Binary:
#####################################################################
qt_internal_add_manual_test(stereo
SOURCES
main.cpp
window.cpp window.h
LIBRARIES
Qt::Gui
Qt::GuiPrivate
)
# Resources:
set_source_files_properties("../shared/color.frag.qsb"
PROPERTIES QT_RESOURCE_ALIAS "color.frag.qsb"
)
set_source_files_properties("../shared/color.vert.qsb"
PROPERTIES QT_RESOURCE_ALIAS "color.vert.qsb"
)
set(stereo_resource_files
"../shared/color.frag.qsb"
"../shared/color.vert.qsb"
)
qt_internal_add_resource(stereo "stereo"
PREFIX
"/"
FILES
${stereo_resource_files}
)
#### Keys ignored in scope 1:.:.:stereo.pro:<TRUE>:
# TEMPLATE = "app"

View File

@ -0,0 +1,36 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
// This is a demo showing stereoscopic rendering.
// For now, the backend is hardcoded to be OpenGL, because that's the only
// supported backend.
#include <QGuiApplication>
#include <QCommandLineParser>
#include "window.h"
int main(int argc, char **argv)
{
QGuiApplication app(argc, argv);
QSurfaceFormat fmt;
fmt.setDepthBufferSize(24);
fmt.setStencilBufferSize(8);
// Request stereoscopic rendering support with left/right buffers
fmt.setStereo(true);
QSurfaceFormat::setDefaultFormat(fmt);
Window w;
w.resize(1280, 720);
w.setTitle(QCoreApplication::applicationName());
w.show();
int ret = app.exec();
if (w.handle())
w.releaseSwapChain();
return ret;
}

View File

@ -0,0 +1,12 @@
TEMPLATE = app
CONFIG += console
QT += gui-private
SOURCES = \
main.cpp \
window.cpp
HEADERS = \
window.h
RESOURCES = stereo.qrc

View File

@ -0,0 +1,6 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="color.vert.qsb">../shared/color.vert.qsb</file>
<file alias="color.frag.qsb">../shared/color.frag.qsb</file>
</qresource>
</RCC>

View File

@ -0,0 +1,264 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "window.h"
#include <QPlatformSurfaceEvent>
#include <QTimer>
#include <QFile>
#include <QtGui/private/qshader_p.h>
#include "../shared/cube.h"
Window::Window()
{
setSurfaceType(OpenGLSurface);
}
void Window::exposeEvent(QExposeEvent *)
{
if (isExposed() && !m_running) {
m_running = true;
init();
resizeSwapChain();
}
const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize();
if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_running && !m_notExposed)
m_notExposed = true;
if (isExposed() && m_running && m_notExposed && !surfaceSize.isEmpty()) {
m_notExposed = false;
m_newlyExposed = true;
}
if (isExposed() && !surfaceSize.isEmpty())
render();
}
bool Window::event(QEvent *e)
{
switch (e->type()) {
case QEvent::UpdateRequest:
render();
break;
case QEvent::PlatformSurface:
if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed)
releaseSwapChain();
break;
default:
break;
}
return QWindow::event(e);
}
void Window::init()
{
QRhi::Flags rhiFlags = QRhi::EnableDebugMarkers | QRhi::EnableProfiling;
m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface());
QRhiGles2InitParams params;
params.fallbackSurface = m_fallbackSurface.get();
params.window = this;
m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params, rhiFlags));
m_sc.reset(m_rhi->newSwapChain());
m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
QSize(),
1,
QRhiRenderBuffer::UsedWithSwapChainOnly));
m_sc->setWindow(this);
m_sc->setDepthStencil(m_ds.get());
m_rp.reset(m_sc->newCompatibleRenderPassDescriptor());
m_sc->setRenderPassDescriptor(m_rp.get());
m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube)));
m_vbuf->setName(QByteArrayLiteral("vbuf"));
m_vbuf->create();
m_vbufReady = false;
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4));
m_ubuf->setName(QByteArrayLiteral("Left eye"));
m_ubuf->create();
m_srb.reset(m_rhi->newShaderResourceBindings());
m_srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
m_ubuf.get())
});
m_srb->create();
m_ubuf2.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4));
m_ubuf2->setName(QByteArrayLiteral("Right eye"));
m_ubuf2->create();
m_srb2.reset(m_rhi->newShaderResourceBindings());
m_srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
m_ubuf2.get())
});
m_srb2->create();
m_ps.reset(m_rhi->newGraphicsPipeline());
QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
premulAlphaBlend.enable = true;
m_ps->setTargetBlends({ premulAlphaBlend });
const QShader vs = getShader(QLatin1String(":/color.vert.qsb"));
if (!vs.isValid())
qFatal("Failed to load shader pack (vertex)");
const QShader fs = getShader(QLatin1String(":/color.frag.qsb"));
if (!fs.isValid())
qFatal("Failed to load shader pack (fragment)");
m_ps->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
{ 3 * sizeof(float) },
{ 2 * sizeof(float) }
});
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, 0 },
{ 1, 1, QRhiVertexInputAttribute::Float2, 0 }
});
m_ps->setVertexInputLayout(inputLayout);
m_ps->setShaderResourceBindings(m_srb.get());
m_ps->setRenderPassDescriptor(m_rp.get());
m_ps->create();
m_ps->setDepthTest(true);
m_ps->setDepthWrite(true);
m_ps->setDepthOp(QRhiGraphicsPipeline::Less);
m_ps->setCullMode(QRhiGraphicsPipeline::Back);
m_ps->setFrontFace(QRhiGraphicsPipeline::CCW);
}
void Window::resizeSwapChain()
{
m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds
const QSize outputSize = m_sc->currentPixelSize();
m_proj = m_rhi->clipSpaceCorrMatrix();
m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
m_proj.translate(0, 0, -4);
}
void Window::releaseSwapChain()
{
if (m_hasSwapChain) {
m_hasSwapChain = false;
m_sc->destroy();
}
}
void Window::render()
{
if (!m_hasSwapChain || m_notExposed)
return;
if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
resizeSwapChain();
if (!m_hasSwapChain)
return;
m_newlyExposed = false;
}
QRhi::FrameOpResult r = m_rhi->beginFrame(m_sc.get());
if (r == QRhi::FrameOpSwapChainOutOfDate) {
resizeSwapChain();
if (!m_hasSwapChain)
return;
r = m_rhi->beginFrame(m_sc.get());
}
if (r != QRhi::FrameOpSuccess) {
qDebug("beginFrame failed with %d, retry", r);
requestUpdate();
return;
}
recordFrame();
m_rhi->endFrame(m_sc.get());
QTimer::singleShot(0, this, [this] { render(); });
}
QShader Window::getShader(const QString &name)
{
QFile f(name);
if (f.open(QIODevice::ReadOnly))
return QShader::fromSerialized(f.readAll());
return QShader();
}
// called once per frame
void Window::recordFrame()
{
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
if (!m_vbufReady) {
m_vbufReady = true;
u->uploadStaticBuffer(m_vbuf.get(), cube);
}
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
const QSize outputSizeInPixels = m_sc->currentPixelSize();
const QColor clearColor = QColor::fromRgbF(0.15f, 0.15f, 0.15f, 1.0f);
const QRhiDepthStencilClearValue depthStencil = { 1.0f, 0 };
const QRhiViewport viewPort = { 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) };
const QRhiCommandBuffer::VertexInput vbufBindings[] = {
{ m_vbuf.get(), 0 },
{ m_vbuf.get(), quint32(36 * 3 * sizeof(float)) }
};
QMatrix4x4 mvp = m_rhi->clipSpaceCorrMatrix();
mvp.perspective(45.0f, outputSizeInPixels.width() / (float) outputSizeInPixels.height(), 0.01f, 100.0f);
m_translation += m_translationDir * 0.05f;
if (m_translation < -10.0f || m_translation > -5.0f) {
m_translationDir *= -1;
m_translation = qBound(-10.0f, m_translation, -5.0f);
}
mvp.translate(0, 0, m_translation);
m_rotation += .5f;
mvp.rotate(m_rotation, 0, 1, 0);
u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData());
float opacity = 1.0f;
u->updateDynamicBuffer(m_ubuf.get(), 64, 4, &opacity);
QMatrix4x4 mvp2 = mvp;
mvp2.translate(-2.f, 0, 0);
u->updateDynamicBuffer(m_ubuf2.get(), 0, 64, mvp2.constData());
u->updateDynamicBuffer(m_ubuf2.get(), 64, 4, &opacity);
cb->resourceUpdate(u);
cb->beginPass(m_sc->currentFrameRenderTarget(QRhiSwapChain::LeftBuffer), clearColor, depthStencil);
cb->setGraphicsPipeline(m_ps.get());
cb->setViewport(viewPort);
cb->setShaderResources(m_srb.get());
cb->setVertexInput(0, 2, vbufBindings);
cb->draw(36);
cb->endPass();
cb->beginPass(m_sc->currentFrameRenderTarget(QRhiSwapChain::RightBuffer), clearColor, depthStencil);
cb->setGraphicsPipeline(m_ps.get());
cb->setViewport(viewPort);
cb->setShaderResources(m_srb2.get());
cb->setVertexInput(0, 2, vbufBindings);
cb->draw(36);
cb->endPass();
}

View File

@ -0,0 +1,56 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef WINDOW_H
#define WINDOW_H
#include <QWindow>
#include <QtGui/private/qrhigles2_p.h>
#include <QOffscreenSurface>
class Window : public QWindow
{
public:
Window();
void releaseSwapChain();
protected:
std::unique_ptr<QOffscreenSurface> m_fallbackSurface;
std::unique_ptr<QRhi> m_rhi;
std::unique_ptr<QRhiSwapChain> m_sc;
std::unique_ptr<QRhiRenderBuffer> m_ds;
std::unique_ptr<QRhiRenderPassDescriptor> m_rp;
bool m_hasSwapChain = false;
QMatrix4x4 m_proj;
private:
void init();
void resizeSwapChain();
void render();
void recordFrame();
void exposeEvent(QExposeEvent *) override;
bool event(QEvent *) override;
bool m_running = false;
bool m_notExposed = false;
bool m_newlyExposed = false;
QShader getShader(const QString &name);
std::unique_ptr<QRhiBuffer> m_vbuf;
bool m_vbufReady = false;
std::unique_ptr<QRhiBuffer> m_ubuf;
std::unique_ptr<QRhiBuffer> m_ubuf2;
std::unique_ptr<QRhiShaderResourceBindings> m_srb;
std::unique_ptr<QRhiShaderResourceBindings> m_srb2;
std::unique_ptr<QRhiGraphicsPipeline> m_ps;
float m_rotation = 0;
float m_translation = -5;
int m_translationDir = -1;
};
#endif