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 또는 어떤 형태의 위젯(Widgets)도 사용하지 않고, 애플리케이션 내에서 모든 렌더링 및 윈도우 인프라를 직접 설정하는 방식입니다.
반면, Qt Quick 또는 Qt Quick 3D 를 사용하여 QML 애플리케이션을 작성하고 여기에 QRhi 기반 렌더링을 추가하려는 경우, 해당 애플리케이션은 Qt Quick 가 이미 초기화한 창 및 렌더링 인프라에 의존하게 되며, QQuickWindow 에서 기존 QRhi 인스턴스를 조회할 가능성이 높습니다. QRhi::create() 처리, Vulkan instances 와 같은 플랫폼/API별 세부 사항, 또는 expose 및 창 크기 조정 이벤트를 올바르게 처리하는 작업은 모두 Qt Quick 에서 관리됩니다. 반면 이 예제에서는 이러한 모든 작업이 애플리케이션 자체에 의해 관리되고 처리됩니다.
참고: 특히 QWidget 기반 애플리케이션의 경우 , QWidget::createWindowContainer()을 사용하면 (네이티브 창을 기반으로 하는) QWindow 를 위젯 기반 사용자 인터페이스에 임베드할 수 있다는 점에 유의해야 합니다. 따라서 main() 에서 필요한 초기화가 제대로 이루어져 있다면, 이 예제의 HelloWindow 클래스는 QWidget 기반 애플리케이션에서도 재사용할 수 있습니다.
3D API 지원
이 애플리케이션은 현재 사용 가능한 모든 QRhi backends 를 지원합니다. 명령줄 인수가 지정되지 않은 경우, 플랫폼별 기본값이 사용됩니다: Windows에서는 Direct 3D 11, Linux에서는 OpenGL, macOS/iOS에서는 Metal이 사용됩니다.
--help 를 실행하면 사용 가능한 명령줄 옵션이 표시됩니다:
- -d 또는 –d3d11: Direct 3D 11
- -D 또는 –d3d12: Direct 3D 12
- -m 또는 –metal: Metal
- -v 또는 –vulkan: Vulkan
- -g 또는 –opengl: OpenGL 또는 OpenGL ES용
- -n 또는 –null: Null backend
빌드 시스템 참고 사항
이 애플리케이션은 전적으로 Qt GUI 모듈에만 의존합니다. Qt Widgets 또는 Qt Quick 는 사용하지 않습니다.
모든 Qt 애플리케이션에서 사용할 수 있지만 호환성 보장이 제한적인 RHI API에 접근하기 위해, ` target_link_libraries ` CMake 명령어는 ` Qt6::GuiPrivate`을 나열합니다. 이를 통해 ` #include <rhi/qrhi.h> ` 인클루드 문이 성공적으로 컴파일될 수 있습니다.
기능
이 애플리케이션의 기능은 다음과 같습니다:
- 크기 조절이 가능한 QWindow,
- 창 크기에 맞춰 적절히 조정되는 스왑체인 및 깊이-스텐실 버퍼,
- QExposeEvent 및 QPlatformSurfaceEvent 와 같은 이벤트를 기반으로 적절한 시점에 초기화, 렌더링 및 해제를 수행하는 로직,
- QImage 에서 QPainter 을 통해 생성된 텍스처를 사용하여 전체 화면 텍스처 쿼드를 렌더링하며(래스터 페인트 엔진을 사용함, 즉 이미지의 픽셀 데이터 생성은 모두 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의 경우 깊이 버퍼(depth buffer)가 사용 가능하도록 보장해야 하며, 이는 ` QSurfaceFormat`를 통해 수행됩니다. 이러한 단계들은 QRhi 의 범위에 포함되지 않습니다. OpenGL 또는 Vulkan용 QRhi 백엔드는 ` QOpenGLContext `이나 ` QVulkanInstance`과 같은 기존의 Qt 기능을 기반으로 구축되기 때문입니다.
// OpenGL의 경우, 창에 깊이/스텐실 버퍼가 있는지 확인합니다.
// 다른 API의 경우 이는 애플리케이션에서 제어합니다(QRhiRenderBuffer 등).
// 따라서 해당 API에는 특별한 설정이 필요하지 않습니다.
QSurfaceFormat fmt;
fmt.setDepthBufferSize(24);
fmt.setStencilBufferSize(8);
// macOS에서 OpenGL을 사용할 수 있도록 하는 특수한 경우.
// (다만, 기본적으로 Metal을 사용하는 것이 권장되는 방법입니다.)
// gl_VertexID는 GLSL 130 기능이므로, macOS에서 제공하는 기본 OpenGL 2.1 컨텍스트
// 만으로는 충분하지 않습니다.
#ifdef Q_OS_MACOS
fmt.setVersion(4, 1);
fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
QSurfaceFormat::setDefaultFormat(fmt);
// Vulkan용.
#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()에서 구현됩니다. 이 메서드는 창이 renderable 상태일 때만 호출되며, 이는 expose event 로 표시됩니다.
사용하는 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()`를 기반으로 한 확인이 필요합니다.
이 expose 이벤트 처리 구현은 견고하고 안전하며 이식성을 갖추도록 설계되었습니다. 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(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());창 크기가 변경되면 스왑체인(swapchain)의 크기도 조정되어야 합니다. 이는 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 `은 `create-function`과 관련하여 약간 다른 동작 방식을 보입니다. 이름인 ` createOrResize()`에서 알 수 있듯이, 출력 창 크기가 스왑체인(swapchain)이 마지막으로 초기화되었을 때의 크기와 일치하지 않을 가능성이 있을 때마다 이 함수를 호출해야 합니다. 깊이-스텐실용 관련 QRhiRenderBuffer 의 size 는 자동으로 설정되며, swapchain의 createOrResize()에서 암시적으로 create()가 호출됩니다.
또한, 우리가 설정한 원근 투영은 출력 화면비에 따라 달라지기 때문에, 이곳은 투영 행렬과 뷰 행렬을 (재)계산하기에 편리한 곳이기도 합니다.
참고: 좌표계 차이를 없애기위해 QRhi 에서 a backend/API-specific "correction" matrix 를 조회하여 투영 행렬에 반영합니다. 이를 통해 애플리케이션은 원점이 왼쪽 하단에 있는 좌표계를 가정하고 OpenGL 스타일의 정점 데이터를 처리할 수 있습니다.
resizeSwapChain() 함수는 현재 보고된 크기가 스왑 체인이 마지막으로 초기화되었을 때의 크기와 더 이상 일치하지 않는 것으로 확인되면 RhiWindow::render()에서 호출됩니다.
자세한 내용은 QRhiSwapChain::currentPixelSize() 및 QRhiSwapChain::surfacePixelSize()을 참조하십시오.
고해상도(High DPI) 지원이 내장되어 있습니다. 이름에서 알 수 있듯이, 크기는 항상 픽셀 단위로 표시되며, 창별 scale factor 를 고려합니다. QRhi (및 3D API) 수준에서는 고해상도 스케일링 개념이 없으며, 모든 것이 항상 픽셀 단위로 처리됩니다. 즉, 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에 구애받지 않고, 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도 참조하십시오 .
© 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.