C++ ⇒ VBA 書いて覚えるための初心者自己中記事

C++ ⇒ VBA 勉強の履歴を付けるというかノート代わりに使ってます

C++ Windows プログラミング⑥ 書いて覚えるための初心者自己中記事

Windowsのグラフィックス アーキテクチャの概要

 

 

アーキテクチャってなんだ?

構造とか構成のことか。

 

Windowsでは数種類のC++/COM APIがある。

Windows グラフィックス API の相関図

 

・グラフィックスデバイスインターフェイス(GDI)

Windowsにもともと実装されていたグラフィックスインターフェイス

16ビットのWindows用に開発され32ビット・64ビットに移植された。

 

・GDI+

GDIの後継(詳細はちょっと意味わからない)

 

Direct3D

3Dグラフィックスをサポート

 

 

・Direct2D

GDI・GDI+の後継

 

・DirectWrite

テキストのレイアウトとラスター化のエンジン

ラスター化されたテキストの描画にはGDI・Direct2Dが使用できる。

 

DirectXグラフィックインフラストラクチャ(DXGI)

出力用フレームなどの基礎レベルのタスクを実行する。

 

 

Direct2DとDirectWriteの仕様がおすすめらしい。

 

ハードウェアアクセラレーション

CPUではなくGPUによって実行されたグラフィックス計算を指す。

GDIはあまりこれをサポートしない。

Direct2Dはほとんどサポートするためパフォーマンスが優れる。

 

 

透過とアンチエイリアシング

Direct2Dはハードウェアアクセラレーションによるアルファブレンド(透過)の実行を完全にサポート。

GDIは微妙。

ハードウェアアクセラレーションによるアルファブレンドではアンチエイリアシングも有効化される。

エイリアシングは連続関数のサンプリングによって生じる不具合。

輪郭と背景をブレンドする方法でアンチエイリアシングは実行される。

 

 

Direct2D のアンチエイリアシング技術を示す図

Direct2D のアンチエイリアシング技術

 

次の画像は、それぞれの円の拡大図です。

 

上の画像の拡大図

上の画像の拡大図

 

凄いなぁ。

アンチエイリアシングはGDIだとほぼされない、GDI+だとされるけどCPUで実行、Dirext2DだとGPUでサポート。

 

ベクターグラフィックス

Direct2Dはベクターグラフィックスをサポート。

ベクターグラフィックスは数式を使用して線や曲線を表現するので画面の解像度に依存しない。pdfの文字とかがあれかな。

 

つぎ、デスクトップウィンドウマネージャー

 

Windows Vista以前のWindowsは画面の描写が直接行われていた。

ビデオカードによって表示されるメモリバッファーにプログラムが直接書き込みを行っていた)

再描画によるアーティファクトを示すスクリーンショット

再描画による不具合を示すスクリーンショット

そのためこんな不具合があった。あ、なったことある。

別のウィンドウをドラッグしてウィンドウが重なったときとかに発生する。

これは両方のウィンドウの描画がメモリの同じ領域に対して実行されているから。

なんかいろいろあったんだね。

Vista以降はデスクトップウィンドウマネージャー(DWM)が導入されウィンドウの描画方法が根本的に変わった。

 

DWM によるデスクトップの合成方法を示す図

DWM によるデスクトップの合成方法を示す図

以前は上の図。ウィンドウはディスプレイバッファーに直接描画されていた。

今はDWMを使用して、オフスクリーンメモリバッファー(オフスクリーンサーフェイス)に描画され、DWMが画面に合成する。

 

 

つぎ、保持モードと直接モード

Direct2Dは直接モードAPI

保持モードAPIの例はWindows Presentation Foundatioin(WPF)がある。

保持モードAPIは宣言型のAPI

保持モードはグラフィックスライブラリがシーンを保存して描画コマンドに変換する。アプリケーションはグラフィックライブラリに対して、シーンを変更するコマンドは出す。

 

直接モードはアプリケーションがシーンの描画コマンドを出す。

 

リテインド モード グラフィックスの図

保持モード グラフィックスの図 

イミディエイト モード グラフィックスの図

