做次世代的遊戲真是難阿!要畫面好看又要能跑的順,真恨不得自己改主機,把記憶體加大,把CPU和GPU變快。這當然是辦不到的事,只好想破腦筋來試試各種方法來加快減少系統的負擔。
其實不管做什麼樣平台的遊戲,一個畫面最多可以畫多少個東西往往是最難評估與測試的。這要考慮到面數,貼圖數量、還有Pixel Field Rate等等的瓶頸所在。既然是開發次世代遊戲,畫面當然不能太差,因此貼圖、面數等等往往都是高標準,再者為了表現場景的壯闊與美感,場景上的物件數量與複雜度都是很可怕的數據。更何況在加上特效與AI等等,這都是效能的瓶頸所在。
遊戲之所以是遊戲,就是要跑的動,讓玩家能夠順暢的遊玩。畫面再漂亮,Gameplay設計的在怎麼好,FPS不到30甚至只有10,只會讓玩家想折片罷了。當然,要讓遊戲跑的動,絕對不是企劃修改規則或是美術把model做的很Low(當然如果資料有問題也是要修改)就可以跑的動。曾經聽過有人一個畫面2萬面FPS還不到10,而且還是用9600來開發,可見效能不彰程式佔了很大的因素。畢竟寫不好的Code,再怎麼優化資料結果並不能改善許多。
要提申效能沒有不二法門,往往在於經驗與不斷的修改與測試,以程式人員來說:
1. 首先就是要最佳化自己的程式碼,能夠預先計算好的資料,就不要在每個Frame都計算或是在每個迴圈裡都做相同的計算等等。
2. 盡量使用硬體加速的指令集。比如說向量的運算,以Dot來講是X1 * X2 + Y1 * Y2 + Z1 * Z2。看似很簡單的運算,卻需要CPU幾十個指令的價格,如果真的只有處理一次那都無所謂。但是遊戲中對向量的計算是很頻繁的,這樣的計算往往每個Frame要做幾百幾千次,這樣看起來就不便宜了。如果能用硬體加速的指令集,將三次乘法和兩次加法用一個指令來取代,哇!是不是快很多ㄚ。
3. 提供更多的技術支援給其他部門。
4. 不斷的進修,提升自己的專業能力。
以美術來說:
1. 所做的資料要以能放進遊戲中執行為主,實際去看問題所在,並與程式和企劃討論修改方向。
2. 製作的檔案要符合規定,面數,頂點數,貼圖格式,記憶體大小,Bone的數量等等都要依據程式給的規範來製作。
3. 製作模型時要在前製時期就做好彈性,避免修改過多,也不要害怕修改。畢竟玩家不是美術,對於美感的呈現見仁見智,不需要位了堅持而降低了遊戲本質。
以企劃來說:
1. 所有Gameplay功能要與程式人員討論與配合,尋求一個最佳解,避免說出我不管啦,我就是要這樣。這麼任性又不負責任的話對遊戲是沒有幫助的。
回歸正題。
如之前所說,目前的遊戲製作量很大,一個畫面通常要畫幾百個物件是很正常的,破千也是家常便飯,如此一來Draw Call的次數明顯就增加了。GPU與CPU是兩個不同的處理單位,CPU要通知GPU要畫什麼東西,首先要把資料準備好然後送給GPU。GPU會有一個資料的堆疊,表示要處理的資料有哪些,GPU就會一個一個的拿出來處理。我們都知道資料的傳遞往往是很慢的,尤其是記憶體內的搬移,所以當你Draw Call的次數越多就會相對的對效能產生嚴重的影響。除了資料的傳遞,也表示GPU要畫這麼多東西,這也是很傷效能的原因。
在這裡分享一個最近實作的一個可以減少Draw Call的方法,方法很陽春,因為我目前的能力還有限,還想不出要用什麼方法。
方法其實很簡單,就是在遊戲場景內放入一些平面,只要在平面後並且會被完全遮擋的物件,就不需要Render。簡單的原理就是把這個平面當作Camera的Near Plane,在利用遊戲中Render的Camera位置來產生新的View Frustum,來計算多重Culling的機制。
聽起來真的很簡單吧,老實說真的不難,在編輯上也不會造成企劃或美術的困擾。接下來說明一下片段的原始碼,提供大家參考。
首先必須先把編輯好的平面,讀入轉換成用數學表示式的平面方程式。平面方程式為:N dot P = C。
這裡的座標為右手座標Z-Up,平面的格式為
3--------1
| / |
| / |
| / |
| / |
2--------0
//傳入平面的3個點
void CreatPlane(const Point3 & vertex0, const Point3 & vertex2, const Point3 & vertex2)
{
//計算平面的Up與Right向量
m_Up = vertex1 - vertex0;
m_Right = vertex2 - vertex0;
//計算中心點
m_Center = vertex1 + ((vertex2 - vertex1) * 0.5f);
//計算中點到邊的距離
m_fUpMag = Length(m_Up) * 0.5f;
m_fRightMag = Length(m_Right) * 0.5f;
//計算平面的normal
m_Normal = Cross(m_Up, m_Right);
//計算平面的常數
m_fConst = Dot(m_Normal, vertex0);
}
紀錄好之後就是在每個迴圈當Camera更新之後,將Camera的資料傳入更新平面的資訊
void Update(Camera * pCamera)
{
Point3 camerapos = pCamera->GetTranslate();
float fDistance = Dot(m_Normal, camerapos) - m_fConst;
if (fDistance < 0.0f)
m_CameraWhichSide = -1; //Camera在平面的反面
else if (fDistance > 0.0f)
m_CameraWhichSide = 1; //Camera在平面的正面
else
m_CameraWhichSide = 0; //Camera與平面相交
//如果Camera與平面相交或是平面與Camera的方向幾近平行就不處理
if (m_CameraWhichSide == 0 || abs(Dot(pCamera->GetDirection(), m_Normal)) <= 0.0001f)
return;
//計算出四個頂點
Point3 kR = m_fRightMag * m_Right;
Point3 kUR = kR + m_fUpMag * m_Up;
Point3 kLR = kR - m_fUpMag * m_Up;
Point3 SidePoint[4] = { m_Center + kUR,
m_Center + kLR,
m_Center - kUR,
m_Center - kLR};
Point3 SideVecs[4] = { -m_Up, -m_Right, m_Up, m_Right };
//計算Camera與平面產生的Furstum的四個邊的平面,不計算Near Plane(本身就是)與Far Plane。
for (int i = 0; i < 4; ++i)
{
//計算Camera位置到頂點的向量
Point3 CV = SidePoint[i] - camerapos;
//計算出平面的normal,camera到各頂點的向量與平面的四個編作Dot取得平面的Normal
Point3 Nor = Cross(CV, SideVecs[i]);
Normalize(Nor);
//預先定義好一個平面陣列把計算好的資料先存起來
m_Sides[i].m_Normal = Nor;
m_Sides[i].m_fConst = Dot(Nor, SidePoint[i]);
}
}
接下來就是最重要的Culling了,一般來說物件是否要Render,通常都會在CPU端先做一次Culling,把完全在Camera外的物件丟掉不畫。如何判斷,最簡單的就是用碰撞球來偵測,不過碰撞球對於物件的外型包覆的太多,對於長寬比差異很懸殊的來計算會很不理想,不過這裡先不討論碰撞的精細度,碰撞的相關文章會在以後做探討。
這裡也是用碰撞球來做測試,只要是碰撞球完全在平面產生的Frustum的四個邊以內的物件都會被Culling掉,就來看看以下的程式碼吧。
bool IsCulling(const BoundSphere & Sphere)
{
//如果攝影機與平面相交就表示物件不被Culling
if (m_iCameraWhichSide == 0)
return false;
//取得測試半徑,判斷正反面是因為要取得測試點的最遠距離
float fRadius = m_iCameraWhichSide == 1 ? Sphere.m_fRadius : -Sphere.m_fRadius;
Point3 TestLocation = fRadius * m_Normal;
TestLocation += Sphere.m_Center;
int WhichSide= 0;
float fDistance = Dot(m_Normal, TestLocation ) - m_fConst;
if (fDistance < 0.0f)
WhichSide= -1; //測試點在平面的反面
else if (fDistance > 0.0f)
WhichSide = 1; //測試點在平面的正面
else
WhichSide = 0; //測試點與平面相交
//如果與攝影機是在同一面表示沒有被平面遮住
if (WhichSide == m_iCameraWhichSide)
return false;
bool bCulled = true;
for (int i = 0; i < 4; ++i)
{
Point3 TestPoint = fRadius * m_Sides[i].m_Normal;
TestPoint += Sphere.m_Center;
fDistance = Dot(m_Normal, TestPoint ) - m_fConst;
if (fDistance < 0.0f)
WhichSide= -1; //測試點在平面的反面
else if (fDistance > 0.0f)
WhichSide = 1; //測試點在平面的正面
else
WhichSide = 0; //測試點與平面相交
bCulled &= WhichSide != m_iCameraWhichSide;
}
return bCulled;
}
就先介紹到這邊吧,有什麼問題可以留言討論喔