Skip to content

About

🥵嫁服物理模擬

Resources

Stars

18 stars

Watchers

0 watching

Forks

Repository files navigation

RuriClothPhysics

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, 后者在首次启用时自动安装。

基本用法

  1. 选中骨架对象, 打开 Properties ▸ Physics ▸ Ruri 布料物理。
  2. 在配置栈里按 + 新建配置(骨架随之登记进本场景的模拟), 在「主设置」里把要摆动的链的 根骨骼加进根骨骼列表(骨架里选中骨骼后可用「从选中骨骼添加」批量加入)。
  3. 面板标题栏的 ▶ 是这副骨架在本场景的实时模拟开关; 链接/追加进来、还没登记的骨架 在这里按 ▶ 即登记并打开。
  4. 播放时间轴即可看到模拟。复位(清空) 回到动画姿势, 复位(保形) 只消除惯性、保留当前形状。
  5. 需要防止穿模时, 打开「碰撞体」面板(在「主设置」正下方)建球/胶囊/平面/网格碰撞体并绑到骨骼上, 再在里面的「本配置的碰撞」里引用它们。详见下一节。
  6. 需要风时, 新建一个 Empty 并在其 Properties ▸ Physics ▸ Ruri 布料物理风区 里「设为风区」 (登记进本场景的风区列表), 全局方向风、球形/盒形范围风、放射风四种模式。
  7. 定稿后用「烘焙为关键帧」把指定帧范围的结果写成关键帧。

模拟是逐帧递推的: 跳到任意帧会从当前状态往下算, 而不是重放该帧的历史结果 (与 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 之前。
  • 转录残留: 赋值后从不读的局部、相邻的重复读。

这些门的存在意义是让"源码说的"和"二进制做的"不可能分叉——出问题时构建直接失败, 而不是给出一个看起来正常的错误结果。

About

🥵嫁服物理模擬

Resources

Stars

18 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages