DirectX11--實現一個3D魔方(3)

前言

(2019/1/9 09:23)上一章我們主要講述了魔方的旋轉,這個旋轉真是有毒啊,搞完這個部分搭鍵鼠操作不到半天應該就可以搭完了吧…

(2019/1/9 21:25)啊,真香


有人發這張圖片問我寫魔方的目的是不是這個。。。噗

現在光是鍵鼠相關的代碼也搭了400行左右。。其中鍵盤相關的調用真的是毫無技術可言,重點實現基本上都被鼠標給耽擱了。

本章將魔方應用層的剩餘實現補全。

章節
實現一個3D魔方(1)
實現一個3D魔方(2)
實現一個3D魔方(3)

Github項目–魔方

最後日常安利一波本人正在編寫的DX11教程。

DirectX11 With Windows SDK完整目錄

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

我就簡單提一下鍵盤的邏輯

鍵盤操作使用的是DXTK經過修改的Keyboard庫。

因爲之前說過,Rubik::RotateX函數在響應了來自鍵盤的輸入後,就會進入自動旋轉模式,此時的鍵盤輸入將不會響應。但後續還需要考慮做棧操作記錄,如果此時魔方正在旋轉,還是要提前結束這個函數:

void GameApp::KeyInput()
{
	Keyboard::State keyState = mKeyboard->GetState();
	mKeyboardTracker.Update(keyState);

	//
	// 整個魔方旋轉
	//

	// 此時正在旋轉的話則提前結束
	if (mRubik.IsLocked())
		return;

	// 公式x
	if (mKeyboardTracker.IsKeyPressed(Keyboard::Up))
	{
		mRubik.RotateX(3, XM_PIDIV2);
		return;
	}
	
	// ...

	//
	// 雙層旋轉
	//

	// 公式r
	if (keyState.IsKeyDown(Keyboard::LeftControl) && mKeyboardTracker.IsKeyPressed(Keyboard::I))
	{
		mRubik.RotateX(-2, XM_PIDIV2);
		return;
	}
	
	// ...


	//
	// 單層旋轉
	//

	// 公式R
	if (mKeyboardTracker.IsKeyPressed(Keyboard::I))
	{
		mRubik.RotateX(2, XM_PIDIV2);
		return;
	}

	// ...
}

我列個表格來描述鍵盤的36種操作,就當做說明書來看吧:

鍵位 對應公式 描述
Up x 整個魔方按x軸順時針旋轉
Down x’ 整個魔方按x軸逆時針旋轉
Left y 整個魔方按y軸順時針旋轉
Right y’ 整個魔方按y軸逆時針旋轉
Pg Up z’ 整個魔方按z軸逆時針旋轉
Pg Down z 整個魔方按z軸順時針旋轉
-------- ---- ------------------------
LCtrl+I r 右面兩層按x軸順時針旋轉
LCtrl+K r’ 右面兩層按x軸逆時針旋轉
LCtrl+J u 頂面兩層按y軸順時針旋轉
LCtrl+L u’ 頂面兩層按y軸逆時針旋轉
LCtrl+U f’ 正面兩層按z軸逆時針旋轉
LCtrl+O f 正面兩層按z軸順時針旋轉
-------- ---- ------------------------
LCtrl+W l’ 左面兩層按x軸逆時針旋轉
LCtrl+S l 左面兩層按x軸順時針旋轉
LCtrl+A d’ 底面兩層按y軸逆時針旋轉
LCtrl+D d 底面兩層按y軸順時針旋轉
LCtrl+Q b 背面兩層按z軸順時針旋轉
LCtrl+E b’ 背面兩層按z軸逆時針旋轉
-------- ---- ------------------------
I R 右面兩層按x軸順時針旋轉
K R’ 右面兩層按x軸逆時針旋轉
J U 頂面兩層按y軸順時針旋轉
L U’ 頂面兩層按y軸逆時針旋轉
U F’ 正面兩層按z軸逆時針旋轉
O F 正面兩層按z軸順時針旋轉
-------- ---- ------------------------
T M 右面兩層按x軸順時針旋轉
G M’ 右面兩層按x軸逆時針旋轉
F E 頂面兩層按y軸順時針旋轉
H E’ 頂面兩層按y軸逆時針旋轉
R S’ 正面兩層按z軸逆時針旋轉
Y S 正面兩層按z軸順時針旋轉
-------- ---- ------------------------
W L’ 右面兩層按x軸順時針旋轉
S L 右面兩層按x軸逆時針旋轉
A D’ 頂面兩層按y軸順時針旋轉
D D 頂面兩層按y軸逆時針旋轉
Q B 正面兩層按z軸逆時針旋轉
E B’ 正面兩層按z軸順時針旋轉

