Skip to content

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

Merged
LIlGG merged 8 commits into
LIlGG:mainfrom
Enterpr1se0:fix/parameter-accumulation-and-idle-motion
Sep 29, 2026
Merged

LIlGG merged 8 commits into
LIlGG:mainfrom
Enterpr1se0:fix/parameter-accumulation-and-idle-motion

Conversation

@Enterpr1se0

@Enterpr1se0 Enterpr1se0 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

  • 在引擎的 beforeModelUpdate 阶段应用参数写入,避免鼠标追踪、呼吸等 add 值被逐帧累加到引擎基线。
  • 区分单帧写入与需要持续生效的值:手动控制、FSM 状态、情绪过渡终点和程序动画终点会在后续帧保持,并在恢复控制、状态切换、下一段动画或模型切换时释放。
  • 恢复显式 exitProfile.semanticParameters 的归零行为;保留队列写入之间的冲突记录,对持续发生的锁定值冲突去重。
  • 无协调器时拒绝创建无法释放的参数锁定,避免接口表面成功但实际不生效。
  • 同一参数的新程序动画接管旧动画,防止旧动画后完成时覆盖新目标;补充引擎事件订阅与销毁的集成测试。
  • 模型销毁或切换时结束进行中的动画等待,并释放动画持有的参数值。

验证:

  • pnpm exec vitest run:224 项测试通过。
  • pnpm build:TypeScript 检查及两个 Vite 构建通过。
  • 本地真实浏览器验证 Cubism 2 模型和 Hiyori .moc3 模型:加法参数不累加;手动和 FSM 控制可恢复;情绪及程序动画终点保持;模型切换清除旧锁定值。

Which issue(s) this PR fixes:

Fixes #84

Does this PR introduce a user-facing change?

修复看板娘动作参数逐帧累加,以及表情和动画完成后立即回弹的问题。

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.
@LIlGG

LIlGG commented Sep 28, 2026

Copy link
Copy Markdown
Owner

我看到你已经实现了基于 beforeModelUpdate事件的处理方案, 不过经过此方法之后,类似调试台的滑块就只会在用户操作时写入一次 override 了。因此我期望你能对持续生效的手动设定或者状态参数进行处理,否则这些地方也只会生效一帧了。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Queued override values may revert after one frame and need to remain persistent.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Fixes frame-to-frame accumulation of Live2D additive parameter writes and resets coordinator state when models change.

Changes:

  • Updates coordinator resolution and reset behavior.
  • Hooks flushing into the engine lifecycle.
  • Adds lifecycle, precision, and regression tests.
File Summary
packages/​live2d/​src/​runtime/​controller/​coordinator.ts Updates write resolution and adds reset support.
packages/​live2d/​src/​runtime/​controller/​controller.ts Hooks coordinator flushing into the engine lifecycle.
packages/​live2d/​src/​runtime/​controller/​__tests__/​coordinator.test.ts Adds lifecycle, stability, precision, reset, and priority coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/live2d/src/runtime/controller/controller.ts
Enterpr1se0 and others added 3 commits September 28, 2026 14:46
A write applied from the engine's `beforeModelUpdate` event only survives
the frame it was queued in: the engine restores its own parameter baseline
at the end of every frame. That is what stops `add` writes from
accumulating, but it also meant a writer that knows its value once - the
DevTools parameter slider, an FSM state profile - only took effect for a
single frame.

`ParameterCoordinator.holdOverride()` re-applies such a value on every
flush until `releaseOverride()` (optionally restricted to one source).
The DevTools slider holds its value as MANUAL, FSM state profiles hold
theirs as FSM and release them on state exit, and `getSemanticParameters()`
reports the held value so the panel shows what is actually being applied.

A queued write suppressed by a hold is not logged as a conflict: the hold
is re-applied every frame, so logging it would flood the conflict log.
`flush()` runs on every engine frame. Tracking which parameters were
already resolved in a fresh `Set` allocated one object per frame even when
nothing is held; the queue itself still holds this frame's parameters at
that point, so it doubles as the lookup.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate findings affect terminal writes, exit-profile behavior, conflict logging, hold release, and lifecycle coverage.

Review effort: Lite
Findings: 2 High severity · 3 Medium severity

Open (5)

Comment thread packages/live2d/src/runtime/controller/coordinator.ts
Comment thread packages/live2d/src/runtime/behavior/fsm.ts
Comment thread packages/live2d/src/runtime/controller/coordinator.ts Outdated
Comment thread packages/live2d/src/runtime/semantic/semantic-parameter-layer.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved critical animation ordering and moderate FSM, conflict de-duplication, and engine-hook coverage issues remain.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (5)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Add integration coverage for engine-hook dispatch and teardown

packages/​live2d/​src/​runtime/​controller/​controller.ts:158

The new engine-hook path is not covered by the controller tests: they exercise ParameterCoordinator with a hand-built lifecycle rig, but never call initialize() with an internalModel emitter to verify that beforeModelUpdate actually flushes/captures and that destroy() removes the listener. A regression in the event name or subscription/cleanup would therefore pass the suite while breaking the runtime fix; add a focused controller integration test for both dispatch and teardown.

Comment thread packages/live2d/src/runtime/procedural/animator.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

The changes span parameter coordination, lifecycle cleanup, animation, emotion, FSM, and engine integration; final human review is prudent.

Review effort: Lite
Findings: None

Resolved since last review (1)

@LIlGG
LIlGG merged commit 08f00ee into LIlGG:main Sep 29, 2026
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] 鼠标追踪甩到极限后卡住:参数写入被引擎每帧基线吸收累加

3 participants