1

什么是live2D技术?可以用来做什么?

请点击看效果:http://ashuai.work:8890/#/16

简而言之:

  • 可以用来创建虚拟角色、数字人的技术
  • 达到类似于动漫、插画、游戏中的人物效果
  • 可动作交互、语音发声
  • 可以用到的平台很多,比如Web、Native、Unity、游戏引擎、JAVA等平台
  • 就前端而言,3D项目使用threejs,2D项目使用pixijs
  • 所以,pixijs搭配live2D就可以在我们的页面上实现一个虚拟角色、数字人、或者说看板娘技术
附上维基百科的介绍:https://zh.wikipedia.org/wiki/Live2D

如下图:

或(类似博客园的看板娘)

live2d的应用之live2d-render插件

  • live2d技术实现的一个个人物、动物模型
  • 所以,首先,我们要搞到一些人物的模型数据文件

关于live2d的模型数据文件

  • live2d模型数据就是由绘画师提前通过live2d的官方制作建模软件搞出来的
  • 绘画师提前制作出的人物、动物
  • 给人物添加外型、轮廓、衣服
  • 再把人物数据导出一个文件夹,文件夹中主要是一个个文件(JSON文件为主)
  • github或者哔哩哔哩上有不少以往的模型文件
  • 当然,官方也提供了一些模型文件给我们学习使用
  • 这里,要注意他们的Licence 的使用范围
  • 官方模型免费下载:https://www.live2d.com/zh-CHS/learn/sample/
  • 如下图:

  • 模型下载解压以后,得到对应文件夹中的模型数据

上述文件夹文件,分别是代表人物模型的数据意思为:

  • kei\_basic\_free.2048 人物衣服材质素材
  • motions 预设的人物动作等
  • sounds 人物自带默认音效
  • .moc3文件是核心模型数据(二进制)
  • .motionsync3.json是动态同步设定文件
  • 等...
  • 我们要关注kei\_basic\_free.model3.json这个文件
  • 这个是引入此模型文件的入口文件
这个模型文件比较少一些,有些丰富人物的模型,还有expressions表情文件夹(存放的预设好的喜怒哀乐等数据...)

live2d-render插件

官方的live2d相关的api比较繁杂,上手成本高一些,笔者简单调研后,找到了两个还不错的封装库

先说简单一些的live2d-render插件的使用

第0步,下载live2d的sdk

第1步,把下载好的live2d的sdk存放在public文件夹中,再在index.html文件中使用script引入

第2步,下载live2d-render插件

  • npm install live2d-render --save
  • 笔者用的是这个版本 "live2d-render": "^0.0.5"

第3步,搞一个.vue文件,把下列代码复制粘贴进去

<template>
  <div>
    <h1>live2d-render</h1>
    <button @click="ccc">表情切换</button>
  </div>
</template>

<script setup>
import { onMounted } from "vue";
import * as live2d from "live2d-render"; // 引入插件

onMounted(() => {
  init();
});

const init = async () => {
  await live2d.initializeLive2D({
    // live2d 所在区域的背景颜色
    BackgroundRGBA: [0.0, 0.0, 0.0, 0.0],

    // live2d 的 model3.json 文件的相对 根目录 的路径
    // ResourcesPath: "/live2d/Haru/Haru.model3.json", // 成熟御姐
    // ResourcesPath: "/live2d/Hiyori/Hiyori.model3.json", // 可爱萝莉
    ResourcesPath: "/live2d/Mao/Mao.model3.json", // 魔法女巫
    // ResourcesPath: "/live2d/Mark/Mark.model3.json", // 大眼睛呆萌男孩
    // ResourcesPath: "/live2d/Natori/Natori.model3.json", // 高挑帅气西装男
    // ResourcesPath: "/live2d/Rice/Rice.model3.json", // 白裙水手服女
    // ResourcesPath: "/live2d/Wanko/Wanko.model3.json", // 碗里面有小狗

    // live2d 的大小
    CanvasSize: {
      height: 600,
      width: 400,
    },

    // 展示工具箱(可以控制 live2d 的展出隐藏,使用特定表情)
    ShowToolBox: false,

    // 是否使用 indexDB 进行缓存优化,这样下一次载入就不会再发起网络请求了
    LoadFromCache: true,
  });
};

