画面分割をDirectX9で実装する方法

unity_chan_logo

概要

最終更新日:2020/02/22

DirectX9で画面分割する方法を書いた記事です。
この記事は以下の内容を知りたい方に向けて書いています。
  • 画面分割ってなに
  • DirectX9で画面分割をしたい
  • 2Dで画面分割をしたい
  • 3Dで画面分割をしたい
  • 分割した画面をまたいでオブジェクトを描画したい

サンプル

サンプルはここからダウンロードできます。
環境については以下の内容となっています。

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9
説明 2Dと3Dでプロジェクトを分けており、どちらも4分割するように作成しています。


画面分割とは

画面分割とは一つのウィンドウを分割することで、複数の内容を描画する手法です。
この方法を使うと、複数のプレイヤーが同時にプレイをする場合に、
一つのウィンドウで各プレイヤー専用の画面を用意することができます。
ステージが画面に収まらないゲームなどでよく使用されます。
今回はこの画面分割をDirectX9を使用して実装します。
DirectX9による画面分割のポイントとなるのはViewportです。
Viewportは描画を行うことができる範囲を設定する機能で、
分割無しのViewportの扱いは、初期化時に設定した後は
そのまま何もしないことが多いと思いますが、
画面分割では余計な描画を行わないように頻繁に更新を行います。

2D版画面分割

2Dの画面分割は以下の手順で行います。
  1. ビューポートの設定をする
  2. バッファをクリアする
  3. 描画する
  4. 分割する画面数だけ①~③を繰り返す

ビューポートの設定をする

ビューポートの設定には各画面の左上座標(原点)と描画範囲のサイズが必要です。
この二つの情報の割り出しはウィンドウサイズと分割数、
分割画面の配置の仕様で決まります。
例えば以下の内容で情報の割り出しを行ったとします。
  • ウィンドウサイズ:(w, h) = (640, 480)
  • 分割数:4
  • 配置:画面を中心に縦と横で4等分する
この内容で割り出しを行った結果の各画面の内容は以下のようになります。

directx_0128

バッファをクリアする

ビューポートの設定が完了したらバッファのクリアをします。
必ずビューポート設定の後にクリアしてください。
そうしないと正常に描画されないことがあります。

描画する

バッファのクリアが完了したらオブジェクトの描画を行いますが、
2Dオブジェクトの画面分割で気を付けて欲しいの点があります。
それは、描画開始位置がウィンドウの左上ではないということです。
ビューポートの設定の際に割り出した各画面の原点にずらす必要があります。
例えば下の絵のオブジェクトは全て(x, y)=(100, 50)の位置に描画していますが
実際に(100, 50)をそのまま描画座標として使用しているのは①だけで、
②~④は描画の際に各画面の原点の位置から(100, 50)になるように変換されています。

directx_0129

変換で行っている計算は以下の内容です。
    オブジェクト座標 + 各画面の原点 => 変換後の座標
この変換は分割画面につき一度だけ行うのではなく、オブジェクト単位で行います。

画面数だけ①~③を繰り返す

見出しの通り、分割をする画面の数だけ①~③を繰り返します。
その為、最適を実装しない画面分割は単純に描画処理の回数が数倍になります。

2D版画面分割実装

画面分割実装方法の説明をしていきます。
説明の順番は「2D版画面分割」と同じ流れで行います。

ビューポートの設定をする

まず、ビューポート設定は「D3DVIEWPORT9」と「SetViewport」で行います。

// 各画面のビューポート設定
bool SetUpViewport(int screen_id)
{
	// スクリーン番号の更新
	g_CurrentScreenId = screen_id;

	// ビューポートパラメータ
	D3DVIEWPORT9 view_port;
	Size screen_size = GetSplitScreenSize();
	Vec2 offset = GetScreenOriginPos(screen_id);
	// ビューポートの左上座標
	view_port.X = offset.X;
	view_port.Y = offset.Y;
	
	// ビューポートの幅
	view_port.Width = screen_size.Width;
	// ビューポートの高さ
	view_port.Height = screen_size.Height;
	// ビューポート深度設定
	view_port.MinZ = 0.0f;
	view_port.MaxZ = 1.0f;

	// ビューポート設定
	if (FAILED(g_D3DDevice->SetViewport(&view_port)))
	{
		return false;
	}

	return true;
}

D3DVIEWPORT9に各画面の原点となる座標を、
WidthとHeightに分割画面のサイズを指定します。
原点座標の取得はオブジェクトの描画にも使用するので、
関数化や、グローバル変数にするなどして、
複数の場所で使いやすいようにした方がやりやすくなります。

// 分割画面の原点取得関数
Vec2 GetScreenOriginPos(int screen_id)
{
	Size window_size = GetWindowSize();
	Vec2 origin_pos[] = {
		Vec2(0.0f, 0.0f),
		Vec2(window_size.Width / 2.0f, 0.0f),
		Vec2(0.0f, window_size.Height / 2.0f),
		Vec2(window_size.Width / 2.0f, window_size.Height / 2.0f),
	};

	return origin_pos[screen_id];
}

バッファをクリアする

バッファのクリアはLPDIRECT3DDEVICE9の「Clear」を使用します。
Clearは通常時の設定と同じ内容で問題ありません。

// バッファクリア
g_D3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 0.0f, 0);

描画する

描画は2Dの描画関数全てに分割画面の原点に変換する計算処理を追加してください。

