学习笔记 --- 3dsMax骨骼绑定(第七篇)
0.前序
笔接上文
学绑骨算是有一年了,以前只会用Biped骨架,到现在对自建骨架,角色绑定的思路方法稍微有一点自己的理解了
不出意外的话这应该就是这个系列笔记的最后一篇了,可能以后还会多出个总结什么的吧(挖坑)
本系列选学的教程是琅泽教育的骨骼绑定课程,老师好像有参与过《企鹅部落》,经验很丰富
不过不好的一点可能是这个教程的老师是主打影视方向的,我这边学绑骨主要是为了给游戏角色绑定骨架,制作动画导出FBX,然后应用到Unity里面,所以一些地方,尤其针对变形动画相关的,以及在骨架节点规范上和老师有比较大的出入,不过zhihu这边的笔记都有补充到,相关问题的汇总可参阅下面这篇文章
另一个一直觉得老师没讲到位的点是绑定中,对于位置/旋转/缩放的参考轴向问题(尤其是在连线参数),对此下面这篇文章有详细的讲解
本篇很多地方涉及MaxScript脚本编码,但文章中只介绍下希望实现的功能,脚本的用法,以及把代码贴出来; 不打算过多赘述有关编程基础,算法思路相关 ,因为我本来就是学编程出身的,本文只打算站在程序员的角度讲下MaxScript这个API该怎么用
编程,算法这方面...都是基本功啦 哈哈(其实学的时候脚本也没跟着老师去写,看看语法,开侦听器看看操作代码,自己照着功能就能写出来了)
如果有美术出身的想学编程,建议从C++ 或者 python3 学起,不打算精研编程,只想调库用工具的话,选python3来入门真的不错,进几年不少AI 图形学相关,面向大众的库都是出py3封装的,而且MaxScript和python3也很像(都是弱类型的),Max本身也支持python3来进行脚本编码。然后可以去 力扣 刷刷题,训练一下,算法什么的不用学太深,有遍历的概念就行
74.面部表情控制骨架搭建
这里来看一下面部表情控制骨架的搭建思路
我们最终整体效果如下:
先给嘴唇的一侧创建一条骨链
从嘴角和上下嘴唇末端的骨骼节点,延伸出三条骨链用于控制面部
注意使用位置对齐对准根骨的位置
可以参考网格上的五星点位置来布置骨骼, 因为在网格拉伸运动时,五星点会成为转折的分流位置
要制作表情动画,对于人物面部的网格布线也有一定的要求, 详见王康惠带师的人物模型布线规则 ,这里就不展开赘述了
下巴中间创建这样一条骨链
随后围绕口轮杂迹,创建这样一段骨链,位置上注意对齐其它骨链上的节点
眼部创建这样的骨架用于控制上下眼眶和眼皮
随后侧面创建如图所示的骨骼
我们还需在人中创建这样的一条骨链,在闭嘴时需要这条骨链带动人中的皮肤
保证人物居中,通过骨骼工具的镜像复制(本质上是通过旋转完成的,不是-1缩放)创建另一侧的骨架
75.通过脚本为骨链创建约束
这里并不打算展开有关编程的基础,因为我本来就是个程序员,这里是站在程序员的角度来看看MaxScrpit该怎么用
基于面部的骨架,我们通过设置骨骼的 约束+伸长装配 ,通过将骨骼约束到虚拟体上来控制面部骨架的拉伸效果
类似于第四篇第46节角色脊椎效果,但这里我们并不打算使用样条线或是IK来控制,而是单纯的使用虚拟体
对于这个装配之前的章节已经讲的很清楚了,这里就不在赘述,不过还是特别要注意,我们最终必须通过连线参数,或是浮点脚本真正的改变骨骼的缩放值,不能使用Max骨骼自带的伸长效果
但是对于角色面部的骨架而言,要装配的骨骼节点实在过多不便于,这里我们就来学习一下如何通过编写脚本,来实现自动化的虚拟体创建与约束装配
---75.1 MaxScript API
下面先通过侦听器来演示MaxScript中一些基本的功能代码:
(这里注意MaxScript是一种弱类型的语言,和python3有些像)
为选中的骨骼,创建两个虚拟点分别对齐到骨骼节点和骨骼的子节点
a=point()
a.transform = $.transform
b=point()
b.transform = $.children[1].transform
调整虚拟点的样式
a.size = 10
a.box = true
a.cross = false
b.size=10
b.box = true
b.cross = false
为骨骼节点指定位置约束(到根部a节点)和方向约束(到子端b节点),调整方向约束上方向节点(到子端b节点),子节点指定位置约束(到子端b节点)
$.pos.controller = position_constraint()
$.pos.controller.appendtarget a 100
$.rotation.controller = lookat_constraint()
$.rotation.controller.appendtarget b 100
$.rotation.controller.lookat_vector_length = 0
$.rotation.controller.viewline_length_abs = off
$.rotation.controller.upnode_world = off --上方向节点设置
$.rotation.controller.pickupnode = b
$.rotation.controller.relative = on --保持初始偏移
$.children[1].pos.controller = position_constraint()
$.children[1].pos.controller.appendtarget b 100
关闭Max骨骼伸长 指定浮点脚本控制器
至于浮点脚本控制器内的代码,这里我们还是使用手动添加的方法
$.boneScaleType=#none
$.scale.controller = ScaleXYZ()
$.scale.controller.X_Scale.controller = float_script()
---75.2 最终的代码和使用
不过最终我们并不是为了一个单一的骨骼创建约束,而是为一整条骨链
这里假定我们先选中了骨链的根骨,之后可以通过下面这段脚本为一整段骨链创建约束
这里和老师的写法不一样,没有用数组,用了while循环,算法相关这里就不讨论了...
最终我们可以创建一个窗口,使用一个按钮来搭载我们的绑骨功能
rollout boneRig "请先选中根骨"
button btn "骨链绑定"
on btn pressed do (
bo = $ --选中的骨骼节点
a = point() --已经在骨骼节点处创建的虚拟节点
a.size=10
a.cross=false
a.box=true
a.transform = bo.transform
while bo.children[1]!=null do (
bo.pos.controller = position_constraint() --先位置约束到已创建的a
bo.pos.controller.appendtarget a 100
bo.boneScaleType=#none --更正缩放控制
bo.scale.controller = ScaleXYZ()
bo.scale.controller.X_Scale.controller = float_script()
a = point() --为子端创建一个新节点
a.size=10
a.cross=false
a.box=true
a.transform = bo.children[1].transform --对齐到子端
bo.rotation.controller = lookat_constraint() --骨骼注视约束到子端的a
bo.rotation.controller.appendtarget a 100
bo.rotation.controller.lookat_vector_length = 0
bo.rotation.controller.viewline_length_abs = off
bo.rotation.controller.upnode_world = off --上方向节点设置
bo.rotation.controller.pickupnode = a
bo.rotation.controller.relative = on --保持初始偏移
bo = bo.children[1] --递进bo赋值为子骨骼
bo.pos.controller = position_constraint() --末端子骨骼位置约束
bo.pos.controller.appendtarget a 100
createdialog boneRig
我们还需手工装配骨链的伸缩控制,这里就不赘述了,如果只是为了Max中的效果,可以直接打开骨链的伸缩
取消勾选冻结长度,拉伸选择缩放模式
76.为节点创建控制物体
---76.1 功能布置
我们需要为面部的骨链节点创建控制物体
在一些位置我们会使用这样的内外双层控制
外层控制物体在运动时,会带动周围的节点一并运动
而内层的控制物体可以操作节点独立运动
一些节点还需跟随下颚张开运动
---76.2 父子结构与创建方法设计
为了实现复杂的运动控制,我们计划搭建如下图所示的父子结构
对于控制物体与父Point,我们通过手工创建,使用Max的放置功能再结合旋转,贴合面部走势
---76.3 通过脚本创建父级节点
对于节点的上一级两个父物体,我们通过脚本编码进行创建
创建父级时,需要对准骨链Point的pos,同时对准控制物体的rotation
通过配置选择集的方法,先选中骨链的point,之后选中控制物体的父point,执行下面的脚本
这里就不赘述编码思路了
注意使用的时候,要先选中骨链绑定产生的Point,再选中控制物体的父Point(配置选择集顺序)
pta = point size:4.5 box:on cross:on wirecolor:[
0,255,0]
ptb = point size:1.2 box:on cross:on wirecolor:[0,255,0]
pta.rotation = selection[selection.count].rotation
ptb.rotation = selection[selection.count].rotation
pta.pos = selection[1].pos
ptb.pos = selection[1].pos
pta.parent = ptb
for a in selection do a.parent = pta
特别的,人中骨链的中段,和下眉骨两个节点,没有控制物体但也需要创建一组父级节点,选中骨链的Point直接运行脚本即可
之前75节中的代码,我们创建了一个窗口按钮进行功能承载
这里还有一种快速的创建功能承载按钮的方式,选中代码drag到Max上方的菜单中
77.通过脚本镜像控制物体
---77.1 Max自带镜像的问题
我们需要将控制物体向左边的脸部镜像一份,这里不能直接使用Max自带的镜像功能(因为有负缩放的问题)
---77.2 获得镜像的旋转姿态
这里我们打算写一个脚本来完成旋转上的镜像复制功能
通过侦听器输出可以看到,Max脚本中rotation旋转是通过四元数记录的(虽然控制器以及动画里面插值是用欧拉角...)
关于四元数 欧拉角 万向锁,这些3D数学相关的知识本文就不过多赘述了,感兴趣的话可以看看隔壁的文章
这里我们只需要知道,四元数有一个轴角对应,transform里面的rotation,就相当于记录了相对于父物体的一个轴-角旋量
如果我们想要获得沿X轴镜像(YZ镜面)的旋转,方法很简单,只需要翻转一下YZ转轴就可以
对应到MaxScript里,我们只需将 xyzw 中间两位取反即可
克隆物体,获得镜像的旋转姿态之后,再将X轴坐标取反,放置在镜像位置,即可完成克隆
---77.3 克隆选择集与加选
侦听器,克隆选择集,并修改选中克隆出的物体,代码如下
maxOps.cloneNodes $ cloneType:#copy newNodes:&nnl --克隆选择集
select nnl --修改选择集
向选择集中加选代码如下
selectMore a --将a添加到选择集中
---77.4 最终的代码与使用
在上一节(76节)中我们已经通过为右边脸部的控制物体创建了两个父级Point节点
因此脚本编码的时候要注意一下,我们不能打破已有的父子绑定,但是镜像克隆过去的链状体,参照我们希望根节点是没有父物体的(从而后面再运用我们76节中的脚本创建两个父级节点)
代码最终的功能,仅针对于链状体克隆,如果要克隆复杂树,需要通过递归来展开子物体
在使用上我们先选中需要克隆的链状体的所有根节点,代码会加选子物体到选择集中,进行克隆,同时调整克隆后物体的位置和旋转姿态,沿X轴(YZ镜面)镜像
原先参照链状体,根节点的父子关系不受影响
贴一下代码
arrypar = #() --创建一个组,记录初始选择集中的根节点
arryp = #() --记录根节点的父节点
for a in selection do (
append arrypar a --将父节点放入组内
append arryp a.parent --记录父节点
a.parent = null --暂时消除父节点 以便于复制后根节点区分
for a in arrypar do ( --因为我们会改变选择集 所以要遍历 arrypar
q = a
while q.children[1] != null do (
selectMore q.children[1]
q = q.children[1] --链节点递进加选 这里只限制复制链状 复杂树需要开递归来遍历子节点
--复制选择集
maxOps.cloneNodes $ cloneType:#copy newNodes:&nnl
select nnl
--恢复父节点
for i=1 to arrypar.count do (
arrypar[i].parent = arryp[i]
--遍历选择集 翻转
for a in selection do (
if a.parent == null then ( --找那些没有父节点的节点 它们是链根
ap = a.pos * [-1,1,1] --先记录pos 之后旋转调整会影响pos
a.rotation = (quat a.rotation.x -a.rotation.y -a.rotation.z a.rotation.w)
a.pos = ap
)
78.面部节点牵动装配
我们将使用列表+多重连线参数进行节点的牵动装配
父子关系、约束、露出变换,它们会形成一种 物体级别的相互依赖关系 ,这种关系不允许成环,因此在一些复杂的绑定的时候经常造成冲突
我们会使用列表+多重连线参数进行节点的控制和牵动绑定,连线参数会形成一种 控制器级别的相互依赖关系 ,同样不允许成环,但相比上面的物体级别依赖, 更细化,在复杂绑定时不易造成冲突
在前文(绑骨系列第六篇66.4节)中我们就使用了连线参数解算位置,来避免冲突问题
关于位移/旋转/缩放的轴向参照问题可以参阅下面这篇回答
---78.1 轴向的调整和注意
我们需要特别注意控制物体轴向(旋向)的布置,这决定了未来控制物体独立移动时的方向、受到牵动时移动的方向(尤其注意牵动时移动的方向性)
我们需要保证控制物体的轴向处于需要的位移轴向上,并且控制物体的父Point,节点的两个父级Point,都对齐到这一轴向
for a in selection do (
b = a.parent
a.parent = null
a.scale = [1,1,1]
a.rotation = b.rotation
a.pos = b.pos
a.parent = b
轴向调整完成后,我们选中所有的控制物体/虚拟物体,冻结变换
---78.2 节点自身的控制绑定
对于内外的双重控制物体,它们都能影响节点整体的位移,我们创建这样的一段连线参数绑定
控制物体的零位置 ---》 第一父级Point的零位置
但是此时移动控制物体,我们发现控制物体相对节点的位移有一个差值
似乎控制物体相对节点产生了2倍的移动
这是由于控制物体作为父Point的子物体,其相对世界的位置本身就受父物体影响
控制物体运动,连线参数控制父物体运动,父物体又向子物体回传了一次运动,从而造成了二次运动
对此,可以创建这样的一条连线参数
控制物体的零位置 ---》 控制物体位置列表 - 可用 (自己控制自己的移动,注意要再乘一个 -1 倍的因子)
链接时位置列表会自动添加一个位置连线控制器接受控制
从而控制物体再自己的控制下会抵消一倍的运动,进而与节点的移动持平
---78.3 节点之间的牵动绑定
表情动画中人物的嘴角是一个很重要的控制位点,嘴角的节点在运动时,需要带动周围的节点一起运动
我们只需将嘴角外层的控制物体零位置,连线控制到,其它节点第一父级Point 位置列表中,注意根据位置远近,参考人脸肌肉的牵动,乘折减系数
创建连线时不必在意顺序问题 ,父级Point自身并不打算移动,因此零位置不必保留,有零位置就连接到零位置上,没有零位置就连接到可用上再创建一层位置连线接受控制
注意78.1节的轴向调整问题
链接完成后,我们会发现,对于双重控制, 无论移动内外层那一方,由于其控制了父级Point的位移,因此另一方都会跟随移动
两方的移动,分别由父级Point,不同层的位置连线接受,因此互不影响,可以进行叠加
并且两方移动后,可用通过变换到零,随时归位
同理我们装配其它节点向周围节点的牵动效果
连线时我们先选中控制方,在连线参数对话框一侧,找到运动发起的控制器
在不断选中被控制物体,刷新对话框另一层,找到被控制的控制器,调整表达式,创建连线
---78.4 人中的控制
人中的节点我们需要特别处理,在76.3节中我们为人中节点(没有控制物体)创建了一组父级Point
这里先将父级Point的轴向对齐上嘴唇中心节点
先创建一条位置连线,由上嘴唇中部外层控制物体零位置运动,控制人中第一父级Point运动,注意乘折减系数
给人中的父级Point,列表中加出一层位置XYZ修改器
本例中(注意轴向),还需再创建一条连线参数,由上嘴唇中部外层控制物体,零位置的Y轴,连线控制到加出的位置XYZ中的Z轴,并乘一个 -0.5 的 反号折减
从而我们操作上唇向下闭唇时,人中的位置能得到拉伸,并且凸出转平,向上翘唇时,人中位置会更加内凹
79.下颚装配
---79.1 下颚张开的节点设计
在74节中,我们特别创建了一段骨骼用于控制下颚张开的运动
当这块骨骼旋转表现下颚张开时,会带动面部的节点(尤其是下巴,下嘴唇)一并运动
我们在76.2节中设计的,节点最高级别的Point,就会应用到这里的装配中
首先我们需要设计一下节点跟随下颚张开的运动幅度,这里区分了3级运动(1级最大,3级最小)
---79.2 主动节点设计
1级完全跟随骨骼的节点,可以直接链接约束到骨骼上(将骨骼作为主动节点)
2级和3级,则需要我们创建一个Point作为主动节点,通过 配置约束权重 的方法,提供削弱的跟随效果
首先将下颚骨链接到头骨上(骨骼---》骨骼 可以直接父子链接, 节点绑定规范详见文章开头的补充文章 ),绑定父物体后记得冻结变换
创建这样的三个Point,对齐到下颚骨,三个Point链接约束到头骨上(虚拟物体--》骨骼 不能用父子链接),冻结变换
最内层粉色的Point用于标记初始姿态,外层两个Point用于作为2级和3级的主动Point
通过方向约束+权重配置,表现削弱的旋转效果
我们还需控制下颚的伸出效果(下颚骨延伸方向位移),因此主动节点还需添加一个位置约束,权重过渡同方向约束
---79.3 节点装配
下巴中部(包括下嘴唇中部),三个节点的最高父级Point,通过链接约束,绑定到骨骼上(虚拟物体--》骨骼 不能用父子链接)实现完全跟随
绑定后记得冻结变换
2级和3级节点,直接通过父子链接(虚拟物体---》虚拟物体 可使用父子链接)跟随主动节点
绑定后记得冻结变换
其余节点的最高父级Point可通过链接约束绑定到头骨上
此时旋转一下下颚骨,检查节点的伸张效果,对于权重可进行调整
---79.4 控制物体装配
在77节中我们为下巴中部节点配置了双层控制,这里内侧控制物体通过78.2节中的绑定独立控制节点
外层的控制物体,我们希望用它的位移来控制下巴的运动
在旋向上我们通过YX轴位移,连线绑定控制下颚骨骨骼的ZY轴旋转上
注意我们 对骨骼进行了冻结变换,因此零旋转需要参照局部坐标系轴向确定 ,位移则永远参考父物体坐标系 (关于这个轴向问题详见文章开头补充的文章)
注意这里是float控制angle,表达式需要 degtorad进行转换
我们还希望控制物体Z轴的移动,能够控制下颚的伸出效果(下颚骨延伸方向进行位移)
(这里因为有这个伸出效果,因此没有用HI进行装配,而是用连线参数)
但这里就出现了一个问题, 位移的控制轴向是参考父坐标系进行的(无论是否冻结变换)(关于这个轴向问题详见文章开头补充的文章)
对于控制物体而言因为它有一个方向对齐的父Point,因此它的父坐标系Z轴和自己的Z轴位移同向
但是下颚骨的父物体是头骨,在父坐标系下它并没有沿着延伸方向的轴向
因此这里我们需要更改一下骨骼节点的设计,这里干脆就 把标记初始姿态的Point作为其父节点 ,从而提供延伸轴向, 这个Point我们就将其当作骨骼节点,加入到骨架体系中
更改父物体之前,骨骼需要在位置/旋转列表中, 切换启用冻结位置和冻结旋转控制器,用于承受修改父物体导致的偏移
此时就可以将控制物体的Z轴位移,绑定到骨骼的X轴位移上,从而控制下颚的伸出效果
由于节点的最高父Point通过链接约束绑定到了骨骼上(79.3节),因此也存在运动回传的问题(78.2节),我们也需要通过连线绑定一个自我控制,消除掉自身的位移运动效果,从而只保留下颚骨回传的旋转/位移,带来的运动
特别的,我们还需要更改一下下巴中部骨骼的上方向节点,改为其根部的虚拟体
75节中骨链绑定,我们是按照其注视目标作为上方向节点,也就是末端节点作为上方向节点的
然而这里注视目标也就是下巴中部那个控制节点,在运动时会受到下颚骨旋转的影响,导致这段骨骼产生不必要的旋转
下颚装配的最终效果:

80.眉骨装配
先用78.2节的方法,对节点的独立控制进行装配
我们希望通过中间的控制物体来统一的牵动,控制眉骨运动
对于X轴,我们希望当控制物体靠近节点时,节点能更多的位移,远离时更少的位移
还是通过连线参数进行绑定
先确保所有的控制物体,辅助物体,都被 冻结变换,为连线参数提供“零位点”,方便表达式的编写
通过 if else 语句 判断X_位置的正负,选择不同的比例应用即可, 注意表达式中X_位置的正负
//右侧 >0 方向: