遍历节点对象
1.Scene系统遍历
class MyTreeEnum : public ITreeEnumProc
{
public:
MyTreeEnum(void) = default;
~MyTreeEnum(void) = default;
public:
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()函数调用EnumTree() 方法
将场景枚举树与导出插件接口相连接,通过DoExport连接他们
DoExport接口是创建File Export 模板,其中自带ExpInterface,可以使用场景遍历。实际上我们可以自己封装一个ExpBase 类将ExpInterface 和Interface 保存起来供自己调用
int MaxExportTest::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)
{
MyTreeEnum tempProc;
ei->theScene->EnumTree( &tempProc );
return TRUE;
}
相关原理:
先来找到Class IScene
再来具体看看EnumTree() 函数
可以看出,这个函数会被系统在合适的时候调用,我们只要给予参数即可(具体可以看下面的例子)。它会枚举场景中的每个结点。对每个结点,它再调用ITreeEnumProc *proc ,这个proc就是用来解析每个结点的东西。
而ITreeEnumProc 接口中有一个callback (pure virtual),该函数参数是INode *node ,其中node就是我们需要的对象,这个函数会让系统传给你你要的node。而我们只要实现这个callback函数即可。如上面所说的,node包含了一个节点的所有几何信息,渲染信息等等
2.手动INode遍历
实际上我们一般更加采用自由一点的INode遍历方式。
INode中和遍历有关的接口如下:
INode::IsRootNode() - determines if a node is the root node (does not have a parent node).
INode::GetParentNode() - returns the parent node of a child.
INode::NumberOfChildren() - gets the number of children of the node.
INode::GetChildNode() - returns an INode pointer to the nth child.
Interface::GetRootNode()
因此只要有模板中给的Interface 指针,就可获得根节点,从而进行递归遍历。
例如:
int skeletal_animation_export::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)
{
INode* node = i->GetRootNode();
DfsSceneTree(node, tempProc);
}
void DfsSceneTree(INode* node, MyTreeEnum& tempProc)
{
if (node == nullptr) return;
if (node->EvalWorldState(0).obj)
{
tempProc.callback(node);
}
auto num = node->NumberOfChildren();
for (int i = 0; i < num; i++)
{
DfsSceneTree(node->GetChildNode(i), tempProc);
}
}
其中callback就是我们遍历到具体节点要写的逻辑,比如判断该节点类型。
int MyTreeEnum::callback(INode *node)
{
if (exportSelected && node->Selected() == FALSE) return TREE_CONTINUE;
ObjectState os = node->EvalWorldState(0);
if (os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID, 0)))
{
TriObject* tri = dynamic_cast<TriObject*>(dynamic_cast<GeomObject*>(os.obj->ConvertToType(0, Class_ID(TRIOBJ_CLASS_ID, 0))));
WriteFileExportPath(AddExportPathSuffix(_exportPath, _dotPos, "AllTriobjectName"), node->GetName());
_cprintf("TriobjectName %s\n", node->GetName());
if (CheckBone(node))
{
MeshExport inodeDataExport;
}
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:
WriteFileExportPath(AddExportPathSuffix(_exportPath, _dotPos, "AllCameraName"), node->GetName());
_cprintf("Camera %s\n", node->GetName());
break;
case LIGHT_CLASS_ID:
WriteFileExportPath(AddExportPathSuffix(_exportPath, _dotPos, "AllLightName"), node->GetName());
_cprintf("Light %s\n", node->GetName());
break;
}
}
return TREE_CONTINUE;
}
同样的,我们也可以自己封装保存模板中的两个接口。
IO流导出文件
实际上导出文件的数据是由cpp提供的,而不是SDK提供的。
以下是测试代码:
测试过程中直接在callback 回调过程中导出文件测试
class MyTreeEnum : public ITreeEnumProc
{
public:
MyTreeEnum(void) = default;
~MyTreeEnum(void) = default;
public:
int callback(INode *node);
};
int MyTreeEnum::callback(INode *node)
{
std::ofstream ofs;
ObjectState os = node->EvalWorldState(10);
if (os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID, 0)))
{
ofs.open("D:\\outputResults\\TRIOBJECT.txt", std::ofstream::out|std::ofstream::app);
ofs << node->GetName() << "\n";
ofs.close();
_cprintf("TRIOBJECT %s\n", node->GetName());
Mtl *pMtl = node->GetMtl();
if (pMtl)
{
ofs.open("D:\\outputResults\\MATERIAL.txt", std::ofstream::out|std::ofstream::app);
ofs << pMtl->GetName() << "\n";
ofs.close();
_cprintf("MATERIAL %s\n", pMtl->GetName());
}
return TREE_CONTINUE;
}
if (os.obj)
{
switch (os.obj->SuperClassID())
{
case CAMERA_CLASS_ID:
ofs.open("D:\\outputResults\\CAMERA.txt", std::ofstream::out|std::ofstream::app);
ofs << node->GetName() << "\n";
ofs.close();
_cprintf("CAMERA %s\n", node->GetName());
break;
case LIGHT_CLASS_ID:
ofs.open("D:\\outputResults\\LIGHT.txt", std::ofstream::out|std::ofstream::app);
ofs << node->GetName() << "\n";
ofs.close();
_cprintf("LIGHT %s\n", node->GetName());
break;
}
}
return TREE_CONTINUE;
}
int skeletal_animation_export::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)
{
#pragma message(TODO("Implement the actual file Export here and"))
AllocConsole();
_cprintf("doing DoExport");
MyTreeEnum tempProc;
ei->theScene->EnumTree(&tempProc);
return true;
}
SDK基本公共类的继承关系图
如下图只是一个基本的继承关系,其中会有一些小出入,但是可以作为整体框架的一个参考。
大多数SDK类源自三个抽象基类,这三个基类的根被称作动作类(Animatable)、该类定义了大多数动画和追踪视图的相关方法。索引需求(ReferenceMaker)就是源自Animatable。这个类允许检索其他物体。索引目标(ReferenceTarget)继承索引需求(ReferenceMaker)。索引(reference)是介于场景与物体之间的双向链接。
获取INode附着的对象
参考文档1:文档讲述的是3dsMax的管道架构
参考文档2:文档讲述的是INode节点返回值随实际情况改变的相关接口
The INode::EvalWorldState() method should be called when a developer needs to work with an object that is the result of the node's pipeline. This is the object that appears in the scene. This may be an object that is not referenced - it may just be an object that has flowed down the pipeline.
INode::EvalWorldState 函数获取节点INode的状态ObjectState ,而ObjectState 中包含Object* obj 。
如果我们单纯要得到管道上最终的Object的状态,只要调用INode->EvalWorldState()就可以,然后再判断Object 可以转换的派生类类型
virtual Object* GetObjectRef()
Returns the object that this node references unless the node has been bound to a Space Warp. In that case this method will not return the WSM derived object even though the node's object reference points at it. Instead it will return the item that the WSM derived object references. Thus, this method will always return essentially the object space portion of the pipeline
如下图:
注:这里的Base Object 是概念,不是BaseObject 类,而经过OS(Object Space)修改器的以Derived Object 表示的概念对应IDerivedObject
GetObjectRef() 接口当未经过WSM(World Space Modifer)时,返回的是INode 节点直接相连的Object
而一旦发生了WSM,此时虽然该指针直连着的仍然是Derived Object ,但是此时返回的不是Derived Object ,而是WSM derived Object 引用的对象
而上面的图发生的并不是WSM变化,因此调用GetObjectRef() 的时候此时对应返回仍然是直接连接的Derived Object
因此此时获得INode附着对象的方法二为:
INode->GetObjectRef()可以得到这个物体的Object。
Object->SuperClassID() == GEN_DERIVED_CLASS_ID的话,就表示这个Object是一个IDerived Object。
DerivedObject->GetObjectRef()得到Derived Object的基类指针
virtual SClass_ID | SuperClassID ()=0 |
---|
| This method returns a system defined constant describing the class this plug-in class was derived from. |
而真正调用WSM变化后的管道图如下所示:
此时我们如果调用INode::GetObjectRef 返回的就不是WSM Derived Object 了,而是WSM Derived Object 引用的对象Base Object
而我们此时如果想要获得WSM Derived Object 则应该调用INode::GetObjOrWSMRef ,该函数返回的仍然是直接相连的Object
WSM和OSM结合的管道图如下:
INode中变换矩阵相关说明
参考文献:
- 3dsmax2017HELP
- 3Ds Max 骨骼动画导出插件的开发
Tips:虽然应该看对应的2012的SDK文档,但是12的文档看的头大和眼睛难受,新版本的文档代码有高亮整体颜色比较柔和以及字体比较符合习惯,同时新文档在api上的解释上一定程度上优于老文档,因此其实可以先看新文档找到相关内容再看老文档。
NodeTM()只包含了INode的TM,而不是物体的TM。每个INode都有个基准点(Pivot Point),该Pivot Point在世界坐标中的状态,就是这个INode的TM,即INode相对世界的TM。而INode上的Object相对这个Pivot Point可能会有其他的变换(如平移,旋转等)。因此NodeTM实际上不能用来变换Object。
获取父亲节点的TM。上个函数阐述NodeTM处于世界坐标系中。因此要想得到当前节点相对父节点的TM就需要GetNodeTM()*Inverse(GetParentTM())
返回的矩阵可以将Object中的坐标变换到世界坐标。相当于GetNodeTM()*(Object相对INode的TM) (因为INode和Object之间还可能存在偏移变换矩阵)
当对象使用的是世界空间修改器(WSM ——WorldSpaceModifer )的时候,对象的坐标会被转换为世界坐标,因此对象的transform也会被初始化为单位矩阵。
而如果对象只使用了对象空间修改器,那么对象的tramsform不会被初始化为单位矩阵。
在调用该函数时会先调用BaseObject::Display() 方法检测对象是否已经被进行了世界空间修改器使用的标志位,再来决定具体返回的是修改前的矩阵还是修改后的单位矩阵。
但是存在一些当已经完成WSM后但是仍需要获得之前变换矩阵的情况,3dsMax提供了INode->GetObjectTMAfterWSM() 和INode->GetObjectTMAfterWSM() 处理这种情况。
- INode->GetObjectTMBeforeWSM()
This method explicitly gets the full node transformation matrix and object-offset transformation effect before the application of any world space modifiers.
该函数返回WSM施加前的INode变换矩阵和Object相对于INode节点的偏移变换矩阵,该作用就相当于获得Object相对世界的矩阵。
- INode->GetObjectTMAfterWSM()
This method explicitly gets the full node transformation matrix and object-offset transformation and world space modifier effect unless the points of the object have already been transformed into world space in which case it will return the identity matrix.
而WorldSpaceModifer 是把Object先变换到世界空间中,因此如果一个INode上的Object已经受到过WorldSpaceModifer 的影响,调用EvalWorldState() 的时候就已经将顶点变换到世界坐标系中了。
调用GetObjectTMAfterWSM() 会返回节点变换矩阵和Object相对节点的变换偏移矩阵以及世界空间修改器的影响,而如果世界空间修改器已经产生影响了,那么返回单位矩阵。因此该函数可以用来检查世界空间修改器是否已经应用到Object上了。
因此如果一个INode上没有WSM,那么GetObjTMAfterWSM() 和GetObjectTM 是相同的。同样,它和GetObjTMBeforeWSM 也是相同。
官方demo:
void Utility::ComputeBBox(Interface *ip)
{
if (ip->GetSelNodeCount())
{
INode *node = ip->GetSelNode(0);
Box3 box;
Matrix3 mat;
Matrix3 sclMat(1);
TimeValue t = ip->GetTime();
Object *obj = node->EvalWorldState(t).obj;
if (node->GetObjTMAfterWSM(t).IsIdentity())
{
mat = Inverse(node->GetObjTMBeforeWSM(t));
}
else
{
mat = node->GetObjectTM(t);
}
AffineParts parts;
decomp_affine(mat, &parts);
ApplyScaling(sclMat, ScaleValue(parts.k*parts.f, parts.u));
obj->GetDeformBBox(t, box, &sclMat);
float sx = box.pmax.x-box.pmin.x;
float sy = box.pmax.y-box.pmin.y;
float sz = box.pmax.z-box.pmin.z;
TSTR title;
title.printf(_T("Result at frame %d"), t/GetTicksPerFrame());
TSTR buf;
buf.printf(_T("The size is: (%.1f, %.1f, %.1f)"), sx, sy, sz);
MessageBox(NULL, buf, title, MB_ICONINFORMATION|MB_OK);
}
}
|