AD

Puzzle Game Implementation Using Cogl/ODE



在開始之前, Cogl跟ODE可能沒什麼人聽過, 先大概介紹一下


1.Introduction


Cogl:
     網址: http://wiki.clutter-project.org/wiki/Cogl
       
     Cogl是一套處理3D幾何的函式庫, 數於Open Source專案ClutterProject裡的一部份,底層使用OpenGL, OpenGLES, 目前仍在開發中, 功能尚沒有很齊全(2011/05), 另外在使用上要很注意, 使用Clutter做開發的軟體, 由於底層也使用Cogl, 如果在其中自己直接使用Cogl, 有可能會對Clutter造成影響, 像是設定viewport等等都有可能會互相影響, 混用的時候要很注意.


ODE:

    網址:http://www.ode.org/

    ODE是一套知名的Open Source物理引擎, 跟市面上現有的物理引擎比較起來功能雖然不強大, 但基礎功能該有的都有, 沒有很要求物理處理的效能以及功能的話用起來還不錯.



2.Implementation

    先來看結果的影片,處理拼圖塊的方式有很多種, 可以利用2d alpha map的方式, 產生無厚度的拼圖塊, 也可以直接做出3d model, 寫讀檔程式自己繪圖, 在這邊我用的是自己建3d model的方式, 以下做論述


結果影片


 .puzzle model/texture

     在puzzle的model部分, 我的方式是自己建模並輸出.obj檔案, 並在程式中寫讀檔以及繪圖, 在這邊有個重點是貼圖座標一定要處理好, 之後在程式中繪圖處理才比較不會太大的問題.

.建立3D模型

    如圖, 把3ds_max開起來, 乖乖的慢慢拉點線面, 建好後輸出obj檔案, 在這邊說明一下, 在選擇輸出格式時要注意選擇有含貼圖資訊的格式, 並不是所有格式都有含貼圖資訊.....



建立3D模型-網格


 建立3D模型-貼圖

.軟體實作
  • 初始化
        首先, 第一步是初始化, 分為兩個部分, Cogl以及ODE環境的初始化.

            這邊為了方便, 使用clutter來處理介面,


clutter初始化
ClutterActor *stage ;

//clutter初始化
clutter_init (&argc, &argv);

//創建clutter基礎元件, 後續基本元件(actor)會加在stage裡
stage = clutter_stage_new();


ODE初始化
//dInitODE舊版ode不需要使用, 新版有些功能需要先呼叫才能使用, 如joint, collision等等
dInitODE();

//ode基礎的元件, 所有的物件產生在world下面
mWorld = dWorldCreate();

//建立一個碰撞空間, ode有幾種空間, dSimpleSpaceCreate較適合小量物件的碰撞
mSpace = dSimpleSpaceCreate(0);

//建立儲存接觸點的群組
mContactgroup = dJointGroupCreate(0);


//為了不讓物體一直掉到下去, 建立個地板, 參數為ax+by+cz=d的a,b,c,d
dCreatePlane(mSpace, 0, -1, 0, -10);

//設定重力
dWorldSetGravity(mWorld, 0, 1.0, 0);

//設定error correcting及constraint force mixing
dWorldSetERP(mWorld, 0.2);
dWorldSetCFM(mWorld, 1e-5);


  • 註冊訊息事件
        初始化完成後, 下一步是註冊我們所需要的訊息, 需要註冊的訊息分別為paint,destroy,key或者button事件


註冊訊息事件
//註冊繪圖事件, 最主要的處理都在這
g_signal_connect_after (actor, "paint", G_CALLBACK (cogl_paint_cb), &gFrameData);

//destroy, 裡面處理釋放資源
g_signal_connect (actor, "destroy", G_CALLBACK (actor_destroy), NULL);

//滑鼠事件
g_signal_connect (actor,
                    "button-press-event",
                    G_CALLBACK (button_press_cb),
                    NULL);