鼠標邏輯相關的實現

鼠標操作用的是DXTK經過修改的Mouse

鼠標相關的實現難度遠比鍵盤複雜多了,我主要分三個部分來講:

  1. 立方體的拾取與判斷拾取到的立方體表面
  2. 根據拖動方向判斷旋轉軸
  3. 鼠標在不同的操作階段對應的處理

在此之前,我先講講在這個項目加的一點點私貨

鼠標的輕微抖動效果

首先來看效果

這個效果的實現比較簡單,現在我使用的是第三人稱攝像機。現規定以遊戲窗口中心爲0偏移點,那麼偏離中心做左右移動會產生繞中心以Y軸旋轉,而做上下移動產生繞中心以X軸旋轉。

相關代碼的實現如下:

void GameApp::MouseInput(float dt)
{
	Mouse::State mouseState = mMouse->GetState();
	// ...

	// 獲取子類
	auto cam3rd = dynamic_cast<ThirdPersonCamera*>(mCamera.get());

	// ******************
	// 第三人稱攝像機的操作
	//

	// 繞物體旋轉,添加輕微抖動
	cam3rd->SetRotationX(XM_PIDIV2 * 0.6f + (mouseState.y - mClientHeight / 2) *  0.0001f);
	cam3rd->SetRotationY(-XM_PIDIV4 + (mouseState.x - mClientWidth / 2) * 0.0001f);
	cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);

	// 更新觀察矩陣
	mCamera->UpdateViewMatrix();
	mBasicEffect.SetViewMatrix(mCamera->GetViewXM());

	// 重置滾輪值
	mMouse->ResetScrollWheelValue();
	
	// ...
}

立方體的拾取與判斷拾取到的立方體表面

現在要先判斷鼠標點擊拾取到哪個立方體,考慮到我們能拾取到的立方體都是可以看到的,這也說明它們的深度值肯定是最小的。因此,我們的Rubik::HitCube函數實現如下:

DirectX::XMINT3 Rubik::HitCube(Ray ray, float * pDist) const
{
	BoundingOrientedBox box(XMFLOAT3(), XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f));
	BoundingOrientedBox transformedBox;
	XMINT3 res = XMINT3(-1, -1, -1);
	float dist, minDist = FLT_MAX;

	// 優先拾取暴露在外的立方體(同時也是距離攝像機最近的)
	for (int i = 0; i < 3; ++i)
	{
		for (int j = 0; j < 3; ++j)
		{
			for (int k = 0; k < 3; ++k)
			{
				box.Transform(transformedBox, mCubes[i][j][k].GetWorldMatrix());
				if (ray.Hit(transformedBox, &dist) && dist < minDist)
				{
					minDist = dist;
					res = XMINT3(i, j, k);
				}
			}
		}
	}
	if (pDist)
		*pDist = (minDist == FLT_MAX ? 0.0f : minDist);
		
	return res;
}

上面的函數會遍歷所有的立方體,找出深度最小且拾取到的立方體的索引值,通過pDist可以返回射線起始點到目標立方體表面的最小距離。這個信息非常有用,稍後我們會提到。

對了,如果沒有拾取到立方體呢?我們可以利用屏幕空白的地方,在拖動這些地方的時候會帶動整個魔方的旋轉。

根據拖動方向判斷旋轉軸

首先給出魔方旋轉軸的枚舉:

enum RubikRotationAxis {
	RubikRotationAxis_X,	// 繞X軸旋轉
	RubikRotationAxis_Y,	// 繞Y軸旋轉
	RubikRotationAxis_Z,	// 繞Z軸旋轉
};

現在讓我們再看一眼魔方:

界面中可以看到魔方的面有+X面,+Y面和-Z面。

在我們拾取到立方體後,我們還要根據這兩個信息來確定旋轉軸:

  1. 當前具體是拾取到立方體的哪個面
  2. 當前鼠標的拖動方向

這又是一個十分細的問題。其中-X面和-Z面在屏幕上是對稱關係,代碼實現可以做鏡像處理,但是+Y面的操作跟其它兩個面又有一些差別。

鼠標落在立方體的-Z面

現在我們只討論拾取到立方體索引[2][2][0]的情況,鼠標落在了該立方體白色的表面上。我們只是知道鼠標拾取到當前立方體上,那怎麼做才能知道它現在拾取的是其中的-Z面呢?

Rubik::HitCube函數不僅返回了拾取到的立方體索引,還有射線擊中立方體表面的最短距離。我們知道-Z面的所有頂點的z值在不產生旋轉的情況下都會爲-3,因此我們只需要將得到的 tt 值帶入射線方程 p=e+td\mathbf{p}=\mathbf{e}+t\mathbf{d} 中,判斷求得的 p\mathbf{p} 其中的z分量是否爲3,如果是,那說明當前鼠標拾取的是該立方體的-Z面。

接下來就是要討論用鼠標拖動魔方會產生怎麼樣的旋轉問題了。我們還需要確定當前的拖動會讓哪一層魔方旋轉(或者說繞什麼軸旋轉)。以下圖爲例:

上圖的X軸和Y軸對應的是屏幕座標系,座標軸的原點爲我鼠標剛點擊時的落點,通過兩條虛線,可以將鼠標的拖動方向劃分爲四個部分,對應魔方旋轉的四種情況。其中屏幕座標系的主+X(-X)拖動方向會使得魔方的+Y面做逆(順)時針旋轉,而屏幕座標系的主+Y(-Y)拖動方向會使得魔方的+X面做逆(順)時針旋轉。

我們可以將這些情況進行簡單歸類,即當X方向的瞬時位移量比Y方向的大時,魔方的+Y面就會繞Y軸進行旋轉,反之則是魔方的+X面繞X軸進行旋轉。

現在新增了用於記錄魔方操作的RubikRotationRecord類:

struct RubikRotationRecord
{
	RubikRotationAxis axis;	// 當前旋轉軸
	int pos;				// 當前旋轉層的索引
	float dTheta;			// 當前旋轉的弧度
};

這裏先把GameApp中所有與鼠標操作相關的新增成員先列出來,後面我就不再重複:

//
// 鼠標操作控制
//
	
int mClickPosX, mClickPosY;					// 初次點擊時鼠標位置
float mSlideDelay;							// 拖動延遲響應時間 
float mCurrDelay;							// 當前延遲時間
bool mDirectionLocked;						// 方向鎖

RubikRotationRecord mCurrRotationRecord;	// 當前旋轉記錄

核心判斷方法如下:

// 判斷當前主要是垂直操作還是水平操作
bool isVertical = abs(dx) < abs(dy);
// 當前鼠標操縱的是-Z面,根據操作類型決定旋轉軸
if (pos.z == 0 && fabs((ray.origin.z + dist * ray.direction.z) - (-3.0f)) < 1e-5f)
{
	mCurrRotationRecord.pos = isVertical ? pos.x : pos.y;
	mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_X : RubikRotationAxis_Y;
}

