Cube RHI 위젯 예제

Qt 3D API이자 셰이딩 언어 추상화 계층인 QRhi 를 사용하여 텍스처가 적용된 큐브를 렌더링하고, QPainter 및 Qt Widgets와 통합하는 방법을 보여줍니다.

복잡한 렌더링 오버레이를 보여주는 애플리케이션

Cube RHI 위젯 예제의 스크린샷

이 예제는 ‘Simple RHI 위젯 예제’를 기반으로 합니다. 간단한 예제는 의도적으로 최소한의 구성으로 최대한 간결하게 설계되어, 창 내에 추가 위젯 없이 삼각형 하나만 렌더링하는 반면, 이 애플리케이션은 다음을 보여줍니다:

  • 창 내에 다양한 위젯을 포함하며, 그중 일부는 ` QRhiWidget ` 서브클래스가 사용하는 데이터를 제어합니다.
  • QRhiWidget 는 지속적으로 업데이트를 요청하는 대신, 관련 데이터가 변경될 때만 백킹 텍스처의 내용을 업데이트합니다.
  • 이 큐브는 ` QRhiTexture `를 사용하여 텍스처링되는데, 이 ` `는 ` QPainter`를 통해 소프트웨어 기반 렌더링이 수행된 ` QImage `에서 콘텐츠를 가져옵니다.
  • QRhiWidget 의 can be read back 에 포함된 내용은 이미지 파일(예: PNG 파일)로 저장됩니다.
  • 실행 시 4배 멀티샘플 앤티앨리어싱 can be toggled. QRhiWidget 서브클래스는 변화하는 샘플 수를 올바르게 처리할 수 있도록 준비되어 있습니다.
  • explicitly specified backing texture size 적용 여부는 동적으로 전환할 수 있으며, 슬라이더를 통해 16x16에서 최대 512x512 픽셀까지 조절할 수 있습니다.
  • QRhiWidget 서브클래스는 변화하는 QRhi 를 올바르게 처리합니다. 이는 위젯을 최상위 레벨(부모 없음; 별도의 창이 됨)로 설정한 다음, 다시 메인 창의 자식 계층 구조로 재연결할 때 실제로 확인할 수 있습니다.
  • 가장 중요한 점은, 반투명성을 가진 일부 위젯조차도 QRhiWidget 위에 배치할 수 있다는 점으로, 이는 올바른 레이어 배열과 블렌딩이 가능함을 입증합니다. 이는 QRhiWidget 가 네이티브 창, 즉 QRhi 기반의 QWindow 를 QWidget::createWindowContainer()를 사용하여 임베딩하는 방식보다 우월한 사례입니다. 이는 일반적인 소프트웨어 렌더링된 QWidget 와 동일한 방식으로 스태킹 및 클리핑을 허용하는 반면, 네이티브 창 임베딩은 플랫폼에 따라 다양한 제한 사항이 있을 수 있기 때문입니다. 예를 들어, 추가 컨트롤을 그 위에 배치하는 것이 종종 어렵거나 비효율적일 수 있습니다.

initialize()의 재구현에서 가장 먼저 해야 할 일은, 마지막으로 작업했던 QRhi 가 여전히 최신 상태인지, 그리고 샘플 수(멀티샘플 앤티앨리어싱용)가 변경되었는지 확인하는 것입니다. 전자의 경우, QRhi 가 변경되면 모든 그래픽 리소스를 해제해야 하기 때문에 중요합니다. 반면, 샘플 수가 동적으로 변경될 경우, 특히 샘플 수를 내부적으로 고정하는 QRhiGraphicsPipeline 객체에서 유사한 문제가 발생합니다. 단순화를 위해 애플리케이션은 이러한 모든 변경 사항을 동일한 방식으로 처리합니다. 즉, scene 구조체를 기본 생성된 구조체로 재설정함으로써 모든 그래픽 리소스를 편리하게 해제합니다. 그런 다음 모든 리소스가 다시 생성됩니다.

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

구현 시, 멀티샘플 앤티앨리어싱(MSAA)을 사용하면 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 를 업데이트하는 작업은, 내부에서 많은 일이 일어나고 있음에도 불구하고 꽤 간단합니다. 먼저 QImage 내에서 QPainter 기반 드로잉이 생성됩니다. 여기에는 사용자가 제공한 텍스트가 사용됩니다. 그런 다음 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 는 깊이 테스트/쓰기를 지원하며, 뒷면(backface)을 제거합니다. 또한 여러 기본값에 의존하는데, 예를 들어 깊이 비교 함수는 기본적으로 Less 로 설정되어 있으며, 이는 우리에게 적합합니다. 또한 앞면(front face) 모드는 반시계 방향이며, 이 역시 그대로 사용하기에 적합하므로 다시 설정할 필요가 없습니다.

    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 의 텍스트가 변경된 경우, 해당 데이터에 따라 내용이 결정되는 그래픽 리소스가 업데이트됩니다.

그런 다음, 단일 드로우 호출을 포함한 단일 렌더링 패스가 기록됩니다. 큐브 메쉬 데이터는 비인터리브(non-interleaved) 형식으로 제공되므로, 두 개의 버텍스 입력 바인딩이 필요합니다. 하나는 위치(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()은 QSlider 의 valueChanged 신호에 연결됩니다. 신호가 발생하면, 연결된 람다 함수가 ExampleRhiWidget에서 setCubeRotation()을 호출합니다. 여기서 값이 이전과 다르면 저장되고, 더티 플래그가 설정됩니다. 그리고 가장 중요한 점은, ExampleRhiWidget에서 ` update()`가 호출된다는 것입니다. 이것이 바로 ` QRhiWidget`의 백킹 텍스처에 새로운 프레임을 렌더링하도록 트리거하는 작업입니다. 이 과정이 없다면 슬라이더를 드래그할 때 ExampleRhiWidget의 내용이 업데이트되지 않을 것입니다.

    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 창 예제도함께 참조하세요 .

© 2026 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.