引擎设计跟踪(九.14.2b) 骨骼动画基本完成

时间:2022-09-17 19:41:06

首先贴一个介绍max的sdk和骨骼动画的文章, 虽然很早的文章, 但是很有用, 感谢前辈们的贡献:

3Ds MAX骨骼动画导出插件编写

1.Dual Quaternion

关于Dual Quaternion, 这里不做太详细的介绍了,贴出来几个链接吧:

http://en.wikipedia.org/wiki/Dual_quaternion

http://www.seas.upenn.edu/~ladislav/kavan08geometric/kavan08geometric.pdf

http://www.xbdev.net/misc_demos/demos/dual_quaternions_beyond/paper.pdf

Qual Quaternion可以用两个quaternion以类似二元数的形式表示( dq = p + ε q, ε^2 = 0), 其中,实部用来表示旋转, 虚部可以解出来偏移量. 一个dq可以表示一个不带缩放的刚体变换:

 DualQuaternion(const Quaternion& rotation, const Vector3& translation)
{
p = rotation;
q = reinterpret_cast<Quaternion&>(Vector4(translation,))*rotation*0.5;
}

需要注意的要点是, Dual Quaternion的插值和混合, 也跟quaternion的插值比较类似.

quaternion的一般线性差值不是匀速平滑的, 如果要精确差值的话, 需要用球面线性插值, 但是在变化量比较小的时候, 可以用线性插值作为近似值,不过需要normalize, 即quaternion的nlerp.

与quaternion类似, Dual Quaternion的线性混合(DLB, Dual-quaternion Linear Blending)也可以用在差量比较小的混合, 作为一个近似值. DLB跟nlerp很类似:

(w0*dq0 + w1*dq1 + ... + wn*dqn..) / | w0*dq0 + w1*dq1 + ... + wn*dqn| (w0+w1+...+wn=1表示权重), 即加权混合后单位化.

而与球面线性插值类似, Dual Quaternion也有平滑精确的插值混合方式, 叫做ScLerp, 两个DQ的ScLerp插值可以推广为n个DQ的一般形式, 具体公式我也记不清了, 记得是用次方? 不说了, 内容全在上面那两个paper里.

在shader里为了效率的考量, 使用DLB来混合骨骼的变换.

2.骨骼空间的选择

这个在开始贴的那篇文章里面已经提到了. 这里也谈谈自己的理解.

导出骨骼动画时, 可以导出两种空间:

  • 世界空间(即max里单独模型的世界空间, 在游戏里面将会变成整个角色的局部空间)
  • 局部空间(相对于父骨骼的变换)

对于世界空间的变换, 相当于预计算了每一帧中的骨骼层级关系, 运行时的计算时相对简单, 每个骨头相当于孤立的控制点, 不需要记录骨头的父子关系, 直接把骨头的变换应用到顶点即可.

而导出局部空间变换时, 首先要像更新场景图那样, 从根节点更新到叶子节点, 计算骨头的变换. 更新完以后, 再把最后的骨骼变换应用到顶点.

局部空间骨骼变换的好处是可以方便的进行动画混合. 因为混合的时候父节点位置会改变, 从而影响到子节点. 但是用世界空间变换, 混合动画时, 父节点混合后的变换无法影响到子节点, 所以会有问题.

而对于之前预研过的骨骼变换公式:

引擎设计跟踪(九.14.2b) 骨骼动画基本完成

这个公式中的matrix[i]是骨头的世界空间变换.

可以看出, 世界空间的变换, 效率很高, 甚至可以不要单独保存TPose? 因为从公式上看 matrix[i]*matrix[i]bindpose-1是可以合并到每一帧里,预计算的.

说道T Pose, 因为顶点相对于骨头的位置, 是有一个固定值的, 比如皮肤到骨骼有段距离, 这段距离通常不会跟随骨骼的旋转/移动而改变. 如果把这个初始相对位置"直接"保存的话, 那么对于每个影响到该顶点的骨头都需要保存一个相对的初始位置, 而且, 一个骨头也可能影响到多个顶点, 总的来说数据量会多一点.

所以取一个骨骼动画的初始"顶点"位置(Vbindpose), 作为一个mesh保存, 加上对应的骨骼的初始变换状态:matrix[i]bindpose,一并保存. 这个初始状态就是Binding Pose, 也叫T Pose("T"字形).T pose还会尽量把无关的骨头分的更远, 避免骨头间的相互影响, 方便美术建模.

Vbindpose是初始顶点位置, 是模型的世界空间,

matrix[i]bindpose是世界空间的初始骨骼变换,

这两个值"间接"保存了顶点相对于骨头的初始位置, 即 matrix[i]bindpose-1*Vbindpose, 有了这个相对位置, 再应用上每一帧动画里的骨骼变换, 顶点就会跟着骨头做变换了.

3.导出时遇到的一些问题

去掉骨骼缩放:

因为我这里的骨骼动画不处理缩放的情况, 而有的骨头带缩放, debug时矩阵的数据非常小, 为了避免产生不必要的误差, 去掉骨骼变换的缩放.

         GMatrix tm = node->GetWorldTM();

         //drop scale
Matrix3 m3 = tm.ExtractMatrix3();
m3.NoScale();
tm = m3;

max sdk的问题:

一开始使用INode取出骨骼变换, 结果不对, 因为INode使用的是Max的坐标系, 而我用的是自己的坐标系, 而且已经通过IGame设置好了, 所以正确的做法是用IGameNode来获取GMatrix变换, 而不适用INode的Matrix3.

另外, Gmaxtrix转换到Matrix3之后的坐标系也不一样:

         //!Extract a Matrix3 from the GMatrix
/*!This is for backward compatibility. This is only of use if you use 3ds Max as a coordinate system, other wise
standard 3ds Max algebra might not be correct for your format.
\returns A 3ds Max Matrix3 form of the GMatrix
*/
IGAMEEXPORT Matrix3 ExtractMatrix3()const ;

最大的坑是, GMatrix解出的quaternion的坐标系也是max的坐标系... 但是不像上面那样有清楚的文档注释, 害得被坑了好久.

         //! Return a essential rotation transformation
IGAMEEXPORT Quat Rotation () const;

最大的问题是调试:

因为导出插件需要调试, 而且要用runtime验证结果, 但是runtime也没写好, 也在调试中(-_-!), 所以最终结果渲染不对的时候, 不知道是runtime代码有问题, 还是导出的时候出了问题.

这个没有很好的办法, 只能慢慢看代码, 单步调了.不过有一些方法还是能够帮助定位问题的:

  • 从渲染结果里面能看出很多东西, 比如动画的根节点是横着放的(其他骨骼也不对,而且整体是乱的), 说明旋转有问题, 而且极有可能是导出坐标系的问题.
  • 还有就是, 先改用用最简单的, 只导出骨骼的世界坐标, 并且可以去掉插值, 这样runtime也更简单: 简化调试, 等到结果正确了, 再提高复杂度, 继续调试, 将问题逐个击破(调试中的分治法?).
  • 另外就是一开始先导出简单的模型测试, 这样可以暂时忽略掉其他复杂情况, 针对简单模型测试, 然后在测试复杂的模型.

最后快完成的时候, 遇到一个法线闪烁的问题, 也折腾了好久. 当去掉法线贴图之后就对了, 于是问题也能找到了: TBN quaternion的w保存的是镜像. 当这个quaterion被骨骼动画旋转的时候, w的符号可能会被改变.