pos爲鼠標拾取到的立方體索引。

鼠標落在立方體的+X面

現在我們拾取到了索引爲[2][2][0]立方體的+X面,該表面所有頂點的x值在不旋轉的情況下爲3。當鼠標拖動時的X偏移量比Y的大時,會使得魔方的+Y面繞Y軸做旋轉,反之則使得魔方的-X面繞X軸做旋轉。

這部分的判斷如下:

// 當前鼠標操縱的是+X面,根據操作類型決定旋轉軸
if (pos.x == 2 && fabs((ray.origin.x + dist * ray.direction.x) - 3.0f) < 1e-5f)
{
	mCurrRotationRecord.pos = isVertical ? pos.z : pos.y;
	mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_Z : RubikRotationAxis_Y;
}

鼠標落在立方體的+Y面

之前+X面和-Z面在屏幕中是對稱的,處理過程基本上差不多。但是處理+Y面的情況又不一樣了,先看下圖:

現在的虛線按垂直和水平方向劃分成四個拖動區域。當鼠標在屏幕座標系拖動時,如果X的瞬時偏移量和Y的符號是一致的(劃分虛線的右下區域和左上區域), 魔方的-Z面會繞Z軸旋轉;如果異號(劃分虛線的左下區域和右上區域),魔方的+X面會繞X軸旋轉。

然後就是魔方+Y面的頂點在不產生旋轉的情況下y值恆爲3,因此這部分的判斷邏輯如下:

// 當前鼠標操縱的是+Y面,要判斷平移變化量dx和dy的符號來決定旋轉方向
if (pos.y == 2 && fabs((ray.origin.y + dist * ray.direction.y) - 3.0f) < 1e-5f)
{
	// 判斷異號
	bool diffSign = ((dx & 0x80000000) != (dy & 0x80000000));
	mCurrRotationRecord.pos = diffSign ? pos.x : pos.z;
	mCurrRotationRecord.axis = diffSign ? RubikRotationAxis_X : RubikRotationAxis_Z;
}

鼠標沒有拾取到魔方

前面我們一直都是在討論鼠標拾取到魔方的立方體產生了單層旋轉的情況。現在我們還想讓整個魔方進行旋轉,可以依靠拖動遊戲界面的空白區域來實現,按下圖的方式劃分成兩片區域:

只要在魔方區域外拖動,且水平偏移量比垂直的大,就會產生繞Y軸的旋轉。在窗口左(右)半部分產生了主垂直拖動則會繞X(Z)軸旋轉。

整個拾取部分的判斷如下:

// 找到當前鼠標點擊的方塊索引
Ray ray = Ray::ScreenToRay(*mCamera, (float)mouseState.x, (float)mouseState.y);
float dist;
XMINT3 pos = mRubik.HitCube(ray, &dist);