直接モード グラフィックスの図

 

シーンとか描画コマンドとか知らんし。ここはこんな感じなのか~くらいで進んだほうがいいのかな。

 

つぎ。

初めてのDirect2Dプログラム

 

2D1D名前空間

Direct2Dのヘルパー関数とヘルパークラスが入っている。

色の値を構成する ClolrFクラス

変換行列を構成する Matrix3x2F

Direct2D構造体を初期化するための一連の関数

が入っている。

 

 

レンダーターゲット、デバイス、リソース

 

レンダーターゲットはプログラムの描画先のこと。

レンダーターゲットは通常、ウィンドウ。

画面に表示されないメモリ内のビットマップの時もある。

レンダーターゲットは ID2D1RenderTarget インターフェイスによって表される。

 

 

バイスは実際にピクセルの描画を実行する何か。

ハードウェアデバイスGPU

ソフトウェアデバイスはCPUを使用する。

アプリケーションはデバイスを作成しない。代わりにアプリケーションがレンダーターゲットを作成するときに暗黙的にデバイスが作成される。

 

 

リソースはプログラムが描画するために使用するオブジェクトのとこ。

Direct2Dで定義されているのは

ブラシ 線と領域の描画を制御(単色ブラシとグラデーションブラシがある)

ストロークスタイル 破線や実線など、線の外観を制御

ジオメトリ 直線と曲線の集合を表す

(ジオメトリはレンダリング前に変換する必要がある)

メッシュ 複数の三角形からなる図形

(メッシュデータはGPUで直接使用できる)

 

ブラシとメッシュはデバイス依存リソース。

バイス依存(ハードウェア(GPU)ソフトウェア(CPU)いずれかの特定デバイスと対応付けられる)

 

ストロークスタイルとジオメトリはCPUメモリ内で維持される。

特定のデバイスに対応付けられていないのでデバイス非依存

 

これらは

I2D1DResourceから派生したインターフェイスで表される。

例 ブラシは ID2D1Brushインターフェイス

 

 

 

Direct2Dファクトリオブジェクト

Direct2Dを使うには最初にDirect2Dファクトリオブジェクトのインスタンスを作成する。

ファクトリとはオブジェクトを作るためのオブジェクトの事。

 

Direct2Dファクトリオブジェクトが作るオブジェクトは

レンダーターゲット

バイス非依存リソース(ストロークスタイル・ジオメトリ)

 

残りのデバイス依存リソースはレンダーターゲットが作る。

 

 このDirect2Dファクトリオブジェクトを作成する関数。

D2D1CreateFactory

 

ID2D1Factory            *pFactory;
MainWindow() : pFactory(NULL)
case WM_CREATE:
	if (FAILED(D2D1CreateFactory(
		D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
	{
		return -1;  // CreateWindowEx が失敗する
	}
	return 0;

 ID2D1Factoryインターフェイスへのポインタを用意して

NULLで初期化して

D2D1CreateFactory関数でファクトリオブジェクトを作成

オブジェクトへのポインタはいつも通り引数に入れておいて書き換えてもらう方法。

第一引数は、作成オプションフラグ。

Direct2Dを呼び出すのが単一スレッドなのか複数スレッドなのかでSINGLE / MULTIを変更する。

これらを行うのはWM_PAINTメッセージより前に行う必要があるので

WM_CREATEメッセージで行わせるといい。

 

ファクトリオブジェクトが作られたら次はリソースを作成。

今回は

レンダーターゲットとブラシ

 

class MainWindow : public BaseWindow<MainWindow>
{
	ID2D1Factory            *pFactory;//Direct2Dファクトリオブジェクトのインターフェイスへのポインタ
	ID2D1HwndRenderTarget   *pRenderTarget;//Direct2Dリソース レンダーターゲットオブジェクトのインターフェイスへのポインタ
	ID2D1SolidColorBrush    *pBrush;//Direct2Dリソース 単色ブラシオブジェクトのインターフェイスへのポインタ

 ファクトリと同じようにインターフェイスへのポインタを用意。

MainWindow() : pFactory(NULL), pRenderTarget(NULL), pBrush(NULL)

コンストラクタで初期化

HRESULT MainWindow::CreateGraphicsResources()//D2D1ファクトリオブジェクトでリソースオブジェクトを作る
{
	HRESULT hr = S_OK;//レンダーターゲットがすでにある場合にはS_OKを返す
	if (pRenderTarget == NULL)
	{
		RECT rc;
		GetClientRect(m_hwnd, &rc);
 
		D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);
 
		hr = pFactory->CreateHwndRenderTarget(//ファクトリでレンダーターゲットオブジェクトを作成する
			D2D1::RenderTargetProperties(),//全種類のレンダーターゲットに共通するオプションを指定。D2D1RenderTargetProperties()は既定オプション
			D2D1::HwndRenderTargetProperties(m_hwnd, size),//ウィンドウハンドルとレンダーターゲットのサイズ
			&pRenderTarget);//書き換えてもらうインターフェイスへのポインタ
 
		if (SUCCEEDED(hr))
		{
			const D2D1_COLOR_F color = D2D1::ColorF(1.0f, 1.0f, 0);//作成するブラシの色を格納した変数を指定
			hr = pRenderTarget->CreateSolidColorBrush(color, &pBrush);//ブラシオブジェクト作成
 
			if (SUCCEEDED(hr))
			{
				CalculateLayout();
			}
		}
	}
	return hr;
}

 

GetClientRectとかはまだ説明ない。

 

リソースオブジェクトを作成したら描画する。

今回やることは

背景を単色で塗りつぶす

塗りつぶした円を描画する

 

レンダーターゲットがウィンドウなのでWM_PAINTメッセージが来たら処理する。

case WM_PAINT:
	OnPaint();
	return 0;

 

void MainWindow::OnPaint()
{
	HRESULT hr = CreateGraphicsResources();//ファクトリオブジェクトでリソース作りするメンバ関数
	if (SUCCEEDED(hr))
	{
		PAINTSTRUCT ps;
		BeginPaint(m_hwnd, &ps);
 
		pRenderTarget->BeginDraw();//メソッドが描画の開始を通知
 
		pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::SkyBlue));//レンダーターゲット全体を単色で塗りつぶす
		pRenderTarget->FillEllipse(ellipse, pBrush);//指定されたブラシを使って塗りつぶした楕円を描画する ellipseにはCalculateLayoutメンバ関数でデータが格納されている
 
		hr = pRenderTarget->EndDraw();//描画の終了を通知 BeginDrawからここに来るまでのメソッドは戻り値がvoid* EndDrawの戻り値でまとめてHRESULTが返される
		if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
		{
			DiscardGraphicsResources();
		}
		EndPaint(m_hwnd, &ps);
	}
}

 

 

バイス消失に対処する

ディスプレイの解像度を変更したり、ディスプレイアダプターを外したり。

そうなると、対応付けられていたリソース・レンダーターゲットも無効になる。

Direct2DではEndDrawの戻り値でD2DERR_RECREATE_TARGETという戻り値がありこれがデバイスの消失を通知する。

 

この戻り値を受け取ってしまったら、レンダーターゲットとデバイス依存リソースを再作成しなければならない。

なのでポインターを破棄する。

 

基本、レンダーターゲットやデバイス依存リソースはその都度破棄しない。

 

 

Direct2Dの描画ループ

  1. バイス非依存のリソースを作成する。
  2. シーンをレンダリングする。
    1. レンダー ターゲットが存在するかどうかを確認する。存在しない場合は、レンダー ターゲットおよびデバイス依存リソースを作成する。
    2. ID2D1RenderTarget::BeginDraw を呼び出す。
    3. 描画コマンドを実行する。
    4. ID2D1RenderTarget::EndDraw を呼び出す。
    5. EndDraw が D2DERR_RECREATE_TARGET を返した場合は、レンダー ターゲットおよびデバイス依存リソースを破棄する。
  3. シーンの更新や再描画が必要な場合は、そのたびに手順 2 を繰り返す。

 

レンダーターゲットがウィンドウの場合はWM_PAINTメッセージが来たら都度2が実行される。

 

 

つぎ、DPIとDIP (デバイス非依存ピクセル)

DPI(Dot Per Inch : 1インチ当たりのドット数)

DIP(Device Independet Pixel : デバイス非依存ピクセル

 

DPIの説明のための予備知識

文字体裁

活字のサイズをポイントという単位で表す。

1ポイントは 1/72インチ

12ポイントは 12/72インチ == 1/6インチ

 

文字によって高さが違うのではみ出す奴が出てくる、それをカバーするために余白が必要。行間という。

上にはみ出す奴は行間で対応。

下にはみ出すやつは、そもそもの文字の位置をベースラインという少し上で表示させておき下にはみ出す奴だけベースラインを下に超える。

このベースラインの上下のエリアを

上ならアセント

下ならディセント

という。

 

ピクセルのサイズにはディスプレイの物理的な大きさと解像度が影響される。

しかしこれらはバラバラ。

なので72/72インチ == 1インチ == 72ポイントのフォントは1論理インチの高さ、と定義されている。

論理インチはピクセル数に変換され、Windowsでは96ピクセルが1論理インチとされている。

 

ユーザーがDPIを100% 150%と設定できる。

 

DWMによるスケール

プログラムがDPIをサポートしない場合は

UI要素の一部が表示されない

レイアウトが崩れる

ビットマップとアイコンがおかしくなる

マウスの座標がおかしくなる

DWMはプログラムがDPI非対応だとUI全体をスケールしてDPI設定(100%~)に一致させる。

ただし、ウィンドウが描画された後に適用されるのでぼやける。

 

 

DPI対応のアプリケーション

プログラム自体でDPI対応を宣言する。

これでDWMはスケールを実行しなくなる。

プログラムでDPI対応を宣言するにはアプリケーションマニュフェストを使用する。これはDLLやアプリケーションについて説明したXMLファイル。

マニュフェストにはDLLの依存関係

要求された特権レベル

そのプログラムが想定しているWindowsのバージョン

などが入っている。

 

プログラムが DPI 対応であることを宣言するには、次の情報をマニフェストに記述します。

 
 
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

 

マニュフェストのほかの部分はVisual Studioのリンカーが作ってくれる。

上記の内容をマニュフェストに追加するVisual Studioでの方法。

  1. [プロジェクト] メニューの [プロパティ] をクリックします。
  2. 左側のペインで [構成プロパティ]、[マニフェスト ツール] を順に展開し、[入力と出力] をクリックします。
  3. [追加のマニフェスト ファイル] テキスト ボックスにマニフェスト ファイル名を入力し、[OK] をクリックします。

 

ファイルは .manifest

 

 

Direct2DはDPI設定と一致するように自動でスケールを実行する。

マウスとウィンドウの座標は物理ピクセルで提供される。

なのでピクセル座標をDPIに変換する必要がある。

DIP 数 = ピクセル数 / (DPI/96.0)

 

DPI設定の値を取得するには

ID2D1Factory::GetDesktopDpiメソッドを呼ぶ。

 
float g_DPIScaleX = 1.0f;
float g_DPIScaleY = 1.0f;

void InitializeDPIScale(ID2D1Factory *pFactory)
{
    FLOAT dpiX, dpiY;

    pFactory->GetDesktopDpi(&dpiX, &dpiY);

    g_DPIScaleX = dpiX/96.0f;
    g_DPIScaleY = dpiY/96.0f;
}

template <typename T>
float PixelsToDipsX(T x)
{
    return static_cast<float>(x) / g_DPIScaleX;
}

template <typename T>
float PixelsToDipsY(T y)
{
    return static_cast<float>(y) / g_DPIScaleY;
}

 

レンダーターゲットのサイズを変更する。

ウィンドウのサイズが変更された場合はレンダーターゲットのサイズを変更する必要がある。

その後、レイアウトの変更、ウィンドウの再描画。

void MainWindow::Resize()
{
	if (pRenderTarget != NULL)
	{
		RECT rc;//クライアントの情報を入れる構造体?
		GetClientRect(m_hwnd, &rc);//クライアントの物理ピクセル数を取得
 
		D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);
 
		pRenderTarget->Resize(size);//レンダーターゲットのピクセル数をResize
		CalculateLayout();//レイアウト計算メンバ関数
		InvalidateRect(m_hwnd, NULL, FALSE);//再描画の実行
	}
}

 

void MainWindow::CalculateLayout()
{
	if (pRenderTarget != NULL)
	{
		D2D1_SIZE_F size = pRenderTarget->GetSize();//レンダーターゲットのGetSizeメソッドはレンダーターゲットのサイズをDPI単位で返す。(GetPixelSizeだとピクセル数で返す)
		const float x = size.width / 2;
		const float y = size.height / 2;
		const float radius = min(x, y);
		ellipse = D2D1::Ellipse(D2D1::Point2F(x, y), radius, radius);
	}
}

 

つぎ、Direct2Dで色指定。

Direct2DはRGBカラーモデルを使用している。

赤、緑、青、アルファ(透明度)

これらを0.0~1.0までの浮動小数点値であらわす。

 

アルファは0.0で完全透明 1.0で完全不透明

0 0 0
1 0 0
0 1 0
0 0 1
0 1 1 シアン
1 0 1 マゼンタ
1 1 0
1 1 1

 色を表現する構造体D2D1_COLOR_F

 

    // マゼンタ色を初期化する

    D2D1_COLOR_F clr;
    clr.r = 1;
    clr.g = 0;
    clr.b = 1;
    clr.a = 1;  // 不透明


D2D1_COLOR_F 構造体から派生した、D2D1::ColorF クラスを使って色を指定することも可能です。

 
 
    // 前の例に同じ

    D2D1::ColorF clr(1, 0, 1, 1);

 

アルファブレンドは前景色と背景色をうまいこと混ぜる。

 色 = αf Cf + (1–αf) Cb

Cf == 前景色

Cb == 背景色

af == 前景色のアルファ値

 

 

 さっきからずっとぼんやり。

 

座標変換を適用する。

pRenderTarget->BeginDraw();//メソッドが描画の開始を通知
 
		pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::SkyBlue));//レンダーターゲット全体を単色で塗りつぶす
		pRenderTarget->FillEllipse(ellipse, pBrush);//指定されたブラシを使って塗りつぶした楕円を描画する ellipseにはCalculateLayoutメンバ関数でデータが格納されている
 
		hr = pRenderTarget->EndDraw();//描画の終了を通知 BeginDrawからここに来るまでのメソッドは戻り値がvoid* EndDrawの戻り値でまとめてHRESULTが返される
		

 

 レンダーターゲットのFillEllipseメソッドでX軸Y軸に沿って楕円を描画した。

 