所以要预先保存下这个镜像符号. 问题看起来确实很简单, 但是实际中有时候要定位到还是需要格外仔细小心. 下面是shader代码(line 28):

 void MeshVSMain(
float4 pos : POSITION,
float4 tbn_quat : NORMAL0, //ubyte4-n compressed quaternion
float4 uv : TEXCOORD0,
#if defined(ENABLE_SKIN)
uint4 bones : BLENDINDICES0,
float4 weights : BLENDWEIGHT0,
#endif uniform float4x4 wvp_matrix,
uniform float4x4 world_matrix, out float4 outPos : POSITION,
out float4 outUV : TEXCOORD0,
out float4 outWorldPos : TEXCOORD1, #if defined(ENABLE_NORMAL_MAP)
out float3 Tangent : TEXCOORD2,
out float3 BiTangent : TEXCOORD3,
out float3 Normal : TEXCOORD4
#else
out float3 outWorldNormal : TEXCOORD2
#endif
)
{
tbn_quat = expand_vector(tbn_quat);
//tbn_quat = normalize(tbn_quat);
float w = sign(tbn_quat.w); //store sign before transform TBN, or w MAY CHANGE after skinning!
#if defined(ENABLE_SKIN)
skin_vertex_tbn_weight4(pos.xyz, tbn_quat, bones, weights);
//pos.xyz = skin_vertex_weight4(pos.xyz, bones, weights);
#endif
outPos = mul(pos, wvp_matrix);
outUV = uv;
outWorldPos = mul(pos,world_matrix); #if defined(ENABLE_NORMAL_MAP)
//because the quaternion's interpolation is not linear (it is spherical linear interpolation)
//we need to extract the normal, tangent vector before pass to pixel shader. //normal map: extract tbn
Tangent = qmul( tbn_quat, float3(,,) );
Normal = qmul( tbn_quat, float3(,,) ); //tangent space to world space
//note: world_matrix MUST only have uniform scale, or we have to use senmatic T(M-1)
Tangent = normalize( mul(Tangent, (float3x3)world_matrix) );
Normal = normalize( mul(Normal, (float3x3)world_matrix) );
BiTangent = normalize( cross(Normal, Tangent) ) * w;
#else
//vertex normal
//tangent space normal (0,0,1) to object space normal
outWorldNormal = qmul( tbn_quat, float3(,,) );
//then to world space
outWorldNormal = mul(outWorldNormal, (float3x3)world_matrix);
#endif
}

另外还遇到了C++里, 继承多个"空父类"时, MSVC的Empty Base Class Optimization失效的问题, 这个在我的C++博客:
http://hi.baidu.com/crazii_chn/item/5557deb54846b6f162388e30原因是Empty Base Class Optimization在C++11之前都不是标准要求必须的, 所以编译器可以随便搞, 这里只能绕过去了.

4.动画的混合

使用动画树(animation blending tree), 暂时只写了接口和简单实现, 还没有使用和测试.

5.数据的优化

  • 数据格式的选择: 目前位置使用的是Float16, 只保存xyz, 旋转使用的是Int16N, 精度应该满足需求, 如果不满足可以改为Float32, 而单位化的quaternion的w分量可以由xyz求得, 所以也只保存xyz:
         typedef struct BoneTransformFormat : public TempAllocatable
    {
    int16 rotation_i16x3n[];
    fp16 position_f16x3[];
    fp32 time_pos;
    //uint32 frame_id;
    }BT_FMT;
    BSTATIC_ASSERT(sizeof(BT_FMT) == , "size/alignment error!");

    可以看出, 目前单个骨骼的一个关键帧大小16字节. 这个数据只是加载/保存的中间/临时数据, 它会在加载时直接转为Dual Quaternion.

  • 关键帧定义里面, 去掉无用的数据.
    比如frame id, 一开始设计的时候加的, 后来发现没什么用处, 除了调试的时候拿来校验, 所以后来去掉了. 在动画非常多, 帧数非常多的时候, 因为基数很大, 所以减掉一个int也能省非常大的空间.
  • 去掉冗余的关键帧, 思路和参考在千里马干大大的博客, 很早在Azure的博客里也看到过, 后来地址失效了:
    http://www.cnblogs.com/oiramario/archive/2010/12/22/1914120.html原理文中有说明, 前面文章里我也记录过. 比如有A,B,C,三个关键帧, 根据A和C的插值结果, 与B比对, 如果非常近似, 那么可以去掉B.
    在比对的时候, 要比对位置和旋转, 所以我用了两个precision threshold - 角度误差和位置误差, 来相对直观的控制精度. 对于根据骨骼节点深度加大精度误差的做法, 我也尝试了, 感觉差别不是很大.
    代码见下:
    #if OPTIMIZE_FRAME
    //accumulated error
    float accumAngle = Blade::Math::Degree2Radian(mConfig.mAngleError);
    float accumPos = mConfig.mPositionError; float angleThreshold = accumAngle / maxBoneDepth;
    float posThreshold = accumPos / maxBoneDepth; BoneKeyframeList::iterator start = keyFrames.begin();
    for(int i = ; i < mBoneList.size(); ++i)
    {
    size_t keyCount = keyCountList[i];
    BoneKeyframeList::iterator iter = start + ;
    for(size_t index = ; index+ < keyCount; )
    {
    //assert( std::distance(start, iter) == index ); //debug too slow, uncomment if needed
    const KeyFrame& kf = *iter;
    const KeyFrame& prev = *(iter - );
    const KeyFrame& next = *(iter + );
    scalar t = (kf.getTimePos() - prev.getTimePos()) / (next.getTimePos() - prev.getTimePos());
    assert( t > && t < ); //possibly iter position error across two bone key frame sequences
    BoneDQ interpolated = prev.getTransform();
    interpolated.sclerpWith(next.getTransform(), t, true);
    interpolated.normalize(); const BoneDQ& dq = kf.getTransform();
    if( interpolated.getRotation().equal(dq.getRotation(), angleThreshold)
    && interpolated.getTranslation().equal( dq.getTranslation(), posThreshold)
    )
    {
    iter = keyFrames.erase(iter);
    --keyCount;
    }
    else
    {
    ++iter;
    ++index;
    }
    }
    keyCountList[i] = keyCount;
    start = iter;
    }
    #endif

    目前最大累积角度误差默认取的是0.4角度, 最大累积位置误差取的是0.004个单位. 如果太大的话动画感觉很松动不流畅, 动作幅度也变小, 产生严重的失真. 这两个参数可以通过导出界面配置, 不过一般来说, 美术不需要修改.

  • 采样率的选择. max默认的FPS是25, 记得以前看过一篇文章说, 人眼可以观察到的动画,极限FPS是30, 超过30以后, 人眼也看不出差别, 所以高于30没有意义. 我在网上搜到的动画采样配置, 有FPS=12的. 为了减少数据量, 个人觉得15FPS左右就可以, 剩下的数据在runtime插值出来. 除非对动画质量有很高的要求, 才需要使用30FPS. 事实上如果FPS>=30, 那么甚至可以不用runtime插值, 比如之前看到过的某些动画代码, 根本没有插值, 而是将时间直接转为frame id去索引关键帧.

通过以上方法, 之前那个70M可以减到20M的骨骼文件, litch king 阿尔萨斯, 在3ds max中有18195个关键帧. 现在在采样率为25的情况下, 骨骼文件大小为3.9M, 在采样率为15的情况下, 骨骼文件大小为2.9M, 而最终动画效果可以接受.

除此之外, 我们公司的动画, 在某些平台上, 还使用了一种变率(VBR)的浮点压缩方式, 不过没有仔细研究也没去搜paper, 大致原理是根据不同的浮点精度, 使用不同的位数来存放.这个确实蛮屌的,但是有精度损失, 可能会有轻微抖动.Blade暂时不使用这种方式.

6.运行时优化 Runtime Optimization

骨骼动画的计算是渲染中比较耗CPU的部分, 所以优化是必须的, 这是我目前想到的和已经做的优化:

  • 使用SIMD, 这个我是把DirectXMath拿过来, 做了相对的调整, 跟DXTK的SimpleMath一样, 嵌入到已有的数学类里面, 做到无缝接合, 并且可以随时关闭(当然需要重编译). 经过测试, 效率确实要比编译器自动SSE优化的快.
  • 数据紧凑 - Compact Data: 不使用节点, 使用有序数组提高cache效率, 这样更能够发挥SIMD的优势.
    因为骨骼节点嘛, 通常都会先想到Node, 事实上需要传入shader的, 只是一个数组. 通常树的节点遍历, 有大量的间接寻址和函数调用. 使用有序数组的cache效率会更高, 甚至可以把这个计算结果直接传给shader.
    比如Ogre的Bone, 是继承自基类Node, 本身Node类就很庞大复杂, 导致Bone的虽然代码简单, 但是实际上数据很复杂, 有很多冗余成员数据. 关于复用, 我个人觉得, 除了基础代码尽量复用以外, 任何时候, 复用的都最好是接口(设计), 这样才能减少代码的侵入性, 减少掣肘, 使得子模块高度定制, 保证其有简单高效的实现.
    整个骨骼的遍历是要求有顺序的, 如果用树表示的话, 是先根遍历(兄弟之间无所谓), 如果用数组就需要排序.
    还记得数据结构里面的"二叉树的数组表示"法么?
    然而, 即便是数组表示的树, 普通的Node有额外的信息, 比如兄弟指针(索引),父指针(索引)等等, 而传入shdader的BonePalette也只是一个DQ数组(以前喜欢叫bone matrices, 现在用了DQ,虽然代码里面叫BoneDQ, 但Bone Palette是更一般的叫法).
    如果想只使用DQ数组, 就得把父节点这些数据单独分开存放(毕竟这些是写在资源里的固定数据,所以不难), 而且要预先排序, 而不需要显式的父子顺序, 然后按顺序计算更新就可以了.
    因为boneIndices是预生成以后保存在模型/骨骼数据里面的,shader里面要用它索引BonePalette, 所以运行时不能再排序, 否则索引就会出错. 所以这个在导出时排序最好: 把父节点放在子节点之前(兄弟之间无所谓), 这样的顺序不是严格有序, 但是足够满足需求了.
         struct BoneCollector : public ITreeEnumProc
    {
    BoneList& listRef;
    BoneCollector(BoneList& list) :listRef(list){} virtual int callback(INode* node)
    {
    if( IsBoneNode(node) )
    {
    IGameScene* game = ::GetIGameInterface();
    listRef.push_back( game->GetIGameNode(node) );
    }
    return TREE_CONTINUE;
    }
    }collector(mBoneList);
    ei->theScene->EnumTree(&collector); //important: sort bones so that parent comes first, this is an optimization for animation runtime
    struct FnIGameBoneCompare
    {
    //check if rhs is descendant of lhs
    inline bool isDescendant(IGameNode* left, IGameNode* right) const
    {
    while(right->GetNodeParent() != NULL)
    {
    right = right->GetNodeParent();
    if( left == right )
    return true;
    }
    return false;
    } bool operator()(IGameNode* lhs, IGameNode* rhs) const
    {
    if( this->isDescendant(lhs, rhs) )
    return true;
    else
    return false;
    }
    }; std::sort( mBoneList.begin(), mBoneList.end(), FnIGameBoneCompare() );

    运行时, 更新完动画混合/插值以后, 只需要按顺序更新数组就可以了, 有先天的cache优势:

                     //update bone hierarchy & calculate bone transforms
    for(size_t i = ; i < boneCount; ++i)
    {
    mBoneDQ[i].normalize(); //apply hierarchy
    uint32 parent = boneData[i].mParent;
    if( parent != uint32(-) )
    {
    //bones already sorted in linear order (by animation exporter), parent always calculated before children
    assert( parent < (uint32)i );
    //apply hierarchy: //note: parent is already applied inversed binding pose, need to get it back
    const BoneDQ& parentBindingPose = boneData[parent].mInitialPose;
    mBoneDQ[i] = mBoneDQ[parent]*mBoneDQ[i];
    }
    else
    ;//mBoneDQ[i] = mBoneDQ[i];
    } //apply animations
    for(size_t i = ; i < boneCount; ++i)
    {
    //reset bone matrices to init pose (T pose) to prepare animation
    const BoneDQ& tposeDQ = boneData[i].mInitialPose; //note: tposeDQ is normalized after loading and never modified
    //and Inverse(dq) == Conjugate(dq), if dq is normalized
    BoneDQ inversedBindingPose = tposeDQ.getConjugate(); mBoneDQ[i] = mBoneDQ[i]*inversedBindingPose;
    }
    }
  • 单遍遍历: 避免多次内存访问, 因为多以次遍历的话, CPU流水线可能需要reload cache, 这个过程可能要比数学指令慢很多. 这处修改还没有profile, 有空的话去看看这个做法到底对不对.
    现在是One Pass就完成了所有的Bone Palette Update了, 不过有冗余的计算(基于上面代码做了简单修改):

                     //update bone hierarchy & calculate bone transforms
    for(size_t i = ; i < boneCount; ++i)
    {
    mBoneDQ[i].normalize(); //reset bone matrices to init pose (T pose) to prepare animation
    const BoneDQ& tposeDQ = boneData[i].mInitialPose; //note: tposeDQ is normalized after loading and never modified
    //and Inverse(dq) == Conjugate(dq), if dq is normalized
    BoneDQ inversedBindingPose = tposeDQ.getConjugate(); //apply hierarchy & animations
    uint32 parent = boneData[i].mParent;
    if( parent != uint32(-) )
    {
    //bones already sorted in linear order (by animation exporter), parent always calculated before children
    assert( parent < (uint32)i );
    //apply hierarchy: //note: parent is already applied inversed binding pose, need to get it back
    const BoneDQ& parentBindingPose = boneData[parent].mInitialPose;
    mBoneDQ[i] = (mBoneDQ[parent]*parentBindingPose)*mBoneDQ[i]*inversedBindingPose;
    }
    else
    mBoneDQ[i] = mBoneDQ[i]*inversedBindingPose;
    }
    }

    这样, 计算出的结果可以直接丢给shader, 一个动画的所有mesh只需要传一次shader就可以了.
    不过这样做的话, 整个动画的骨骼数量就太受限了. 为了突破骨骼数量限制, 可以像Ogre那样, 对于每个mesh保存一个shader cache, 每个mesh从骨骼的计算结果里复制需要的数据, 传一次shader constant, 这样每个mesh的骨骼数量有限制, 但是整个动画没有了.

  • 在需要的地方加上必要的memory prefetch.

  • 另外还可以考虑多线程, 这个目前没有计划.