g_signal_connect (actor,
                    "button-release-event",
                    G_CALLBACK (button_release_cb),
                    NULL);
g_signal_connect (actor,
                    "motion-event",
                    G_CALLBACK (mouse_cb),
                    NULL);

  • 繪圖與物理處理(paint事件)
        接下來處理最重要paint事件部分, 這個部分包含model的繪圖, ODE物理部分的碰撞, 以及raycast處理.

        先處理Cogl的3d model繪圖, obj檔案的讀取部分可以在網路上找到現成的code, 由於是不是我自己寫的, 此部分就略掉不講, 假設目前已經把3d點座標與貼圖座標都讀取到陣列存放, 以下是主要的程式碼


Cogl Load 3d puzzle model
void PuzzleModel::LoadWithMesh(string modelFilePath,Vector3D position)
{
    float maxx,maxy,maxz;
    float minx,miny,minz;
    maxx=maxy=maxz= -999999;
    minx=miny=minz= 999999;

    mMesh = new ObjData(modelFilePath);
 ObjMesh *pMesh = mMesh->mObjMesh;

#ifdef _DEBUG_MODEL_INFO
    printf("all point size : %d\n", pMesh->m_iNumberOfVertices);
    printf("all texture point size : %d\n", pMesh->m_iNumberOfTexCoords);
    printf("all normal point size : %d\n", pMesh->m_iNumberOfNormals);   
    printf("faces size : %d\n", pMesh->m_iNumberOfFaces);
#endif
 //1 face = 3 vertex = 3 texture = 3 normal..1面3點, 每點都有texture以及normal
    mDrawPointSize = pMesh->m_iNumberOfFaces*3;//pMesh->m_iNumberOfVertices;
    
    mTexturePointSize = pMesh->m_iNumberOfFaces*3;//pMesh->m_iNumberOfTexCoords;
    mNormalPointSize = pMesh->m_iNumberOfFaces*3;//pMesh->m_iNumberOfNormals;

    if( pMesh->m_aNormalArray   != NULL &&
     pMesh->m_aTexCoordArray != NULL )
    {

        unsigned int i;
        unsigned int iDraw = 0,iTexture = 0,iNormal=0;
        mDrawPointGroup = new float[mDrawPointSize*3];
        mTexturePointGroup = new float[mTexturePointSize*2];
        mNormalPointGroup = new float[mNormalPointSize*3];

        for(i=0;i<m_iNumberOfFaces;i++)
       {
                    unsigned int j;
      ObjFace *pf = &pMesh->m_aFaces[i];

      /*
      ** Draw the polygons with normals & uv co-ordinates
      */
      for(j=0;j<3;j++)
      {
                float fx,fy,fz;

                fx=pMesh->m_aTexCoordArray[ pf->m_aTexCoordIndicies[j] ].u;
                fy=pMesh->m_aTexCoordArray[ pf->m_aTexCoordIndicies[j] ].v;
                mTexturePointGroup[iTexture++]=fx;
                mTexturePointGroup[iTexture++]=fy;
                
                fx=pMesh->m_aNormalArray[ pf->m_aNormalIndices[j] ].x;
                fy=pMesh->m_aNormalArray[ pf->m_aNormalIndices[j] ].y;
                fz=pMesh->m_aNormalArray[ pf->m_aNormalIndices[j] ].z;
                mNormalPointGroup[iNormal++] = fx;
                mNormalPointGroup[iNormal++] = fy;
                mNormalPointGroup[iNormal++] = fz;

                fx=pMesh->m_aVertexArray[ pf->m_aVertexIndices[j] ].x;
                fy=pMesh->m_aVertexArray[ pf->m_aVertexIndices[j] ].y;
                fz=pMesh->m_aVertexArray[ pf->m_aVertexIndices[j] ].z;
                mDrawPointGroup[iDraw++] = fx;
                mDrawPointGroup[iDraw++] = fy;
                mDrawPointGroup[iDraw++] = fz;
                if(fx>maxx)
                    maxx=fx;
                if(fy>maxy)
                    maxy=fy;              
                if(fz>maxz)
                    maxz=fz;
                if(fx<minx)
                    minx=fx;
                if(fy<miny)
                    miny=fy;              
                if(fz<minz)
                    minz=fz;
      }
     }
#ifdef _DEBUG_MODEL_INFO
        //設定bounding box, 左上為原點
        //右下
printf("max:%f,%f,%f\n",maxx,maxy,maxz);
printf("min:%f,%f,%f\n",minx,miny,minz);
#endif
        //為了設定物理物件的大小, 此處計算bounding box
        mBoundingBox[0] = Vector3D(minx,miny,minz);
        mBoundingBox[1] = Vector3D(maxx,maxy,maxz);
        
         //建立ode物理body, 後面說明
        CreatePhysicsBody(position);

        //Cogl繪圖需要先創建buffer,gl_Vertex畫點,  
        //gl_Normal設定normal, 要繪圖至少要有gl_Vertex
        mVBO = cogl_vertex_buffer_new (mDrawPointSize);
        cogl_vertex_buffer_add (mVBO,
                              "gl_Vertex",
                              3,
                              COGL_ATTRIBUTE_TYPE_FLOAT,
                              FALSE,
                              sizeof (float)*3,
                              mDrawPointGroup);

        cogl_vertex_buffer_add (mVBO,
                              "gl_Normal",
                              3,
                              COGL_ATTRIBUTE_TYPE_FLOAT,
                              FALSE,
                              sizeof (float)*3,
                              mNormalPointGroup);

     
        //設定texture, 此處為了方便寫死texture.png, 真的實做請自己實做一個resource manager處理可能比較適當
        CoglTextureFlags flags = COGL_TEXTURE_NO_SLICING;
        
        CoglPixelFormat internalFormat = COGL_PIXEL_FORMAT_ANY;

        CoglHandle texture = cogl_texture_new_from_file ("texture.png",OGL_TEXTURE_NO_SLICING,COGL_PIXEL_FORMAT_ANY, NULL);
        if(texture != NULL)
        {
            //同樣是為了繪圖需要的buffer, 此處是畫材質用的貼圖座標
            cogl_vertex_buffer_add (mVBO,
                          "gl_MultiTexCoord0",
                          2,
                          COGL_ATTRIBUTE_TYPE_FLOAT,
                          FALSE,
                          sizeof (float)*2,
                          mTexturePointGroup);
            mModelTexture = cogl_material_new ();

            cogl_material_set_layer ((CoglMaterial*)mModelTexture, 0, texture);       
        }

    }

        處理完model繪圖後, 接下來要替此物件建立一個物理body, 使之產生物理的效果


建立物理body
void PuzzleModel::CreatePhysicsBody(Vector3D position)
{

    float sizex = mBoundingBox[1].mx-mBoundingBox[0].mx;
    float sizey = mBoundingBox[1].my-mBoundingBox[0].my;
    float sizez = mBoundingBox[1].mz-mBoundingBox[0].mz;

    mPhyObj = PhysicsManager::Singleton()->createBox(sizex,sizey,sizez);

    dMatrix3 R;
    VECTOR tempVect(0.0, 0.0, 0.0);
    dBodySetLinearVel(mPhyObj->mBody, tempVect.x, tempVect.y, tempVect.z);
    dRFromAxisAndAngle(R, 0, 1, 0, 0);
    dBodySetRotation(mPhyObj->mBody, R);
    dBodySetPosition(mPhyObj->mBody,  position.mx, position.my, position.mz);

#ifdef _DEBUG_MODEL_INFO
//設定bounding box, 左上為原點
//右下
printf("GEOM(box) size:%f,%f,%f\n",sizex,sizey,sizez);
printf("GEOM(box) pos:%f,%f,%f\n",position.mx, position.my, position.mz);
#endif
}


        model的位移以及旋轉, 也就是其空間矩陣值由ODE來處理, 成功建立物理body後, 在paint重繪處理時一併更新ODE物理計算, 將ODE計算出來的空間矩陣設定給此物件, 透過Cogl繪出即可, 此處要注意的是ODE的空間矩陣與Cogl(同OpenGL)放置方式不一樣, 所以需要再多一個轉換步驟


Puzzle model的繪圖Update處理
void PuzzleModel::Update(float elasptime)
{
    if(!mDrawPointGroup || mDrawPointSize <=0 ) return;
    
    cogl_push_matrix (); 

    //scale與translate順序會影響位移大小, scale先放則移動會乘上倍率
    cogl_scale (mScale.mx, mScale.my,  mScale.mz); 
    //物理處理    
    if(mPhyObj)
    {
/*      | 0  1  2  3  |            | 0  4  8  12 |
        |             |            |             |
    R = | 4  5  6  7  |            | 1  5  9  13 |
        |             |        M = |             |
        | 8  9  10 11 |            | 2  6  10 14 |
                                   |             |
    p = | 0  1  2 |                | 3  7  11 15 |
*/
        MATRIX geoMatrix;
        geoMatrix.LoadIdentity();
        const dReal *pos = dGeomGetPosition(mPhyObj->mGeom);
        const dReal *R = dGeomGetRotation(mPhyObj->mGeom);
#ifdef _DEBUG_MODEL_MATRIX
    printf("\n*************\n");
    printf("{%f,%f,%f,%f}\n{%f,%f,%f,%f}\n{%f,%f,%f,%f}\n",R[0],R[1],R[2],R[3],
                                                            R[4],R[5],R[6],R[7],         
                                                            R[8],R[9],R[10],R[11]);
    printf("\n(%f,%f,%f)\n", pos[0],pos[1],pos[2]);
    printf("*************\n");
#endif
        //arkkk 這裡一定要注意ode的dReal define是否為double精度, 否則會出問題(看ode\common.h, dDOUBLE/dSINGLE)
        geoMatrix.ODEtoOGL((const float*)pos, (const float*)R);
#ifdef _DEBUG_MODEL_MATRIX
    printf("\n==============\n");
    printf("{%f,%f,%f,%f}\n{%f,%f,%f,%f}\n{%f,%f,%f,%f}\n{%f,%f,%f,%f}\n",geoMatrix.Element[0],geoMatrix.Element[4],geoMatrix.Element[8],geoMatrix.Element[12],
                                                            geoMatrix.Element[1],geoMatrix.Element[5],geoMatrix.Element[9],geoMatrix.Element[13],         
                                                            geoMatrix.Element[2],geoMatrix.Element[6],geoMatrix.Element[10],geoMatrix.Element[14],
                                                            geoMatrix.Element[3],geoMatrix.Element[7],geoMatrix.Element[11],geoMatrix.Element[15]);
    printf("===============\n\n");
#endif

        CoglMatrix matrix_cogl,matrix_gl_ode,matrix_result;

        //gl to cogl matrix
        cogl_matrix_init_from_array(&matrix_gl_ode, geoMatrix.Element);
        //取出現在matrix
        cogl_get_modelview_matrix(&matrix_cogl);
        
        cogl_matrix_multiply(&matrix_result, &matrix_cogl, &matrix_gl_ode);
        cogl_set_modelview_matrix(&matrix_result);
    }
    else 
    {
        cogl_translate (mPosition.mx, mPosition.my,  mPosition.mz);

        cogl_rotate (mRotate.mx, 0, 0, 1);
        cogl_rotate (mRotate.my, 0, 1, 0);
        cogl_rotate (mRotate.mz, 1, 0, 0);
    }
    cogl_set_depth_test_enabled (TRUE);

    if(mModelTexture)
        cogl_set_source (mModelTexture);

    cogl_vertex_buffer_draw (mVBO,
                           COGL_VERTICES_MODE_TRIANGLES,
                           0,
                           mDrawPointSize);

    cogl_set_depth_test_enabled (FALSE);

    cogl_pop_matrix ();
}


釋放資源也是很重要的
PuzzleModel::~PuzzleModel()
{
    if(mMesh)
    {
        cogl_vertex_buffer_submit(mVBO);
        delete mMesh;
        mMesh = NULL;
        cogl_vertex_buffer_delete(mVBO,"gl_Vertex");
        cogl_vertex_buffer_delete(mVBO,"gl_Normal");
    }

    if(mModelTexture)
    {
        cogl_vertex_buffer_submit(mVBO);
        cogl_vertex_buffer_delete(mVBO,"gl_MultiTexCoord0");
        cogl_material_ref(mModelTexture);
        mModelTexture = NULL;
    }
}

        model的處理到此大部分完成了, 接下來把paint重繪的部分補上


paint繪圖處理
static void cogl_paint_cb (ClutterActor *actor, FrameData *data)
{

    cogl_set_viewport (0, 0, data->mFrameWidth, data->mFrameHeight);
    

    cogl_perspective (60, /* field of view */
                    1, /* aspect ratio */
                    0.1, /* distance to near z plane */
                    100); /* distance to far z plane */

    setup_2d_device_coordinates_modelview (data->mFrameWidth,
                                         data->mFrameHeight);

    cogl_clear (&gWhite, COGL_BUFFER_BIT_COLOR);

    //center
    cogl_translate(gFrameData.mFrameWidth/2, gFrameData.mFrameHeight/2, 0.0f);

    //注意, NearCallback函式關係到物理碰撞時的處理
    dSpaceCollide(mSpace, 0, NearCallback);

    dWorldQuickStep(mWorld, 0.05);

    dJointGroupEmpty(mContactgroup);

    UpdateAllModels();
}


        到這一步執行下去應該還是會發現各個puzzle model的碰撞並無效果, 各位應該已經發現到dSpaceCollide的NearCallback尚未處理, 最後一步來看物理body碰撞時的處理


ODE body碰撞處理
static void NearCallback(void *data, dGeomID o1, dGeomID o2)
{
    int i;
    dBodyID b1 = dGeomGetBody(o1);
    dBodyID b2 = dGeomGetBody(o2);

    dContact contact[MAX_CONTACTS];   // up to MAX_CONTACTS contacts per box-box

    for (i = 0; i < MAX_CONTACTS; i++)
    {
        contact[i].surface.mode = dContactBounce | dContactSoftCFM;
        contact[i].surface.mu = dInfinity;
        contact[i].surface.mu2 = 0;
        //表面彈性, 0~1
        contact[i].surface.bounce = 0.5;//0.9;
        //反彈速度
        contact[i].surface.bounce_vel = 0.5;//0.1;
        contact[i].surface.soft_cfm = 0.01;
    }

    if (int numc = dCollide(o1, o2, MAX_CONTACTS, &contact[0].geom, sizeof(dContact)))
    {
        for (i = 0; i < numc; i++)
        {
            dJointID c = dJointCreateContact(mWorld, mContactgroup, contact + i);
            dJointAttach(c, b1, b2);
        }
    }
} 


3.後記
     
    由於Cogl尚在開發中, 目前我並沒有在裡面發現有實做類似gluProject以及gluUnProject等函式, 我自己取其轉換矩陣下去計算發現滑鼠點選位置與3d空間會差一個位移, 原因我也不太確定, 導致用滑鼠點不太好讓ODE的raycast準確的打到物件, 後續有在實作再行補上點選拉動部分, 也許Cogl馬上就會有這功能, 或者其實有, 只是我沒發現吧, 另外, 以上程式碼為了只拿出重點部分, 所以是從原來架構精簡搬出來, 大錯誤應該是沒有, 不過可能會有小編譯錯誤, 以上~~

沒有留言:

張貼留言