【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 3923|回复: 1

[转帖]3DS Max 插件制作学习心得

[复制链接]
发表于 2007-11-7 10:31:13 | 显示全部楼层 |阅读模式
要从3ds Max中导出场景信息,大概有两种方式:1.利用3ds Max的sdk制作插件。2.利用3ds Max的Max Script编写场景输出脚本。两种方式各有优劣,这里仅叙述我用了数天研究3ds Max SDK并制作插件的学习心得。
        1.前期环境配置工作。

       首先肯定要安装带SDK的3ds Max,我安装的是3ds Max7。安装后在maxsdk\help下有个sdkapwz.zip,把这个文件解压到VS 6.0或VS 2003的application wizard路径下,启动VS就会有3ds Max plug-in的应用程序向导来生成插件程序的框架。
       2.制作插件需要了解的几个基本概念。

       2.1 我所了解的插件原理是:3ds Max会公布一些接口,插件制作者需要做的是实现这些接口。例如利用向导生成一个用于场景导出的插件。就会发现在生成的程序中有一个类继承于class SceneExport,而 SDK 中关于这个接口的描述是:

       This is a base class for creating file export plug-ins. The plug-in implements methods of this class  to describe the properties of the export plug-in and a method that handles the actual export  process. 

       再看下面的函数说明,可以看到函数是虚函数的方式声明的,所以必须要将其所有的函数进行实现。

       2.2 3ds Max是怎样识别插件的接口?还是以上面的场景到处插件为例,程序中还会生成一个继承于 ClassDesc2的类,这个类会实现一些关于类的ID,层次信息处理的函数。估计系统就是根据这个识别处我的插件的接口,没有具体去研究,只了解个大概。

       2.3 如何调试所编写的插件,SDK中有说明,我这里简单说一下,将工程属性设置为Hybrid(默认是Debug),并且把输出dle文件的路径设为3ds Max的plug-in的路径,再把调试的可执行程序设为3ds Max.exe,这样 调试的时候就会启动3ds Max主程序,其他诸如设置断点,单步等调试手段和普通程序的调试方法一样。
       3.通过一个例子学习插件编程。 

    这部分还真不好写,涉及到一些代码,代码中又有很多API需要讲解,API中又有很多基本知识需要说明,唉,硬着头皮来吧。

       还是以那个场景导出类为例,可以看到,SceneExport中有个非常重要的函数需要实现:

       virtual int DoExport(const TCHAR *name,ExpInterface *ei,Interface *i,
       BOOL suppressPrompts=FALSE, DWORD options=0) = 0;

       先看看参数:

      name 表示要导出的文件名。

      ei 用来枚举场景,需要注意的是:由于这个函数是由系统调用的,所以这个参数是系统传递的,不      用去思考怎么实现ExpInterface这个接口。

      i 提供一个用来调用3ds Max方法的指针,可以把它视作一个指向3ds Max的指针。同样,这个指针      也是由系统传递的。

      剩下两个参数暂不关心。

      现在来研究ei和i两个参数:

      class ExpInterface仅包含一个成员:IScene *theScene。这样的类设计的简直是“无耻”。再去研究IScene吧。IScene中有一个很重要的函数:

virtual int EnumTree( ITreeEnumProc *proc )=0;

       根据SDK的说明,该函数的功能是:用来枚举场景中的每个INode。因此需要一个ItreeEnumProc*作为参数,由于是自己调用整个函数,因而必须自己实现ItreeEnumProc接口,还好这个接口不是很复杂,把这个回调函数实现就可以了:

        virtual int callback( INode *node )=0;

    因为是回调函数,所以node也是系统传递进来的,为了证明这一点,我们可以编程实验一下:

class MyEnumProc: public ITreeEnumProc
...{
public: int callback( INode *node )
    ...{
             int a = 0;//在这里设置断点
             return a; 
        }
}

在MyExport中添加这一个成员变量:
MyEnumProc MyProc;

int    MyExport:oExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)
...{
        /**//*在函数中添加这行代码*/
    ei->theScene->EnumTree(&this->MyProc );

    if(!suppressPrompts)
        DialogBoxParam(hInstance, 
                MAKEINTRESOURCE(IDD_PANEL), 
                GetActiveWindow(), 
                MY5OptionsDlgProc, (LPARAM)this);
    return FALSE;
}

     调试这个例子,在场景中绘制三个立方体,可以看到系统会调用callback三次,这说明一个物体就是一个Node。那么怎么来导出一个Node几何信息呢?看下面这个代码。

