握持姿势
GripPose 让角色的手保持你指定的姿势:pistol、handle、cup。姿势只描述每根手指弯多少、张开多少、拇指去够哪里,不涉及任何骨骼名字,所以同一个姿势可以用在任何骨架上。弯曲的方向在第一次握持时,根据每个角色自己的手算出来。
它只负责手型。把手放到物体上由挂点负责,通常是手部关节下的子实体,加上手臂的 two-bone IK。
把 GripPose 放在角色的根实体上,给左右手各指定一个姿势:
import { GripPose } from 'esengine';
world.insert(hero, GripPose, { rightPose: 'pistol', leftPose: 'support' });
// 扣扳机:食指从搭放变成扣下,其他手指不动。world.update(hero, GripPose, (grip) => { grip.rightTrigger = 1; });| 字段 | 作用 |
|---|---|
rightPose、leftPose |
每只手的姿势。留空时这只手淡出,交还给其他驱动它的东西。 |
rightWeight、leftWeight |
姿势应用多少,叠加在动画片段对手指的控制之上。 |
rightTrigger、leftTrigger |
食指从搭放到扣下的程度,要姿势同时定义了这两个状态才有效。 |
blendSeconds |
切换姿势大约花多久。从静止起步,平稳停下,没有跳变。 |
rightHand、leftHand |
手部关节,写从这个实体出发的路径。留空时按名字查找。 |
它在动画系统之后运行,所以走路动画继续带动手臂,姿势负责手指;没有动画系统时也能用。它只在游戏运行时生效,布置场景时不生效。
| 名字 | 用途 |
|---|---|
open |
张开的手,即骨架的静止状态。 |
relaxed |
自然垂放的手。 |
fist |
握拳,拇指压在手指上。 |
pistol |
手枪握,食指沿枪身伸直。trigger 让它扣下。 |
support |
托在步枪前握下方的手。 |
handle |
锤子、剑、工具的手柄。 |
pinch |
拇指和食指捏合。 |
cup |
捧杯或握球,手指张开。 |
要添加自己的姿势,或者按名字替换内置姿势:
import { registerGripPose } from 'esengine';
registerGripPose('claw', { thumb: { opposition: 1, gap: 0.2 }, index: { curl: 0.9, base: 0.2 }, middle: { curl: 0.9, base: 0.2 }, ring: { curl: 0.9, base: 0.2 }, pinky: { curl: 0.9, base: 0.2 },});| 数值 | 含义 |
|---|---|
curl |
手指弯多少,0 是骨架静止状态,1 是完全握紧。 |
base |
第一个关节弯多少,用于和其余关节不同的情况,比如手指沿枪身伸直或者勾起。默认等于 curl。 |
spread |
手指张开多少,-1 到 1。 |
bend |
每个关节相对放松姿势弯多少度,从根节开始:[30, 45, 20]。优先于 curl 和 base。编辑器写的就是它。 |
fan |
这根手指偏离手掌中线多少度:食指和中指偏向拇指一侧,无名指和小指偏向另一侧。优先于 spread。 |
trigger |
写在姿势上:rightTrigger 到 1 时食指的状态。 |
opposition |
拇指:从静止位置走向目标的程度,0 到 1。 |
to |
拇指:fingers(中指中节,用于握拳、握柄)或 index(食指指尖,用于捏合)。 |
gap |
拇指:在目标前停多远,以食指长度为单位,相当于中间夹着的握把或杯子。 |
拇指没有自己的弯曲数值,它是“去够”的:指尖走向其余手指当帧位置上的一个点(在手掌一侧),两个指节一起弯曲来够到它。所以握拳时拇指压在手指上,握东西时绕过握把。够不着时(比如手指伸直)就指向那个位置。
gripPoseByJoint(pose) 把任意姿势展开成逐个关节的形式,手的样子完全不变:这是改单个关节的起点。bend 和 fan 是按手掌自己的坐标轴量的,不是按骨骼,所以逐关节的姿势仍然适用于任何骨架和任意一只手。
适配你的骨架
Section titled “适配你的骨架”手部按名字查找:名字类似 RightHand 或 hand_r 的关节,下面的手指名字包含 thumb、index、middle、ring、pinky 或 little。手掌内的骨骼(index_metacarpal_r)会被跳过。如果骨架名字完全对不上,就在 rightHand 里写明关节路径;如果手指也没有这样的名字,就无法使用。
手指较少的骨架会跳过缺失的手指,并在日志里说明。gripPoseStatus(app, entity) 可以查询每只手找到了什么。
分成三个部分,各回答一个问题:
| 部分 | 负责 |
|---|---|
GripProfile(在物体上) |
每只手的掌心放在物体的什么位置:主手、副手各一个锚点实体,以及各自的手指姿势。 |
| 手臂 | 肩、肘、腕怎样把掌心送到那里:two-bone IK,手肘保持动画原本弯向的一侧,手腕转到锚点的朝向。 |
| 手指 | 每根手指怎么握:截取下来的手指姿势按 slerp(clip, grip, weight) 叠加在动画上,或者用一个 GripPose 名字。 |
锚点就是物体下的普通子实体,用现有的移动、旋转 gizmo 摆放。它的原点是掌心中心(手部关节到中指根的中点),+Z 是手指指向,+Y 是掌心朝向。
import { GripHolder, GripPlacement, GripProfile } from 'esengine';
world.insert(rifle, GripProfile, { mainAnchor: gripAnchor, supportAnchor: foregripAnchor, mainPose: 'pistol', supportPose: 'support' });world.insert(hero, GripHolder, { held: rifle, placement: GripPlacement.Object });GripHolder 字段 |
作用 |
|---|---|
held |
握着的物体:一个带 GripProfile 的实体。为空时松手。 |
mainHand |
Right 或 Left:主手。 |
placement |
Hand:物体跟着动画里的主手走。Object:物体由游戏放置(比如瞄准),主手手臂被带到物体上。 |
weight、supportWeight |
握持在动画之上应用多少,以及副手在此之下应用多少。 |
blendSeconds |
握住、松开、换物体分别花多久。每个关节都从原来的位置平滑过渡,起步和停止都平稳,手不会跳。 |
rightHand、leftHand |
名字找不到手时,手部关节的路径。 |
GripProfile 字段 |
作用 |
|---|---|
mainAnchor、supportAnchor |
锚点实体。没有副手锚点就是单手握。 |
mainPose、supportPose |
每只手的手指:GripPose 名字,或截取的手指姿势。 |
mainFingers、supportFingers |
每只手逐个关节的手指,GripPose 的 JSON(编辑器写入)。设置后优先于名字。 |
hand |
锚点是照哪只手摆的。另一只手主握时,两个锚点都会镜像。 |
mirror |
用物体自己的哪个平面镜像:垂直于 X(默认)、Y 或 Z 的那个。 |
wrap |
手指是否合拢到物体的 GripSurface 形状上,每根碰到就停。默认打开;没有表面时不起作用。 |
它在动画系统之后运行,每帧按固定顺序单向计算,所以不会形成循环:动画姿态;然后物体(跟着主手,或者在游戏放的位置);然后从物体当前位置读出副手锚点;然后副手手臂;最后两只手的手指。物体从不读取副手。每帧都从动画姿态开始求解,从不在自己上一帧的结果上继续;除了游戏时钟不读任何时钟。所以同样的输入和步进得到逐位相同的骨骼。
captureFingerPose(world, handJoint) 把手下面每个有名字的关节当前的旋转截取成普通 JSON;registerFingerPose(name, pose) 保存它,供 GripProfile 引用。用 GripPose、动画片段或手动摆好手型后截取下来,把 JSON 和游戏一起保存。截取的姿势用的是骨骼名字,所以属于那套骨架;GripPose 名字则适用于任何骨架。
姿势决定手指合拢多少,合拢到什么上由物体决定。在物体上、或它下面的任何实体上,为手要握住的每个部分加一个 GripSurface:握把或枪管用圆柱或胶囊,球形用球,方块握把用盒。尺寸按所在实体自己的单位,和碰撞体一样:
GripSurface 字段 |
作用 |
|---|---|
shape |
圆柱、胶囊、球或盒。 |
direction |
圆柱或胶囊沿哪根轴:实体的 X、Y 或 Z。 |
center |
中心,按实体自己的单位。 |
radius、height |
圆柱、胶囊或球的半径,以及圆柱或胶囊的总长。 |
size |
盒的尺寸。 |
wrap 打开时,每根手指从张开往姿势的角度合拢,所有关节一起动;最先碰到表面的那节骨头连同它之前的关节停在那里,之后的关节继续合拢。拇指沿它伸出的路径合拢,同样碰到就停。手指停在离形状大约一个手指粗细的地方(食指长度的 8%),也不会比姿势弯得更多:姿势里张开的手指保持张开。一开始就在表面里面的骨头(比如压在掌心的握把)可以继续动,但不会更深。算的只是点到形状的距离,每只手几百次:不用物理、不读网格,每次结果都一样。
所以 fist 加上 wrap,不管握把多粗,每根手指都会合拢到握把上;pistol 则让食指仍然沿着枪身伸直。
锚点只摆一次,按 hand 指定的那只手。持有者用另一只手主握时,两个锚点都按物体的 mirror 平面镜像:位置反射,朝向变成另一只手的掌心。按名字的手指和逐关节的手指描述的是手的形状而不是骨骼,所以同样会镜像。截取的姿势不会:它记录的是一只手的骨骼。
选中带 GripProfile 的物体,细节面板里它的分区就是握持编辑;握着它的角色会在视口里实时显示握持,不用进 Play:
- 预览。 选一个角色。没有
GripHolder的会被加上一个,握住这个物体、摆放方式为物体,于是它会去够场景里物体所在的位置。主手开关改的是持有者的mainHand,用它查看镜像后的握持。掌心够不着锚点时,会显示差多远。 - 锚点。 选一个实体,或者在物体下新建一个。「选中」会切到它,用移动、旋转 gizmo 摆放。视口按实际使用的位置画出每个掌心(另一只手主握时画镜像后的):一个框表示掌心,箭头是手指方向,虚线是掌心朝向,短线在拇指一侧;主手黄色,副手蓝色。
- 动画。 选一个时间轴动画,拖时间条或播放:视口里角色按它摆出姿势,握持叠在上面,和 Play 里一样。只动视口;去掉动画、关掉预览或离开这个分区时姿势复原。
- 手指。 从一个 GripPose 开始,再按度数改任意关节的弯曲或任意手指的张开。第一次修改会把姿势逐关节写进
mainFingers或supportFingers;「重置」回到姿势本身。视口里预览角色的每个手指关节上有一个点(主手黄色,副手蓝色),拖动它就弯曲那个关节,一次拖动一步撤销。 - 贴合表面。
wrap开关、当前贴住表面的手指,以及物体上的表面列表。「新建」会在主手锚点下加一个横过掌心的圆柱,按角色手掌定大小,再用 gizmo 和字段调到握把上。视口里表面用绿色线框和掌心标记一起画出。
预览用的是视口的 预览特效 开关,和弹簧骨同一个。它只动视口里的骨骼和被握的物体,不写场景;关掉后它们回到场景里的样子。
锚点表示掌心的位置,不是手部关节的位置,手臂每帧按所在的身体求解,所以手臂更长或更短的身体仍然能把掌心放到锚点上。够不着时,手臂伸直指向锚点,手腕仍然转到锚点朝向:掌心停在锚点前方,每次结果都一样。
- 手指贴合的是简单形状,不是网格。 物体的表面就是它声明的那些形状;形状奇怪的握把用几个拼起来。
- 拇指在视口里没有拖动点。 拇指是按「够向」求的,不是按角度弯的,所以用数值设置;截取的姿势也不能在面板里编辑。
- 编辑器在握持下面播放的是时间轴,不是 Animator 状态机; 状态机本身在 Play 里运行。
Hand模式只弯副手手臂,Object模式先弯主手; 肩膀和脊柱不会为了够到而倾斜。- 一个持有者只能握一个物体。
- 单独的
GripPose不接触任何东西。 姿势只是手型:握把太粗时手指可能陷进去,太细时可能碰不到。用GripHolder握住物体并在物体上加GripSurface,手指就会贴合上去。 - 手的参数只计算一次, 基于第一次握持时骨架的状态。如果那时手正处在动画中,轴就按当时的状态算,实际差别不大。
- 不支持手臂带负缩放的骨架。 镜像骨骼会转错方向。
- 只在一种真实骨架上检查过。 姿势是在一个 Mixamo 命名的角色上调的。用骨骼朝向不同、名字不同的同一只手测试,指尖位置一致,但还没有看过第二套真实骨架。
- 没有在真机上测过。 每帧最多 30 个关节,每个几次四元数运算;在没有动画片段驱动的骨架上,手稳定后就不再有开销。