Skip to content

fix: 修复运行时参数写入逐帧累加 - #85

Open
Enterpr1se0 wants to merge 2 commits into
LIlGG:mainfrom
Enterpr1se0:fix/parameter-accumulation-and-idle-motion
Open

Enterpr1se0 wants to merge 2 commits into
LIlGG:mainfrom
Enterpr1se0:fix/parameter-accumulation-and-idle-motion

Conversation

@Enterpr1se0

@Enterpr1se0 Enterpr1se0 commented Sep 21, 2026 •

Copy link
Copy Markdown

Closes #84

背景

看板娘的鼠标追踪会甩到极限后卡住不回来、呼吸恒定在 1。定位到问题出在参数写入:add 写入被引擎每帧的基线吸收而逐帧累加(详见 #84)。

改动(1 个 commit)

fix: stop additive parameter writes from accumulating across frames

引擎每帧 saveParameters() 会把「当前值」(含插件上一帧写入)存成基线,因此 add 不能叠在「当前值」上。

  • coordinator 现在显式记录每个参数实际应用的贡献与写入后读回的值(读回是为了让 Float32Array 精度可以精确比较),add 改为写入 引擎基线 + 贡献 的绝对值;引擎这一帧自己改过的参数则交还给引擎。
  • 停止写入的参数会撤回贡献(例如关掉鼠标追踪模块后头部归位)。
  • 新增 reset(),在 controller.initialize() 中调用,避免换模型时把上一个模型的待写入/贡献带到新模型。
  • override 语义不变(含同优先级「先入队赢」的规则)。
  • 顺带把 resolveParameter 里每参数每帧 4 次数组分配(filter()/filter()/reduce()/reduce())换成单趟遍历,使本次修复的开销 < 0.2µs/帧。

为什么改在 coordinator:插件有 8 个参数写入点(procedural / motionLayer / FSM ×2 / emotion / animator / devtools / controller),由于 setSemantic() 只要挂了 coordinator 就改走 queueWrite(),它们全部汇聚到 ParameterCoordinator.flush(),所以这是唯一的汇聚点,不需要改动语义层、调用方,也不需要 patch 引擎内部。

验证

  • 单测:188 个,187 通过(packages/live2d/src/runtime/controller/__tests__/coordinator.test.ts 新增 8 个)。唯一失败的 Live2dDevTools > toggle switches visibility state 是仓库既有问题(测试调用了组件里不存在的 _toggleVisible()),与本 PR 无关。

  • 类型检查:tsc -b exit 0。

  • 浏览器实测(真实后端 + 真实模型 + 真实鼠标事件,读语义层参数;鼠标移到画布右侧再移回中心):

    模型 修复前 修复后
    Cubism 4(未声明 Motions) 0 → 30(永久卡死) 0 → 15 → 0
    Cubism 2(Potion-Maker/Pio,自带 idle) 0 → 29.8 → 8.7 → 0(大部分时间卡在 30) 0 → 30 → 0
    换模型 新模型残留 12.87 0

兼容性

  • 公开 API 只增不改:queueWrite / flush / getConflictLog / clearConflictLog 签名不变,仅新增 reset()。
  • 参数语义不变:override、参数钳位、add 求和、override + add、冲突解析与冲突日志顺序/内容均与原实现一致(已逐项对照,并有测试覆盖)。
  • 有意的行为变化仅 3 处,都是修复本身:add 的参考点(当前值 → 引擎基线)、写入方停止后撤回贡献、换模型时丢弃待写入。
  • 边界:动作淡入期(weight < 1)引擎是 values * (1 - w) + motion * w,会把我们上一帧贡献的 (1 - w) 漏进基线,此时基线还原只是近似(已在代码注释中说明)。

已知未处理

引擎自带的 CubismBreath 每帧用 addParameterValueById 给 ParamAngleX/Y/Z、ParamBodyAngleX 叠加正弦摆动(振幅 15/8/10/4),与插件自己的呼吸模块重复。这是设计取舍(关掉会让模型明显变呆),如需处理建议单独 PR + 配置开关。上表 Potion-Maker/Pio 的 angleY 会漂到 -30 上限也来自这里(未打补丁的上游同样如此)。

The engine saves the current parameter values as its baseline at the start of
every frame and restores them at the end, so a write made after
`internalModel.update()` is baked into the next frame's baseline. `add` writes
are relative to the current value, so every frame stacked on top of the previous
frame's own result: the parameter reached its limit within a few frames and
stayed there (head pinned at +/-30, `breath` pinned at 1).

- apply `add` as `engine baseline + contribution` instead of
  `current value + contribution`
- release the contribution once a writer stops
- `reset()` drops pending writes and bookkeeping on a model switch
- `override` semantics unchanged

Also drops the per-parameter `filter()/filter()/reduce()/reduce()` allocations.
@Enterpr1se0
Enterpr1se0 force-pushed the fix/parameter-accumulation-and-idle-motion branch from f67d06b to 8daccf7 Compare September 21, 2026 10:38
@Enterpr1se0 Enterpr1se0 changed the title fix: 修复运行时参数写入逐帧累加 + 引擎自动重播 idle 动作 fix: 修复运行时参数写入逐帧累加 Sep 21, 2026

@LIlGG LIlGG left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢提交 PR。 不过目前通过扣除旧贡献的方法可能会留下偏移,我建议使用引擎已有的 beforeModelUpdate 来处理这段逻辑可能会更加合适,这样也无需通过数值来猜测上一帧的贡献。

@Enterpr1se0
Enterpr1se0 force-pushed the fix/parameter-accumulation-and-idle-motion branch 2 times, most recently from de61f4c to 8daccf7 Compare September 24, 2026 07:01
…event

The engine saves the parameter values as a baseline every frame and restores
that baseline at the end of the same frame, so a write made after
`internalModel.update()` became part of the next frame's baseline: an `add`
write stacked on top of its own previous result until the parameter reached its
limit.

The previous approach recovered the baseline by subtracting our own previous
contribution, which can leave an offset behind - a fading motion blends on top
of our leftover, so the recovered value is only approximate for the duration of
that fade.

Apply the writes from the engine's `beforeModelUpdate` event instead: it runs
after the baseline was saved and before the model is rendered with those
parameters, so a write is visible for exactly one frame and the engine drops it
afterwards. No contribution bookkeeping or value guessing is needed.

- `add` is applied on top of the engine's current value
- `override` keeps its conflict resolution, now resolved in the same single pass
- drops `AppliedAdd`, `appliedAdds`, `releaseStaleContributions` and the
  write-back comparison

Note: a one-shot `override` (the DevTools parameter slider) is now applied for
the frame it was queued in; a caller that needs it to hold has to queue it every
frame.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 鼠标追踪甩到极限后卡住:参数写入被引擎每帧基线吸收累加

2 participants