public: int callback( INode *node )
...{
    Object *lobj;
    lobj = node->GetObjectRef();
    if (lobj->SuperClassID()== GEOMOBJECT_CLASS_ID)
    ...{
        GeomObject* gobj = (GeomObject*)lobj;
        Class_ID triID = Class_ID(TRIOBJ_CLASS_ID,0);
        Class_ID boxID = Class_ID(BOXOBJ_CLASS_ID,0);      
        if (lobj->ClassID()==boxID)
        ...{
            if (lobj->CanConvertToType(triID))
            ...{

                TriObject *triobj = (TriObject *)lobj->ConvertToType
                    (0,triID);
                Mesh mesh = triobj->mesh;
                int numVerts = mesh.getNumVerts();
            }

            IParamArray* array = lobj->GetParamBlock();
            float length = 0.0f;
            float height = 0.0f;
            float width = 0.0f;
                
           array->Getvalue( lobj->GetParamBlockIndex(BOXOBJ_LENGTH),
                0, length, FOREVER);
           array->Getvalue( lobj->GetParamBlockIndex(BOXOBJ_HEIGHT),
                0, height, FOREVER);
           array->Getvalue( lobj->GetParamBlockIndex(BOXOBJ_WIDTH),
                0, width, FOREVER);
        }
}
   

首先node->GetObjectRef()会返回这个节点的物体引用。关于ObjectRef有一套几何流水线的说明,这里实在是没功夫写了。接着首先判断这物体的SuperClassID是否为GEOMOBJECT_CLAS- S_ID,如果是,则再看它是否能转换为TriObject,即由三角形组成的物体,至于为什么要这样转,我只能说只有这个类可以返回一个Mesh,而通 过Mesh能够获得诸如顶点,法线,面等一般3D程序所需要的几何信息(这里只获取了该Mesh的面的个数)。当然,对于一个Box,我们可能只想获得它 的长宽高,所以,代码中又提供了另一个方法来返回其几何信息。

      虽然只实现了这么短的代码,但却花了数天的时间,主要对3ds Max的结构不熟悉,加上SDK写得真叫一个乱,还好总算有些进展,正所谓万事开头难。下一步将研究如何导出场景的光照,物体的纹理贴图等信息。

      这里介绍几个网站,感谢它们对我提供的帮助:

      http://www.cgsir.com
      http://discussion.autodesk.com/index.jspa
      也希望看到这篇文章的人能够给予指正,毕竟我是刚开始学3ds Max plug-in.

 楼主| 发表于 2007-11-7 10:31:36 | 显示全部楼层
这几天研究了一下UI部分的制作方法,感觉要了解的东西挺多,加上时间不允许,我本着“够用就行”的心态学习了两种向3ds Max添加自己控件的方法。一种是添加RollupPage,一种是添加ToolBar。因为没有深入研究,我只能做到添加简单的控件,实现基本交互功能,这里介绍一下,希望大家指点:(程序截图在附件中)
一. 添加RollupPage的方式:
可以利用ApplicationWizard生成Utility程序,查看工程资源可以看到一个对话框:
    1.jpg
该对话框的静态文本部分使用StaticEdit控件,交互部分使用的是VC自带的CustomCtrl控件。需要注意的是:一定要指定CustomCtrl的class,例如,如果控件用作表示文本输入框,则class的类型为:CustEdit。如果为按钮,则类型为:CustButton。几个常用的控件类型如下:
Custom Edit control - CustEdit
Custom Spinner Control - SpinnerControl
Custom Button control - CustButton
Custom Toolbar control - CustToolbar
Custom Image control - CustImage
Custom Status control - CustStatus
Color Swatch control - ColorSwatch
Custom Rollup Window - RollupWindow
Custom DragAndDrop Window control - DragDropWindow
Custom TCB Graph – TCBGraph
下面再来看看程序部分:
首先要实现下面这个函数:
void RollupPageTest::BeginEditParams(Interface *ip,IUtil *iu) 
{
this->iu = iu;
this->ip = ip;
hPanel = ip->AddRollupPage(
hInstance,
MAKEINTRESOURCE(IDD_PANEL),
RollupPageTestDlgProc,
GetString(IDS_PARAMS),
0);
}
这里的RollupPageTest继承于UtilityObj,我们可以使用UtilityObj来创建自己的Utility。
而我们需要实现的虚函数BeginEditParams,当用户选择使用该插件时,系统就会掉用这个函数,在这里该函数用来向command anel添加RollupPage。代码”hPanel = ip->AddRollupPage(…);”用来实现这样的功能,注意其中的参数RollupPageTestDlgProc,这是一个函数的入口地址,也就是一个函数名,主要用来提供控件的消息回调函数。
另一个需要实现的函数是:
void RollupPageTest::EndEditParams(Interface *ip,IUtil *iu) 
{
this->iu = NULL;
this->ip = NULL;
ip->DeleteRollupPage(hPanel);
hPanel = NULL;
}
这个函数在用户结束使用控件的时候调用,需要执行一些收尾和释放的工作。
上面这两个函数ApplicationWizard会为你填充好。所以你不用亲自去编写代码。我们只需了解它的原理就行。如果这时候调试插件,可以发现在Utility里面已经多了一个插件,但是没有任何信息交互的功能。
所以现在需要添加消息回调函数:
static BOOL CALLBACK RollupPageTestDlgProc(
HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_INITDIALOG:
theRollupPageTest.Init(hWnd); //当控件初始化的时候调用。
break;
case WM_DESTROY:
theRollupPageTest.Destroy(hWnd);
break;
case WM_COMMAND:
switch (LOWORD(wParam)) 
{
/*表示如果选择了IDC_CUSTOM_BUTTON
该控件为我用custom ctrl创建的按钮控件*/
case IDC_CUSTOM_BUTTON:
char buf[20];
/*获取插件中的文本框控件*/
ICustEdit* iEditTest = GetICustEdit( GetDlgItem(hWnd,IDC_EDIT) );
/*获取文本框中的文本信息*/
iEditTest->GetText(buf,20);
/*显示文本信息*/
MessageBox(NULL,buf,NULL,MB_OK);
ReleaseICustEdit(iEditTest);
break;
}
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MOUSEMOVE:
theRollupPageTest.ip->RollupMouseMessage(hWnd,msg,wParam,lParam); 
break;
default:
return FALSE;
}
return TRUE;
}
该代码的说明见注释,程序结果如图:
     2.jpg