const ccc = () => {
  live2d.setRandomExpression(); // 随机切换表情
};
</script>

第4步,效果出来了哦

效果图:

插件文档地址:https://document.kirigaya.cn/docs/live2d-render/vue-install.html

上述代码中,笔者提到了一些模型,包括code,都在笔者的github中。届时直接去github上pull代码即可,github仓库地址在文末

接下来,我们使用pixi-live2d-display来做一个可以根据音频说话的虚拟数字人角色

live2d的应用之pixi-live2d-display插件

关于pixi-live2d-display

需要注意版本:pixi.js 不能超过6

如下安装:

npm i pixi-live2d-display@0.4.0 --save

npm i pixi.js@6.5.10 --save
关于pixi.js的中文文档,可以看这里:https://pixijs.huashengweilai.com/

简单版代码

不赘述,很简单

<template>
  <div class="canvasWrap">
    <canvas id="myCanvas" />
  </div>
</template>

<script setup>
import { onMounted, onBeforeUnmount } from "vue";

import * as PIXI from "pixi.js";
import { Live2DModel } from "pixi-live2d-display/cubism4"; // 只需要 Cubism 4

window.PIXI = PIXI; // 为了pixi-live2d-display内部调用

let app; // 为了存储pixi实例
let model; // 为了存储live2d实例

onMounted(() => {
  init();
});

onBeforeUnmount(() => {
  app = null;
  model = null;
});

const init = async () => {
  // 创建PIXI实例
  app = new PIXI.Application({
    // 指定PixiJS渲染器使用的HTML <canvas> 元素
    view: document.querySelector("#myCanvas"),
    // 响应式设计
    resizeTo: document.querySelector("#myCanvas"),
    // 设置渲染器背景的透明度 0(完全透明)到1(完全不透明)
    backgroundAlpha: 0,
  });
  // 引入live2d模型文件
  model = await Live2DModel.from("/live2d/Haru/Haru.model3.json", {
    autoInteract: false, // 关闭眼睛自动跟随功能
  });
  // 调整live2d模型文件缩放比例(文件过大,需要缩小)
  model.scale.set(0.12);
  // 调整x轴和y轴坐标使模型文件居中
  model.y = 0;
  model.x = -24;
  // 把模型添加到舞台上
  app.stage.addChild(model);
};
</script>

<style lang="less" scoped>
#myCanvas {
  width: 240px;
  height: 360px;
}
</style>

效果图:

让人物说话——嘴唇动弹

  • 使用库提供的api
  • model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", n)
  • 这里的n介于0\~1之间,表示嘴唇开合的幅度
  • 所以我们可以这样写:
  • 点击按钮后,让人物嘴唇不断变化
<button @click="mouthFn">嘴型变换</button>

const mouthFn = () => {
  setInterval(() => {
    let n = Math.random();
    console.log("随机数0~1控制嘴巴Y轴高度-->", n);
    model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", n);
  }, 100);
};

效果图:

踩坑之模型嘴唇不变化

  • 官方提供的第一个模型,是不会出现这个问题的

  • 但是,有些模型有原本预设的嘴唇变化幅度逻辑,或其他情况,导致会出现这个语句不生效的情况
  • model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", n)
  • 以haru模型为例:
  • 这个时候,我们需要在motions/haru\_g\_idle.motion3.json文件中做如下更改

  • 这样就能够解决问题了
至此,数字人已经可以张嘴说话了,不过还没有声音,接下来让声音播放即可。笔者翻阅资料,找到了一个方案如下

使用AudioContext播放声音,并转成音频频谱做分析,再转成音量大小,从而控制嘴唇的开合大小

如下代码:

<button @click="speakFn">人物说话</button>

const speakFn = async () => {

  // 请求加载一个音频文件
  const response = await fetch(audioFile);
  // 将音频读取为原始的二进制数据缓冲区(ArrayBuffer)。音频本身是二进制格式,要先将其加载为 ArrayBuffer 才能进一步处理
  const audioData = await response.arrayBuffer();
  // 将 ArrayBuffer 格式的音频数据解码成 AudioBuffer 对象,可以直接用于播放或处理音频数据。
  const audioBuffer = await audioContext.decodeAudioData(audioData);
  // 创建一个音频源节点(AudioBufferSourceNode),该节点用于播放音频数据
  const source = audioContext.createBufferSource();
  // 创建一个音频分析节点。这个节点用于实时分析音频数据,提供诸如频谱分析、波形分析等功能
  const analyser = audioContext.createAnalyser();
  // 将之前解码得到的 audioBuffer(即音频数据)赋值给 source 节点的 buffer 属性。这样就将加载的音频文件与 source 节点绑定,准备播放。
  source.buffer = audioBuffer;
  //  将 音频分析节点 连接到音频上下文的最终目标(即扬声器)
  analyser.connect(audioContext.destination);
  // 音频分析节点 将能够分析通过音频源流动的音频数据,并提供频谱或其他音频信息。
  source.connect(analyser);

  // 监听音频播放完毕
  let requestId = null;
  source.onended = function () {
    cancelAnimationFrame(requestId); // 清除请求动画帧
    model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", 0); // 闭上嘴巴
  };
  /**
   * 启动音频源的播放,从头开始(这样的话,页面就能够听到声音了)
   * 接下来需要让人物嘴巴更新动弹,即有声音的同时,且能够说话
   * 即为:updateMouth函数
   * */
  source.start(0);

  /**
   * 这个 updateMouth 函数通过从 analyser 获取音频数据并计算音量,动态地更新一个模型的嘴巴张开程度。它的实现方式是每帧都更新一次,
   * 通过音频的音量强度来决定嘴巴的开合程度,从而实现与音频的实时互动。
   * */
  const updateMouth = () => {
    // analyser.frequencyBinCount 表示音频频谱的 bin(频率段)的数量。它是一个整数,表示从频率数据中可以获取多少个频率段的值
    // 使用 analyser 对象的 getByteFrequencyData 方法填充 dataArray 数组。
    // getByteFrequencyData 将音频的频率数据转化为 0-255 范围内的字节值,并存储在 dataArray 中。这个数据表示了音频信号在不同频率范围内的强度。
    // 该方法会将频谱分析的结果填充到 dataArray 数组中,每个元素代表一个频率段的音量强度。
    // 使用 reduce 方法计算 dataArray 数组的所有值的总和,并通过除以数组长度来求得平均值。这个平均值表示音频信号的总体“强度”或“音量”。
    // 这里的 a + b 累加所有音频频段的强度值,最终计算出一个平均值。
    // dataArray.length 是频率数据的总数,通常它等于 analyser.frequencyBinCount。
    // 将计算出的 volume 除以 50,以缩放它到一个合适的范围,得到一个表示“嘴巴张开程度”的值。volume 越大,mouthOpen 越大。
    // 使用 Math.min(1, volume / 50) 保证 mouthOpen 的值不会超过 1,也就是说嘴巴张开程度的最大值是 1。
    // 这意味着,如果音量足够大,mouthOpen 会接近 1,表示嘴巴完全张开;如果音量较小,mouthOpen 会接近 0,表示嘴巴几乎没张开。
    const dataArray = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(dataArray);
    const volume = dataArray.reduce((a, b) => a + b) / dataArray.length;
    const mouthOpen = Math.min(1, volume / 50);

    // 通过调用 setParameterValueById 方法,将 mouthOpen 的值传递给 model 的内部模型(控制嘴巴大小张开幅度)
    model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", mouthOpen);
    requestId = requestAnimationFrame(updateMouth);
  };

  requestId = requestAnimationFrame(updateMouth);
};
这样的话,数字人就能够张口说话了,话说完,再让数字人闭嘴即可

踩坑之数字人动作自动切换导致人物不张嘴说话了

  • 笔者的理解是:
  • 数字人有的预设好几个动作
  • 在一段时间内自动循环播放动作切换
  • 当A动作切换到B动作时候
  • 会导致处于A动作开口动作被覆盖
  • 这里需要设置动作的权重(pixi-live2d-display文档中也有,需要自行探索)
  • 笔者是直接删除那个多余的动作,只保留一个动作
  • 就不会出现这个问题了😅😅😅
  • 如下图示,修改.model3.json文件即可

github仓库

参考资料文档博客


水冗水孚
1.1k 声望587 粉丝

每一个不曾起舞的日子,都是对生命的辜负