7. UI

UI遇到了一些恶心的问题, 主要是之前的UI不满足需求...

proerpty grid + 数据绑定遇到的问题:

动画列表想用下拉框, 但是无法实现. 目前数据绑定的下拉列表选项是固定的, 选中的选项被绑定到类成员数据或者函数上. 但是动画的列表不是静态的, 需要跟绑定对象关联, 才能做到.
这个功能在现有机制上可以添加, 但是不想改UI了, 所以改用其他方式:
用collection的数据绑定, 可以展开多个item, 选中item的事件在编辑端处理, 并发送动画变更给动画组件, 完成动画的切换.

导出动画的配置界面, 也需要复杂的UI. 比如单个动画序列, 需要有名字,起始帧,结束帧, 是否循环等等, 如果要导出多个动画, 现有的UI很难满足需求.目前的workaround是导出多个动画序列时, 使用配置文件...不过由于动画可能是由不同的artist制作的, 而且在游戏开发过程中, 会不停加入新内容, 所以一般最好的方式是一个一个导出, 然后用工具合并, 想到这里, 就暂时没有改动UI了, 勉强先这样用.

trackview - 简单的接口定义. 为了实现UI与具体的逻辑解耦, 即UI可以handle不同类型的数据, 比如以后的过场动画(CutScene即in-game cinematic)的视轨编辑, 做了以下抽象:

     /************************************************************************/