添加RollupPage的方式比较简单,而且有向导生成,比较方便。

二.
添加ToolBar的方式:(声明这里使用了OSGExp的代码和资源)
首先还是利用ApplicationWizar来生成一个Utility。然后在BeginEditParams里添加这样的代码:
this->iu = iu;
this->ip = ip;
/* 返回MAX的窗口句柄*/
HWND hParent = ip->GetMAXHWnd();
/*创建一个独立的Frame,其中包含工具条,菜单,命令面板等等*/
HWND hWnd = CreateCUIFrameWindow(hParent, _T("My Export Toolbar"), 0, 0, 100, 100);
/*获取所创建的Frame指针 */
ICUIFrame* iFrame = ::GetICUIFrame(hWnd);
/*设置frame的内容类型*/
iFrame->SetContentType(CUI_TOOLBAR);
/*设置位置类型*/
iFrame->SetPosType(CUI_HORIZ_DOCK | CUI_VERT_DOCK | CUI_FLOATABLE | CUI_SM_HANDLES);
/*创建一个ToolBar窗体*/
HWND hToolbar = CreateWindow(
    CUSTTOOLBARWINDOWCLASS,
NULL,
WS_CHILD | WS_VISIBLE,
0, 0, 250, 100,
hWnd,
NULL,
hInstance,
NULL);
/*讲ToolBar控件和窗体连接起来*/
ICustToolbar *iToolBar = GetICustToolbar(hToolbar);
/*This method links this toolbar to the CUI frame whose window handle and message handler are passed.*/
iToolBar->LinkToCUIFrame(hWnd, NULL);

