以卡通角色的帽子的物理骨骼建立与绑定为例,介绍实现过程。这次需要的知识背景较多,我也尽量清楚阐释。
需要注意的是,这个脚本并不是从无到有完成物理骨骼的绑定,旨在解决物理骨骼绑定中重复繁琐的步骤。
前置的准备
知识背景
首先我们需要知道的是blender实现物理仿真的流程:
首先物理仿真分为四部分:
最底层的肯定还是我们的mesh和绑定mesh对应的defrom rig(后简称defrig),一般来说帽子这种物体想做物理效果,自动绑的效果基本上支持不了,只能手动分一下定点组和权重。
其次是physics rig(以后简称phyrig),即在defrig和mesh之间起到承接的作用,一方面,通过复制旋转约束,使ddefrig能够与phyrig实现旋转同步,进而控制网格。
phyrig和physics mesh之间的联动,则通过阻尼追踪phymesh对应的顶点组来实现。(physics mesh,即用于物理模拟的mesh,通过编辑模式+顶点吸附建立defrig对应的曲线,通过网格-曲线-曲线倒角-网格实现体积塑造)
数据准备
1.建立帽子对应的hatmesh
2.建立deformrig,面向hatmesh完成顶点组和权重分配
3.建立phymesh,完成顶点组和权重分配
为什么这么要求,因为一般物理仿真不像人骨骼一样这么规律,骨骼拓扑取决于物体结构,而物理仿真精度也取决于骨骼的拓扑以及数目,对应的Phymesh也需要去人工指定锚定顶点组来固定phymesh,所以这里的前期准备部分仍需要人工来做骨骼的布置和phymesh的制作。(当然我要能直接脚本搞定这个。。。那进个autodesk啥的上班也绰绰有余了吧)
完成绑定的hatmesh
deformRig
physics mesh完成顶点组和权重分配后,注意这里为了方便后续的绑定,需要按照骨骼顺序来做顶点组按序分配
播放,我们可以看到phymesh已经有物理效果,而帽子没有
解决的痛点
所以我们脚本的目的是什么?顺便在这一步我们梳理脚本的业务流程:
- 创建phyrig:复制defrig并重命名,虽然blender有批量重命名功能,但是blender复制的时候会直接将骨骼的后缀序号叠加,强迫症发作的我会顺便重命名并将其序号调整至正确
- 完成phyrig与phymesh的绑定:对应的bone和vertex group,赋予阻尼追踪约束
- 完成defrig与phyrig的绑定:对应的bone之间,赋予复制旋转约束
编写脚本
获取用户选中的物体
像骨骼绑定需要选中两个物体一样,我们的脚本也需要用户选中两个物体,然后我们需要对用户选中的情况进行检查,并反馈检查结果,避免未达成绑定条件就进入后续的计算。
需要注意的是,由于blender scripting本身不支持中文,所以我写了很多英文注释,文章中也予以保留,中文注释都是写博文的时候补充的,狠狠考验英文水平了捏。
main():
selectedList = bpy.context.selected_objects
#用户需要选中两个物体,多了少了都不行
if(len(selectedList)!= 2):
print("must select at least 2 objects...")
return -1
#blender can provide correct selected order of objects in selected_objects
#注意这里,实际上blender.context.selected_objects无法获取准确的物体选中顺序,所以这段判断代码实际上并不能使用
if(selectedList[0].type != "MESH" and selectedList[1].type != "ARMATURE"):
print("first selected must be physics_mesh, last selected must be amature.")
return -1
这里我们就遇到了第一个坑,按理说我们是应该检查一下用户的选择物类型,并检查下选择顺序:
但很快belnder直接给了我们一巴掌,blender.context.selected_objects的顺序不是基于用户选择的顺序,而是基于场景的顺序,就是说不管用户选择顺序的先后,selected_objects给出的元素顺序总是一样的。。。
但是后续操作中又会涉及到选择物体和激活物体(最后一个选择物体)的差异,如果激活物体选成了mesh,就会报错。
我只能说bpy整的一手好活,大哥,我都用selected_objects了,我怎么可能不在乎选择顺序呢?
这里建议一方面检查选中物体的类型,另外通过active_object来检查激活物体的类型,我就不再阐述了。
下一步很清晰,就是根据选中物体的类型,分别用一个变量记录:hatmesh以及defrig
for mem in selectedList:
if mem.type == "MESH":
Tarmesh = mem
if mem.type == "ARMATURE":
RigObject = mem
创建phyrig
基于当前的骨骼,进入编辑模式将其复制,完成重命名,返回phyrig,phyrig/defrig骨骼总数,defrig。
def createPhyBones(DeformRig):
if(bpy.context.active_object.mode != "EDIT"):
print("not edit mode, now switch to edit mode")
bpy.ops.object.mode_set(mode = "EDIT")
#select all bones
#切换到编辑模式后,我们全选当前骨骼
bpy.ops.armature.select_all(action = 'SELECT')
RigLength = len(bpy.context.selected_bones)
DeformRig = bpy.context.selected_bones
#after copy, blender will automatically select all new bones in edit mode
#全选当前骨骼后,完成复制,blender会自动切换到副本骨骼
bpy.ops.armature.duplicate()
PhyRig = bpy.context.selected_bones
#针对副本骨骼完成重命名,phyrig创建完成
for i in range(RigLength):
PhyRig[i].name = ("HatBonePhy.%03d" % i)
print(PhyRig[i].name)
return PhyRig, RigLength, DeformRig
完成phyrig和phymesh的绑定
回到main函数部分:
#创建phyrig
PhyRig, RigLength, DeformRig = createPhyBones(RigObject)
#切换物体模式,重新选中当前骨骼,确保当前armature object已经更新到了phyrig和defrig共存的状态
bpy.ops.object.mode_set(mode = "OBJECT")
PosePhyObject = bpy.context.active_object
#切换姿态模式
bpy.ops.object.mode_set(mode = "POSE")
for i in range(RigLength):
#by createPhyBones() we get PhyRig: the list of copied bones that named "...phy"
#but we cant add constraint directly for PhyRig: as mem in PhyRig is edit bones
# the switch from different data structure of belnder is complex, be careful
try:
#获取对应骨骼,完成阻尼追踪约束的赋予
Tarbone = PosePhyObject.pose.bones[PhyRig[i].name]
unitCons = Tarbone.constraints.new(type="DAMPED_TRACK")
unitCons.target = Tarmesh
unitCons.subtarget = Tarmesh.vertex_groups[i+1].name
except Exception as e:
print("error occur for: no." + str(i))
print(e)
return 1
这里可能有人又要问,你这不是都返回了PhyRig,直接从PhyRig里面做约束不行吗,为什么要重新在PosePhyObject里面选定呢。
这是因为在编辑模式下复制完的骨骼,是editbone,而editbone是edit的子分支,可能熟悉blender的人就能猜的着:
就像编辑模式下不能添加pose constraint,editbone也不存在constraint属性。
我们只能老老实实重新从object里选对应的骨骼,切换到pose子属性下来赋予约束。
blender返回值传递偏差导致的utf-8编码错误
到这一步,如果你是跟着做的话,很有可能一跑下来就会发现,phyrig创建和绑定的效果很不稳定,骨骼的检索会出现随机错误。
我们直接打印骨骼检索的情况来做检查:
带来的结果就是,phyrig绑定的效果很不稳定,下图是三次执行的结果,每次都有不同的骨骼出现绑定问题。(实际上就是检索问题,对应的key value未能检索到对应的bone)
当然我第一反应也是基于传统的编码问题的求解思路,用encode,decode方法来做编码和解码,结果完全不能解决问题。
没办法,我们只能逐步排查,首先我们在createPhyBones对phyrig完成命名复制后,直接打印对应phyrig的name属性,看看是否有问题。
好,至少看来赋值是没有问题了,然后我们检查参数回传后的情况。
获取到phyrig后,我们直接打印其元素。
果然是返回值出现了偏差,导致后续出现了检索错误。
这里我也没能理解,为什么python的返回值会被更改?我本身只跑这一个创建物理骨骼的脚步,也没能在骨骼上搜到相应情况的反馈和修改方法,实在是太奇怪了。。。
那没办法了,本来我还想好好做人,既然返回的局部变量有问题,那就别怪我不客气了!我直接改成全局变量,不需要返回值了!
反正都是给个人用户用的,还在乎什么编码规范啊。
简单来说就是,phyrig和deformrig这两个变量都改成全局的。
然后就绑定成功了。。。
完成defrig和phyrig的绑定及收尾
for i in range(PhyRigLen):
#by createPhyBones() we get PhyRig: the list of copied bones that named "...phy"
#but we cant add constraint directly for PhyRig: as mem in PhyRig is edit bones
# the switch from different data structure of belnder is complex, be careful
try:
print("start seeking... " + PhyRigNameList[i])
Tarbone = PosePhyObject.pose.bones[PhyRigNameList[i]]
#赋予阻尼追踪约束:phyrig to phymesh
unitCons = Tarbone.constraints.new(type="DAMPED_TRACK")
unitCons.target = Tarmesh
unitCons.subtarget = Tarmesh.vertex_groups[i+1].name
#赋予复制旋转约束:defrig to phyrig, in local space
TarDeformbone = PosePhyObject.pose.bones[DeformRigNameList[i]]
boneCons = TarDeformbone.constraints.new(type="COPY_ROTATION")
boneCons.target = PosePhyObject
boneCons.subtarget = PhyRigNameList[i]
boneCons.target_space = "LOCAL"
boneCons.owner_space = "LOCAL"
except Exception as e:
print("error occur for: no." + str(i))
print(e)
另外,为了方便操作,生成好的phyrig我们直接移入一个空图层
# in createPhyBones():
# bone.layers is an array of 32 boolean values tells you whether the bone is present on each of the 32 layers.
#需要注意的是,blender使用的layers来管理对应armature object的bonelayer,它是一个32位的布尔向量,在需要把当前选中骨骼转移到对应图层时,把对应位赋予true即可,从对应图层中清除该骨骼时,赋予对应位false即可
emptyLayer = 0
recentLayer = 0
breakTag = 0
#获取指向空图层及当前图层的位置
for i in range(32):
if (breakTag == 2):
break
if(bpy.context.selected_bones[0].layers[i] == False):
emptyLayer = i
breakTag +=1
else:
recentLayer = i
breakTag +=1
for i in range(RigLength):
PhyRig[i].name = ("HatBonePhy.%03d" % i)
PhyRigNameList.append("HatBonePhy.%03d" % i)
DeformRigNameList.append(DeformRig[i].name)
print(PhyRig[i].name)
#对应bone直接移入空图层,并将其移出defrig所在图层
PhyRig[i].layers[emptyLayer] = True
PhyRig[i].layers[recentLayer] = False
效果演示:
一键即可完成物理骨骼创建及绑定
phyrig自动移入空图层,与defrig分开
整体效果
后续还需要跟body armature做父子级绑定,这个就是手工操作的后话了。