/* */
/************************************************************************/
class ITrack
{
public:
typedef enum ETrackFeature
{
TF_SEEK = 0x00000001,
TF_SETLENGTH= 0x00000002,
TF_KEYFRAME = 0x00000004|TF_SEEK,
TF_ADDKEY = 0x00000008|TF_KEYFRAME,
TF_REMOVEKEY= 0x000000010|TF_KEYFRAME,
}FEATURE_MASK;
public:
/* @brief */
virtual scalar getDuration() const = ;
/* @brief get current play pos */
virtual scalar getPosition() const = ;
/* @brief FEATURE_MASK */
virtual int getFeatures() const = ; /* @brief */
virtual bool play() = ;
/* @brief */
virtual bool pause() = ;
/* @brief */
virtual bool isPlaying() const = ; /* @brief get current animation name, if have any */
virtual const tchar* getCurrentAnimation() const {return NULL;} /* @brief TF_SEEK */
virtual bool setPosition(scalar pos) {BLADE_UNREFERENCED(pos); return false;} /* @brief TF_SETLENGTH */
virtual bool setDuration(scalar length) {BLADE_UNREFERENCED(length); return false;} /* @brief TF_KEYFRAME */
virtual size_t getKeyFrameCount() const {return ;}
virtual scalar getKeyFrame() const {return ;} /* @brief TF_ADDKEY */
virtual index_t addKeyFrame(scalar pos) {BLADE_UNREFERENCED(pos); return INVALID_INDEX;} /* @brief TF_REMOVEKEY */
virtual bool removeKeyFrame(index_t index){BLADE_UNREFERENCED(index); return false;} };//class ITrack /************************************************************************/
/* */
/************************************************************************/
class BLADE_EDITOR_API ITrackManager : public InterfaceSingleton<ITrackManager>
{
public:
virtual ~ITrackManager() {} /* @brief */
virtual size_t getTrackCount() const = ; /* @brief get bound track */
virtual ITrack* getTrack(index_t index) const = ; /* @brief */
virtual index_t getTrackIndex(ITrack* track) const = ; /* @brief bind track to view */
virtual bool addTrack(ITrack* track) = ; /* @brief */
virtual bool removeTrack(index_t index) = ;
inline bool removeTrack(ITrack* track)
{
return this->removeTrack(this->getTrackIndex(track));
}
};

