3ds max sdk導出插件編寫的心得
作者:yhchinabest
來自:CG先生-
3D圖形插件開發網http://www.cgsir.com/
目錄

寫在前面

環境配置

第一個導出程序

Mesh,Material,Light,Camera,讓我們找到它們

Mesh,Material,Light,Camera,讓我們解析他們


 

 寫在前面

       為什么要寫這個心得?去年11月份的時候我寫過一篇3ds Max導出程序的一些嘗試,抱著學習的態度把一些心得發到網上供大家參考,不過當時寫的還很不完善,很多東西沒說清楚。今年6月份開始又做了3ds Max導出導入程序的一些研究,感覺3ds Max SDK實在是博大精深,初學者入門還是很不方便,所以覺得以前發的心得應該得到補充,因而寫了這樣一個導出程序介紹,還是抱著學習的態度,不過還是希望能夠對大家有所幫助。

    由于時間精力有限,只寫出導出程序的一些體會,以后會寫出導入程序的體會。希望大家多批評指教。
環境配置

步驟1.首先你得有VS2005,3ds Max 9,如果有就好辦了,否則想辦法搞到手吧,在中國做到這點應該不難。至于其他相近版本的IDEMAX,情況基本類似。

 

步驟2.3ds Max9 SDK"maxsdk"howto"3dsmaxPluginWizard中有個readme.txt,它會向你介紹如何配置3ds Max9 plugin的向導。

 

步驟3.啟動vs2005,新建Visual C++項目,如果在右側的模板組中能夠找到”3dsmaxPluginWizard”,并且選擇后能夠彈出歡迎界面,說面配置已經成功了。


第一個導出程序

這里僅僅是為了讓大家更好的了解導出插件是如何工作的,所以什么都不導出,做個測試而已。

1.       plugin Type中選擇File Export。選擇下一步,然后給你的導出類起個名字,比如”MyExport”。選擇下一步,再輸入你的MAXSDK路徑,插件存放的路徑,3dsmax.exe存放路徑。然后Finish

 

2.       找到class MyExport的函數const TCHAR *MyExport::Ext(int n)定義。該函數用來顯示導出文件的擴展名,改一下,例如return _T(“My3D”)

 

3.       再找到const TCHAR *MyExport:: ShortDesc ()的定義,該函數顯示插件的描述信息,也改一下,例如return _T(MyExportPlugin)

 

4.       為了了解導出程序的入口,在函數DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)內添加:

    AllocConsole();

    _cprintf( "Export Begin"n" );//記得#include <conio.h>

 

生成并調試你的插件,系統會執行3dsmax.exe以啟動3ds Max,然后選擇“文件”->導出,希望你能看到MyExportPlugin (*.My3D)的選項,然后隨便敲個什么名字,“確定”,如果能看到控制臺輸出Export Begin,那么第一個導出程序的實驗便成功了。當然,你一定不會對導出文件的描述字符串和擴展名感興趣,那么請你重點關注DoExport這個函數,特別是ExpInterface *ei這個參數,請在3ds Max SDK中查閱ExpInterface的相關信息,下一章將會使用到它。另外,你應該已經了解到,導出程序是從DoExport這個函數開始的。


Mesh,Material,Light,Camera,讓我們找到它們

1. 首先說說上一章提到的ExpInterface,在3ds Max9SDK中找到它,可以看到,它繼承于MaxHeapOperators,并包含IScene *theScene。按照它的描述,thsScene是用來枚舉場景中所有node的。看來這個node就是我們要尋找的對象。先不急著看node,先來看看IScene *theScene

 

2. IScene有個重要的函數: int EnumTree ( ITreeEnumProc *proc )。看看這個函數的描述:

Remarks:

Implemented by the System..
This may be called to enumerate every INode in the scene. The callback may flag any of these nodes (using INode::FlagForeground()).

Parameters:

ITreeEnumProc *proc
This callback object is called once for each INode in the scene.

Returns:

Nonzero if the process was aborted by the callback (TREE_ABORT); otherwise 0.

可以看出,這個函數會被系統自動調用。它會枚舉場景中的每個結點。對每個結點,它再調用ITreeEnumProc *proc,估計這個proc就是用來解析每個結點的東西。

 

3. 再看看ITreeEnmuProc的描述:

Description:

This is the callback object used by IScene::EnumTree(). To use it, derive a class from this class, and implement the callback method.

它有個一個成員函數int callback(INode *node)看來我們需要的就是它了,這個函數會讓系統傳給你你要的node,你來實現這個callback函數。

 

4.    看來我們要寫些代碼了(我估計你也早等不及了),讓我們寫一個繼承ITreeEnumProc的類:

class MyTreeEnum : public ITreeEnumProc

{

public:

    MyTreeEnum(void);

    ~MyTreeEnum(void);

public:

    int callback( INode *node );

};

然后實現int callback( INode *node ):

int MyTreeEnum::callback(INode *node)

{

ObjectState os = node->EvalWorldState(10);

if ( os.obj->CanConvertToType( Class_ID(TRIOBJ_CLASS_ID, 0) ) )

    {

        _cprintf( "TRIOBJECT %s"n", node->GetName());

        Mtl *pMtl = node->GetMtl();

        if ( pMtl )

        {

            _cprintf( "MATERIAL %s"n",pMtl->GetName() );

        }

        return TREE_CONTINUE;

    }

 

    if (os.obj)

    {

        switch(os.obj->SuperClassID())

        {

            case CAMERA_CLASS_ID:

            _cprintf( "CAMERA %s"n", node->GetName());

            break;

 

            case LIGHT_CLASS_ID:

            _cprintf( "LIGHT %s"n", node->GetName());

            break;

        }

}

    return TREE_CONTINUE;

}

 

接著,讓我們調用這個函數,這只需要修改DoExport()函數

int MaxExportTest::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)

{

    MyTreeEnum tempProc;

ei->theScene->EnumTree( &tempProc );

    return TRUE;

}

最后,編譯它,開始調試,找一個有物體,材質,燈光,攝像機的場景進行導出,如果你能在控制臺輸出窗口看到每個結點的名字,說明你的代碼成功了。

 

5.    讓我們再來看這些代碼,首先來關注INode,根據3ds max sdk的說明,INode是場景中每個結點的接口,它可以代表不同類型的物體,如幾何體,燈光,攝像機。要訪問這些物體,你就得調用INode::EvalWorldState,它會返回一個ObjectState,這個ObjectState又包含一個Object *obj,越說越復雜了,這兩個類型都很重要,但我們現在只需要這個obj來幫助我們判斷當前傳入的node是屬于什么類型。這就需要兩個函數,canConvertToType()SuperClassID(),它們是obj的成員函數。在這之前,先看看Super Class IDClass ID,這是Super Class ID的一段摘要:

GEOMOBJECT_CLASS_ID - Used by geometric objects.

CAMERA_CLASS_ID - Used by plug-in cameras.

LIGHT_CLASS_ID - Used by plug-in lights.

SHAPE_CLASS_ID - Used by spline shapes.

HELPER_CLASS_ID - Used by helper objects.

SYSTEM_CLASS_ID - Used by system plug-ins.

OSM_CLASS_ID - Used by Object Space Modifiers.

WSM_CLASS_ID - Used by Space Warp Modifiers (World Space Modifiers).

 

這是Class ID的一段摘要:

Subclasses of GEOMOBJECT_CLASS_ID

Built into core

TRIOBJ_CLASS_ID - TriObject

PATCHOBJ_CLASS_ID - PatchObject

Subclasses of LIGHT_CLASS_ID:

OMNI_LIGHT_CLASS_ID - Omni Light

SPOT_LIGHT_CLASS_ID - Spot Light

DIR_LIGHT_CLASS_ID - Directional Light

FSPOT_LIGHT_CLASS_ID - Free Spot Light

TDIR_LIGHT_CLASS_ID - Target Directional Light

 

由此可見,Class ID 應該是Super Class ID的一個子集,比如要判斷是否是燈光,只要看它的Super Class ID是否是LIGHT_CLASS_ID,函數SuperClassID()可以達到這個目的。而要看它具體是哪種燈光,就需要canConvertToType函數了。不過讓我不解的是,攝像機和燈光的目標節點也被歸為GEOMOBJECT_CLASS_ID了,我不知道3ds Max為什么要這樣設計,所以我只好用canConverToType來判斷這個物體是否為三角網物體。

 

6.    好了,我們大概找到了我們需要的東西,下一章,我會示范如何從這些較大的范圍中得到我所感興趣的具體的信息,如燈光的位置和方向,以及最重要的Mesh的頂點信息等。


Mesh,Material,Light,Camera,讓我們解析他們

1.         首先說說meshmaterial吧,這兩者結合相當密切。上一章說到如何獲得TriObject,通過它可以獲得一個mesh

Mesh* pMesh = &tri->GetMesh();

 

SDK里查看mesh的描述,發現它可以導出很多信息,而我們一般希望從mesh中獲得頂點坐標,法線向量,紋理坐標,頂點顏色等信息,以及頂點的索引值。對于只貼了一個紋理的mesh,我們可以簡單的獲得這些信息。

Mesh* pMesh = &tri->GetMesh();

int VerticesNum = pMesh->getNumVerts()

for ( int i=0; i<VerticesNum; i++ )

{

    Point3 Coord, Normal, TCoord, VColor;

    if( pMesh->getNumVerts()>0 ) //導出頂點坐標

    {

        Coord = pMesh->getVert( i );

    }

 

    if ( pMesh->faces )          //導出法線向量

    {

        Normal = pMesh->getNormal( j );

    }

 

    if ( pMesh->getNumTVerts()>0 )//導出紋理坐標

    {

        TCoord = pMesh->tVerts[j] ;

    }

 

if ( pMesh-> vertCol ) //導出頂點顏色

{

    VColor = pMesh->vertCol[i];

}

}

然后是這個mesh使用的紋理,這里僅列舉漫反射貼圖:

Mtl *pMtl = pNode->GetNode 

if ( pMtl!=NULL )

{              

    Texmap *pTexMap = pMtl->GetSubTexmap(ID_DI); //獲取漫反射材質的貼圖

    BitmapTex *pBMPTex = (BitmapTex *) pTexMap

    if ( pBMPTex )

    {

        char *MapName = pBMPTex->GetMapName();    //獲取漫反射貼圖的路徑

    }

}

而對于貼了多個紋理的情況,就要復雜的多,例如一個立方體,每個面都貼了一個紋理,那么就需要知道這個mesh面的數量,材質的數量,面和材質的對應關系,面和頂點的對應關系,等等。我設計了一種解析方法,經過一些模型的測試,結果正確,拿出來供大家參考參考。

先說說具體思想,假設一個立方體每個面都貼了一張紋理,那么可以把這個mesh看作劃分了6個子mesh,一個子mesh就是一個面。首先,遍歷原來mesh的所有面,計算非重復的材質ID。從而得知這個mesh的子mesh個數。然后再次遍歷原來mesh所有的面,將所有具有相同材質ID的面的頂點集合到一個子mesh,這些頂點只存儲該頂點在原mesh中的索引。當然,需要重新計算這些頂點在子mesh里的索引值。因此再遍歷原mesh的所有面。

struct FaceVertex

{

    int m_Index;     //表示該頂點在原mesh里的索引值

    int m_FaceIndex; //表示該頂點所屬的面在原mesh里的索引值

    int m_TriIndex; //表示該頂點在所屬三角形里的索引值,值為0,1,2

 

    bool operator == (const FaceVertex &refVertex)

    {

        if ( m_Index == refVertex.m_Index )

        {

            return true;

        }

        else

        {

            return false;

        }

    }

};

 

class MaxDivideMesh

{

public:

    vector<FaceVertex> m_VertexArray;

    vector<int> m_IndexArray;

};

 

void MyTreeEnum::CreateMutilMesh(  INode *pNode, Mesh *pMesh, Mtl *pMtl )

{

    vector <int> MeshMtls; //該Mesh用到的子材質的數量,用來計算子Mesh的劃分

                           //每個元素表示一個材質ID。

    for( int i=0; i<pMesh->getNumFaces(); i++ )

    {

        /*計算子Mesh數量,通過計算所有面使用的非重復材質數量而得*/

        int MatID = pMesh->getFaceMtlIndex(i);

vector<int>::iterator MatIndex = find( MeshMtls.begin(), MeshMtls.end(), MatID );

        if ( MatIndex == MeshMtls.end() )

        {

MeshMtls.push_back(MatID );//該材質未在MeshMtls里出現過,說明是個

//新材質

        }

    }

 

    //DivideMeshArray,計算Mesh劃分的拓撲信息

    vector<MaxDivideMesh> DivideMeshArray;

    DivideMeshArray.resize( MeshMtls.size() );//指定劃分數量

 

    //

    //此處有內存的分配

//GMeshD3D是我自己設計的一個類型,用來表示一個子Mesh

    //GTextureD3D用來表示Texture

    GMeshD3D *pMeshArray = new GMeshD3D[MeshMtls.size()];

    GTextureD3D *pTextureArray = new GTextureD3D[MeshMtls.size()];

 

    GObjectMAXD3D tempObj; //GObjectMAXD3D表示一個模型,有n個mesh和texture組成

    tempObj.SetMeshNum( MeshMtls.size() );

    tempObj.SetTextureNum( MeshMtls.size() );

   

    for ( int i=0; i<MeshMtls.size(); i++ )

    {

        if ( pMtl!=NULL )

        {              

            Mtl *pSubMtl = pMtl->GetSubMtl( MeshMtls[i] );

            Texmap *pTexMap = pSubMtl->GetSubTexmap(ID_DI); //獲取漫反射材質的貼//圖

            BitmapTex *pBMPTex = (BitmapTex *) pTexMap

            if ( pBMPTex )

            {

                char *MapName = pBMPTex->GetMapName();    //獲取漫反射貼圖名稱

                if ( MapName!=NULL )

                {

                    pTextureArray[i].SetMapName(MapName);

                }

            }

            pMeshArray[i].m_MatID = i;

            tempObj.SetTexture( &pTextureArray[i], i );    

        }

    }

 

    /*這里開始對原有mesh進行重新劃分*/

    for( int i=0; i<pMesh->getNumFaces(); i++ )

    {

        int MatID = pMesh->getFaceMtlIndex(i); //計算該面的材質ID

vector<int>::iterator MatIndex = find( MeshMtls.begin(), MeshMtls.end(), MatID );

        int MeshID = MatIndex - MeshMtls.begin(); //計算該MatID在TextureArray的紋理索引,使MeshID從0開始編號

        for ( int j=0; j<3; j++)

        {

            int Index = pMesh->faces[i].v[j];//Index表示在全局頂點數組里的索引

            FaceVertex tempVertex;

            tempVertex.m_Index = Index;

vector<FaceVertex>::iterator VertexIter = find( DivideMeshArray[MeshID].m_VertexArray.begin(),

             DivideMeshArray[MeshID].m_VertexArray.end(), tempVertex );

            if ( VertexIter == DivideMeshArray[MeshID].m_VertexArray.end() )

//在DivideMeshArray里尋找頂點索引值相同的頂點,如果沒找到該頂點,表示//要添加該頂點

            {

int VertexIndex = VertexIter - DivideMeshArray[MeshID].m_VertexArray.begin();

                FaceVertex tempFVertex;

                tempFVertex.m_Index = Index;

                tempFVertex.m_FaceIndex = i;

                tempFVertex.m_TriIndex = j;

                DivideMeshArray[MeshID].m_VertexArray.push_back( tempFVertex );

            }

        }

    }

 

/*計算頂點在每個子mesh中的索引*/

    for( int i=0; i<pMesh->getNumFaces(); i++ )

    {

        int MatID = pMesh->getFaceMtlIndex(i);

vector<int>::iterator MatIndex = find( MeshMtls.begin(), MeshMtls.end(), MatID );

        int MeshID = MatIndex - MeshMtls.begin(); //計算該MatID的紋理索引

        for ( int j=0; j<3; j++)

        {

            int Index = pMesh->faces[i].v[j];

            FaceVertex tempVertex;

            tempVertex.m_Index = Index;

//若在子mesh里能找到該點,則計算該點在子mesh的索引

vector<FaceVertex>::iterator VertexIter = find( DivideMeshArray[MeshID].m_VertexArray.begin(), DivideMeshArray[MeshID].m_VertexArray.end(), tempVertex );

            if ( VertexIter != DivideMeshArray[MeshID].m_VertexArray.end() )

            {

int VertexIndex = VertexIter - DivideMeshArray[MeshID].m_VertexArray.begin();//VertexIndex表//示該頂點在子Mesh的索引

                DivideMeshArray[MeshID].m_IndexArray.push_back( VertexIndex );

            }

        }

    }

 

/*余下部分開始到處頂點信息*/

    for ( int i=0; i<MeshMtls.size(); i++ )

    {

        pMeshArray[i].SetFVF( GFVF );

        pMeshArray[i].SetVerticeNum( DivideMeshArray[i].m_VertexArray.size() );

        int VerticesNum;

        pMeshArray[i].GetVerticeNum( VerticesNum );

        for ( int j=0; j<VerticesNum; j++ )

        {

            GVertex tempVertex;

            Point3 Coord,Normal,TCoord;

            if( pMesh->getNumVerts()>0 ) //導出頂點坐標

            {

                int index = DivideMeshArray[i].m_VertexArray[j].m_Index;

                Coord = pMesh->getVert(

DivideMeshArray[i].m_VertexArray[j].m_Index );

                tempVertex.PosCoord = D3DXVECTOR3( Coord.x, Coord.y, -Coord.z );

            }

 

            if ( pMesh->faces )          //導出法線向量

            {

                Normal = pMesh->getNormal( DivideMeshArray[i].m_VertexArray[j].m_Index );

                tempVertex.NormalVector = D3DXVECTOR3( Normal.x, Normal.y, Normal.z );

            }

 

            if ( pMesh->getNumTVerts()>0 )//導出紋理坐標

            {

                FaceVertex tempFVertex = DivideMeshArray[i].m_VertexArray[j];

                TCoord = pMesh->tVerts[pMesh->tvFace[tempFVertex.m_FaceIndex].getTVert(tempFVertex.m_TriIndex)] ;

                int TCoordIndex = pMesh->tvFace[tempFVertex.m_FaceIndex].getTVert(tempFVertex.m_TriIndex);

                _cprintf( "TextureCoord Index%d"n", TCoordIndex );

                tempVertex.TexCoord = D3DXVECTOR2( TCoord.x, TCoord.y );

            }

 

            DWORD VColor = 0xffffffff;

            tempVertex.Color = VColor;

 

            pMeshArray[i].SetVertex( tempVertex, j );

 

        }

 

        pMeshArray[i].SetFaceNum( DivideMeshArray[i].m_IndexArray.size()/3 );

        WORD FaceNum;

        pMeshArray[i].GetFaceNum( FaceNum );

        for ( int j=0; j<FaceNum; j++ )

        {

            pMeshArray[i].SetIndex( DivideMeshArray[i].m_IndexArray[j*3], j*3 );

            pMeshArray[i].SetIndex( DivideMeshArray[i].m_IndexArray[j*3+2], j*3+1 );

            pMeshArray[i].SetIndex( DivideMeshArray[i].m_IndexArray[j*3+1], j*3+2 );

 

        }

    }

    tempObj.WritetoFile();

 

    delete []pMeshArray;

    delete []pTextureArray;

}

 

2.       導出攝像機,我們一般關注攝像機的位置,方向,FOV角,近遠平面之類的信息。還好3ds Max SDK 里提供CameraObjectCameraStateCameraGenCamera等對象來訪問這些信息。不過攝像機的位置和方向等信息需要通過節點函數來訪問。比較簡單,直接見代碼。

void MyTreeEnum::CreateCamera( INode *pNode )

{

    ObjectState os = pNode->EvalWorldState( 10 );

    CameraObject* CameraObj = (CameraObject*)os.obj;

    struct CameraState cs;

    Interval valid = FOREVER;

    CameraObj->EvalCameraState( 10, valid, &cs );

    Matrix3 SourceMat = pNode->GetNodeTM( 10 );//獲取攝像機源點的變換矩陣

    Matrix3 destMatrix;

    pNode->GetTargetTM( 0, destMatrix ); //獲取攝像機目標點的變換矩陣

    cs.fov;      //獲取FOV角

    cs.hither;   //獲取攝像機近平面

    cs.yon;      //獲取攝像機遠平面

}

 

3.       導出燈光,與攝像機差不多,也是位置和方向需要結點函數來獲得,而其余信息通過訪問LightLightStateGenLightLightObject獲得。

void MyTreeEnum::CreateLight( INode *pNode )

{

    ObjectState os = pNode->EvalWorldState(10);

    GenLight* light = (GenLight*)os.obj;

    struct LightState ls;

    Interval valid = FOREVER;

    light->EvalLightState( 10, valid, &ls );

    Matrix3 SourceMat = pNode->GetNodeTM( 10 );

    Matrix3 TargetMat;

    pNode->GetTargetTM( 10, TargetMat );

    float Theta, Phi;

    Theta = ls.hotsize;

    Phi = ls.fallsize;

    switch(ls.type) //導出燈光類型

{

        case OMNI_LIGHT_cprintf( "%s"n", "ID_LIGHT_TYPE_OMNI" ); break;

        case TSPOT_LIGHT: _cprintf( "%s"n", "ID_LIGHT_TYPE_TARG" ); break;

        case DIR_LIGHT:   _cprintf( "%s"n", "ID_LIGHT_TYPE_DIR" );  break;

        case FSPOT_LIGHT: _cprintf( "%s"n", "ID_LIGHT_TYPE_FREE" ); break;

    }

}

呼。。。。。終于寫完了,感覺還是很多東西沒說清楚。希望大家多學學3ds Max SDK,給我多提出意見,呵呵。