在 Qt 使用 Direct3D10 來繪圖
前言
在之前的 特製 Widget 裡有提到如何特製一個 Widget ,利用 Qt 提供的繪圖來繪製,如果要用 Direct3D 來繪圖可以嗎?答案是肯定的,在 [ github.com ] QtDirect3D 裡有提供完整的範例,但由於該專案的使用習慣跟我的習慣不太一樣,所以我將參照了該專案的範例做了一個範例,在此把學習的過程做個紀錄。
內容
先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/CustomDirect3D10Widegt,執行結果如下
(HelloQt' directory)/Widget/CustomDirect3D10Widegt,執行結果如下
範例的執行結果 |
畫面上的藍色區塊是用 Direct3D10 來繪製的一個特製的 Widget ,如果不熟悉如何特製 Widget 可以參考 特製 Widget 。
為何使用 Direct3D10 ?這個理由只是因為 Direct3D10 的初始化流程較為簡單,可以用比較少的程式碼看到結果,方便於理解 Qt 是如何跟 Direct3D 合併使用。
接著來看到特製的 Widget 的部分,宣告的程式碼如下
#include <QWidget> #include <QTimer> #include <QEvent> // #include <exception> // #include <d3d10.h> #include <DirectXMath.h> #include <D3Dcompiler.h> class Direct3D10Widget : public QWidget { Q_OBJECT public: explicit Direct3D10Widget(QWidget *parent = nullptr); ~Direct3D10Widget(); void release(); private: bool event(QEvent * event) override; void showEvent(QShowEvent * event) override; QPaintEngine *paintEngine() const override; void paintEvent(QPaintEvent * event) override; void resizeEvent(QResizeEvent * event) override; signals: void widgetResized(); void rendered(); void d3dReady(); private Q_SLOTS: void onFrame(); void onReset(); private: bool m_bIsTimerStart; QTimer m_cFrameTimer; // ID3D10Device* m_pDevice; IDXGISwapChain* m_pSwapChain; ID3D10RenderTargetView* m_pRTView; };
比上次的 特製 Widget 還要複雜不少,這次需要一個 Timer 來做為畫面的刷新基礎,這部分不會太難理解,就是每隔一段時間觸發事件來繪圖,也就是"m_cFrameTimer", Direct3D 的初始化整個程式只需做一次,會在某個事件裡做檢查,所以需要"m_bIsTimerStart",剩下的三個成員就是 Direct3D 的相關成員。
實作的部分先看到 Direct3DWidget::Direct3DWidget() ,程式碼如下
Direct3D10Widget::Direct3D10Widget(QWidget *parent) : QWidget(parent), m_bIsTimerStart(false), m_pDevice(nullptr), m_pSwapChain(nullptr), m_pRTView(nullptr) { QPalette pal = palette(); pal.setColor( QPalette::Background, Qt::black ); setAutoFillBackground( true ); setPalette( pal ); // setFocusPolicy( Qt::StrongFocus ); setAttribute( Qt::WA_NativeWindow ); // setAttribute( Qt::WA_PaintOnScreen ); setAttribute( Qt::WA_NoSystemBackground ); }
這裡設定 Widget 的預設的顏色與必要的屬性,接著看到 Direct3DWidget::showEvent(),程式碼如下
void Direct3D10Widget::showEvent(QShowEvent *event) { if(!m_bIsTimerStart) { // DXGI_SWAP_CHAIN_DESC sd = {}; sd.BufferCount = 2; sd.BufferDesc.Width = width(); sd.BufferDesc.Height = height(); sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = reinterpret_cast<HWND>(winId()); sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; UINT iCreateFlags = 0; #if defined(DEBUG) || defined(_DEBUG) iCreateFlags |= D3D10_CREATE_DEVICE_DEBUG; #endif auto hr = D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_HARDWARE, NULL, iCreateFlags, D3D10_SDK_VERSION, &sd, &m_pSwapChain, &m_pDevice); if (FAILED(hr)) { throw std::runtime_error("D3D10CreateDeviceAndSwapChain() has error"); } // onReset(); emit d3dReady(); // connect(&m_cFrameTimer, &QTimer::timeout, this, &Direct3D10Widget::onFrame); m_cFrameTimer.start(16); m_bIsTimerStart=true; } // QWidget::showEvent(event); }
這個事件會在 Qt 需要更新畫面時啟動, Diretc3D 的初始化就放在這裡,但由於這個事件不只觸發一次,所以需要"m_bIsTimerStart"來輔助, Direct3D 初始化完後會喚起 onRest() ,這個 Function 的功用待會再說明,接著觸發 d3dReady() ,這是給使用 Widget 時綁定的事件,接著就綁定 Timer 的事件,並透過 start() 來啟動。看到 Direct3DWidget::resizeEvent() ,程式碼如下
void Direct3D10Widget::resizeEvent(QResizeEvent *event) { if( m_bIsTimerStart ) { onReset(); emit widgetResized(); } // QWidget::resizeEvent( event ); }
這個事件會發生在 Widget 的大小改變時,一樣會喚起 onResst() ,並觸發 widgetResized()。接著看到 Direct3DWidget::onFrame() ,這個 Function 會在 Timer 觸發,程式碼如下
void Direct3D10Widget::onFrame() { // const float clearColor[ 4 ] = { 0.0f , 0.0f , 1.0f , 1.0f }; m_pDevice->OMSetRenderTargets( 1, &m_pRTView, nullptr ); m_pDevice->ClearRenderTargetView( m_pRTView , clearColor ); // emit rendered(); // if ( FAILED ( m_pSwapChain->Present( 1, 0 ) ) ) { onReset(); } }
這裡就是 Direct3D 常見的把畫面清除成指定顏色的程式碼,並且觸發 rendered() ,當 Present() 失敗時喚起 onReset()。最後看到之前一直沒解釋的 Direct3DWidget::onReset() ,程式碼如下
void Direct3D10Widget::onReset() { ID3D10Texture2D* pBackBuffer; if( m_pRTView != nullptr ) { m_pRTView->Release(); m_pRTView = nullptr; } auto hr = m_pSwapChain->ResizeBuffers( 0, width() , height() , DXGI_FORMAT_UNKNOWN , 0 ); if (FAILED( hr ) ) { throw std::runtime_error("ResizeBuffers() has error"); } hr = m_pSwapChain->GetBuffer( 0, IID_PPV_ARGS( &pBackBuffer ) ); if (FAILED( hr ) ) { throw std::runtime_error("GetBuffer() has error"); } hr = m_pDevice->CreateRenderTargetView( pBackBuffer , nullptr , &m_pRTView ); if (FAILED( hr ) ) { throw std::runtime_error("CreateRenderTargetView() has error"); } // if( pBackBuffer != nullptr ) { pBackBuffer->Release(); pBackBuffer = nullptr; } // qDebug("wiget resize"); }
這個部分是重新製作一塊跟目前 Widget 一樣大小的繪圖緩衝區,這個部分在一般的 Direct3D 範例是不會這樣用,因為範例幾乎都是固定大小的視窗,所以這邊的處理我是第一次看到,因為做成 Widget ,難免需要改變大小,所以這裡我個人認為是最重要的部分。
最後一個小提醒,由於 Direct3D 只能在 Windows 平台工作,所以 Qt 專案的編譯器請選擇 MSVC ,由於選擇 MSVC ,所以可以用特別的方式鏈結(Link),如下
#pragma comment(lib, "d3d10.lib")
這個就是鏈結(Link)"d3d10.lib",但這個用法只能用在 MSVC ,正統的作法應該是在 Qt 的專案檔搭配平台的參數來鏈結(Link),如不清楚請參考 Qt 的專案檔 。
沒有留言:
張貼留言