// 判斷當前主要是垂直操作還是水平操作
bool isVertical = abs(dx) < abs(dy);
// 當前鼠標操縱的是-Z面,根據操作類型決定旋轉軸
if (pos.z == 0 && fabs((ray.origin.z + dist * ray.direction.z) - (-3.0f)) < 1e-5f)
{
	mCurrRotationRecord.pos = isVertical ? pos.x : pos.y;
	mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_X : RubikRotationAxis_Y;
}
// 當前鼠標操縱的是+X面,根據操作類型決定旋轉軸
else if (pos.x == 2 && fabs((ray.origin.x + dist * ray.direction.x) - 3.0f) < 1e-5f)
{
	mCurrRotationRecord.pos = isVertical ? pos.z : pos.y;
	mCurrRotationRecord.axis = isVertical ? RubikRotationAxis_Z : RubikRotationAxis_Y;
}
// 當前鼠標操縱的是+Y面,要判斷平移變化量dx和dy的符號來決定旋轉方向
else if (pos.y == 2 && fabs((ray.origin.y + dist * ray.direction.y) - 3.0f) < 1e-5f)
{
	// 判斷異號
	bool diffSign = ((dx & 0x80000000) != (dy & 0x80000000));
	mCurrRotationRecord.pos = diffSign ? pos.x : pos.z;
	mCurrRotationRecord.axis = diffSign ? RubikRotationAxis_X : RubikRotationAxis_Z;
}
// 當前鼠標操縱的是空白地區,則對整個魔方旋轉
else
{
	mCurrRotationRecord.pos = 3;
	// 水平操作是Y軸旋轉
	if (!isVertical)
	{
		mCurrRotationRecord.axis = RubikRotationAxis_Y;
	}
	// 屏幕左半部分的垂直操作是X軸旋轉
	else if (mouseState.x < mClientWidth / 2)
	{
		mCurrRotationRecord.axis = RubikRotationAxis_X;
	}
	// 屏幕右半部分的垂直操作是Z軸旋轉
	else
	{
		mCurrRotationRecord.axis = RubikRotationAxis_Z;
	}
}	

鼠標在不同的操作階段對應的處理

鼠標拖動魔方旋轉可以分爲三個階段:鼠標初次點擊、鼠標產生拖動、鼠標剛釋放。

確定拖動方向

在鼠標初次點擊的時候不一定會產生偏移量,但我們必須要在這個時候判斷鼠標是在做垂直拖動還是豎直拖動來確定當前的旋轉軸,以限制魔方的旋轉。

現在要考慮這樣一個情況,我鼠標在初次點擊魔方時可能會因爲手抖或者鼠標不穩產生了一個以下方向爲主的瞬時移動,然後程序判斷我現在在做向下的拖動,但實際情況卻是我需要向右方向拖動鼠標,程序卻只允許我上下拖動。這就十分尷尬了。

由於鼠標的拖動過程相對程序的運行會比較緩慢,我們可以給程序加上一個延遲判斷。比如說我現在可以根據鼠標初次點擊後的0.05s內產生的累計垂直/水平偏移量來判斷此時是水平拖動還是豎直拖動。

此外,一旦確定這段時間內產生了偏移值,必須要加上方向鎖,防止後續又重新判斷旋轉方向。

這部分代碼實現如下:

// 此時未確定旋轉方向
if (!mDirectionLocked)
{
	// 此時未記錄點擊位置
	if (mClickPosX == -1 && mClickPosY == -1)
	{
		// 初次點擊
		if (mMouseTracker.leftButton == Mouse::ButtonStateTracker::PRESSED)
		{
			// 記錄點擊位置
			mClickPosX = mouseState.x;
			mClickPosY = mouseState.y;
		}
	}
			
	// 僅當記錄了點擊位置才進行更新
	if (mClickPosX != -1 && mClickPosY != -1)
		mCurrDelay += dt;
	// 未到達滑動延遲時間則結束
	if (mCurrDelay < mSlideDelay)
		return;

	// 未產生運動則不上鎖
	if (abs(dx) == abs(dy))
		return;

	// 開始上方向鎖
	mDirectionLocked = true;
	// 更新累積的位移變化量
	dx = mouseState.x - mClickPosX;
	dy = mouseState.y - mClickPosY;

	// 找到當前鼠標點擊的方塊索引
	Ray ray = Ray::ScreenToRay(*mCamera, (float)mouseState.x, (float)mouseState.y);
	// ...剩餘部分就是上面的代碼
}

拖動時更新魔方狀態

這部分實現就比較簡單了。只要鼠標左鍵按下,且確認方向鎖,就可以進行魔方的旋轉。

如果是繞X軸的旋轉,鼠標向右移動和向上移動都會產生順時針旋轉。
如果是繞Y軸的旋轉,只有鼠標向左移動纔會產生順時針旋轉。
如果是繞Z軸的旋轉,鼠標向左移動和向上移動都會產生順時針旋轉。

