作者 任晓宇
1 基本概念
1,Morpher Modifier
在3dsmax中,使用Morpher Modifier可以改变mesh, patch, NURBS model的形状,同时支持Material morphing,通常用来实现复杂的表情动画。
为Mesh添加Morpher Modifier:选择一个mesh->Modify面板->Modifier List->Morpher。
2,Channel
每个channel对应一个不同的Mesh,代表一种表情。
通过指定在不同关健帧时每个channel的权重,来实现不同表情的组合过渡。
2 准备工作
3dsmax6 sdk并没有暴露访问morpher modifier的接口,但是我们可以在3dsmax6/maxsdk/samples/modifiers/morpher/wm3.h中找到它的定义和ClassID:
#define MR3_CLASS_ID Class_ID(0x17bb6854, 0xa5cba2a3)
…
class MorphR3 : public Modifier, TimeChangeCallback
{
…
};
将此文件include到你的工程中就可以访问morpher modifier接口了。注意,把下面两行代码注释才可以联接通过:
…
//static GetMorphMod theModPickmode;
…
//static GetMorphNode thePickMode;
3 实现代码
1,得到morpher modifier
Modifier *wxFindMorpherModifier(INode *pINode)
{
#if MAX_RELEASE >= 4000
// get the object reference of the node
Object *pObject;
pObject = pINode->GetObjectRef();
if(pObject == 0) return 0;
// loop through all derived objects
while(pObject->SuperClassID() == GEN_DERIVOB_CLASS_ID)
{
IDerivedObject *pDerivedObject;
pDerivedObject = static_cast<IDerivedObject *>(pObject);
// loop through all modifiers
int stackId;
for(stackId = 0; stackId < pDerivedObject->NumModifiers(); stackId++)
{
// get the modifier
Modifier *pModifier;
pModifier = pDerivedObject->GetModifier(stackId);
// check if we found the morpher modifier
if(pModifier->ClassID() == MR3_CLASS_ID) return pModifier;
}
// continue with next derived object
pObject = pDerivedObject->GetObjRef();
}
#endif
return 0;
}
2,得到每个channel相对于原始Mesh的变化顶点和偏移量:
MorphR3*pMorpherModifier = static_cast<MorphR3*>( wxFindMorpherModifier(pNode));
if(pMorpherModifier != NULL )
{
intiNumPoses = (int)pMorpherModifier->chanBank.size();
for(int i = 0; i<iNumPoses; ++i )
{
morphChannel&currChannel =pMorpherModifier->chanBank[i];
if( !currChannel.mActive || !currChannel.mActiveOverride ||!currChannel.cblock || !currChannel.mNumPoints )
{
continue;
}
wxPose pose;
//表情的名称
memcpy( pose.chName, currChannel.mName, sizeof(char)*64);
wxPose::wxOffsetVertex offsetVertex;
//遍历所有顶点
for( int iVertexID = 0; iVertexID<currChannel.mNumPoints;++iVertexID )
{
//判断是否有偏移
if(currChannel.mDeltas[iVertexID] == Point3(0,0,0) )
continue;
//保存顶点ID和偏移量
offsetVertex.index =iVertexID;
offsetVertex.pos =currChannel.mPoints[iVertexID] - m_aVertexArray[iVertexID].vPos;
pose.m_VertexOffsetMap.push_back(offsetVertex);
}
3,得到channel的关健帧信息
// 权重的变化范围
float fRange = currChannel.mSpinmax - currChannel.mSpinmin;
Control *Controller = currChannel.cblock->GetController(0);
IKeyControl *pIKeyControl = GetKeyControlInterface(Controller);
// 关健帧数目
int iNumKeys = pIKeyControl->GetNumKeys();
for( int j = 0; j < iNumKeys; ++j )
{
IBezFloatKey FKey;
pIKeyControl->GetKey( j, &FKey);
// 权重
fInfluence = FKey.val / fRange;
// 时间
fTime = FKey.time;
}
4 TODO
1,morphChannel类的数据成员mDeltas,mWeights的具体含义。发现mDeltas里面近似保存了每个顶点的偏移量乘以0.01,有何作用?mWeights按名字理解是顶点权重,不理解。
// Actualmorphable points
std::vector<Point3> mPoints;
std::vector<Point3> mDeltas;
std::vector<double> mWeights;
2,求权重范围的方法。目前的方法是求SpinControl的取值范围:
float fRange = currChannel.mSpinmax - currChannel.mSpinmin;
在实验过程中发现这个范围并不准确,当SpinControl取值范围为{0,100}时,会得到大于100和小于0的数。
3,支持导出material morpher数据。支持material morpher可以做到材质的混合与过渡,实现更逼真的效果。比如在笑的同时脸色变红润,眼角出现皱纹等等。
4,目前我将表情动画的数据序列化到模型格式里,但是3dsmax6不能将morpher modifier编辑的结果存成单独的文件(类似于骨骼动画里的.bip文件),如果一个模型对应多个morph动画文件就不容易序列化了。不知道高版本的max有没有此功能。
5, 各个MorphTarget法向量的计算
for each face....
FaceEx *pFaceEx = pGameMesh->GetFace(faceId);
// 得到位置
int vertexId = pFaceEx->vert[faceVertexId];
Point3 vertex;
if( pGameMesh->GetVertex(vertexId, vertex) )
pVertexData->m_position = vertex;
// 得到法向量
int normId = pFaceEx->norm[faceVertexId];
Point3 normal;
if( pGameMesh->GetNormal(normId, normal) )
pVertexData->m_normal = normal;
// 得到纹理坐标
// mapchannel代表一层纹理坐标, 如果mapchannel个数为1,则直接使用GetTexVertex()函数
// 否则就遍历所有的TextureMap,计算本TextureMap的纹理坐标
if( pGameMesh->GetActiveMapChannelNum().Count() == 1 )
{
int texCoordId = pFaceEx->texCoord[faceVertexId];
Point2 texCoord;
if( pGameMesh->GetTexVertex(texCoordId, texCoord) )
pVertexData->AddTextureCoordinate( texCoord.x, 1.f - texCoord.y );
}
else
{
IGameMaterial *pGameMaterial = pGameMesh->GetMaterialFromFace( pFaceEx );
int iNumTexture = pGameMaterial->GetNumberOfTextureMaps();
for( int i = 0; i < iNumTexture; ++i )
{
IGameTextureMap *pTexture = pGameMaterial->GetIGameTextureMap(i);
TCHAR *tcName = pTexture->GetBitmapFileName();
int iChannel = pTexture->GetMaxTexmap()->GetMapChannel();
DWORD vertexId[3];
Point3 uvVert;
if( pGameMesh->GetMapFaceIndex(iChannel, faceId, vertexId) )
{
if( pGameMesh->GetMapVertex(iChannel, vertexId[faceVertexId], uvVert) )
{
IGameUVGen *pGameUVGen = pTexture->GetIGameUVGen();
GMatrix gmUV = pGameUVGen->GetUVTransform();
uvVert = uvVert * gmUV;
pVertexData->AddTextureCoordinate( uvVert.x, 1.f - uvVert.y );
}
}
}
}
// 得到skin信息
if( pGameMesh->IsObjectSkinned() )
{
IGameSkin *pGameSkin = NULL;
pGameSkin = pGameMesh->GetIGameSkin();
int iBoneID;
float fWeight;
int type = pGameSkin->GetVertexType(vertexId);
if(type==IGameSkin::IGAME_RIGID)
{
iBoneID = pGameSkin->GetBoneID(vertexId,0);
IGameNode *pBoneNode = g_Exporter.GetIGameScene()->GetIGameNode(ULONG(iBoneID));
TCHAR *chBoneName = pBoneNode->GetName();
fWeight = 1.f;
pVertexData->AddInfluence(iBoneID, fWeight);
}
else //blended
{
for(int b=0;b<pGameSkin->GetNumberOfBones(vertexId);++b)
{
iBoneID = pGameSkin->GetBoneID(vertexId,b);
IGameNode *pBoneNode = g_Exporter.GetIGameScene()->GetIGameNode(ULONG(iBoneID));
TCHAR *chBoneName = pBoneNode->GetName();
fWeight = pGameSkin->GetWeight(vertexId,b);
pVertexData->AddInfluence(iBoneID, fWeight);
}
}
}