// 分割画面の原点に変換
Vec2 offset = GetScreenOriginPos(g_CurrentScreenId);
for (int i = 0; i < 4; i++)
{
	v[i].X += offset.X;
	v[i].Y += offset.Y;
}

この処理を実装するためには「g_CurrentScreenId」のように
現在描画している分割画面の情報が必要です。

分割数分①~③を繰り返す

繰り返しはBeginScene~EndSceneまでの間で①~③を行います。

// 描画開始
if (DrawStart() == true)
{
	// 分割画面の分だけ描画
	for (int i = 0; i < MaxScreenSplitNum; i++)
	{
		// ビューポート設定
		SetUpViewport(i);

		// バッファクリア
		ClearBuffer();
		
		DrawBg();

		DrawNpc();

		DrawPlayer();

	}

	// 描画終了
	DrawEnd();
}

これで、2Dの画面分割は終了です。

directx_0131

3D版画面分割

3Dの画面分割は以下の手順で行います。
  1. ビューポートの設定をする
  2. バッファをクリアする
  3. ビューマトリクスの設定をする
  4. 描画する
  5. 分割する画面数だけ①~③を繰り返す
①、⑤は2Dの内容と同じ説明になることから、
④は2Dは違い通常の描画を行っても問題ないので省略します。

バッファをクリアする

3Dもビューポートの設定終了後にバッファのクリアをします。
こちらもビューポート設定の後にクリアしてください。
そうしないと正常に描画されないことがあります。
2Dとは違い、Zバッファを使用していると思いますので、
それらのバッファのクリアを忘れないようにしてください。

ビューマトリクスの設定をする

3Dは2Dの各画面の原点指定の代わりにビューマトリクスの設定で
各画面の描画位置指定を行います。
設定も難しことは全くなくて、各画面ごとにカメラ座標と注視点を設定して、
「D3DXMatrixLookAtLH」を行い、「SetTransform」で設定するだけです。

3D版画面分割実装

実装についてもビューマトリクスの設定以外は2Dの項目で説明が終わっていたり、
必要が無かったりするので、省略します。
ビューマトリクスの設定は、分割カメラの数だけカメラ座標と注視点が必要です。

// 3D用カメラ
struct Camera3D
{
	Vec3 Position;		// 座標
	Vec3 LookPosition;	// 注視点
};

Camera3D camera[] =
{
	{ Vec3(0.0f, 0.0f, -300.0f), Vec3(0.0f, 0.0f, 0.0f) },
	{ Vec3(0.0f, 0.0f, 300.0f), Vec3(0.0f, 0.0f, 0.0f) },
	{ Vec3(300.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, 0.0f) },
	{ Vec3(-300.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, 0.0f) },
};

このカメラ情報をビューポート設定関数に引数等で渡せるようにして、
各画面の描画時に対応したカメラ情報を渡すようにします。

// ビュー行列の設定
SetUpViewMatrix(camera[i].Position, camera[i].LookPosition);

ビュー行列設定は特に変更点はありません。
「D3DXMatrixLookAtLH」を使って行列を作成し、「SetTransform」で設定します。

// ビュー行列設定
void SetUpViewMatrix(Vec3 camera_pos, Vec3 look_pos)
{
	D3DXMATRIXA16 matView;

	D3DXVECTOR3 cam = D3DXVECTOR3(camera_pos.X, camera_pos.Y, camera_pos.Z);
	D3DXVECTOR3 look = D3DXVECTOR3(look_pos.X, look_pos.Y, look_pos.Z);
	D3DXVECTOR3 up_vector(0.0f, 1.0f, 0.0f);	// カメラの向き
	D3DXMatrixLookAtLH(&matView,
		&cam,					// カメラ座標
		&look,					// 注視点座標
		&up_vector);				// カメラの上の向きのベクトル

	g_D3DDevice->SetTransform(D3DTS_VIEW, &matView);
}

これで3D分割画面の実装は終了です。

directx_0130

画面をまたいでオブジェクトを描画する方法

分割画面をまたいでオブジェクトを描画するには以下のポイントを
抑えて描画を行ってください。
  1. 分割画面の描画の後で描画する
  2. バッファのクリアはしない/li>
  3. ビューポートの範囲は画面全体に行う
これらのポイントを抑えて描画を行えば画面をまたいだオブジェクトの描画ができます。

directx_0132

ビューポートの範囲は画面全体に行う

画面をまたいで描画するオブジェクトのサイズがどのサイズでも対応できるように
ビューポートの設定は画面全体を範囲とします。

// スクリーン全体の場合はウィンドウサイズを使用する
Size screen_size = GetSplitScreenSize();

if (g_CurrentScreenId == FullScreenOriginPosNum)
{
	screen_size = GetWindowSize();
}

// ビューポートの幅
viewport.Width = screen_size.Width;
// ビューポートの高さ
viewport.Height = screen_size.Height;

分割画面の描画の後で描画する

分割画面の内容が全て描画されていることが理想です。
その為、分割画面の描画を全て終えた後に描画を行います。

// 分割画面の描画

// またいで描画するオブジェクトの描画


バッファのクリアはしない

画面全体をビューポートの設定としており、なおかつ分割画面の後で描画をするので、
バッファのクリアは行いません。
もし、クリアしてしまったら、そのフレーム内の描画内容が
全てクリアされてしまうので注意してください。