큐브 RHI 위젯 예제

QRhi Qt의 3D API와 셰이딩 언어 추상화 레이어를 사용하여 텍스처 큐브를 렌더링하고 QPainter 및 위젯과 통합하는 방법을 보여줍니다.

큐브 RHI 위젯 예제 스크린샷

이 예제는 단순 RHI 위젯 예제를 기반으로 합니다. 단순 예제는 창에 추가 위젯 없이 단일 삼각형만 렌더링하여 의도적으로 최대한 간결하게 만들었지만, 이 애플리케이션은 이를 보여줍니다:

  • 창에 다양한 위젯이 있고, 그 중 일부는 QRhiWidget 하위 클래스에서 소비되는 데이터를 제어합니다.
  • 여기서 QRhiWidget 는 지속적으로 업데이트를 요청하는 대신 일부 관련 데이터가 변경될 때만 백그라운드 텍스처의 콘텐츠를 업데이트합니다.
  • 큐브는 QPainter 로 수행된 소프트웨어 기반 렌더링이 포함된 QImage 에서 콘텐츠를 소싱하는 QRhiTexture 를 사용하여 텍스처링됩니다.
  • QRhiWidget can be read back 의 콘텐츠를 이미지 파일(예: PNG 파일)에 저장합니다.
  • 런타임에 4x 멀티샘플 안티앨리어싱 can be toggled. QRhiWidget 서브클래스는 변화하는 샘플 수를 올바르게 처리할 수 있도록 준비되어 있습니다.
  • 명시적으로 지정된 백킹 텍스처 크기를 동적으로 토글하고 슬라이더로 16x16에서 최대 512x512 픽셀 사이에서 제어할 수 있습니다.
  • QRhiWidget 서브클래스는 QRhi 변경을 올바르게 처리합니다. 이는 위젯을 최상위 수준(부모 없이 별도의 창이 됨)으로 만든 다음 다시 메인 창의 하위 계층 구조로 부모를 다시 지정할 때 실제로 작동하는 것을 볼 수 있습니다.
  • 가장 중요한 것은 반투명도를 가진 일부 위젯을 QRhiWidget 위에 배치하여 올바른 스태킹 및 블렌딩이 가능하다는 것을 증명할 수 있다는 것입니다. 이는 QRhiWidgetQWidget::createWindowContainer()을 사용하는 QRhi 기반 QWindow 과 같은 네이티브 창을 임베드하는 것보다 우수한 경우로, 일반적인 소프트웨어 렌더링 QWidget 과 동일한 방식으로 스택 및 클리핑이 가능하지만 네이티브 창 임베드는 플랫폼에 따라 다양한 제한이 있을 수 있으며 추가 컨트롤을 위에 배치하는 것이 어렵거나 비효율적일 수 있기 때문이죠.

initialize()를 다시 구현할 때 가장 먼저 해야 할 일은 마지막으로 작업한 QRhi 이 여전히 최신 상태인지, 샘플 수(다중 샘플 앤티앨리어싱의 경우)가 변경되었는지 확인하는 것입니다. QRhi 가 변경되면 모든 그래픽 리소스를 해제해야 하므로 전자가 중요하지만, 샘플 수가 동적으로 변경되면 샘플 수를 구울 때 QRhiGraphicsPipeline 객체에도 비슷한 문제가 발생합니다. 애플리케이션은 모든 그래픽 리소스를 편리하게 삭제하는 기본 구성 구조로 scene 구조체를 재설정하여 이러한 모든 변경 사항을 동일한 방식으로 처리합니다. 그런 다음 모든 리소스가 다시 생성됩니다.

백킹 텍스처 크기(렌더링 대상 크기)가 변경되면 특별한 조치가 필요하지 않지만 편의를 위해 main()이 오버레이 레이블의 위치를 변경할 수 있도록 신호가 전송됩니다. 3D API 이름도 QRhi::backendName()를 쿼리하여 QRhi 이 변경될 때마다 신호를 통해 노출됩니다.

멀티샘플 앤티앨리어싱은 colorTexture()가 nullptr 인 반면 msaaColorBuffer()는 유효하다는 것을 구현해야 합니다. 이는 MSAA를 사용하지 않을 때와는 반대입니다. 서로 다른 유형(QRhiTexture, QRhiRenderBuffer)을 구분하여 사용하는 이유는 멀티샘플 텍스처를 지원하지 않지만 멀티샘플 렌더버퍼를 지원하는 3D 그래픽 API에서 MSAA를 사용할 수 있도록 하기 위해서입니다. 그 예로 OpenGL ES 3.0을 들 수 있습니다.

최신 픽셀 크기와 샘플 수를 확인할 때 편리하고 간결한 해결책은 QRhiRenderTarget 을 통해 쿼리하는 것입니다. 이렇게 하면 colorTexture()와 msaaColorBuffer() 중 어느 것이 유효한지 확인할 필요가 없기 때문입니다.