有了以上接口, 就可以绑定到给UI, 用来显示和多动播放进度等等. 当然目前的设计还很简单, 需要继续完善, 比如多个track的编辑和插入关键帧,编辑关键帧等等. 其实要做好这一块还是很难的, 如果要兼顾复杂度和用户体验的话, 需要花精力慢慢做.

trackview的实现, 这个没什么说的了. 但是遇到了一个有点诡异的东西: MFC的 CSlider事件, 用NM_CUSTOMDRAW不行. 比如CEdit的EN_SELCHANGE, 只有用户改变的时候才会发消息, 而CSlider的NM_CUSTOMDRAW, 代码里更改了slider的位置, 也会发这个消息, 这不符合需求. 最后用的是Scroll事件 - 是的, CSlider会给父窗口发滚动事件. 最诡异的就是这里的强制类型转换, 把输入参数CScrollBar转换为CSlider.

 void CTrackViewUI::OnHScroll(UINT /*nSBCode*/, UINT /*nPos*/, CScrollBar* pScrollBar)
{
if( mTrack != NULL && (mTrack->getFeatures()&ITrack::TF_SEEK) )
{
CSliderCtrl* slider = reinterpret_cast<CSliderCtrl*>( pScrollBar );
assert( slider == this->GetDlgItem(IDC_TRACKVIEW_TRACK) ); //this is the only slider/scrollbar we have.
if( slider != NULL )
{
int pos = slider->GetPos();
mTrack->pause();
mTrack->setPosition( (scalar)pos / (scalar)mFPS );
this->updateUI(true);
}
}
}

这里的reinterpret_cast (line 5) 有点诡异和丑陋, 明显有点生硬的感觉, 但是还好有详细的文档 http://msdn.microsoft.com/en-us/library/ekx9yz55.aspx.

所以MS的开发者友好度大赞, 比android什么的强了不止几倍, 不过MSDN也是积累了n年才有如此好的开发生态圈, android目前确实比不了.

8.遗留问题

  • 目前骨骼数量定的是120, 对于256个vs constant, 除了骨骼变换数组占用的120x2=240个, 还剩下16个, 虽然有点少, 目前够用了, 没有什么问题. 如果单个mesh的骨骼数量太多(> 120), 需要分割mesh, 拆分到多个draw call里, 这个暂时先不做.
  • 动画混合树: 这个以后慢慢完善.
  • 动画合并工具: 这个准备作为模型浏览器(modelviewer)插件的一个功能, 集成在编辑器里, 以后慢慢完善.
  • IK, 下一步着手去做.

最后还是惯例, 发截图:

引擎设计跟踪(九.14.2b) 骨骼动画基本完成

引擎设计跟踪(九.14.2b) 骨骼动画基本完成