ほかの形、

傾斜した楕円を示す図

傾斜した楕円を示す図

 こんなばあいの方法。

座標変換を行うと

  • 点を中心とした回転
  • スケール(拡大縮小)
  • 平行移動 (X 軸または Y 軸方向への変位)
  • 傾斜 (または "傾き" とも呼ばれる)

 こんな風に出来る。

 

 

座標変換とは一連の点を新たな一連の点とマッピングする数学演算。????

点を中心とした回転を示す図

点を中心とした回転を示す図

 それぞれの点が新たな点にマッピングされた。

 

座標変換は行列を使用して実装される。

Direct2Dで座標変換を適用するには

ID2D1RenderTarget::SetTransFormメソッドを呼び出す。

 

このメソッドでD2D1_MATRIX_3X2_F構造体を使用して定義する。

D2D1::Matrix3x2Fクラスで??各メソッドを呼び出すとこの構造体を初期化できる。??

 

このクラスは座標変換に対応する行列を返す以下のメソッドをもつ、

 

    pRenderTarget->SetTransform(
        D2D1::Matrix3x2F::Rotation(20, D2D1::Point2F(100,100))); 

 100、100の点を中心に20度の回転

 

この座標変換は後続の描画にも適用される。

    pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity()); 

 解除はこれ。

 

~~~~~~~~~~~~~~

 

あぁーだめだ。わけわからなくなった。

勉強しなおそう。

ここまで。