Skip to content

Commit c055f0c

Browse files
committed
Refactor Live2DManager to enhance mouth parameter handling and improve emotion mapping persistence.
1 parent c043b25 commit c055f0c

4 files changed

Lines changed: 105 additions & 3023 deletions

File tree

static/live2d-core.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ class Live2DManager {
6060
this.mouthValue = 0; // 0~1
6161
this.mouthParameterId = null; // 例如 'ParamMouthOpenY' 或 'ParamO'
6262
this._mouthOverrideInstalled = false;
63-
this._origUpdateParameters = null;
64-
this._origExpressionUpdateParameters = null;
63+
this._origCoreModelUpdate = null; // 保存原始的 coreModel.update 方法
6564
this._mouthTicker = null;
6665

6766
// 记录最后一次加载模型的原始路径(用于保存偏好时使用)

static/live2d-model.js

Lines changed: 67 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,15 @@ Live2DManager.prototype.loadModel = async function(modelPath, options = {}) {
1414
this.teardownPersistentExpressions();
1515
this.initialParameters = {};
1616

17-
// 尝试还原之前覆盖的 updateParameters,避免旧引用在新模型上报错
17+
// 还原 coreModel.update 覆盖
1818
try {
19-
const mm = this.currentModel.internalModel && this.currentModel.internalModel.motionManager;
20-
if (mm) {
21-
if (this._mouthOverrideInstalled && typeof this._origUpdateParameters === 'function') {
22-
try { mm.updateParameters = this._origUpdateParameters; } catch (_) {}
23-
}
24-
if (mm && mm.expressionManager && this._mouthOverrideInstalled && typeof this._origExpressionUpdateParameters === 'function') {
25-
try { mm.expressionManager.updateParameters = this._origExpressionUpdateParameters; } catch (_) {}
26-
}
19+
const coreModel = this.currentModel.internalModel && this.currentModel.internalModel.coreModel;
20+
if (coreModel && this._mouthOverrideInstalled && typeof this._origCoreModelUpdate === 'function') {
21+
coreModel.update = this._origCoreModelUpdate;
2722
}
2823
} catch (_) {}
2924
this._mouthOverrideInstalled = false;
30-
this._origUpdateParameters = null;
31-
this._origExpressionUpdateParameters = null;
25+
this._origCoreModelUpdate = null;
3226
// 同时移除 mouthTicker(若曾启用过 ticker 模式)
3327
if (this._mouthTicker && this.pixi_app && this.pixi_app.ticker) {
3428
try { this.pixi_app.ticker.remove(this._mouthTicker); } catch (_) {}
@@ -329,80 +323,83 @@ Live2DManager.prototype.loadModel = async function(modelPath, options = {}) {
329323
// 不再需要预解析嘴巴参数ID,保留占位以兼容旧代码调用
330324
Live2DManager.prototype.resolveMouthParameterId = function() { return null; };
331325

332-
// 安装覆盖:在 motion 参数更新后强制写入口型参数
326+
// 安装覆盖:覆盖 coreModel.update 方法,在 SDK 程序化动画之后强制写入参数
327+
// 这是最可靠的方式,因为 coreModel.update 是在所有参数修改之后、渲染之前调用的
333328
Live2DManager.prototype.installMouthOverride = function() {
334-
if (!this.currentModel || !this.currentModel.internalModel || !this.currentModel.internalModel.motionManager) {
329+
if (!this.currentModel || !this.currentModel.internalModel) {
335330
throw new Error('模型未就绪,无法安装口型覆盖');
336331
}
337332

338-
const mm = this.currentModel.internalModel.motionManager;
339-
340-
// 如果之前装过在其他模型上,先尝试还原
341-
try {
342-
if (this._mouthOverrideInstalled) {
343-
if (typeof this._origUpdateParameters === 'function') {
344-
try { mm.updateParameters = this._origUpdateParameters; } catch (_) {}
345-
}
346-
if (mm.expressionManager && typeof this._origExpressionUpdateParameters === 'function') {
347-
try { mm.expressionManager.updateParameters = this._origExpressionUpdateParameters; } catch (_) {}
348-
}
349-
this._mouthOverrideInstalled = false;
350-
this._origUpdateParameters = null;
351-
this._origExpressionUpdateParameters = null;
352-
}
353-
} catch (_) {}
333+
const internalModel = this.currentModel.internalModel;
334+
const coreModel = internalModel.coreModel;
335+
336+
if (!coreModel) {
337+
throw new Error('coreModel 不可用');
338+
}
354339

355-
if (typeof mm.updateParameters !== 'function') {
356-
throw new Error('motionManager.updateParameters 不可用');
340+
// 如果之前装过,先还原
341+
if (this._mouthOverrideInstalled && typeof this._origCoreModelUpdate === 'function') {
342+
try { coreModel.update = this._origCoreModelUpdate; } catch (_) {}
343+
this._origCoreModelUpdate = null;
357344
}
358345

359-
// 绑定原函数并覆盖
360-
const orig = mm.updateParameters.bind(mm);
361-
mm.updateParameters = (coreModel, now) => {
362-
const updated = orig(coreModel, now);
346+
// 口型参数列表(这些参数不会被常驻表情覆盖)
347+
const lipSyncParams = ['ParamMouthOpenY', 'ParamMouthForm', 'ParamMouthOpen', 'ParamA', 'ParamI', 'ParamU', 'ParamE', 'ParamO'];
348+
349+
// 保存原始的 coreModel.update 方法
350+
const origCoreModelUpdate = coreModel.update ? coreModel.update.bind(coreModel) : null;
351+
this._origCoreModelUpdate = origCoreModelUpdate;
352+
353+
// 缓存参数索引,避免每帧查询
354+
const mouthParamIndices = {};
355+
for (const id of ['ParamMouthOpenY', 'ParamO']) {
363356
try {
364-
const mouthIds = ['ParamMouthOpenY', 'ParamO'];
365-
for (const id of mouthIds) {
357+
const idx = coreModel.getParameterIndex(id);
358+
if (idx >= 0) mouthParamIndices[id] = idx;
359+
} catch (_) {}
360+
}
361+
362+
// 覆盖 coreModel.update 方法
363+
// 在调用原始 update 之前写入参数(因为 update 会将参数应用到模型)
364+
coreModel.update = () => {
365+
try {
366+
// 1. 强制写入口型参数(使用索引直接设置)
367+
for (const [id, idx] of Object.entries(mouthParamIndices)) {
366368
try {
367-
if (coreModel.getParameterIndex(id) !== -1) {
368-
coreModel.setParameterValueById(id, this.mouthValue, 1);
369-
}
369+
coreModel.setParameterValueByIndex(idx, this.mouthValue);
370370
} catch (_) {}
371371
}
372-
} catch (_) {}
373-
return updated;
374-
};
375-
this._origUpdateParameters = orig; // 保存可还原的实现(已绑定)
376-
377-
// 也覆盖 expressionManager.updateParameters,防止表情参数覆盖嘴巴
378-
if (mm.expressionManager && typeof mm.expressionManager.updateParameters === 'function') {
379-
const origExp = mm.expressionManager.updateParameters.bind(mm.expressionManager);
380-
mm.expressionManager.updateParameters = (coreModel, now) => {
381-
const updated = origExp(coreModel, now);
382-
try {
383-
const mouthIds = ['ParamMouthOpenY', 'ParamO'];
384-
for (const id of mouthIds) {
385-
try {
386-
if (coreModel.getParameterIndex(id) !== -1) {
387-
coreModel.setParameterValueById(id, this.mouthValue, 1);
372+
373+
// 2. 强制写入常驻表情参数(跳过口型参数)
374+
if (this.persistentExpressionParamsByName) {
375+
for (const name of (this.persistentExpressionNames || [])) {
376+
const params = this.persistentExpressionParamsByName[name];
377+
if (Array.isArray(params)) {
378+
for (const p of params) {
379+
// 跳过口型参数
380+
if (lipSyncParams.includes(p.Id)) continue;
381+
try {
382+
const idx = coreModel.getParameterIndex(p.Id);
383+
if (idx >= 0) {
384+
coreModel.setParameterValueByIndex(idx, p.Value);
385+
}
386+
} catch (_) {}
388387
}
389-
} catch (_) {}
388+
}
390389
}
391-
} catch (_) {}
392-
return updated;
393-
};
394-
this._origExpressionUpdateParameters = origExp;
395-
} else {
396-
this._origExpressionUpdateParameters = null;
397-
}
398-
399-
// 若此前使用了 ticker 覆盖,确保移除
400-
if (this._mouthTicker && this.pixi_app && this.pixi_app.ticker) {
401-
try { this.pixi_app.ticker.remove(this._mouthTicker); } catch (_) {}
402-
this._mouthTicker = null;
403-
}
390+
}
391+
} catch (e) {
392+
// 静默处理错误
393+
}
394+
395+
// 调用原始的 update 方法(将参数应用到模型顶点)
396+
if (origCoreModelUpdate) {
397+
origCoreModelUpdate();
398+
}
399+
};
404400

405401
this._mouthOverrideInstalled = true;
402+
console.log('已安装参数覆盖(口型 + 常驻表情),使用 coreModel.update 前置覆盖方式');
406403
};
407404

408405
// 设置嘴巴开合值(0~1)

0 commit comments

Comments
 (0)