RHI 창 예시
이 예는 QRhi 을 사용하여 최소한의 QWindow 기반 애플리케이션을 만드는 방법을 보여줍니다.
Qt 6.6은 애플리케이션용으로 가속화된 3D API와 셰이더 추상화 레이어를 제공하기 시작했습니다. 이제 애플리케이션은 Qt 자체에서 Qt Quick 시나리오 그래프 또는 Qt Quick 3D 엔진을 구현하는 데 사용하는 것과 동일한 3D 그래픽 클래스를 사용할 수 있습니다. 이전 Qt 버전에서는 QRhi 및 관련 클래스가 모두 비공개 API였습니다. 6.6부터 이러한 클래스는 QPA 클래스 패밀리와 비슷한 범주에 속합니다: 완전한 공개도 비공개도 아닌, 그 중간 정도이며 공개 API에 비해 호환성 약속이 더 제한적입니다. 반면에 QRhi 및 관련 클래스는 이제 공개 API와 유사하게 전체 문서와 함께 제공됩니다.
QRhi 을 사용하는 방법은 여러 가지가 있으며, 여기 예시는 가장 낮은 수준의 접근 방식인 QWindow 을 타깃으로 하면서 Qt Quick, Qt Quick 3D 또는 위젯을 어떤 형태로든 사용하지 않고 애플리케이션에서 모든 렌더링 및 윈도우 인프라를 설정하는 방법을 보여줍니다.
반대로 Qt Quick 또는 Qt Quick 3D 로 QML 애플리케이션을 작성하고 여기에 QRhi 기반 렌더링을 추가하려는 경우, 이러한 애플리케이션은 이미 초기화된 창 및 렌더링 인프라 Qt Quick 에 의존하고 QQuickWindow 에서 기존 QRhi 인스턴스를 쿼리할 것입니다. QRhi::create(), Vulkan instances 과 같은 플랫폼/API 세부 사항을 처리하거나 expose 및 창 크기 조정 이벤트를 올바르게 처리하는 것은 모두 Qt Quick 에서 관리합니다. 반면 이 예제에서는 모든 것이 애플리케이션 자체에서 관리되고 처리됩니다.
참고: 특히 QWidget 기반 애플리케이션의 경우 QWidget::createWindowContainer()을 사용하면 위젯 기반 사용자 인터페이스에 QWindow (기본 창으로 지원됨)을 임베드할 수 있다는 점에 유의해야 합니다. 따라서 이 예제의 HelloWindow
클래스는 main()
에서 필요한 초기화가 되어 있다는 가정 하에 QWidget 기반 애플리케이션에서 재사용할 수 있습니다.
3D API 지원
이 애플리케이션은 현재의 모든 QRhi backends 을 지원합니다. 명령줄 인수를 지정하지 않으면 플랫폼별 기본값이 사용됩니다: Windows의 경우 Direct 3D 11, Linux의 경우 OpenGL, macOS/iOS의 경우 Metal.
--help
로 실행하면 사용 가능한 명령줄 옵션이 표시됩니다:
- Direct 3D 11의 경우 -d 또는 -d3d11
- Direct 3D 12의 경우 -d 또는 -d3d12
- 금속의 경우 -m 또는 -metal
- Vulkan의 경우 -v 또는 -vulkan
- OpenGL 또는 OpenGL ES의 경우 -g 또는 -opengl
- -n 또는 -null Null backend
빌드 시스템 노트
이 애플리케이션은 Qt GUI 모듈에만 의존합니다. Qt Widgets 또는 Qt Quick 을 사용하지 않습니다.
모든 Qt 응용 프로그램에서 사용할 수 있지만 제한된 호환성 약속과 함께 제공되는 RHI API에 액세스하기 위해, target_link_libraries
CMake 명령은 Qt6::GuiPrivate
을 나열합니다. 이를 통해 #include <rhi/qrhi.h>
include 문이 성공적으로 컴파일될 수 있습니다.
기능
애플리케이션 기능:
- 크기 조정이 가능한 QWindow,
- 창 크기를 적절히 따르는 스왑체인 및 깊이 스텐실 버퍼,
- QExposeEvent 및 QPlatformSurfaceEvent 과 같은 이벤트에 따라 적절한 시점에 초기화, 렌더링 및 해체하는 로직,
- QPainter 을 통해 QImage 에서 생성된 텍스처를 사용하여 전체 화면 텍스처 쿼드 렌더링(래스터 페인트 엔진 사용, 즉 이미지의 픽셀 데이터 생성은 모두 CPU 기반이며, 해당 데이터는 GPU 텍스처로 업로드됨),
- 원근 투영을 사용하여 블렌딩 및 깊이 테스트가 활성화된 삼각형을 렌더링하는 동시에 매 프레임마다 변경되는 모델 트랜스폼을 적용합니다,
- requestUpdate()를 사용하는 효율적인 크로스 플랫폼 렌더링 루프입니다.
셰이더
이 애플리케이션은 두 세트의 버텍스 및 프래그먼트 셰이더 쌍을 사용합니다:
- 하나는 버텍스 입력을 사용하지 않고 조각 셰이더가 텍스처를 샘플링하는 전체 화면 쿼드용입니다(
quad.vert
,quad.frag
), - )와 버텍스 버퍼에 버텍스 위치와 색상이 제공되고 모델뷰-투영 행렬이 균일 버퍼에 제공되는 트라이앵글용 한 쌍(
color.vert
,color.frag
)을 사용합니다.
셰이더는 Vulkan 호환 GLSL 소스 코드로 작성됩니다.
Qt GUI 모듈 예제이기 때문에 이 예제는 Qt Shader Tools 모듈에 대한 종속성을 가질 수 없습니다. 즉, qt_add_shaders
같은 CMake 헬퍼 함수를 사용할 수 없습니다. 따라서 이 예제에는 shaders/prebuilt
폴더에 사전 처리된 .qsb
파일이 포함되어 있으며, qt_add_resources
을 통해 실행 파일 내에 간단히 포함됩니다. 이 접근 방식은 일반적으로 애플리케이션에 권장되지 않으며, .qsb
파일을 수동으로 생성하고 관리할 필요가 없는 qt_add_shaders를 사용하는 것을 고려하세요.
이 예제에서 .qsb
파일을 생성하기 위해 qsb --qt6 color.vert -o prebuilt/color.vert.qsb
등의 명령이 사용되었습니다. 이렇게 하면 SPIR-V로 컴파일한 다음 GLSL(100 es
및 120
), HLSL(5.0) 및 MSL(1.2)로 트랜스파일링합니다. 그런 다음 모든 셰이더 버전을 QShader 에 함께 패키징하여 디스크에 직렬화합니다.
API별 초기화
일부 3D API의 경우 main() 함수가 적절한 API별 초기화를 수행해야 합니다(예: Vulkan을 사용할 경우 QVulkanInstance 생성). OpenGL의 경우 깊이 버퍼를 사용할 수 있는지 확인해야 하며, 이는 QSurfaceFormat 을 통해 수행됩니다. 이러한 단계는 QOpenGLContext 또는 QVulkanInstance 와 같은 기존 Qt 기능을 기반으로 하는 QRhi 백엔드에 구축되므로 QRhi 의 범위에는 포함되지 않습니다.
// OpenGL의 경우 창에 대한 깊이/스텐실 버퍼가 있는지 확인합니다. // 다른 API의 경우 이것은 애플리케이션의 제어하에 있으므로 (QRhiRenderBuffer 등) // 특별한 설정이 필요하지 않습니다. QSurfaceFormat fmt; fmt.setDepthBufferSize(24); fmt.setStencilBufferSize(8); // 특별한 경우 macOS에서 OpenGL 사용을 허용합니다. // (기본 Metal이 권장되지만) // gl_VertexID는 GLSL 130 기능이므로 macOS에서 얻는 기본 OpenGL 2.1 문맥으로는 // 충분하지 않습니다.#ifdef Q_OS_MACOSfmt.setVersion(4, 1); fmt.setProfile(QSurfaceFormat::CoreProfile);#endif QSurfaceFormat::setDefaultFormat(fmt); // 벌칸의 경우.#if QT_CONFIG(vulkan) QVulkanInstance inst; if (graphicsApi== == QRhi::Vulkan) { // 가능한 경우 유효성 검사를 요청합니다. 이것은 완전히 선택 사항이며 // 성능에 영향을 미치므로 프로덕션 사용에서는 피해야 합니다.inst.setLayers({ "VK_LAYER_KHRONOS_validation" }); // QRhi와 잘 어울립니다.inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions()); if (!inst.create()) {. qWarning("Failed to create Vulkan instance, switching to OpenGL"); graphicsApi = QRhi::OpenGLES2; } }#endif
참고: Vulkan의 경우 적절한 확장이 활성화되도록 하기 위해 QRhiVulkanInitParams::preferredInstanceExtensions()가 어떻게 고려되는지 확인하세요.
HelloWindow
는 RhiWindow
의 서브클래스이며, 이는 다시 QWindow 입니다. RhiWindow
에는 스왑체인(및 깊이 스텐실 버퍼)으로 크기 조정 가능한 창을 관리하는 데 필요한 모든 것이 포함되어 있으며 다른 애플리케이션에서도 재사용할 수 있습니다. HelloWindow
에는 이 특정 예제 애플리케이션에 특정한 렌더링 로직이 포함되어 있습니다.
QWindow 서브클래스 생성자에서 표면 유형은 선택한 3D API에 따라 설정됩니다.
RhiWindow::RhiWindow(QRhi::Implementation graphicsApi) : m_graphicsApi(graphicsApi) { switch (graphicsApi) { case QRhi::OpenGLES2: setSurfaceType(OpenGLSurface); break; case QRhi::Vulkan: setSurfaceType(VulkanSurface); break; case QRhi::D3D11: case QRhi::D3D12: setSurfaceType(Direct3DSurface); break; case QRhi::Metal: setSurfaceType(MetalSurface); break; case QRhi::Null: break; // RasterSurface } }
QRhi 객체를 생성하고 초기화하는 것은 RhiWindow::init()에서 구현됩니다. 이 함수는 창이 expose event 으로 표시되는 renderable
인 경우에만 호출됩니다.
사용하는 3D API에 따라 적절한 InitParams 구조체를 QRhi::create()에 전달해야 합니다. 예를 들어 OpenGL의 경우 애플리케이션에서 QOffscreenSurface (또는 다른 QSurface)을 생성하여 QRhi 에 제공해야 합니다. Vulkan의 경우 성공적으로 초기화된 QVulkanInstance 이 필요합니다. Direct 3D 또는 Metal과 같은 다른 언어는 초기화할 수 있는 추가 정보가 필요하지 않습니다.
void RhiWindow::init() { if (m_graphicsApi==. QRhi::Null) { QRhiNullInitParams params; m_rhi.reset(QRhi::create(QRhi::Null, ¶ms)); }#if QT_CONFIG(opengl) if (m_graphicsApi== ( QRhi::OpenGLES2) { m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface()); QRhiGles2InitParams params; params.fallbackSurface = m_fallbackSurface.get(); params.window = this; m_rhi.reset(QRhi::create(QRhi::OpenGLES2, ¶ms)); }#endif#if QT_CONFIG(vulkan) if (m_graphicsApi==( QRhi::Vulkan) { QRhiVulkanInitParams params; params.inst = vulkanInstance(); params.window = this; m_rhi.reset(QRhi::create(QRhi::Vulkan, ¶ms)); }#endif#ifdef Q_OS_WIN if (m_graphicsApi== QRhi::D3D11) { QRhiD3D11InitParams params; // 가능한 경우 디버그 레이어를 활성화합니다. 이는 선택 사항이며 // 프로덕션 빌드에서는 피해야 합니다.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D11, ¶ms)); } else if (m_graphicsApi== ( QRhi::D3D12) { QRhiD3D12InitParams params; // 가능한 경우 디버그 레이어를 활성화합니다. 이는 선택 사항이며 // 프로덕션 빌드에서는 사용하지 않는 것이 좋습니다.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D12, ¶ms)); }#endif#if QT_CONFIG(metal) if (m_graphicsApi== QRhi::Metal) { QRhiMetalInitParams params; m_rhi.reset(QRhi::create(QRhi::Metal, ¶ms)); }#endif if (!m_rhi) qFatal("Failed to create RHI backend");
이 외에도 모든 렌더링 코드는 완전히 크로스 플랫폼이며 3D API에 특정한 분기나 조건이 없습니다.
이벤트 노출
renderable
의 정확한 의미는 플랫폼별로 다릅니다. 예를 들어, macOS에서는 완전히 가려진 창(다른 창 뒤에 완전히 가려진 창)은 렌더링할 수 없지만 Windows에서는 가려짐이 아무런 의미가 없습니다. 다행히도 애플리케이션은 이에 대한 특별한 지식이 필요하지 않습니다: Qt의 플랫폼 플러그인은 노출 이벤트 뒤에 있는 차이점을 추상화합니다. 그러나 exposeEvent() 재구현은 빈 출력 크기(예: 너비와 높이가 0인 경우)도 렌더링할 수 없는 상황으로 처리해야 한다는 점을 인식해야 합니다. 예를 들어 Windows에서는 창을 최소화할 때 이런 일이 발생합니다. 따라서 QRhiSwapChain::surfacePixelSize()를 기준으로 확인합니다.
이 노출 이벤트 처리 구현은 강력하고 안전하며 이식성을 갖추기 위해 노력했습니다. Qt Quick 자체도 렌더 루프에서 매우 유사한 로직을 구현합니다.
void RhiWindow::exposeEvent(QExposeEvent *) { // initialize and start rendering when the window becomes usable for graphics purposes if (isExposed() && !m_initialized) { init(); resizeSwapChain(); m_initialized = true; } const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize(); // stop pushing frames when not exposed (or size is 0) if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_initialized && !m_notExposed) m_notExposed = true; // Continue when exposed again and the surface has a valid size. Note that // surfaceSize can be (0, 0) even though size() reports a valid one, hence // trusting surfacePixelSize() and not QWindow. if (isExposed() && m_initialized && m_notExposed && !surfaceSize.isEmpty()) { m_notExposed = false; m_newlyExposed = true; } // always render a frame on exposeEvent() (when exposed) in order to update // immediately on window resize. if (isExposed() && !surfaceSize.isEmpty()) render(); }
requestUpdate()에서 생성된 UpdateRequest 이벤트에 대한 응답으로 호출되는 RhiWindow::render()에서는 스왑체인 초기화에 실패하거나 창이 렌더링할 수 없게 된 경우 렌더링을 시도하지 못하도록 다음과 같은 검사를 시행합니다.
void RhiWindow::render() { if (!m_hasSwapChain || m_notExposed) return;
스왑체인, 뎁스 스텐실 버퍼 및 크기 조정
QWindow 에 렌더링하려면 QRhiSwapChain 이 필요합니다. 또한 그래픽 파이프라인에서 뎁스 테스트를 활성화하는 방법을 보여주기 위해 뎁스 스텐실 버퍼 역할을 하는 QRhiRenderBuffer 도 생성됩니다. 일부 레거시 3D API에서는 윈도우의 뎁스/스텐실 버퍼를 관리하는 것이 해당 윈도우 시스템 인터페이스 API(EGL, WGL, GLX 등)의 일부이므로 뎁스/스텐실 버퍼가 window surface
와 함께 암시적으로 관리되지만, 최신 API에서는 윈도우 기반 렌더 타깃의 뎁스-스텐실 버퍼를 관리하는 것이 오프스크린 렌더 타깃과 다르지 않습니다. QRhi 는 이를 추상화하지만 최상의 성능을 위해 QRhiRenderBuffer 가 used with together with a QRhiSwapChain 로 표시되도록 해야 합니다.
QRhiSwapChain 은 QWindow 및 뎁스/스텐실 버퍼와 연결됩니다.
std::unique_ptr<QRhiSwapChain> m_sc; std::unique_ptr<QRhiRenderBuffer> m_ds; std::unique_ptr<QRhiRenderPassDescriptor> m_rp; m_sc.reset(m_rhi->newSwapChain()); m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(), // no need to set the size here, due to UsedWithSwapChainOnly 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());
창 크기가 변경되면 스왑체인도 크기를 조정해야 합니다. 이는 resizeSwapChain()에서 구현됩니다.
void RhiWindow::resizeSwapChain() { m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds const QSize outputSize = m_sc->currentPixelSize(); m_viewProjection = m_rhi->clipSpaceCorrMatrix(); m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f); m_viewProjection.translate(0, 0, -4); }
다른 QRhiResource 서브클래스와는 달리 QRhiSwapChain 은 생성 함수와 관련하여 약간 다른 의미를 가집니다. createOrResize ()라는 이름에서 알 수 있듯이, 출력 창 크기가 스왑체인이 마지막으로 초기화된 것과 동기화되지 않을 수 있다는 것이 알려질 때마다 호출해야 합니다. 뎁스 스텐실과 연결된 QRhiRenderBuffer 은 size 설정을 자동으로 가져오고, create()는 스왑체인의 createOrResize()에서 암시적으로 호출됩니다.
이것은 또한 우리가 설정한 투시 투영이 출력 종횡비에 따라 달라지므로 투영 및 뷰 매트릭스를 (재)계산하기에 편리한 장소입니다.
참고: 좌표계 차이를 없애기 위해 QRhi 에서 a backend/API-specific "correction" matrix 을 쿼리하여 투영 행렬에 구워 넣습니다. 이를 통해 애플리케이션은 원점이 왼쪽 하단에 있는 좌표계를 가정하여 OpenGL 스타일의 정점 데이터로 작업할 수 있습니다.
현재 보고된 크기가 스왑체인이 마지막으로 초기화된 크기와 더 이상 같지 않다는 것이 발견되면 RhiWindow::render()에서 resizeSwapChain() 함수가 호출됩니다.
자세한 내용은 QRhiSwapChain::currentPixelSize() 및 QRhiSwapChain::surfacePixelSize()을 참조하세요.
높은 DPI 지원 내장: 이름에서 알 수 있듯이 크기는 항상 픽셀 단위로 표시되며, 창별 scale factor 을 고려합니다. QRhi (및 3D API) 수준에서는 높은 DPI 스케일링 개념이 없으며 모든 것이 항상 픽셀 단위로 표시됩니다. 즉, size()가 1280x720이고 devicePixelRatio()가 2인 QWindow 는 (픽셀) 크기가 2560x1440인 렌더링 대상(스왑체인)입니다.
// If the window got resized or newly exposed, resize the swapchain. (the // newly-exposed case is not actually required by some platforms, but is // here for robustness and portability) // // This (exposeEvent + the logic here) is the only safe way to perform // resize handling. Note the usage of the RHI's surfacePixelSize(), and // never QWindow::size(). (the two may or may not be the same under the hood, // depending on the backend and platform) // if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) { resizeSwapChain(); if (!m_hasSwapChain) return; m_newlyExposed = false; }
렌더 루프
애플리케이션이 프레젠테이션 속도(vsync)에 따라 스로틀링되어 연속적으로 렌더링됩니다. 이는 현재 녹화된 프레임이 전송되면 RhiWindow::render()에서 requestUpdate()를 호출하여 보장됩니다.
m_rhi->endFrame(m_sc.get()); // Always request the next frame via requestUpdate(). On some platforms this is backed // by a platform-specific solution, e.g. CVDisplayLink on macOS, which is potentially // more efficient than a timer, queued metacalls, etc. requestUpdate(); }
이는 결국 UpdateRequest 이벤트로 이어집니다. 이는 event()의 재구현에서 처리됩니다.
bool RhiWindow::event(QEvent *e) { switch (e->type()) { case QEvent::UpdateRequest: render(); break; case QEvent::PlatformSurface: // this is the proper time to tear down the swapchain (while the native window and surface are still around) if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) releaseSwapChain(); break; default: break; } return QWindow::event(e); }
리소스 및 파이프라인 설정
애플리케이션은 두 개의 다른 그래픽 파이프라인을 사용하여 두 개의 드로우 호출을 실행하는 단일 렌더 패스를 기록합니다. 하나는 QPainter-생성된 이미지가 포함된 텍스처가 있는 "배경"이며, 그 위에 블렌딩이 활성화된 단일 삼각형이 렌더링됩니다.
삼각형과 함께 사용되는 버텍스 및 균일 버퍼는 다음과 같이 생성됩니다. 셰이더가 유니폼 블록에 mat4
및 float
멤버를 지정하므로 유니폼 버퍼의 크기는 68바이트입니다. std140 레이아웃 규칙에 주의하세요. mat4
뒤에 오는 float
멤버는 추가 패딩 없이 올바른 정렬을 가지므로 이 예제에서는 놀랄 일이 아니지만 다른 애플리케이션, 특히 vec2
또는 vec3
과 같은 유형으로 작업할 때 문제가 될 수 있습니다. 확실하지 않은 경우 QShaderDescription 에서 QShader 을 확인하거나 .qsb
파일에서 qsb
도구를 -d
인수와 함께 실행하여 사람이 읽을 수 있는 형식으로 메타데이터를 검사하는 것이 더 편리할 수 있습니다. 인쇄된 정보에는 무엇보다도 유니폼 블록 멤버 오프셋, 크기 및 각 유니폼 블록의 총 크기(바이트)가 포함됩니다.
void HelloWindow::customInit() { m_initialUpdates = m_rhi->nextResourceUpdateBatch(); m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData))); m_vbuf->create(); m_initialUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData); static const quint32 UBUF_SIZE = 68; m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE)); m_ubuf->create();
버텍스 및 조각 셰이더는 모두 바인딩 지점 0에 균일한 버퍼가 필요합니다. 이는 QRhiShaderResourceBindings 개체에 의해 보장됩니다. 그런 다음 셰이더와 여러 추가 정보로 그래픽 파이프라인이 설정됩니다. 이 예제에서는 여러 가지 편리한 기본값(예: 기본 토폴로지 Triangles)을 사용하지만 이는 기본값이므로 명시적으로 설정되지 않았습니다. 자세한 내용은 QRhiGraphicsPipeline 을 참조하세요.
토폴로지 및 다양한 상태를 지정하는 것 외에도 파이프라인을 연결해야 합니다:
- QRhiVertexInputLayout 형태의 버텍스 입력 레이아웃. 각 버텍스 입력 위치의 유형과 컴포넌트 수, 버텍스당 총 바이트 수 및 기타 관련 데이터를 지정합니다. QRhiVertexInputLayout 실제 네이티브 리소스가 아닌 데이터만 보유하며 복사할 수 있습니다.
- 유효하고 성공적으로 초기화된 QRhiShaderResourceBindings 객체. 셰이더가 기대하는 리소스 바인딩(균일 버퍼, 텍스처, 샘플러)의 레이아웃을 설명합니다. 이 객체는 드로우 호출을 기록할 때 사용되는 QRhiShaderResourceBindings 또는 다른 객체( layout-compatible with it)여야 합니다. 이 간단한 애플리케이션은 전자의 접근 방식을 취합니다.
- 유효한 QRhiRenderPassDescriptor 객체. 또는 be compatible with 렌더링 타겟에서 검색해야 합니다. 이 예제에서는 QRhiSwapChain::newCompatibleRenderPassDescriptor()을 통해 QRhiRenderPassDescriptor 객체를 생성하여 전자를 사용합니다.
m_colorTriSrb.reset(m_rhi->newShaderResourceBindings()); static const QRhiShaderResourceBinding::StageFlags visibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; m_colorTriSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, visibility, m_ubuf.get()) }); m_colorTriSrb->create(); m_colorPipeline.reset(m_rhi->newGraphicsPipeline()); // Enable depth testing; not quite needed for a simple triangle, but we // have a depth-stencil buffer so why not. m_colorPipeline->setDepthTest(true); m_colorPipeline->setDepthWrite(true); // Blend factors default to One, OneOneMinusSrcAlpha, which is convenient. QRhiGraphicsPipeline::TargetBlend premulAlphaBlend; premulAlphaBlend.enable = true; m_colorPipeline->setTargetBlends({ premulAlphaBlend }); m_colorPipeline->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/color.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 5 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) } }); m_colorPipeline->setVertexInputLayout(inputLayout); m_colorPipeline->setShaderResourceBindings(m_colorTriSrb.get()); m_colorPipeline->setRenderPassDescriptor(m_rp.get()); m_colorPipeline->create();
getShader()는 .qsb
파일을 로드하고 QShader 파일을 역직렬화하는 헬퍼 함수입니다.
static QShader getShader(const QString &name) { QFile f(name); if (f.open(QIODevice::ReadOnly)) return QShader::fromSerialized(f.readAll()); return QShader(); }
color.vert
셰이더는 버텍스 입력으로 다음을 지정합니다:
layout(location = 0) in vec4 position; layout(location = 1) in vec3 color;
그러나 C++ 코드는 버텍스 데이터를 위치는 2개의 실수로, 색상은 3개의 실수로 인터리빙합니다. (각 버텍스에 대해x
, y
, r
, g
, b
) 따라서 보폭은 5 * sizeof(float)
이고 위치 0과 1의 입력은 각각 Float2
과 Float3
로 지정됩니다. 이는 유효하며 vec4
위치의 z
및 w
은 자동으로 설정됩니다.
렌더링
프레임 녹화는 QRhi::beginFrame()를 호출하여 시작하고 QRhi::endFrame()를 호출하여 완료합니다.
QRhi::FrameOpResult result = m_rhi->beginFrame(m_sc.get()); if (result== QRhi::FrameOpSwapChainOutOfDate) { resizeSwapChain(); if (!m_hasSwapChain) return; result = m_rhi->beginFrame(m_sc.get()); } if (result ! = QRhi::FrameOpSuccess) { qWarning("beginFrame failed with %d, will retry", result); requestUpdate(); return; } customRender();
일부 리소스(버퍼, 텍스처)는 애플리케이션에 정적 데이터가 있으므로 콘텐츠가 변경되지 않습니다. 예를 들어 버텍스 버퍼의 콘텐츠는 초기화 단계에서 제공되며 이후에는 변경되지 않습니다. 이러한 데이터 업데이트 작업은 m_initialUpdates
에 기록됩니다. 아직 완료되지 않은 경우 이 리소스 업데이트 배치의 명령은 프레임별 배치에 병합됩니다.
void HelloWindow::customRender() { QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); if (m_initialUpdates) { resourceUpdates->merge(m_initialUpdates); m_initialUpdates->release(); m_initialUpdates = nullptr; }
프레임별 리소스 업데이트 배치는 모델뷰-투영 행렬과 불투명도가 매 프레임마다 변경되는 균일한 버퍼 콘텐츠가 필요하기 때문에 필요합니다.
m_rotation += 1.0f; QMatrix4x4 modelViewProjection = m_viewProjection; modelViewProjection.rotate(m_rotation, 0, 1, 0); resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData()); m_opacity += m_opacityDir * 0.005f; if (m_opacity < 0.0f || m_opacity > 1.0f) { m_opacityDir *= -1; m_opacity = qBound(0.0f, m_opacity, 1.0f); } resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity);
렌더 패스 기록을 시작하려면 QRhiCommandBuffer 을 쿼리하고 출력 크기를 결정하는데, 이는 필요한 경우 뷰포트를 설정하고 전체 화면 텍스처의 크기를 조정하는 데 유용합니다.
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); const QSize outputSizeInPixels = m_sc->currentPixelSize();
렌더 패스를 시작한다는 것은 렌더 대상의 색상 및 뎁스 스텐실 버퍼를 지우는 것을 의미합니다(렌더 대상 플래그에 달리 표시되지 않는 한 텍스처 기반 렌더 대상의 경우에만 해당 옵션이 적용됩니다). 여기서는 색은 검정, 깊이는 1.0f, 스텐실은 0(미사용)으로 지정합니다. 마지막 인수인 resourceUpdates
는 배치에 기록된 데이터 업데이트 명령이 커밋되도록 하는 역할을 합니다. 또는 QRhiCommandBuffer::resourceUpdate()를 대신 사용할 수도 있습니다. 렌더 패스는 스왑체인을 대상으로 하므로 currentFrameRenderTarget()를 호출하여 유효한 QRhiRenderTarget 을 가져옵니다.
cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);
삼각형에 대한 드로 콜을 기록하는 것은 파이프라인을 설정하고, 셰이더 리소스를 설정하고, 버텍스/색인 버퍼를 설정하고, 드로 콜을 기록하는 간단한 과정입니다. 여기서는 정점이 3개만 있는 비색인 드로우를 사용합니다.
cb->setGraphicsPipeline(m_colorPipeline.get()); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass();
setShaderResources() 호출에는 인수가 지정되지 않으므로 활성 QRhiGraphicsPipeline (m_colorPipeline
)과 연결된 m_colorTriSrb
을 사용합니다.
전체 화면 배경 이미지의 렌더링에 대한 자세한 내용은 다루지 않겠습니다. 이에 대해서는 예제 소스 코드를 참조하세요. 하지만 텍스처 또는 버퍼 리소스의 크기를 '조정'하는 일반적인 패턴에 주목할 필요가 있습니다. 기존 네이티브 리소스의 크기를 변경하는 것은 불가능하므로 텍스처 또는 버퍼 크기를 변경한 후에는 반드시 create() 함수를 호출하여 기본 네이티브 리소스를 해제하고 다시 생성해야 합니다. QRhiTexture 이 항상 필요한 크기를 갖도록 하기 위해 애플리케이션은 다음 로직을 구현합니다. m_texture
은 윈도우의 전체 수명 기간 동안 유효하므로 QRhiShaderResourceBindings 과 같은 객체 참조는 항상 유효합니다. 시간이 지남에 따라 왔다가 사라지는 것은 기본 네이티브 리소스뿐입니다.
또한 그리려는 창과 일치하는 디바이스 픽셀 비율을 이미지에 설정합니다. 이렇게 하면 드로잉 코드가 DPR에 구애받지 않고 DPR에 관계없이 동일한 레이아웃을 생성하는 동시에 추가 픽셀을 활용하여 충실도를 향상시킬 수 있습니다.
void HelloWindow::ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u) { if (m_texture && m_texture->pixelSize() == pixelSize) return; if (!m_texture) m_texture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, pixelSize)); else m_texture->setPixelSize(pixelSize); m_texture->create(); QImage image(pixelSize, QImage::Format_RGBA8888_Premultiplied); image.setDevicePixelRatio(devicePixelRatio());
QImage 가 생성되고 QPainter 기반 드로잉이 완료되면 uploadTexture()를 사용하여 리소스 업데이트 배치에 텍스처 업로드를 기록합니다:
u->uploadTexture(m_texture.get(), image);
QRhi, QRhiSwapChain, QWindow, QRhiCommandBuffer, QRhiResourceUpdateBatch, QRhiBuffer, QRhiTexture 을참조하세요 .
© 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.