2020年9月8日 星期二

在 Qt 使用 Direct3D10 來繪圖

在 Qt 使用 Direct3D10 來繪圖

前言

  在之前的 特製 Widget 裡有提到如何特製一個 Widget ,利用 Qt 提供的繪圖來繪製,如果要用 Direct3D 來繪圖可以嗎?答案是肯定的,在 [ github.com ] QtDirect3D 裡有提供完整的範例,但由於該專案的使用習慣跟我的習慣不太一樣,所以我將參照了該專案的範例做了一個範例,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(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 的專案檔 。

參考資料


相關文章與資料

沒有留言:

張貼留言