@dreamerbird/threeik
v0.2.1
Published
Humanoid skeleton editing and IK library for three.js, ported from Godot's skeleton architecture
Readme
threeik(npm:@dreamerbird/threeik)
three.js 的人形骨架编辑基础库。骨架数据层采用扁平化分解存储(rest / base / work 三套局部姿势 + 懒计算全局姿势缓存),modifier 管线提供 CCD / FABRIK / Two-Bone 三种 IK 求解器、关节角锥限制(ConeJointLimitation)、Aim / CopyTransform 约束,以及基于骨名映射的动画重定向(RetargetModifier,内置 Mixamo / VRM / ReadyPlayerMe 预设)。算法与架构参考 Godot 4 的 SkeletonModifier3D / SkeletonIK3D 体系移植。
安装
npm i @dreamerbird/threeik threethree 为 peer 依赖(>= 0.160.0);除此之外无运行时依赖。
最小用例
import { SkeletonRig, CCDIkModifier } from '@dreamerbird/threeik';
import { Object3D } from 'three';
// rootBone:骨架根 Bone(v1 假定单根人形骨架)
const rig = new SkeletonRig(rootBone);
// IK target 是场景里的普通 Object3D,拖动它即可驱动骨骼
const target = new Object3D();
scene.add(target);
rig.addModifier(new CCDIkModifier([
{ rootBone: 'UpperArm', endBone: 'Hand', target },
]));
function animate(dt: number) {
mixer?.update(dt); // 1. 动画先写入骨骼
rig.captureBasePose(); // 2. 采基准姿势
rig.update(dt); // 3. modifier 管线求解并写回骨骼
renderer.render(scene, camera); // 4. 渲染
}更新顺序契约
每帧严格按以下顺序调用:
mixer.update(dt)—— AnimationMixer 先把动画姿势写入骨骼;rig.captureBasePose()—— 把骨骼当前 TRS 采为基准姿势(base);rig.update(dt)—— modifier 管线在 base 上求解(base 与工作姿势隔离,求解结果不累积);renderer.render(...)。
漏调 captureBasePose() 时 base 停在旧帧:无动画的纯 FK 编辑场景可以不调(base 即 rest),接了 AnimationMixer 的场景每帧必须调用,否则 modifier 在过期基准上求解。
重定向时目标 rig 在源 rig 之后 update(RetargetModifier 读取源 rig 的当前姿势);顺序反了结果滞后一帧。
target 双轨解析
链/约束配置中的 target、poleTarget、referenceObject 均接受两种形式:
Object3D直接引用;- 字符串 key,经
rig.targetResolver = (key) => Object3D | null解析(供 theatre 等接入层按名字注入场景对象)。
未解析到时该链/约束本帧跳过,并通过 warnOnce 触发一次 warning 事件(rig.on('warning', ({ key, message }) => ...)),不刷屏不崩溃;target 位置/四元数为 NaN 时同样守卫跳过。
rebind 注意
编辑骨架树(增删 Bone、改层级、重命名,或 theatre 快照编辑器 clone 场景)后,调用 rig.rebind(newRoot):rig 重建全部内部缓冲(骨列表、父子索引、rest 姿势),并逐个 detach/attach 已挂载的 modifier,重建其索引与链缓存。注意 rebind 以骨骼当前 TRS 为新的 rest pose。
配置替换惯例
modifier 配置在构造时整体传入;运行时改配置的惯例是整体替换:
rig.removeModifier(oldModifier);
rig.addModifier(new CCDIkModifier(newChains));IterateIKModifier(CCD / FABRIK 的基类)另有 setChains() 可原地替换链配置。同一 modifier 实例重复 addModifier 会被忽略。
接入指南:控制点装配(@dreamerbird/threeik/controls)
核心层之上是交互编辑层:声明式控制点装配器,入口为子路径 @dreamerbird/threeik/controls。一句话模型:球/标记 = 控制对象(常显),箭头/环 = 操纵器(仅选中显示)。
import { createSkeletonControls } from '@dreamerbird/threeik/controls';
const ctl = createSkeletonControls({
rig, scene, camera,
dom: renderer.domElement, // 指针事件宿主
facing, // 角色朝向(世界系),缺省 (0,0,-1)
controls: [
{ kind: 'root', name: 'hips', bone: 'Hips', rotation: true },
{ kind: 'limb', name: 'armL', rootBone: 'LeftArm', middleBone: 'LeftForeArm', endBone: 'LeftHand',
endRotation: true, rootRotation: true, pole: { color: 0xccff66, position: [0.38, 0.98, -0.2] } },
{ kind: 'chain', name: 'spine', rootBone: 'Spine', endBone: 'Neck' },
{ kind: 'bone', name: 'chest', bone: 'Spine2' },
{ kind: 'lookAt', name: 'head', rootBone: 'Neck', endBone: 'Head', position: [0, 1.7, 0.9] },
],
});
// 每帧:接在上面「更新顺序契约」的第 3 步之后
ctl.update(); // 携带 → 环跟随 → 引导线 → 操纵器屏幕恒定大小五种内置 kind:root(重心)、limb(四肢 TwoBone + pole 肘/膝朝向)、chain(脊柱 FABRIK)、bone(直接掰骨 FK)、lookAt(注视 CCD)。同骨架上多个控制点的 modifier 求解顺序由装配器按骨深度自动排(浅的先解)。ctl.get(name) 取句柄:钳制参数(setReachScale/setKeepAlive 等)、setActive 开关都在句柄上。自定义控制点用 registerControlKind 注册,走同一装配管线。
bone 的变体:shareWithParent: 0.4 进入分摊模式——拖环时父骨先转 40%、本骨补足剩余(解剖学分节旋转)。分摊 modifier 只在拖拽期间激活(常开会与同链求解器反馈互锁),松手即关闭、姿势归下游求解器接手;句柄的 onDragStart/onDragEnd 回调供宿主接线保持姿势(如重坐注视球)。单独使用松手即回弹。
头部通常不需要单独声明 bone 点:lookAt 加 rotateShare: 0.4 即合成单控制点双通道——W 拖注视球(CCD),E 出头骨环(分摊旋转),松手自动把注视球绕颈根按拖拽增量重坐、保持朝向,无需任何接线。
换模型
库不绑定模型,任何单根骨骼树都行。换模型 = 重建:
- 加载新模型,
new SkeletonRig(newRoot)(拓扑变更用rig.rebind,注意它以当前 TRS 为新 rest); - 控制点按骨骼名声明,把新模型的骨名填进 spec——Mixamo / VRM / ReadyPlayerMe 的命名差异可用重定向模块的预设与
suggestBoneMap解析; ctl.dispose()后用新 rig 重新createSkeletonControls;- 模型朝向不是 -Z 时传
facing。
初始姿势
- spec 的
position设控制球初始世界位置,装配首解即把骨架解过去(手球放低 = 手臂下垂);不设则保持 rest; - rest pose = rig 构造/rebind 时刻骨骼的 TRS:创建 rig 前把骨骼预摆到目标姿势,它即成为 rest(
rig.resetToRest()回到它)。
操纵器模式与选中
Maya 式 W/E:ctl.setManipulatorMode('move' | 'rotate')——移动/旋转操纵器由 three.js TransformControls 提供,W = 位置球 + 移动 gizmo,E = 旋转环;双通道控制点的球在 E 退化为可点标记(大小不变,只切可拖性)。选中机制:只有选中的控制点显示操纵器,点操纵器自动选中,点空白失焦。子选中('armL:elbow' / 'armL:root')让肘部/肩部成为独立选中目标。纯旋转控制点(bone、肩/髋根环)选中即出环,不看 W/E。
默认操纵器是 TransformControlsDriver;想接自研 gizmo 可在 createSkeletonControls 传 manipulator: ManipulatorDriver 注入自定义驱动——装配器只依赖 ManipulatorDriver 接口(setMode/attach/onDragStart/onDragChange/onDragEnd/dispose),实现可整体替换。生命周期注意:注入的 driver 由调用方持有(ctl.dispose() 不会 dispose 它);缺省由库内部创建的那个才随装配器一并释放。模式切换可经 onManipulatorModeChange?: (mode) => void 订阅(如 GUI 下拉框随键盘同步)。
快捷键
装配器内置 Maya 式快捷键:W 切 move、E 切 rotate、Escape 取消选中,默认绑在 window 上。可整体覆盖或关闭:
const ctl = createSkeletonControls({
// ...
// 数组 = 一个动作绑多键;缺省字段回落默认表;某动作传空数组 = 禁用该动作
hotkeys: { move: ['w', 'W'], rotate: 'e', deselect: ['Escape'] },
hotkeyTarget: someElement, // 限定监听范围(iframe / 自绘 UI);缺省 window
});
ctl.setHotkeys({ rotate: [] }); // 运行期换绑
ctl.setHotkeys(false); // 完全自绑出口:卸下全部监听,自行调 setManipulatorMode / 取消选中Breaking:0.0.x 操纵器换芯(TransformControls)
0.0.x 迭代中操纵器层迁移到 three.js TransformControls,以下旧 API 已变更:
DragTarget.setAxisHandles/PoleOrbit.setAxisHandles删除——轴箭头由 TransformControls 移动 gizmo 取代;RotateRings构造签名变化:不再接收camera/dom,只保留rings/ringRadius/viewRing等环自身参数;渲染相关方法setInteractive/setVisible与depthUniforms成员删除——显隐与交互由装配器统一管理;RotateRings.onRotateDrag改名onDragDelta,按下快照从onPress挪到onDragStart(kinds 在onDragStart捕获快照)。
颜色与外观
- 声明期:spec 的
color(球)、pole.color、ballRadius、ringRadius; - 运行期换色:
target.setColor(hex)/pole.setColor(hex)(经句柄取到:ctl.get('armL')!.target.setColor(0xff0000));选中高亮中保持亮黄,取消选中落回新色; - 视觉语言常量:
MARKER_SCALE(常驻标记球身份尺寸)、MARKER_SELECTED_COLOR(选中高亮黄)——自定义 kind 做标记球时直接引用,观感与内置一致。
已知限制
- v1 假定单根人形骨架(多根骨 / 多棵骨树未支持)。
CopyTransformModifier不支持全局 scale 拷贝(copyScale仅警告并忽略)。TwoBoneIkModifier要求 middle 是 root 的直接子骨、end 是 middle 的直接子骨(attach 时校验)。
Playground
cd playground
npm install
npm run fetch-assets # 下载 Soldier.glb(three.js 官方示例模型,被 .gitignore 排除不入库)
npm run dev # http://localhost:5199四个页签:IK、约束、重定向、动画+IK,对应库的四条主用法路径。
