1. 程式人生 > >大型3D地圖優化渲染技術

大型3D地圖優化渲染技術

技術簡介:

如果需要渲染一個大型3D地圖,由於資料量,需要渲染的東西非常多,所以尤其一些慢一點的機器就會變得非常卡。

如下面這些會造成幀率(FPS)下降的圖:

這樣的圖:

還有這樣的魔獸世界的圖:

還有這樣的圖:

上圖我是指她的背景圖啦,O(∩_∩)O~

到底暴雪是如何讓這些場景流暢地渲染的呢?

那就必須想辦法提高渲染速度,也就是幀率(FPS)要提高,才能使得遊戲流暢。

像魔獸這樣大型的遊戲具體是用什麼技術的,那不敢確定,但是會用到的相關技術會有:

1 地圖分塊剔除

2 kd樹

3 BSP樹

4 LOD技術

等等,最後把場景優化到極致,才能創造出偉大的遊戲。每一種技術都需要挺長篇幅介紹的,本文就分析第一種技術:地圖分塊剔除技術(Sub-Grid Culling)

地圖分塊剔除技術分析

1 3D投影剔除(Frustum Culling)

把一個大地圖分隔成由多個小地圖實現,那麼本來需要呼叫一次就畫成的地圖,現在需要多次呼叫,但是我們可以利用Frustum Culling(3D投影剔除-我覺得比較滿意的翻譯名詞吧)技術優化渲染。

Frustum Culling的思路是:

 每一個小塊地圖,我們計算相對應的AABB包圍體;

如果這個AABB和Frustum不相交,那麼就不用畫這個小塊地圖。

Frustum是下面的形狀:

如下圖:大三角形代表Frustum,小方格代表小塊地圖,灰色小方格代表與Frustum相交,需要渲染,但是白色地帶就不與Frustum相交,所以都不需要渲染,那麼我們可以看出這就省了很多渲染內容,也就速度大大提高了。

也可以不使用分格和AABB方法進行Frustum Culling:

把所有的組成地圖的三角形輸入顯示卡,讓顯示卡自動裁剪不在Frustum中的三角形。但是其實這個裁剪髮生在Clipping Stage,就差不多是渲染管道的最後的渲染工作了,那麼這些不用渲染你的三角形就會通過大部分渲染管道,如:Vertex Shader,那麼就浪費了很多計算呢時間。

使用分格和AABB的方法就可以簡單的利用一個AABB相交測試就可以丟棄一個不用渲染的小塊地圖資料了,效率可以更大地提高。

掌握這個分塊的數量也很重要。如下圖:

這樣就是更加精細的分塊了。需要渲染的面積更小,但是相對而言節省的也不多,而精細的分塊也需要花費更多的計算時間,如:畫基本元幾何圖形的函式呼叫次數也更多了,還有AABB相交測試也增多;

所謂過猶不及,要掌握好度很重要。到底分多少個小地圖塊是合適的,就需要實際分析了。

而且這裡的Frustum Culling主要是節省了vertex shading的計算時間,如果一個圖形是主要花費在pixel shanding上的話,比如大量的particles需要渲染,那麼這種Frustum Culling方法就不會有太大的提速效果。

2 小地圖資料結構體:

首先需要定義一個結構體來表示小地圖塊的資料,如下:

struct SubGrid
	{
		ID3DXMesh* mesh;
		AABB box;

		// For sorting.
		bool operator<(const SubGrid& rhs)const;

		const static int NUM_ROWS  = 33;
		const static int NUM_COLS  = 33;
		const static int NUM_TRIS  = (NUM_ROWS-1)*(NUM_COLS-1)*2;
		const static int NUM_VERTS = NUM_ROWS*NUM_COLS;
	};

主要功能就是這個比較操作符,是根據該小地圖塊和Camera的距離來比較大小的。把所有的小地圖塊都按照離Camera的距離由進到遠排序,那麼就方便由進到遠(前到後)的渲染方式渲染。這種方式的渲染可以提高速度,尤其是在渲染管道中pixel shader渲染是瓶頸的時候,當然如果vertex shader是瓶頸,那麼就提高不明顯。

基本原理就是:

1 更新畫素(pixel)的時候,渲染器都進行了depth testing(深度測試),只有在距離鏡頭進的物體才會被渲染,否則就不渲染。

2 如果排序好的物體,前面渲染的物體都是距離近的,那麼後面渲染的物體如果被前面的物體擋住了,那麼就根本不需要渲染了。

3 如果不是排序好的話,前面渲染了距離遠的物體,那麼後面渲染的物體距離鏡頭更加近,就需要重新更新當前畫素,造成重複浪費渲染。

bool Terrain::SubGrid::operator<(const SubGrid& rhs) const
{
      D3DXVECTOR3 d1 = box.center() - gCamera->pos();
      D3DXVECTOR3 d2 = rhs.box.center() - gCamera->pos();
      return D3DXVec3LengthSq(&d1) < D3DXVec3LengthSq(&d2);
}

3 計算出Frustum:

Frustum是從投影矩陣抽出來的。因為Frustum就是由投影矩陣定義的,也可以說兩者都在實際上是一致的:

如投影示意圖:

Frustum:

其實是概念上不一樣,但是實際上一樣的東西。不過應用也不一樣。

下面是從投影矩陣抽出Frustum的程式:

