Shapes in Maya
maya中的shapes是显示在3D中可选择的DAG object。Meshs, Nurbs surfaces, curves,locator仅仅是shape的一部分例子而已。shapes也是DG node,该node可以拥有属性并能连接到其他node。
shapes可以看做是geometry的容器。这个容器的作用就是为交互显示和操作而显示geometry。
shapes既被控制点cvs形成也被其控制。控制点通常是shape的属性,表现为点数组。控制点变成可交互后,就可以选择和并通过和component连接来控制。
Components包含两方面信息,类型和索引或者范围。例如,一个mesh的顶点component。这个component代表了mesh shape的顶点(pnts)属性。vtx[0] 代表了mesh中的顶点0,vtx[0:7]代表了前8个顶点。
定义surface的shape,比如 Nurbs surface和mesh支持硬件材质,交互环境下的硬件光照,还有硬件渲染缓冲区。
- user-defined shapes
自定义shape以及相关的shapes都是为了给自定义的数据一个壳,让其作为一个DAG(DG)节点,或者可以讲数据传送给DG。
MPxSurfaceShape就可以让我们用Maya API来写shape。写shape 节点和写独立节点类似,事实上用户定义的shape(MPxSurfaceShape)继承自MPxNode。
MPxSurfaceShape和MPxNode最大的区别就是额外的component处理,drawing,selection函数。可重载的函数被分成了两个类,DG节点函数都在MPxSurfaceShape,绘制drawing和选择select函数在MPxSurfaceShapeUI中。
用户自定义shape非常适合基于控制点的shape比如polygonal mesh或者基于样条线的物体。但这不意味着你不能写其他种类的shape,仅说明了MPxSurfaceShape是为基于控制点的shape准备的。
注册shape使用MFnPlugin::registerShape函数。此函数和registerNode类似,但包含了一个额外的参数说明了MPxSurfaceShapeUI类的creator。注销函数使用MFnPlugin::deregisterNode,这和MPxNode类的注销类似。
Shape classes
下面列出了你需要继承的代理类:
- MPxSurfaceShape MPxComponentShape(必须): DG node 类
- MPxSurfaceShapeUI(必须): 绘制和选择 drawing selection
- MPxGeometryIterator(可选): component的迭代器。
- MPxGeometryData(可选): 用来将数据传过DG
- MTextureEditorDrawInfo(可选): 被MPxSurfaceShapeUI的drawUV()函数用来说明当前物体的绘制状态。
- MPxSurfaceShape 或者 MPxComponentShape(必须)
- 这个类定义了shape中非UI的部分。这个类的目的就是在Maya的DAG中代表用户定义的geometry。MPxComponentShape类集成自MPxSurface,其支持带component的shape。
- MPxSurfaceShapeUI(必须)
- 这个类定义了shape中drawing和selection的部分。
- MPxGeometryIterator(可选)
- 这个类提供了自定义shape的控制点迭代器。此迭代器查询并设置自定义geometry中的点。当shape中的component和属性相连时,才会使用迭代器。
- MPxGeometryData(可选)
- 此类为你shape的geometry提供了一个容器,其可以在DG连接中使用。将你的geometry的所有信息作为data块传送要比分别传送效率高。如果自定义shape想和maya的deformer交互,你就必须给自定义shape提供data class的输入, attribute作为输出。
- MTextureEditorDrawInfo(可选)
- 此函数被MPxSurfaceShapeUI的drawUV函数使用来说明当前UV的绘制状态。API开发者必须重载MPxSurfaceShapeUI的canDrawUV方法来获得drawUV的回调。
- 此函数被MPxSurfaceShapeUI的drawUV函数使用来说明当前UV的绘制状态。API开发者必须重载MPxSurfaceShapeUI的canDrawUV方法来获得drawUV的回调。
With a shape
写一个shape的目的就是将自己定义的geometry类以DAG 对象的形式整合到maya里面。
将几何相关的信息保存在自己的geometry类中是个好主意。这样将geometry表示成DG data就更容易了。在自己的geometry类中保存几何信息将让绘制drawing更加容易,因为你必须将几何信息作为绘制数据传送给maya的绘制队列。
- 从那开始
- 用户定义的shape要比DG节点复杂,因为必须考虑额外的 drawing,selection,componet函数。在动手写代码前做好设计是好习惯。这包括定义输入input,输出output属性,决定属性间的关系。
- 最简单的shape就是没有component或者geometry数据的,例如,一个圆形输入为半径r,或许还有x,y分量,并从这些数据计算geometry,最后送到openGL渲染。
- 分阶段来写自定义shape也不错。第一步,从MPxSurfaceShape和MPxSurfaceShapeUI。写MPxSurfaceShape和其他MPxNode类一样。额外的虚函数可以作为函数来重载。
- 注册和注销shape
- 注册shape和注册DG 节点类似。唯一的差别是shape有一个单独的UI类,需要和shape节点一起注册。MFnPlugin::resgisterShape函数是用来注册shape的。注销shape和注销节点完全一样。下面是一个例子
MStatus initializePlugin( MObject obj )
{
MFnPlugin plugin( obj,"Autodesk","2.0", "Any");
return plugin.registerShape( "yourShape", yourShape::id,
&yourShape::creator,
&yourShape::initialize,
&yourShapeUI::creator );
}
MStatus uninitializePlugin( MObject obj )
{
return plugin.deregisterNode( yourShape::id );
}
Drawing and refresh
drawing是一个两个步骤的过程。第一步,几何图元和材质被预估,绘制所需的信息都被放到了队列里面。第二步,就是OpenGL绘制阶段。 这两部允许利用maya的多线程绘制来提升性能。
当摄像机变化或者视口被刷新时,就会重绘。当重绘发生时,MPxSurfaceShapeUI::getDrawRequest被调用。这是maya询问哪些数据需要绘制的方式。在这个函数内部,你需要查询绘制状态,那就使用MDrawInfo,然后重建一个新的绘制请求(MDrawRequest)放到队列里面。你可能想在此函数里将多个绘制请求放到队列里,在共享模式下,如果shape被选中,你可能会添加一个渲染有材质物体的命令,另一个请求则要在彩色物体上面绘制线框。
绘制数据MDrawData保存了自定义shape的数据,这些maya并不完全清楚。绘制数据就像一个容器,将自定义shape的数据传到绘制请求队列里。你创建的每个绘制请求,都要添加一个包含几何信息的绘制数据包,此包在MPxSurfaceShapeUI::draw中使用。
为了创建绘制数据,使用MPxSurfaceShapeUI::getDrawData,并使用MDrawRequest::setDrawData将其添加到绘制队列。下面的代码说明了如何创建绘制请求并绘制你的几何体。
void yourShapeUI::getDrawRequests( const MDrawInfo & info,
bool objectAndActiveOnly,
MDrawRequestQueue & queue )
{
MDrawData data;
MDrawRequest request = info.getPrototype( *this );
yourShape* shapeNode = (yourShape*)surfaceShape();
yourGeom* geom = shapeNode->geometry();
getDrawData( geom, data );
request.setDrawData( data );
...
}
绘制请求(MDrawRequest)应该在重载的MPxSurfaceShapeUI::getDrawRequests里面创建。一旦请求被创建,合适的set方法就应该定义什么该被绘制。然后就用MDrawRequestQueue::add将其添加到绘制请求队列中。
maya会处理绘制请求队列,对每个绘制请求都会调用对应的绘制函数。这种情况下,你自定义的MPxSurfaceShapeUI::draw会被调用。
enum {
kDrawVertices, // component token
kDrawWireframe,
kDrawWireframeOnShaded,
kDrawSmoothShaded,
kDrawFlatShaded,
kLastToken
};
- Drawing in shaded mode
- 支持渲染模式显示也是两个步骤。第一个步骤在你的getDrawRequests函数,这里你必须“预估”或者设置材质,这样在绘制时才会显示。第二个步骤发生在draw函数里,你必须设置视口来显示材质。
- 下面的代码显示了如何在getDrawRequests中设置材质,如果绘制请求是shaded显示模式。
-
MDagPath path = info.multiPath(); M3dView view = info.view(); MMaterial material = MPxSurfaceShapeUI::material( path ); material.evaluateMaterial( view, path ); if ( material.materialIsTextured() ) { material.evaluateTexture( data ); } request.setMaterial( material );
- 在你的绘制函数里需要设置场景视口,这样就可以显示材质了。这使用MMaterial::setMaterial。让场景视口显示纹理,使用MMaterial::applyTexture。下面是示例代码:
void yourShapeUI::draw( const MDrawRequest& request,
M3dView & view ) const
{
...
MMaterial material = request.material();
material.setMaterial( request.isTransparent() );
drawTexture = material.materialIsTextured();
if ( drawTexture ) glEnable(GL_TEXTURE_2D);
if ( drawTexture ) {
material.applyTexture( view, data );
}
...
}
- 注意所有的OpenGL调用都必须在M3dView::beginGL()和M3dView::endGL()中间。
Selection
当在maya里面选中一个物体时,就会根据摄像机角度和鼠标位置创建一条选择射线。选择有两个步骤,第一,相交射线检测每个物体的碰撞盒,当和碰撞盒相交时调用物体的select函数。
自定义shape的选择函数是通过重载MPxSurfaceShapeUI的select函数实现的。你必须重载此函数,为特定的选择队列添加对象,MSelectInfo负责提供选择状态信息。
查看apiMeshShape中的apiMeshShapeUI::select和apiMeshShapeUI::selectVertices来了解物体和物体组件的选择。
Components
Components
有些物体需要虚拟反馈和操作物体属性,比如基于控制点的物体就必须把component和attribute关联起来。四边形mesh或者样条线曲面就有控制点,其可被选择和编辑。这些控制点是作为属性存在于物体上的,在maya里面被作为component来显示。
components包含两方面的信息,component的类型和索引值或者范围。一个列子就是,mesh的顶点component vtx[0]就代表了顶点0,vtx[0:7]代表了前8个顶点。
用来创建,编辑,和查询components的类:
- MFnComponent
- MFnDoubleIndexedComponent
- MFnSingleIndexedComponent
- MFnTripleIndexedComponent
componet可以被标记成完全的,表示componet代表全部的索引从0到numElements-1.
- Writing a Component Shape
- 如果你想创建一个支持component的曲面,那么MPxComponentShape就比MPxSurfaceShape好。MPxComponetShape继承自MPxSurfaceShape并提供了操作component的基本功能。
- 如果你想编辑图形上显示界面,请继承MPxSurfaceShapeUI。这个类允许绘制和选择图形中任何componet。
- Mapping attributes to components
- 在maya里,componets是作为字符串的。每类componet都有不同的字符名字。在API层面,componet是根据他们的API类型来区分的(请看MFn.h). 例如,一个mesh的顶点componet在maya里可以被解读为vtx[0],在插件里它是作为一个MObject,其类型为MFn::kMeshVertComponent. 索引信息可以通过继承MFnComponent来抽取。
- 想将一个component和自定义shape的attribute相连,你需要选maya已经存在的componet类型,重载MPxSurfaceShape::componetToPlugs来将componet类型转换成plugs。
- 下面是一个列子,将mesh的顶点componet和shape的mControlPoints attribute相连。
-
void yourShape::componentToPlugs( MObject& component,
MSelectionList& list )const
{
if ( component.hasFn(MFn::kMeshVertComponent) ) {
MFnSingleIndexedComponent fnVtxComp( component );
MObject thisNode = thisMObject();
MPlug plug( thisNode, mControlPoints );
int len = fnVtxComp.elementCount();
for ( int i = 0; i < len; i++ ) {
MPlug vtxPlug = plug.elementByLogicalIndex(
fnVtxComp.element(i) );
list.add( vtxPlug );
}
}
}
- Component matching
- 属性可以在mel里面声明为字符串。自定义shape必须确保这些字符串和实际物体,索引对应起来。MPxSurfaceShape::matchComponent就是这个目的。
-
virtual MatchResult matchComponent( const MSelectionList& item,
const MAttributeSpecArray& spec,
MSelectionList& list );
- 此方法从组件的字符串名字确保物体存在,并将对应的物体添加到选择链中。选择命令比如select shape1.vtx[0:7]被确认后,对应的component被添加到了选择链里面。
- 属性规范(MAttributeSpec)是一个提供获取属性描述信息的类。其包括属性名,索引,范围。
- Componet iteration
- 想要maya能get和setcomponent的位置,就要重载MPxGeometryIterator为自己的图形定义迭代器。图形迭代器是在物体被选中后,进行平移、缩放、旋转操作时这些控制器用来控制自己位置的。形变也同样需要图形迭代器,这需要重载setPoint和point这两个方法。一般情况,你需要重载MPxGeometryIterator的方法如下:
-
MPxGeometryIterator( void * userGeometry,
MObjectArray & components );
MPxGeometryIterator( void * userGeometry,
MObject & components );
virtual void reset();
virtual MPoint point() const;
virtual void setPoint( const MPoint & ) const;
- 你必须重载MPxsurfaceShape的方法,将迭代器和自定义图元连接起来。
-
virtual MPxGeometryIterator*
geometryIteratorSetup( MObjectArray&, MObject&, bool );
virtual bool acceptsGeometryIterator( bool writeable );
virtual bool acceptsGeometryIterator( MObject&,
bool, bool );
- components的平移、缩放、旋转工具
- 为了支持平移、旋转、缩放工具,你要重载MPxSurfaceShape::transformUsing方法。其参数是component的矩阵和数组。矩阵就是实际应用的矩阵,component说明了变换使用到的属性索引。
- 有大量控制点的模型,不要过分依赖maya的计算机制来设置属性值。这种情况下可以直接用MPxNode::forceCache来直接获取节点的datablock,不通过计算直接设置节点的属性。需要注意的是修改属性后,相关联的属性也会被更新。例如,如果一个mesh的顶点变了,法线和包围盒子一样要变。如果想要移动工具正常工作就要重载vertexOffsetDirection。
Tweaks and internal attributes
有输入历史的图形,例如,图元数据由另一个节点提供,就需要存储顶点的偏移或者扭曲。输入图元每次变化都要重新计算图形。tweak可以存储为单独属性,这样扭曲就可以被添加到形成最终输出的输入顶点上了。
如果没有输入历史,就不必另存tweak扭曲。顶点移动就可以直接应用到输出曲面。
通过MFnAttribute::setInternal可以标记属性为内部的。这样就可以重载setAttr和getAttr,用完全不同的方法来处理tweak,就看有没有输入历史了。
注意:用不用内部属性完全取决于你。使用内部属性不是处理tweak的唯一方法。
输入历史意味着其他节点给你的图形提供输入数据了。创建节点是独立的,其负责创建图形的特殊数据类型。典型的,一个图形有一个或者多个创建节点。例如,一个polygonal图形有分别创建圆形和立方体的节点。请查看apiMeshCreator中的创建节点。
Geometry data
Geometry data
为了支持set和形变,自定义图形要能通过属性连接传输自定义数据类型。想要为自己的图形定义点数据,要从MPxGeometryData继承一个新类。其和MPxData很像,但包含支持set(group)和component迭代器的方法。
MPxGeometryData的作用就是为你的图形数据提供一个壳,这样它就可以像其他数据一样在DG连接中传输。
如果你的图元数据对应的迭代器,那么将迭代器和你的数据关联起来就可以支持maya的形变。
想完成上面的功能需要重载下面的函数。
virtual MPxGeometryIterator* iterator( MObjectArray&
MObject&, bool );
virtual MPxGeometryIterator* iterator( MObjectArray&,
MObject&,
bool, bool );
File IO
没有自动数据的图形就不用操心文件IO了。默认情况,节点上的属性都会存下来,如果你将其设为可存储的话(这是默认设置)并且这些属性没有输入连接。如果你想选择性地保存,就重载MPxNode的shouldSave()方法。
如果你定义了一种新数据类型,就重载MPxData的writeASCII(), writeBinary()吧,不然到文件IO就废了。
对于支持tweak属性的图形(比如在输入历史上加偏移量),那你就得说清楚哪些数据该存下来了。
Deformers
变形器作用于图形上的控制点,这些有component的图形必须定义控制点属性。
为了支持maya的变形器,需要提供如下内容:
- MPxGeometryData 继承其类,封装自己的图元。
- localShapeInAttr函数
- locakShapeOutAttr函数
- worldShapeOutAttr函数
- MPxGeometryIterator 自己的图元要有一个
- match函数
- createFullVertexGroup函数
- geometryData函数
- MPxGeometryData
- 想要变形器能干活,就必须给自己的数据找个壳封装一下。
- localShapeAttr
- 必须重载此函数来返回与输入历史对应的属性。这个属性的类型要和自己图元类型一样
- localShapeOutAttr
- 必须重载此函数以返回和输出图元对应的属性。属性类型要和自定义的一致。
- wordShapeOutAttr
- 必须重载这个函数来返回属性数组,此数组代表了你输出几何图元的实例。属性类型必须和自定义的一样。数组中的每个元素代表了图形的一个实例。
- MPxGeometryIterator
- 点数据是通过你定义并实现的迭代器来操控的。
- match
- 必须重载这个函数来比对选中的元素类型和实际物体的类型。这被set和deformer使用来确保选中的元素落入vertex这个类型。
- createFullVertexGroup
- 当需要创建一个包含所有顶点的component时,maya会用到此函数。如果你对整个物体变形就会用到它。在object模式下选择模型,并给他添加一个变形器。
- geometryData
- 此函数该为surface返回输入数据。maya会在内部调用此函数来获取模型的group信息。
Marking Menus
MPxSurfaceShape物体可以有对应的菜单。可以利用mel为node定义菜单的函数来实现,创建格式: $nodeName + "DagMenuProc" . 例子如下:
global proc apiMeshDagMenuProc( string $parent, string $child )
{
setParent -m $parent;
// Simple marking menu with 3 items at
// N north, W west and E east.
string $cmd = "hide " + $child;
menuItem -l "Hide" -rp "N" -c $cmd;
$cmd = "boundingBoxDisplayCtrl(1,\"\");";
menuItem -l "Show bounding box" -rp "W" -c $cmd;
$cmd = "boundingBoxDisplayCtrl(0,\"\");";
menuItem -l "Hide bounding box" -rp "E" -c $cmd;
}
执行上面的代码,然后在自定义物体上右键,就可以调用菜单命令。
ps: 中午老犯困,翻译点东西换下脑子,水平有限,欢迎讨论。