void ExampleRhiWidget::initialize(QRhiCommandBuffer *)
{
    if (m_rhi != rhi()) {
        m_rhi = rhi();
        scene = {};
        emit rhiChanged(QString::fromUtf8(m_rhi->backendName()));
    }
    if (m_pixelSize != renderTarget()->pixelSize()) {
        m_pixelSize = renderTarget()->pixelSize();
        emit resized();
    }
    if (m_sampleCount != renderTarget()->sampleCount()) {
        m_sampleCount = renderTarget()->sampleCount();
        scene = {};
    }

나머지는 설명이 필요 없습니다. 필요한 경우 버퍼와 파이프라인이 (재)생성됩니다. 큐브 메시의 텍스처링에 사용되는 텍스처의 내용이 업데이트됩니다. 씬은 투시 투영을 사용하여 렌더링됩니다. 이 뷰는 현재로서는 단순한 번역일 뿐입니다.

    if (!scene.vbuf) {
        initScene();
        updateCubeTexture();
    }

    scene.mvp = m_rhi->clipSpaceCorrMatrix();
    scene.mvp.perspective(45.0f, m_pixelSize.width() / (float) m_pixelSize.height(), 0.01f, 1000.0f);
    scene.mvp.translate(0, 0, -4);
    updateMvp();
}

균일 버퍼 쓰기의 실제 큐잉을 수행하는 함수는 사용자가 제공한 회전도 고려하여 최종 모델뷰-투영 행렬을 생성합니다.

void ExampleRhiWidget::updateMvp()
{
    QMatrix4x4 mvp = scene.mvp * QMatrix4x4(QQuaternion::fromEulerAngles(QVector3D(30, itemData.cubeRotation, 0)).toRotationMatrix());
    if (!scene.resourceUpdates)
        scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.get(), 0, 64, mvp.constData());
}

큐브를 렌더링할 때 조각 셰이더에서 샘플링되는 QRhiTexture 을 업데이트하는 것은 많은 일이 일어나지만 매우 간단합니다. 먼저 QPainter 기반 드로잉이 QImage 내에 생성되며, 여기에는 사용자가 제공한 텍스트가 사용됩니다. 그런 다음 CPU 측 픽셀 데이터가 텍스처에 업로드됩니다(더 정확하게는 업로드 작업이 QRhiResourceUpdateBatch 에 기록된 다음 나중에 render()에서 제출됩니다).

void ExampleRhiWidget::updateCubeTexture()
{
    QImage image(CUBE_TEX_SIZE, QImage::Format_RGBA8888);
    const QRect r(QPoint(0, 0), CUBE_TEX_SIZE);
    QPainter p(&image);
    p.fillRect(r, QGradient::DeepBlue);
    QFont font;
    font.setPointSize(24);
    p.setFont(font);
    p.drawText(r, itemData.cubeText);
    p.end();

    if (!scene.resourceUpdates)
        scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->uploadTexture(scene.cubeTex.get(), image);
}

그래픽 리소스 초기화는 간단합니다. 버텍스 버퍼만 있고 인덱스 버퍼는 없으며 4x4 행렬(16개의 부동 소수점)만 있는 균일 버퍼가 있습니다.

QPainter-생성된 드로잉이 포함된 텍스처의 크기는 512x512입니다. QRhi 로 작업할 때 모든 크기(텍스처 크기, 뷰포트, 가위, 텍스처 업로드 영역 등)는 항상 픽셀 단위입니다. 셰이더에서 이 텍스처를 샘플링하려면 sampler object 이 필요합니다( QRhi-기반 애플리케이션은 일반적으로 GLSL 셰이더 코드에서 결합된 이미지 샘플러를 사용하며, 이는 일부 셰이딩 언어를 사용하여 텍스처와 샘플러 객체를 분리하거나 다른 텍스처와 샘플러 객체를 결합한 상태로 유지될 수 있으므로 3D API에 따라 실행 시간에 실제로 기본 샘플러 객체가 없을 수도 있지만 애플리케이션에 모두 투명합니다).

버텍스 셰이더는 바인딩 지점 0의 균일 버퍼에서 읽으므로 scene.ubuf 이 해당 바인딩 위치에 노출됩니다. 프래그먼트 셰이더는 바인딩 포인트 1에서 제공된 텍스처를 샘플링하므로 해당 바인딩 위치에 대해 결합된 텍스처-샘플러 쌍이 지정됩니다.

QRhiGraphicsPipeline 은 깊이 테스트/쓰기를 활성화하고 뒷면을 컬링합니다. 또한 여러 기본값에 의존하는데, 예를 들어 깊이 비교 기능은 기본값이 Less 으로 설정되어 있으며, 앞면 모드는 시계 반대 방향으로 설정되어 있는데, 이 역시 그대로 사용해도 괜찮으므로 다시 설정할 필요가 없습니다.

    scene.vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube)));
    scene.vbuf->create();

    scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->uploadStaticBuffer(scene.vbuf.get(), cube);

    scene.ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
    scene.ubuf->create();

    scene.cubeTex.reset(m_rhi->newTexture(QRhiTexture::RGBA8, CUBE_TEX_SIZE));
    scene.cubeTex->create();

    scene.sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
                                               QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
    scene.sampler->create();

    scene.srb.reset(m_rhi->newShaderResourceBindings());
    scene.srb->setBindings({
        QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, scene.ubuf.get()),
        QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, scene.cubeTex.get(), scene.sampler.get())
    });
    scene.srb->create();

    scene.ps.reset(m_rhi->newGraphicsPipeline());
    scene.ps->setDepthTest(true);
    scene.ps->setDepthWrite(true);
    scene.ps->setCullMode(QRhiGraphicsPipeline::Back);
    scene.ps->setShaderStages({
        { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/texture.vert.qsb")) },
        { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/texture.frag.qsb")) }
    });
    QRhiVertexInputLayout inputLayout;
    // The cube is provided as non-interleaved sets of positions, UVs, normals.
    // Normals are not interesting here, only need the positions and UVs.
    inputLayout.setBindings({
        { 3 * sizeof(float) },
        { 2 * sizeof(float) }
    });
    inputLayout.setAttributes({
        { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
        { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
    });
    scene.ps->setSampleCount(m_sampleCount);
    scene.ps->setVertexInputLayout(inputLayout);
    scene.ps->setShaderResourceBindings(scene.srb.get());
    scene.ps->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
    scene.ps->create();

render()를 다시 구현할 때는 먼저 사용자가 제공한 데이터를 확인합니다. 회전을 제어하는 QSlider 에서 새 값을 제공했거나 큐브 텍스트가 있는 QTextEdit 에서 텍스트가 변경된 경우 해당 데이터에 따라 콘텐츠가 달라지는 그래픽 리소스가 업데이트됩니다.

그런 다음 단일 드로우 호출이 포함된 단일 렌더링 패스가 기록됩니다. 큐브 메시 데이터는 비인터리브 형식으로 제공되므로 두 개의 버텍스 입력 바인딩(하나는 위치(x, y, z), 다른 하나는 UV(u, v)이며 시작 오프셋은 36개의 x-y-z 플로트 쌍에 해당)이 필요합니다.

void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
{
    if (itemData.cubeRotationDirty) {
        itemData.cubeRotationDirty = false;
        updateMvp();
    }

    if (itemData.cubeTextDirty) {
        itemData.cubeTextDirty = false;
        updateCubeTexture();
    }

    QRhiResourceUpdateBatch *resourceUpdates = scene.resourceUpdates;
    if (resourceUpdates)
        scene.resourceUpdates = nullptr;

    const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
    cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);

    cb->setGraphicsPipeline(scene.ps.get());
    cb->setViewport(QRhiViewport(0, 0, m_pixelSize.width(), m_pixelSize.height()));
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBindings[] = {
        { scene.vbuf.get(), 0 },
        { scene.vbuf.get(), quint32(36 * 3 * sizeof(float)) }
    };
    cb->setVertexInput(0, 2, vbufBindings);
    cb->draw(36);

    cb->endPass();
}

사용자가 제공한 데이터는 어떻게 전송되나요? 예를 들어 회전을 예로 들어보면 main()은 QSlidervalueChanged 신호에 연결됩니다. 신호가 전송되면 연결된 람다는 예시 리위젯에서 setCubeRotation()을 호출합니다. 여기서 값이 이전과 다르면 값이 저장되고 더티 플래그가 설정됩니다. 그런 다음 가장 중요한 것은 예시 리위젯에서 update()가 호출된다는 것입니다. 이것이 QRhiWidget 의 백킹 텍스처에 새 프레임을 렌더링하는 트리거입니다. 이 함수가 없으면 슬라이더를 드래그할 때 예시 리 위젯의 콘텐츠가 업데이트되지 않습니다.

    void setCubeTextureText(const QString &s)
    {
        if (itemData.cubeText == s)
            return;
        itemData.cubeText = s;
        itemData.cubeTextDirty = true;
        update();
    }

    void setCubeRotation(float r)
    {
        if (itemData.cubeRotation == r)
            return;
        itemData.cubeRotation = r;
        itemData.cubeRotationDirty = true;
        update();
    }

예제 프로젝트 @ code.qt.io

QRhi, 간단한 RHI 위젯 예제RHI 창 예제도참조하세요 .

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.