void Camera::buildWorldFrustumPlanes()
{
	// Note: Extract the frustum planes in world space.

	D3DXMATRIX VP = mView * mProj;

	D3DXVECTOR4 col0(VP(0,0), VP(1,0), VP(2,0), VP(3,0));
	D3DXVECTOR4 col1(VP(0,1), VP(1,1), VP(2,1), VP(3,1));
	D3DXVECTOR4 col2(VP(0,2), VP(1,2), VP(2,2), VP(3,2));
	D3DXVECTOR4 col3(VP(0,3), VP(1,3), VP(2,3), VP(3,3));

	// Planes face inward.
	mFrustumPlanes[0] = (D3DXPLANE)(col2);        // near
	mFrustumPlanes[1] = (D3DXPLANE)(col3 - col2); // far
	mFrustumPlanes[2] = (D3DXPLANE)(col3 + col0); // left
	mFrustumPlanes[3] = (D3DXPLANE)(col3 - col0); // right
	mFrustumPlanes[4] = (D3DXPLANE)(col3 - col1); // top
	mFrustumPlanes[5] = (D3DXPLANE)(col3 + col1); // bottom

	for(int i = 0; i < 6; i++)
		D3DXPlaneNormalize(&mFrustumPlanes[i], &mFrustumPlanes[i]);
}

其中的數學原理卻是比較複雜的,需要相當的圖形學基礎。

4 Frustum和AABB包圍體的碰撞檢測

下面是檢測Frustum六個平面與AABB包圍體碰撞的程式碼:

bool Camera::isVisible(const AABB& box)const
{
	// Test assumes frustum planes face inward.

	D3DXVECTOR3 P;
	D3DXVECTOR3 Q;

	//      N  *Q                    *P
	//      | /                     /
	//      |/                     /
	// -----/----- Plane     -----/----- Plane    
	//     /                     / |
	//    /                     /  |
	//   *P                    *Q  N
	//
	// PQ forms diagonal most closely aligned with plane normal.

	// For each frustum plane, find the box diagonal (there are four main
	// diagonals that intersect the box center point) that points in the
	// same direction as the normal along each axis (i.e., the diagonal 
	// that is most aligned with the plane normal).  Then test if the box
	// is in front of the plane or not.
	for(int i = 0; i < 6; ++i)
	{
		// For each coordinate axis x, y, z...
		for(int j = 0; j < 3; ++j)
		{
			// Make PQ point in the same direction as the plane normal on this axis.
			if( mFrustumPlanes[i][j] >= 0.0f )
			{
				P[j] = box.minPt[j];
				Q[j] = box.maxPt[j];
			}
			else 
			{
				P[j] = box.maxPt[j];
				Q[j] = box.minPt[j];
			}
		}

		// If box is in negative half space, it is behind the plane, and thus, completely
		// outside the frustum.  Note that because PQ points roughly in the direction of the 
		// plane normal, we can deduce that if Q is outside then P is also outside--thus we
		// only need to test Q.
		if( D3DXPlaneDotCoord(&mFrustumPlanes[i], &Q) < 0.0f  ) // outside
			return false;
	}
	return true;
}

因為Frustum有六個平面,所以要迴圈檢測六次,只要任何一次AABB包圍體是在平面負面的,那麼就返回false,表示沒有在Frustum內。

這樣檢測可以不用考慮這些平面並不是無限延伸的,而是當作一般平面來檢測,簡化了計算。

因為每個平面四面都有被其他平面包圍著,如果AABB與該平面在Frustum外相交,那麼AABB就會是在其他四個平面任一個平面的負面,所以會在和其他平面檢測的時候,檢測出來該AABB不在Frustum內。

5 最後就是渲染:

void Terrain::draw()
{
	// Frustum cull sub-grids.
	std::list<SubGrid> visibleSubGrids;
	for(UINT i = 0; i < mSubGrids.size(); ++i)
	{
		if( gCamera->isVisible(mSubGrids[i].box) )
			visibleSubGrids.push_back(mSubGrids[i]);
	}

	// Sort front-to-back from camera.
	visibleSubGrids.sort();

	mFX->SetMatrix(mhViewProj, &gCamera->viewProj());
	mFX->SetTechnique(mhTech);
	UINT numPasses = 0;
	mFX->Begin(&numPasses, 0);
	mFX->BeginPass(0);

	for(std::list<SubGrid>::iterator iter = visibleSubGrids.begin(); iter != visibleSubGrids.end(); ++iter)
		HR(iter->mesh->DrawSubset(0));

	mFX->EndPass();
	mFX->End();
}

渲染步驟:

1  先檢測是否可見,即是否與Frustum相交,相交的放入一個list容器中。

std::list<SubGrid> visibleSubGrids;
	for(UINT i = 0; i < mSubGrids.size(); ++i)
	{
		if( gCamera->isVisible(mSubGrids[i].box) )
			visibleSubGrids.push_back(mSubGrids[i]);
	}


2 按照離鏡頭的遠近排序:

// Sort front-to-back from camera.
	visibleSubGrids.sort();

3 最後就是逐個小地圖塊渲染

for(std::list<SubGrid>::iterator iter = visibleSubGrids.begin(); iter != visibleSubGrids.end(); ++iter)
		HR(iter->mesh->DrawSubset(0));


其他程式碼是Shader渲染設定。

6 實現地圖分塊剔除技術關鍵步驟總結:

1 地圖分塊,定義好儲存資料的結構體,包含AABB包圍體

2 根據投影矩陣,計算Frustum

3 檢測Frustum和小地圖塊的AABB包圍體是否碰撞,儲存好碰撞的小地圖塊

4 根據離鏡頭遠近排序可見的(即碰撞的)小地圖塊,按循序渲染

下面是應用了本技術的效果圖: