参考地址:vue3中实现live2D技术的应用虚拟角色数字人live2d-render、pixi-live2d-display - 掘金
依赖
"dependencies": {
"pixi-live2d-display": "^0.4.0",
"pixi.js": "6.5.10",
"vue": "^3.3.4"
},
代码
<template>
<canvas ref="liveCanvas" id="myCanvas" width="520" height="520" />
<div class="control">
<!-- <button @click="expression('默认')">默认</button>
<button @click="expression('开心')">开心</button>
<button @click="expression('忧伤')">忧伤</button>
<button @click="expression('愤怒')">愤怒</button>
<button @click="expression('吐舌')">吐舌</button> -->
<button @click="expressionFn" v-show="modelIndex === 1">表情切换</button>
<button @click="motionFn" v-show="modelIndex === 1">动作切换</button>
<!-- <button @click="stopMotionFn">停止动作</button> -->
<button @click="mouthFn">嘴型变换</button>
<button @click="modelChange">模型切换</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import * as PIXI from "pixi.js";
// @ts-ignore
import { Live2DModel } from "pixi-live2d-display/cubism4"; // 只需要 Cubism 4
window.PIXI = PIXI; // 为了pixi-live2d-display内部调用
let app: PIXI.Application;
let model: any;
const liveCanvas = ref();
let modelSrc = [{
src: "./kei_basic_free/runtime/kei_basic_free.model3.json",
scale: 0.5,
},
{
src: "./Haru/Haru.model3.json",
scale: 0.12,
}]
let modelIndex = ref(0);
let motionIndex = 0;
let haruMotionIndex = 0; // 专门用于 Haru 模型的动作索引
onMounted(async () => {
app = new PIXI.Application({
// 指定PixiJS渲染器使用的HTML <canvas> 元素
view: document.querySelector("#myCanvas"),
// 响应式设计
resizeTo: document.querySelector("#myCanvas"),
autoStart: true,
// resizeTo: window,
backgroundAlpha: 0,
});
// model = await Live2DModel.from("./gougou_vts/狗狗.model3.json");
// model = await Live2DModel.from("./mikoto/mikoto.model.json");
model = await Live2DModel.from(modelSrc[modelIndex.value].src);
// 引入live2d模型文件
// model = await Live2DModel.from("/live2d/Haru/Haru.model3.json", {
// autoInteract: false, // 关闭眼睛自动跟随功能
// });
model.scale.set(modelSrc[modelIndex.value].scale);
app.stage.addChild(model);
// 调整x轴和y轴坐标使模型文件居中
// model.y = 24;
// model.x = -24;
// 调试信息:检查模型加载状态
console.log('模型加载完成:', modelSrc[modelIndex.value].src);
console.log('模型对象:', model);
console.log('模型动作组:', model.internalModel?.motionManager?.definitions);
});
onBeforeUnmount(() => {
model?.destroy();
app?.destroy();
});
function expression(type: string) {
model.expression(type);
}
let Interval = null;
const expressionFn = () => {
try {
// 根据当前模型选择不同的表情
if (modelIndex.value === 0) {
// kei_basic_free 模型没有表情文件,使用默认表情
console.log('kei_basic_free 模型没有表情文件');
} else if (modelIndex.value === 1) {
// Haru 模型的表情
const expressions = ["F01", "F02", "F03", "F04", "F05", "F06", "F07", "F08"];
const randomIndex = Math.floor(Math.random() * expressions.length);
// 强制停止当前所有动作,确保表情切换正常
if (model && model.internalModel && model.internalModel.motionManager) {
model.internalModel.motionManager.stopAllMotions();
console.log('停止当前动作以切换表情');
}
model.expression(expressions[randomIndex]);
console.log('切换到表情:', expressions[randomIndex]);
}
} catch (error) {
console.error('切换表情时出错:', error);
}
};
const motionFn = () => {
try {
// 强制停止当前所有动作
if (model && model.internalModel && model.internalModel.motionManager) {
model.internalModel.motionManager.stopAllMotions();
console.log('停止当前动作');
// 等待一小段时间确保动作完全停止
setTimeout(() => {
playNextMotion();
}, 100);
} else {
// 如果没有动作管理器,直接播放
playNextMotion();
}
} catch (error) {
console.error('停止动作时出错:', error);
// 出错时也尝试播放新动作
playNextMotion();
}
};
const playNextMotion = () => {
try {
// 根据当前模型选择不同的动作
if (modelIndex.value === 0) {
// kei_basic_free 模型的动作组名是空字符串
model.motion("", motionIndex % 4);
console.log('播放 kei_basic_free 动作,索引:', motionIndex % 4);
} else if (modelIndex.value === 1) {
// Haru 模型的动作组名和具体动作
const motionGroups = ["TapBody"];
const groupIndex = motionIndex % motionGroups.length;
if (motionGroups[groupIndex] === "Idle") {
// Idle 动作组只有一个动作
model.motion("Idle");
console.log('播放 Haru Idle 动作');
} else if (motionGroups[groupIndex] === "TapBody") {
// TapBody 动作组有4个动作,可以指定具体索引
const tapBodyActions = [
"haru_g_m26 (说话动作)",
"haru_g_m06 (信息动作)",
"haru_g_m20 (普通动作)",
"haru_g_m09 (信息动作)"
];
model.motion("TapBody", haruMotionIndex % 4);
console.log('播放 Haru TapBody 动作:', tapBodyActions[haruMotionIndex % 4], '索引:', haruMotionIndex % 4);
haruMotionIndex++;
}
}
motionIndex++;
} catch (error) {
console.error('播放新动作时出错:', error);
}
};
const stopMotionFn = () => {
try {
if (model && model.internalModel && model.internalModel.motionManager) {
model.internalModel.motionManager.stopAllMotions();
console.log('手动停止所有动作');
}
} catch (error) {
console.error('停止动作时出错:', error);
}
};
const mouthFn = () => {
if (Interval) {
mouthFnStop();
return;
}
// // 强制停止当前所有动作,确保嘴型变换正常
// if (model && model.internalModel && model.internalModel.motionManager) {
// model.internalModel.motionManager.stopAllMotions();
// console.log('停止当前动作以开始嘴型变换');
// }
Interval = setInterval(() => {
let n = Math.random();
console.log("随机数0~1控制嘴巴Y轴高度-->", n);
model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", n);
}, 150);
};
const mouthFnStop = () => {
clearInterval(Interval);
Interval = null;
model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", 0);
};
async function modelChange() {
modelIndex.value = modelIndex.value === modelSrc.length - 1 ? 0 : modelIndex.value + 1;
console.log('modelIndex.value', modelIndex.value);
model?.destroy();
model = null;
model = await Live2DModel.from(modelSrc[modelIndex.value].src);
model.scale.set(modelSrc[modelIndex.value].scale);
app.stage.addChild(model);
// 调试信息:检查模型切换后的状态
console.log('模型切换完成:', modelSrc[modelIndex.value].src);
console.log('新模型对象:', model);
console.log('新模型动作组:', model.internalModel?.motionManager?.definitions);
}
</script>
<style scoped>
canvas {
width: 520px;
height: 520px;
}
.control button {
margin-right: 10px;
}
</style>