相关文章推荐
讲道义的烈酒  ·  C# 连接SQL server 2022 ...·  7 月前    · 
安静的八宝粥  ·  ffmpeg compilation ...·  1 年前    · 
个性的小虾米  ·  CVPR2023 ...·  1 年前    · 

学习笔记 --- 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上方的菜单中

注意是从代码发起drag,不是上方的脚本块,拖拽到上方菜单中,鼠标显示+号时释放

77.通过脚本镜像控制物体

---77.1 Max自带镜像的问题

我们需要将控制物体向左边的脸部镜像一份,这里不能直接使用Max自带的镜像功能(因为有负缩放的问题)

移动,视图或世界坐标模式,坐标系轴心模式,沿X镜像可获得另一侧的复制
但是这个自带的镜像会产生负缩放的问题,影响旋转,影响子节点

---77.2 获得镜像的旋转姿态

这里我们打算写一个脚本来完成旋转上的镜像复制功能

通过侦听器输出可以看到,Max脚本中rotation旋转是通过四元数记录的(虽然控制器以及动画里面插值是用欧拉角...)

关于四元数 欧拉角 万向锁,这些3D数学相关的知识本文就不过多赘述了,感兴趣的话可以看看隔壁的文章


这里我们只需要知道,四元数有一个轴角对应,transform里面的rotation,就相当于记录了相对于父物体的一个轴-角旋量

只不过笔记里四个数顺序是 wxyz ,MaxScript里面是 xyzw 注意一下就好


如果我们想要获得沿X轴镜像(YZ镜面)的旋转,方法很简单,只需要翻转一下YZ转轴就可以

对应到MaxScript里,我们只需将 xyzw 中间两位取反即可

克隆物体,获得镜像的旋转姿态之后,再将X轴坐标取反,放置在镜像位置,即可完成克隆

参考Max世界,如果我们向获得沿X轴镜像(YZ镜面)旋转,那么X轴上下是不变的,而ZY水平和扭转需要翻转一下


---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的零位置

绑定完成后父级Point的零位置XYZ控制器名称会变更为 位置连线

但是此时移动控制物体,我们发现控制物体相对节点的位移有一个差值

似乎控制物体相对节点产生了2倍的移动

这是由于控制物体作为父Point的子物体,其相对世界的位置本身就受父物体影响

控制物体运动,连线参数控制父物体运动,父物体又向子物体回传了一次运动,从而造成了二次运动

对此,可以创建这样的一条连线参数

控制物体的零位置 ---》 控制物体位置列表 - 可用 (自己控制自己的移动,注意要再乘一个 -1 倍的因子)

链接时位置列表会自动添加一个位置连线控制器接受控制

从而控制物体再自己的控制下会抵消一倍的运动,进而与节点的移动持平

连线完成时,位置列表会自动加出一个位置连线控制器接受控制,注意表达式要乘-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节中骨链绑定,我们是按照其注视目标作为上方向节点,也就是末端节点作为上方向节点的

然而这里注视目标也就是下巴中部那个控制节点,在运动时会受到下颚骨旋转的影响,导致这段骨骼产生不必要的旋转


下颚装配的最终效果:

https://www.zhihu.com/video/1473346615993483264

80.眉骨装配

先用78.2节的方法,对节点的独立控制进行装配

我们希望通过中间的控制物体来统一的牵动,控制眉骨运动

对于X轴,我们希望当控制物体靠近节点时,节点能更多的位移,远离时更少的位移

还是通过连线参数进行绑定

先确保所有的控制物体,辅助物体,都被 冻结变换,为连线参数提供“零位点”,方便表达式的编写

通过 if else 语句 判断X_位置的正负,选择不同的比例应用即可, 注意表达式中X_位置的正负

//右侧 >0 方向: