Blender 骨架的骨骼布料模拟插件。把骨骼链当作粒子系统求解, 播放或拖动时间轴时实时出结果, 也可以把结果烘焙成关键帧。用于头发、裙摆、尾巴、飘带、袖子这类跟随身体运动摆动的部件。
一个配置指定若干根骨骼。根骨骼保持动画姿势不动, 它们的全部子骨骼成为粒子。 每帧按固定频率(默认 90Hz)推进若干步, 每步施加重力/风/惯性后, 用位置约束迭代求解粒子位置, 最后把粒子位置反解成 pose bone 的旋转写回骨架。约束包括距离、角度复原、角度制限、 三角弯曲、系绳、移动限制、碰撞体碰撞与自碰撞。
求解在 GPU 或 CPU 上跑, 两者是同一份 kernel 源编译出来的两个目标(见「编译目标」)。 整帧的全部 kernel 发射被录制成一张执行图, 每帧回放一次。
模拟输入是骨架的静止姿势与当前动画姿势, 与被蒙皮的角色网格、形态键无关; 唯一读网格的是 网格碰撞体, 它每帧读自己那份顶点, 因此可以跟着形变。
- 骨骼布料: 骨骼链本身按父子关系连成线, 也可以把多条链横向连成网格(裙摆一圈、披风一片)。 网格模式会额外生成三角形, 从而支持弯曲刚性与自碰撞。
- 骨骼弹簧: 根骨骼自身围绕动画姿势做受限弹簧摆动, 不改变链的形状。用于胸部这类 只需要局部微幅晃动的部件。
「全局模拟设置」里的编译目标决定求解器编译到哪儿:
- CUDA(默认): kernel 源编译成 PTX 跑在显卡上。
- CPU: 同一份 kernel 源编译成主机代码。没有显卡时用, 慢一到两个数量级。
两个目标共用同一份 kernel 源与同一条录制/回放路径, 不存在第二套实现。 切换目标会重建设备状态并重新录图, 第一次求解要等 kernel 编译(编译结果由 warp 缓存在磁盘上, 之后不再重编)。
配置里的每一处骨骼引用(根骨骼、属性覆盖、碰撞体绑定、碰撞骨骼、蒙皮骨骼、锚骨骼、
法线对齐骨骼)存的都不是骨骼名, 而是一个写在该骨骼自定义属性 ruri_cloth_bone_uid 上的
唯一标识。面板里显示和填写的仍然是骨骼名, 那只是这个标识解析出来的视图。
因此重命名骨骼不会断开任何绑定——无论是在编辑模式里改、用脚本改 bone.name,
还是在没装本插件的 Blender 里改, 引用都会跟着走。骨骼被复制时标识也会被复制,
下一次给它赋值时会自动换发一个新的以保证唯一。
标识随骨骼存在 .blend 里。如果整套骨架被删掉重新导入(而不是重命名), 标识不复存在, 此时引用会显示为空, 需要重新指定; 这是刻意让它显式失败, 而不是悄悄绑到同名的别的骨骼上。
把目录放进 Blender 的 scripts/addons/, 在偏好设置里启用 "Ruri Cloth Physics"。
需要 Blender 4.2 以上。依赖 numpy(Blender 自带)与 warp-lang, 后者在首次启用时自动安装。
- 选中骨架对象, 打开
Properties ▸ Physics ▸ Ruri 布料物理。 - 在配置栈里按
+新建配置(骨架随之登记进本场景的模拟), 在「主设置」里把要摆动的链的 根骨骼加进根骨骼列表(骨架里选中骨骼后可用「从选中骨骼添加」批量加入)。 - 面板标题栏的 ▶ 是这副骨架在本场景的实时模拟开关; 链接/追加进来、还没登记的骨架 在这里按 ▶ 即登记并打开。
- 播放时间轴即可看到模拟。
复位(清空)回到动画姿势,复位(保形)只消除惯性、保留当前形状。 - 需要防止穿模时, 打开「碰撞体」面板(在「主设置」正下方)建球/胶囊/平面/网格碰撞体并绑到骨骼上, 再在里面的「本配置的碰撞」里引用它们。详见下一节。
- 需要风时, 新建一个 Empty 并在其
Properties ▸ Physics ▸ Ruri 布料物理风区里「设为风区」 (登记进本场景的风区列表), 全局方向风、球形/盒形范围风、放射风四种模式。 - 定稿后用「烘焙为关键帧」把指定帧范围的结果写成关键帧。
模拟是逐帧递推的: 跳到任意帧会从当前状态往下算, 而不是重放该帧的历史结果 (与 Blender 自带布料的行为一致)。要得到确定的一段结果, 从头顺序播放, 或者直接烘焙。
谁参与模拟是场景登记的事实, 插件从不在场景里搜索。场景持有三张显式列表:
- 骨架: 本场景模拟的骨架, 各带一个实时模拟开关(标题栏 ▶)。
- 场景级碰撞体: 本场景每一条布料配置都碰的碰撞体(墙面、地面), 在碰撞体自己的面板里登记。
- 风区: 吹本场景布料的风区空物体。
配置直接引用它碰的碰撞体; 除此之外的碰撞只来自场景级碰撞体, 不存在默认全场景碰撞。 帧处理、视口覆盖与面板只沿这些列表向外走, 没登记的物体一个字节都不读; 列表为空的场景每帧零开销。删掉的物体在列表里读作空并被跳过, 下一次写列表时清掉。
「碰撞体」面板是一个顶层面板, 位置在「主设置」正下方, 碰撞体池与「本配置的碰撞」 (检测模式、摩擦、引用列表)都在这里, 不用再翻到物理参数深处。
面板顶部的编辑碰撞体开关默认关闭, 不干扰正常视图。打开后:
- 视口里画出本场景布料用得到的碰撞体(被登记骨架的配置引用的、场景级的)与当前选中的碰撞体线框: 当前选中的为橙色, 其余为暗蓝色, 已禁用的为灰色。 画出来的形状与求解器实际使用的完全一致(同一套几何函数), 所见即所碰。
- 当前碰撞体上出现拖拽控制器, 颜色与线框刻意区分开: 白色方块拖半径、灰色方块拖终端半径(仅两端半径分离时出现)、 绿色箭头拖长度、品红圆点拖中心偏移。手柄大小随碰撞体尺寸走, 拖动即时生效。
- 显示与模拟是否开启无关, 不播放也能调。
胶囊沿骨骼方向铺满 length, 两端各内缩一个半径; start_radius 落在骨骼末端,
end_radius 落在骨骼根部。「从选中骨骼生成」会按骨骼长度直接建好一批。
网格碰撞体用物体自身的三角形, 其顶点每帧上传, 因此形变、形态键、蒙皮都会被跟上; 拓扑(面的角点与半边配对)只在绑定时读一次。绕向不一致的曲面会被自动统一, 无法统一的曲面在绑定时就被拒绝而不是悄悄给出错误的内外侧。
多数数值参数支持「随深度曲线调制」: 打开后用曲线控件按粒子在链上的归一化深度 (根为 0, 末端为 1)分别给值, 例如让阻尼从根到梢递减、让角度制限从根到梢放开。 曲线在内部采样成 16 点查找表随配置一起上传。
| 面板 | 作用 |
|---|---|
| 力 | 重力(大小/方向/初始姿势衰减)、阻尼、复位后稳定时间 |
| 角度复原 / 角度制限 | 朝静止姿势的回弹力, 与相对父骨的最大摆角 |
| 形状复原 | 距离刚性、系绳收缩限界、三角弯曲刚性 |
| 惯性 | 世界/局部惯性比、平滑、限速、传送检测、深度惯性、离心加速、锚对象 |
| 碰撞体碰撞 | 点/边两种检测模式, 摩擦与推出限距 |
| 自碰撞 | 完全网格(点-三角 + 边-边 + 交叉冻结), 支持与另一配置相互碰撞 |
| 风 | 影响率、频率、乱流、噪声混合、同步率、移动风 |
| 弹簧 | 弹簧类型专用: 强度、移动限制距离、法线方向限制、非同步化 |
| 剔除 | 视口不可见剔除与距离剔除 |
| 视口显示 | 粒子、轴、线/三角、基线、碰撞体、惯性中心的覆盖线框 |
场景级参数(编译目标、模拟频率、单帧最大步数、全局时间缩放)在同一面板的「全局模拟设置」里。
三层, 依赖方向单向: blender_host → cloth_engine → cloth_kernel。
cloth_kernel/ 纯 NumPy 主机层, 不依赖 bpy 也不依赖 warp
defs.py 常量与判据, 每个数值带测量出处
host_math.py 向量化数学(四元数/几何原语/LUT/噪声), 与设备端同语义
world.py 全场景唯一 SoA 竞技场: 队伍/粒子/变换/碰撞体/约束条目/
自碰撞基元共享扁平数组, 按块分配与回收
compile.py 拓扑快照 → 约束表(距离/弯曲/角度分层/后置链), 带 token 缓存
program.py 竞技场 → 设备程序: CSR 表、分层展平、按根分组、自碰撞容量
contact_plan.py 自碰撞任务表, 含配置之间的相互碰撞
fixed_point.py 定点累加器规格, 以及构建期的整数溢出上界证明
frame.py 帧掩码 io.py 逐帧输入/输出与风区接口
cloth_engine/ Warp 设备层: 既是 kernel, 也是把帧编译成执行图的编译器
target.py 编译目标表(CUDA / CPU)
state.py 设备侧 slab 存储、平面表、空间索引(BVH)
device_state.py 由平面表动态生成 Warp 结构体
dmath.py 设备端数学 kernels.py 碰撞体场、连续碰撞检测、约束数学
families.py 每个 family 一个元素函数与一个 kernel
schedule.py 帧排程: family 表、帧序表、循环上下文
effects.py 从 kernel 源 AST 提取读写集; 未认领的访问点拒绝构建
dataflow.py hazard DAG、分层, 以及死写/未初始化读/累加窗口的证明门
lint.py 转录残留检查(赋值后从不读的局部、相邻重复读)
plan.py 整帧录成一张执行图: CUDA 按 hazard 分层多流, CPU 单流拓扑序
engine.py 结构签名与增量上传/下载, 图缓存 pipeline.py 帧入口
blender_host/ 所有 bpy 调用集中在这一层
runtime.py 注册表、帧处理器、输入采集、结果写回
armature.py 骨架快照、主机端 FK、矩阵批量读写
bone_binding.py 骨骼唯一标识与名字解析
world.py 场景模拟世界: 骨架/场景级碰撞体/风区三张登记表
chain.py collider_geom.py shapes.py wind_geom.py
链、碰撞体、风区的绑定与几何
properties.py ui.py operators.py presets.py config_io.py curve_host.py
属性、面板、操作符、预设、配置导入导出、曲线控件
viewport.py viewport_draw.py viewport_handles.py layer_*.py selection_sync.py
视口覆盖、拖拽控制器、选择同步
bake.py 烘焙为关键帧 compat.py 版本兼容
并行求解里所有的累加走 int64 定点而不是浮点, 所以求和顺序不影响结果。这条性质是整个 设备层的地基: 它使得 CUDA 上的多流并发、kernel 内的循环内化、发射几何的改变都不会改变 输出的任何一位, 也使得同一份 kernel 源在 CPU 与 CUDA 上各自可复现。
cloth_engine 在导入时跑一批证明门, 任何一条不过就拒绝加载, 而不是运行时报警:
- 效应覆盖: 从 kernel 源提取每个 family 的读写集; 有任何一处状态访问没被规则认领就拒绝, 因为读写集是后面所有推导的输入, 漏一处推导就是错的。
- 数据流: 被写而无人读的平面(计算白做或信号无人接收)、本帧被写之前就被读的临时平面、 以及每个定点累加窗口的最坏同号求和上界, 三者都在导入期判死。
- 排程一致性: family 与 kernel 同名同签名、发射宽度引用的平面存在、 空间索引的刷新一定排在所有查询它的 family 之前。
- 转录残留: 赋值后从不读的局部、相邻的重复读。
这些门的存在意义是让"源码说的"和"二进制做的"不可能分叉——出问题时构建直接失败, 而不是给出一个看起来正常的错误结果。