引擎设计跟踪(九.14.2b) 骨骼动画基本完成的更多相关文章

  1. 引擎设计跟踪&lpar;九&period;14&period;2a&rpar; 导出插件问题修复和 Tangent Space 裂缝修复

    由于工作很忙, 近半年的业余时间没空搞了, 不过工作马上忙完了, 趁十一有时间修了一些小问题. 这次更新跟骨骼动画无关, 修复了一个之前的, 关于tangent space裂缝的问题: 引擎设计跟踪( ...

  2. 引擎设计跟踪&lpar;九&period;14&period;2i&rpar; Android GLES 3&period;0 完善

    最近把渲染设备对应的GLES的API填上了. 主要有IRenderDevice/IShader/ITexture/IGraphicsResourceManager/IIndexBuffer/IVert ...

  3. 引擎设计跟踪&lpar;九&period;14&period;2f&rpar; 最近更新&colon; OpenGL ES &amp&semi; tools

    之前骨骼动画的IK暂时放一放, 最近在搞GLES的实现. 之前除了GLES没有实现, Android的代码移植已经完毕: [原]跨平台编程注意事项(三): window 到 android 的 移植 ...

  4. 引擎设计跟踪&lpar;九&period;14&period;3&period;4&rpar; mile stone 2 - model和fbx导入的补漏

    之前milestone2已经做完的工作, 现在趁有时间记下笔记. 1.设计 这里是指兼容3ds max导出/fbx格式转换等等一系列工作的设计. 最开始, Blade的3dsmax导出插件, 全部代码 ...

  5. 引擎设计跟踪&lpar;九&period;14&period;2 final&rpar; Inverse Kinematics&colon; CCD 在Blade中的实现

    因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://gr ...

  6. 引擎设计跟踪&lpar;九&period;14&period;2c&rpar; 最近一些小的更新

    1. bump map与normal map 昨天拿了crytek sponza(http://www.crytek.com/cryengine/cryengine3/downloads)场景测试, ...

  7. 引擎设计跟踪&lpar;九&period;14&period;2j&rpar; TableView工具填坑以及多国语言

    Blade的UI都是预定义的接口, 然后由插件来负责实现, 目前只有MFC的插件. 最近加上了TableView的视图, 用于一些文件的查看和编辑, 比如前面在文件包的笔记中提到需写一个package ...

  8. 引擎设计跟踪&lpar;九&period;14&period;2g&rpar; 将GNUMake集成到Visual Studio

    最近在做纹理压缩工具, 以及数据包的生成. shader编译已经在vs工程里面了, 使用custom build tool, build命令是调用BladeShaderComplier, 并且每个文件 ...

  9. 引擎设计跟踪&lpar;九&period;14&period;2d&rpar; &lbrack;翻译&rsqb; shader的跨平台方案之2014

    Origin: http://aras-p.info/blog/2014/03/28/cross-platform-shaders-in-2014/ 简译 translation: 作者在2012年写 ...

随机推荐

  1. iOS学习笔记——多控制器管理

    NavigationController 在StoryBoard中添加NavigationController 在上网看到很多都是用xib添加,使用StoryBard的有两种办法,但我觉得下面用到那种 ...

  2. 分析setting源代码获取sd卡大小

    分析setting源代码获取sd卡大小 android系统有一个特点,即开源,我们可以得到任何一个应用的源代码,比如我们不知道这样的android代码怎么写,我们可以打开模拟器里面的设置(settin ...

  3. Windows Phone 8 解锁提示IpOverUsbSvc问题——IpOverUsbEnum返回No connected partners found解决方案

    我的1520之前总是无法解锁,提示:IpOverUsbSvc服务没有开启什么的. 根据网上网友的各种解决方案: 1. 把手机时间设置为当前时间,并且关闭“自动设置” 2. 确保手机接入了互联网 3.确 ...

  4. search支持多种标签

    织梦的搜索页面支持dede标签的方法一 打开文件:include/arc.searchview.class.php 找到: require_once(DEDEINC."/taglib/hot ...

  5. shell 运算

    一个下午折腾一个脚本,shell好久不用,重新学起 一个小成果 size= ] do table=albums_index_${table_num} count=$size times= while ...

  6. 查看buffer cache命中率

    SQL> select name,value from v$sysstat where name in('db block gets','consistent gets','physical r ...

  7. Verilog HDL常用的行为仿真描述语句

    一.循环语句 1.forever语句 forever语句必须写在initial模块中,主要用于产生周期性波形. 2.利用for.while循环语句完成遍历 for.while语句常用于完成遍历测试.当 ...

  8. Inno Setup入门(二)&mdash&semi;&mdash&semi;修改安装过程中的图片

    修改安装过程中的图片 一般编译之后,安装过程中出现在左边图片是是下图这个样子的: 其实也可以修改它,只需要在setup段中作一点稍微的修改,加一行代码即可: [setup] AppName=Test ...

  9. 【重磅】PRO基础版免费,是时候和ExtJS说再见了!

    三石的新年礼物 9 年了,FineUI(开源版)终于迎来了她的继任者 - FineUIPro(基础版),并且完全免费!   FineUIPro(基础版)作为三石奉献给社区的一个礼物,绝对让你心动: 拥 ...

  10. linux入门--Linux发行版本详解

    从技术上来说,李纳斯•托瓦兹开发的 Linux 只是一个内核.内核指的是一个提供设备驱动.文件系统.进程管理.网络通信等功能的系统软件,内核并不是一套完整的操作系统,它只是操作系统的核心.一些组织或厂 ...