這裏的Rotate函數最後一個參數必須要傳遞true以告訴內部不要進行預旋轉操作。

// 上了方向鎖才能進行旋轉
if (mDirectionLocked)
{
	// 進行旋轉
	switch (mCurrRotationRecord.axis)
	{
	case RubikRotationAxis_X: mRubik.RotateX(mCurrRotationRecord.pos, (dx - dy) * 0.008f, true); break;
	case RubikRotationAxis_Y: mRubik.RotateY(mCurrRotationRecord.pos, -dx * 0.008f, true); break;
	case RubikRotationAxis_Z: mRubik.RotateZ(mCurrRotationRecord.pos, (-dx - dy) * 0.008f, true); break;
	}
}

拖動完成後的操作

完成拖動後,需要恢復方向鎖和滑動延遲,並且鼠標剛釋放時產生的偏移我們直接丟掉。現在Rotate函數僅用於發送進行預旋轉的命令:

// 鼠標左鍵是否點擊
if (mouseState.leftButton)
{
	// ...
}
// 鼠標剛釋放
else if (mMouseTracker.leftButton == Mouse::ButtonStateTracker::RELEASED)
{
	// 釋放方向鎖
	mDirectionLocked = false;
	// 滑動延遲歸零
	mCurrDelay = 0.0f;
	// 座標移出屏幕
	mClickPosX = mClickPosY = -1;
	// 發送完成指令,進行預旋轉
	switch (mCurrRotationAxis)
	{
	case RubikRotationAxis_X: mRubik.RotateX(mCurrRotationRecord.pos, 0.0f); break;
	case RubikRotationAxis_Y: mRubik.RotateY(mCurrRotationRecord.pos, 0.0f); break;
	case RubikRotationAxis_Z: mRubik.RotateZ(mCurrRotationRecord.pos, 0.0f); break;
	}
}

最終鼠標拖動的效果如下:

鍵盤的效果如下:

撤銷操作的支持

回顧一下RubikRotationRecord類的定義:

struct RubikRotationRecord
{
	RubikRotationAxis axis;	// 當前旋轉軸
	int pos;				// 當前旋轉層的索引
	float dTheta;			// 當前旋轉的弧度
};

pos爲0-2時,均爲單層魔方的旋轉,-1和-2爲雙層魔方的旋轉,3則爲整個魔方的旋轉。

我們使用一個棧來記錄用戶的操作,它放在了GameApp類中:

std::stack<RubikRotationRecord> mRotationRecordStack;

對於鍵盤操作來說特別簡單,只需要在每次操作後記錄即可:

// 公式x
if (mKeyboardTracker.IsKeyPressed(Keyboard::Up))
{
	mRubik.RotateX(3, XM_PIDIV2);
	// 此處新增
	mRotationRecordStack.push(RubikRotationRecord{ RubikRotationAxis_X, 3, XM_PIDIV2 });
	return;
}

而鼠標操作是一個連續的過程,並且記錄要點如下:

  1. 鼠標正在拖動的過程不記錄,只記錄釋放鼠標的瞬間
  2. 對於鼠標旋轉角度(-pi/2 + 2kpi, pi/2 + 2kpi)的操作都不要記錄,這些操作相當於沒有實質性旋轉
  3. 對於實質性的旋轉最終角度都按pi/2的倍數來記錄

鼠標釋放部分經過修改後:

// 鼠標剛釋放
if (mMouseTracker.leftButton == Mouse::ButtonStateTracker::RELEASED)
{
	// 釋放方向鎖
	mDirectionLocked = false;
	// 滑動延遲歸零
	mCurrDelay = 0.0f;
	// 座標移出屏幕
	mClickPosX = mClickPosY = -1;

	// 發送完成指令,進行預旋轉
	switch (mCurrRotationRecord.axis)
	{
	case RubikRotationAxis_X: mRubik.RotateX(mCurrRotationRecord.pos, 0.0f); break;
	case RubikRotationAxis_Y: mRubik.RotateY(mCurrRotationRecord.pos, 0.0f); break;
	case RubikRotationAxis_Z: mRubik.RotateZ(mCurrRotationRecord.pos, 0.0f); break;
	}

	// 此處新增
	// 若這次旋轉有意義,記錄到棧中
	int times = static_cast<int>(round(mCurrRotationRecord.dTheta / XM_PIDIV2)) % 4;
	if (times != 0)
	{
		mCurrRotationRecord.dTheta = times * XM_PIDIV2;
		mRotationRecordStack.push(mCurrRotationRecord);
	}
	// 旋轉值歸零
	mCurrRotationRecord.dTheta = 0.0f;
}

開局打亂魔方

上面的那個棧不僅可以用來記錄用戶操作記錄,還可以用來存儲打亂魔方的操作。即遊戲剛開始先給這個棧塞入一堆隨機操作,然後每執行一個操作就退棧一次,直到棧空時打亂操作完成,用戶可以開始對魔方進行操作,同時這個棧也開始記錄用戶操作。

GameApp::Shuffle的操作如下:

void GameApp::Shuffle()
{
	// 清棧
	while (!mRotationRecordStack.empty())
		mRotationRecordStack.pop();
	// 往棧上塞30個隨機旋轉操作用於打亂
	RubikRotationRecord record;
	srand(static_cast<unsigned>(time(nullptr)));
	for (int i = 0; i < 30; ++i)
	{
		record.axis = static_cast<RubikRotationAxis>(rand() % 3);
		record.pos = rand() % 4;
		record.dTheta = XM_PIDIV2 * (rand() % 2 ? 1 : -1);
		mRotationRecordStack.push(record);
	}
}

添加開場攝像機旋轉動畫

這是一個簡單的攝像機移動過程,包含的繞Y軸的旋轉和鏡頭的推進。這個動畫過程需要根據幀時間間隔做更新。整體動畫時間爲5s,在沒有結束前GameApp::PlayCameraAnimation會返回false,完成動畫後則返回true

bool GameApp::PlayCameraAnimation(float dt)
{
	// 獲取子類
	auto cam3rd = dynamic_cast<ThirdPersonCamera*>(mCamera.get());

	// ******************
	// 第三人稱攝像機的操作
	//
	mAnimationTime += dt;
	float theta, dist;

	theta = -XM_PIDIV2 + XM_PIDIV4 * mAnimationTime * 0.2f;
	dist = 20.0f - mAnimationTime * 2.0f;
	if (theta > -XM_PIDIV4)
		theta = -XM_PIDIV4;
	if (dist < 10.0f)
		dist = 10.0f;

	cam3rd->SetRotationY(theta);
	cam3rd->SetDistance(dist);

	// 更新觀察矩陣
	mCamera->UpdateViewMatrix();
	mBasicEffect.SetViewMatrix(mCamera->GetViewXM());

	if (fabs(theta + XM_PIDIV4) < 1e-5f && fabs(dist - 10.0f) < 1e-5f)
		return true;
	return false;
}

注意GameApp::PlayCameraAnimation絕對不能同GameApp::MouseInput或者GameApp::KeyInput共存!

開場動畫+打亂效果如下:

至此魔方的應用層就講述到這裏,剩下的邏輯部分實現可以參考源碼,本系列教程到這裏就結束了。該DX11實現的魔方的功能跟DX9比起來有多的地方,也有少的地方,個人感覺沒必要再增加新的東西。畢竟作爲一個遊戲來說,它算是一個合格的作品了。

此外我覺得沒有必要展開大的篇幅再來講底層的實現,我更希望的是你能跟着我的DX11教程把底層好好的過一遍,裏面有些部分的內容是在龍書裏面沒有涉及到的。

Github項目–魔方

DirectX11 With Windows SDK完整目錄

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章