pixi-live2d-display 虚拟数字人交互

参考地址: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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容