close

做次世代的遊戲真是難阿!要畫面好看又要能跑的順,真恨不得自己改主機,把記憶體加大,把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;
}

就先介紹到這邊吧,有什麼問題可以留言討論喔

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 kgsprogrammer 的頭像
    kgsprogrammer

    太陽系後援會

    SnakeEater 發表在 痞客邦 留言(1) 人氣()