/*这两个估计是设置外形的,不深究*/
iToolBar->SetBottomBorder(FALSE);
iToolBar->SetTopBorder(FALSE);
/* 创建一个新的消息处理器*/
tbMsgHandler = new TBMsgHandler(this);
/*装载这个消息处理器*/
iFrame->InstallMsgHandler(tbMsgHandler);
/*以下代码向工具栏添加图标和按钮*/
// Get the 0th icon from the CUITest icon file.
// Note that the index is 1-based.
MaxBmpFileIcon* pIcon = new MaxBmpFileIcon(_T("osgexp_simple"), 1);
iToolBar->AddTool(TBITEM(CTB_PUSHBUTTON, pIcon, ID_TB_0));
pIcon = new MaxBmpFileIcon(_T("osgexp_simple"), 2);
iToolBar->AddTool(TBITEM(CTB_PUSHBUTTON, pIcon, ID_TB_1));
// Add a separator
iToolBar->AddTool(ToolSeparatorItem(8)); 

pIcon = new MaxBmpFileIcon(_T("osgexp_simple"), 3);
iToolBar->AddTool(TBITEM(CTB_PUSHBUTTON, pIcon, ID_TB_2));
pIcon = new MaxBmpFileIcon(_T("osgexp_simple"), 4);
iToolBar->AddTool(TBITEM(CTB_PUSHBUTTON, pIcon, ID_TB_3));
// Add a separator
iToolBar->AddTool(ToolSeparatorItem(8)); 
pIcon = new MaxBmpFileIcon(_T("osgexp_simple"), 5);
iToolBar->AddTool(TBITEM(CTB_PUSHBUTTON, pIcon, ID_TB_4));
pIcon = new MaxBmpFileIcon(_T("osgexp_simple"), 6);
iToolBar->AddTool(TBITEM(CTB_PUSHBUTTON, pIcon, ID_TB_5));
SIZE sz; RECT rect;
iToolBar->GetFloatingCUIFrameSize(&sz);
rect.top = 100; rect.left = 100;
rect.right = rect.left+sz.cx; rect.bottom = rect.top+sz.cy;
GetCUIFrameMgr()->FloatCUIWindow(hWnd, &rect);
MoveWindow(hWnd, rect.left, rect.right, sz.cx, sz.cy, TRUE);
// We are done, release the toolbar and frame handles
ReleaseICustToolbar(iToolBar);
        iToolBar = NULL;
ReleaseICUIFrame(iFrame);


工具栏按钮和函数TBITEM的定义:
#defineTBITEM(type, pIcon, cmd)ToolButtonItem(type,pIcon,GetCUIFrameMgr()->GetImageSize(),\GetCUIFrameMgr()->GetImageSize(),GetCUIFrameMgr()->GetButtonWidth(),\GetCUIFrameMgr()->GetButtonHeight(),cmd,0)

#define ID_TB_0 10000
#define ID_TB_1 10001
#define ID_TB_2 10002
#define ID_TB_3 10003
#define ID_TB_4 10004
#define ID_TB_5 10005

消息处理器的实现:
class TBMsgHandler : public CUIFrameMsgHandler {
MyUtilityToolBar *tb;
  public:
TBMsgHandler( MyUtilityToolBar *tb) { this->tb = tb; }
int rocessMessage(UINT message, WPARAM wParam, LPARAM lParam);
};
接着填充消息相应函数:
int TBMsgHandler:rocessMessage(UINT message, WPARAM wParam, LPARAM lParam) {
BOOL suppressPrompts = TRUE;
DWORD MAXOptions = 0;
switch(message) 
{
case WM_COMMAND:
switch (LOWORD(wParam)) {
case ID_TB_0:
MessageBox(NULL,"0",NULL,MB_OK);
break;
case ID_TB_1:
MessageBox(NULL,"1",NULL,MB_OK);
break;
case ID_TB_2:
MessageBox(NULL,"2",NULL,MB_OK);
break;
case ID_TB_3:
MessageBox(NULL,"3",NULL,MB_OK);
break;
case ID_TB_4:
MessageBox(NULL,"4",NULL,MB_OK);
break;
case ID_TB_5:
MessageBox(NULL,"5",NULL,MB_OK);
return TRUE;
default: 
return FALSE;
}
}
return FALSE;
}
这里将消息响应处理做了很大的简化,实现的功能是点击按钮会显示相应的数字。编译调试,可以看到新生成的工具栏:
     3.jpg
虽然功能实现的较简单,但学习的过程却不轻松,还是一句话,请大家多给批评指教。
(另外,怎么在帖子中添加图片啊?谁能教我一下)

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-5-6 11:26

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表