笔记:live2d4.0 sdk 博客园网页动画

1.准备

1.1下载live2d

live2d官网:https://www.live2d.com
在这里插入图片描述

在这里插入图片描述

1.2安装live2d

一路next,安装好后会有两个文件:
Live2D Cubism Viewer 4.0(这个是查看模型的软件)
Live2D Cubism Editor 4.0(这个是制作模型的软件)
Live2D Cubism Editor 4.0有pro版和free版,用free版就行

1.3 下载sdk

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.制作模型

https://www.bilibili.com/video/av73648216?p=1
这个教程是2.0的,2.0导出的是 .moc 文件,而新版的导出的是 .moc3 文件
在这里插入图片描述
注意:导出前要 Ctrl + T 再点击ok一下(生成纹理),不然无法导出
导出后是个文件夹,将文件夹中的 .moc3 文件拖入Live2D Cubism Viewer 4.0软件就能查看效果

3.sdk

官方sdk api:https://docs.live2d.com/cubism-sdk-tutorials/sample-build-web/

3.1运行实例

需要环境:node.js TypeScript Webpack (TypeScript和Webpack安装慢可以使用淘宝镜像)
编辑器打开sdk项目,具体的目录是什么内容可以看项目根目录下的 README.md 文件(windows用户可以使用Typora软件打开.md文件)

  1. 打开控制台跳转到Demo文件夹下输入 npm install 命令(安装了淘宝镜像的可以使用cnpm install命令安装的快一些)
    install命令会根据package.json 文件中的配置下载安装需要的插件在这里插入图片描述
    这里已经安装好了,安装好后在Demo文件夹下回多出一个 node_modules 文件夹
  2. 运行 npm run build命令
    在这里插入图片描述
    运行完成后会在Demo文件夹下生成一个 dist 文件夹,该文件夹下有一个bundle.js,这个就是集合打包好的js文件,在index.html文件中有引用
  3. 运行 npm run serve命令,启动服务器
    在这里插入图片描述
    4.在浏览器中输入 http://localhost:5000/Samples/TypeScript/Demo/ 就能看到
    在这里插入图片描述
    具体的其他命令可以查看跟Demo文件夹同级的 README.md 文件, 在package.json文件中也能看到
    在这里插入图片描述

3.2源码

想把一些配置放到html中,比如画布(canvas)的大小位置,模型保存的路径等等信息
否则每次需要改变模型的时候都要改代码,重新编译,麻烦
lappdefine.ts //定义基本的参数
lappdelegate.ts //初始化,释放资源,事件绑定
lapplive2dmanager.ts //模型的管理类,进行模型生成和废弃、事件的处理、模型切换。
lappmodel.ts //模型类,定义模型的基本属性
lappal.ts //读取文件,抽象文件数据(算是工具类)
lappsprite.ts //动画精灵类,(学python时知道了精灵类和精灵组)
lapptexturemanager.ts//纹理管理类,进行图像读取和管理的类
lappview.ts //视图类,生成模型的图像被lapplive2dmanager管理
main.ts //主程序启动程序
touchmanager.ts //事件的管理类(比如移动鼠标,点击鼠标,触摸屏触碰等)

这里所有的类都实行单例模式

3.2.2 界面(index.html)

这里用了flask框架(别问为啥简单方便快)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=1900">
  <title>TypeScript HTML App</title>
  <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='live2d/css/live2d.css') }}"/>
  <style>
    html, body {
        margin: 0;
        background-color: #22d7dd;
    }

  </style>
  <script type="text/javascript" src="{{ url_for('static', filename='live2d/js/jquery.js') }}"></script>
  <!-- Pollyfill script -->
  <script src="//i2.wp.com/unpkg.com/[email protected]/minified.js"></script>
  <!-- Live2DCubismCore script -->
  <script src="{{ url_for('static', filename='live2d/js/live2dcubismcore.js') }}"></script>
  <!-- Build script -->
  <script src="{{ url_for('static', filename='live2d/js/bundle.js') }}"></script>

</head>
<body>
    1234567890
    <div class="live2d-main">
        <div class="live2d-tips"></div>
        <!-- 这里可以定义画布的大小位置 -->
        <canvas id="live2d" width="280" height="250" class="live2d"></canvas>
        <div class="tool">
            <span class="fui-home"></span>
            <span class="fui-chat"></span>
            <span class="fui-eye"></span>
            <span class="fui-user"></span>
            <span class="fui-photo"></span>
            <span class="fui-info-circle"></span>
            <span class="fui-cross"></span>
        </div>
    </div>
</body>
<script src="{{ url_for('static', filename='live2d/js/message.js') }}"></script>
<script type="text/javascript">
  var resourcesPath = '/live2d/model/';  // 指定资源文件(模型)保存的路径
  var backImageName = ''; // 指定背景图片
  var modelDir = 'Haru,Hiyori,Mark,Natori,Rice,zwt';  // 指定需要加载的模型
  init();  // 初始化模型,属于message.js文件
</script>
</html>
3.2.2 管理(message.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 初始化
function init(){   
    var resourcesPaths = `${resourcesPath}`;
    var backImageNames = `${backImageName}`;
    var modelDirString = `${modelDir}`;
    var modelDirs = modelDirString.split(',');

    initDefine(resourcesPaths, backImageNames, modelDirs);  // lappdefine.ts开放的接口用于初始化常量被编译到bundle.js文件里
}

// 监听复制(这里简单添加了一些事件,可以添加更多的事件,比如报时等)
(function() {
    document.addEventListener('copy',(e)=>{
      e.preventDefault();
      e.stopPropagation();
      showMessage('你都复制了些什么呀,能让我看看吗?', 5000, true); // 显示信息
    })
}());
// 工具栏的点击事件
$('.tool .fui-home').click(function (){
});

$('.tool .fui-eye').click(function (){
});

$('.tool .fui-chat').click(function (){
});

$('.tool .fui-user').click(function (){
});

$('.tool .fui-info-circle').click(function (){
});

$('.tool .fui-cross').click(function (){
});

$('.tool .fui-photo').click(function (){
});


function showMessage(text, timeout, flag){
    if(flag || sessionStorage.getItem('waifu-text') === '' || sessionStorage.getItem('waifu-text') === null){
        if(Array.isArray(text)) text = text[Math.floor(Math.random() * text.length + 1)-1];
        //console.log(text);
        if(flag) sessionStorage.setItem('waifu-text', text);
        $('.live2d-tips').stop();
        $('.live2d-tips').html(text).fadeTo(200, 1);
        if (timeout === undefined) timeout = 5000;
        hideMessage(timeout);
    }
}

function hideMessage(timeout){
    $('.live2d-tips').stop().css('opacity',1);
    if (timeout === undefined) timeout = 5000;
    window.setTimeout(function() {sessionStorage.removeItem('waifu-text')}, timeout);
    $('.live2d-tips').delay(timeout).fadeTo(200, 0);
}

3.2.2 基本参数(lappdefine.ts)

由于使用Webpack打包,Typescript文件中的变量和函数被层层括号包围(封装)变成了内部变量和内部函数(具体可以百度Webpack的打包原理),在外部的js文件是调用不到里面的方法的,所以将一些函数或变量挂载到window下,成为全局变量或函数,使外部的js文件也能调用到
在lappdefine.ts文件最后添加

1
2
3
4
5
6
7
8
export const win: any = window

win.initDefine=function(resourcesPath: string, backImageName: string, modelDir: string[]){
    ResourcesPath = resourcesPath;
    BackImageName = backImageName;
    ModelDir = modelDir;
    ModelDirSize = modelDir.length;
}

这里将initDefine挂载到window下是函数能在外部调用,函数在message.js中调用到
(注意:这里要将ResourcesPathBackImageNameModelDirModelDirSize变量声明成let属性 const 是常量只允许在声明的时候赋值,并且只能赋值一次)

3.2.2 主函数(main.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
import { LAppDelegate } from './lappdelegate';

// 浏览器装入后的处理(打开页面)
window.onload = (): void => {
  // create the application instance
  if (LAppDelegate.getInstance().initialize() == false) {
    return;
  }
  LAppDelegate.getInstance().run();
};

//结束时的处理 (刷新或关闭页面)
window.onbeforeunload = (): void => LAppDelegate.releaseInstance(); //lambda 匿名函数

LAppDelegate.getInstance().initialize() 获得这个类的实例并初始化

3.2.3 LAppDelegate.getInstance().initialize()(lappdelegate.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public initialize(): boolean {
    // 创建画布
    // canvas = document.createElement('canvas');
    // canvas.width = LAppDefine.RenderTargetWidth;
    // canvas.height = LAppDefine.RenderTargetHeight;
    // 原来是用js动态在网页上创建画布,画布的长宽在lappdefine.ts指定,现在直接在html中已经有了画布直接拿过来使用就行
    canvas = <HTMLCanvasElement>document.getElementById("live2d"); // index.html中的id为live2d的画布
    canvas.width = canvas.width;
    canvas.height = canvas.height;
    canvas.toDataURL("image/png");

    // 这个是index.html工具栏中的眼睛图标,点击眼睛图标就切换下一个模型
    // 正规来说应该留个切换模型的口子,在message.js中调用,因为懒就直接在这里写了
    fui_eye = <HTMLSpanElement>document.getElementsByClassName("fui-eye")[0];

    // 初始化gl上下文 (代码段结束后有解释)
    // @ts-ignore
    gl = canvas.getContext('webgl',{alpha: true }) || canvas.getContext('experimental-webgl');

    if (!gl) {
      alert('Cannot initialize WebGL. This browser does not support.\n不能初始化WebGL,该浏览器不支持WebGL,请切换浏览器重试');
      gl = null;
      document.body.innerHTML =
        '该浏览器不支持 <code><canvas></code> 标签元素,请切换浏览器重试 .';
      // gl初期化失敗
      return false;
    }

    // 向DOM添加画布
    // document.body.appendChild(canvas);  

    if (!frameBuffer) {
      frameBuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
    }

    // 透明设置
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    const supportTouch: boolean = 'ontouchend' in canvas;  //是否支持触碰(触摸屏)

    if (supportTouch) {   // 没有触屏电脑(两种事件都要注册)
      // 注册触摸相关的回掉函数  (触摸屏)
      canvas.ontouchstart = onTouchBegan;
      canvas.ontouchmove = onTouchMoved;
      canvas.ontouchend = onTouchEnded;
      canvas.ontouchcancel = onTouchCancel;
    } else {
      // 注册鼠标相关的回呼函数
      canvas.onmousedown = onClickBegan;
      // canvas.onmousemove = onMouseMoved;   //原来是在画布上注册鼠标移动事件,鼠标移出画布就监听不到
      window.onmousemove = onMouseMoved;  //对整个window窗口监听,是角色跟随鼠标,需要对鼠标坐标获取做调整
      canvas.onmouseup = onClickEnded;
      fui_eye.onmousedown = (): void => {   // 工具栏眼睛图标点击事件
        const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance();
        live2DManager.nextScene();
      };
    }

    // AppView的初始化
    this._view.initialize();

    // Cubism SDK的初始化
    this.initializeCubism();

    return true;
  }

contextType参数有以下四种:
注:早期WebGL的context,还不能通过正式的名称webgl来获取,必须使用experimental-webgl来获取context对象。

2d”,创建一个CanvasRenderingContext2D对象作为2D渲染的上下文。
webgl”(或“experimental-webgl”),创建一个WebGLRenderingContext对象作为3D渲染的上下文,只在实现了WebGL 2的浏览器上可用,实验性特性。
webgl2”,创建一个WebGL2RenderingContext对象作为3D渲染的上下文,只在实现了WebGL 3的浏览器上可用。
bitmaprenderer”,创建一个ImageBitmapRenderingContext,用于将位图渲染到canvas上下文上,实验性特性。

原文链接:https://blog.csdn.net/acoolgiser/article/details/85800799

onMouseMoved(lappdelegate.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 鼠标移动后的回掉
function onMouseMoved(e: MouseEvent): void {
  // if (!LAppDelegate.getInstance()._captured) {  // 判断是否单击,原来是要按住鼠标左键图像才会跟着鼠标动
  //   return;
  // }
  if (!LAppDelegate.getInstance()._view) {   //获得lappview.ts的实例对象
    LAppPal.printMessage('view notfound');
    return;
  }
  // e.clientX和e.clientY获取的坐标点都是以左上角为原点
  const rect = (e.target as Element).getBoundingClientRect();
  // const posX: number = e.clientX - rect.left;
  // const posY: number = e.clientY - rect.top;
  let posX: number = e.clientX;
  let posY: number = e.clientY - window.innerHeight + canvas.height;

  // 图像在网页的坐下角,简单处理坐标将超过画布边界坐标就等与边界坐标
  posX = (posX > canvas.width) ? canvas.width : posX;
  posY = (posY < 0) ? 0 : posY;

  LAppDelegate.getInstance()._view.onTouchesMoved(posX, posY);// 这个就不做解释,就是转换坐标,调用LAppLive2DManager类重新绘制图像
}

假设屏幕是一个九宫格(万能神奇的九宫格哈哈)
在这里插入图片描述
回到**initialize()**函数,在initialize()后有两个初始化的函数
this._view.initialize() (lappview.ts)主要就是指定一些图像的参数,例如画面的范围相对,设置当前矩阵的放大率等
this.initializeCubism()(lappdelegate.ts)

3.2.4 this.initializeCubism() (lappdelegate.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public initializeCubism(): void {
    // setup cubism 设置cubism
    this._cubismOption.logFunction = LAppPal.printMessage;  //初始化控制台打印信息工具,就是console.log
    this._cubismOption.loggingLevel = LAppDefine.CubismLoggingLevel;  //指定打印日志的等级
    Csm_CubismFramework.startUp(this._cubismOption);

    // initialize cubism 初始化设置cubism
    Csm_CubismFramework.initialize();

    // load model 加载模型
    LAppLive2DManager.getInstance();

    // 更新时间
    LAppPal.updateTime();

    this._view.initializeSprite();
  }

Csm_CubismFramework.initialize()底层的初始化设置
LAppLive2DManager.getInstance()模型管理类的初始化,单例模型没什么好说的,注意这个类在构造方法中会加载模型下段代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// (lapplive2dmanager.ts)
public changeScene(index: number): void {
    this._sceneIndex = index;
    if (LAppDefine.DebugLogEnable) {   //要是调试的情况下打印信息
      LAppPal.printMessage(`[APP]model index: ${this._sceneIndex}`);
    }

    // 从ModelDir[]中保存的目录名称
    // 要使目录名和model 3.json的名字一致。
    const model: string = LAppDefine.ModelDir[index];
    const modelPath: string = LAppDefine.ResourcesPath + model + '/';
    let modelJsonName: string = LAppDefine.ModelDir[index];
    modelJsonName += '.model3.json';    //拼接生成模型路径

    this.releaseAllModel();    //清除原来显示的模型
    this._models.pushBack(new LAppModel());   // 推入管理栈堆
    this._models.at(0).loadAssets(modelPath, modelJsonName);  //加载模型,lappmodel.ts异步请求服务器模型资源
  }
 
  //  构造器
  constructor() {
    this._viewMatrix = new Csm_CubismMatrix44();
    this._models = new Csm_csmVector<LAppModel>();
    this._sceneIndex = 0;
    this.changeScene(this._sceneIndex);     //第一次加载模型
  }

这里在往下深入就是 lappmodel.ts 加载定义相关的模型信息例如模型的大小等
loadAssets(modelPath, modelJsonName)异步加载模型的json文件到缓存中===>
CubismModelSettingJson(buffer, size)模型json文件的缓存,和缓存大小===>
this.setupModel(setting: CubismModelSettingJson); 根据模型json文件中的信息异步请求去加载模型及相关的文件(例如动作文件、物理文件等)===>
loadModel(buffer: ArrayBuffer) 模型文件缓存,去加载生成模型===>
this._modelMatrix = new CubismModelMatrix(this._model.getCanvasWidth(), this._model.getCanvasHeight());根据画布的大小去生成模型===>
this.setHeight(1.0);根据画布的高度去生成一个正方形模型坐标系(4*4)调整里面的参数可以调整模型区域的大小(玄学的数字为什么没看懂,有待研究)

3.2.5 this._view.initializeSprite() (lappview.ts)

回到 LAppDelegate.initialize()(lappdelegate.ts),在initialize()的最后会调用 this._view.initializeSprite()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  // 进行图像的初始化,一些不重要的元素初始化。这里有一个齿轮设置的图像,里面的内容替换成了眼睛的图标,没用所以注释掉,还加了一个背景图片加载的判断,没有背景图片就不加载
  public initializeSprite(): void {
    const width: number = canvas.width;
    const height: number = canvas.height;

    const textureManager = LAppDelegate.getInstance().getTextureManager();   // 从LAppDelegate类中得到纹理管理器
    const resourcesPath = LAppDefine.ResourcesPath;

    let imageName = '';

    // 背景图像初始化
    imageName = LAppDefine.BackImageName;

    if(imageName != "" && imageName != null){ //如果指定了背景图片,就加载
      // 由于异步,创建回调函数
      const initBackGroundTexture = (textureInfo: TextureInfo): void => {
        const x: number = width * 0.5;  //背景图片出现宽度的位置
        const y: number = height * 0.5; //背景图片出现高度的位置

        const fwidth = textureInfo.width * 2.0;   //背景图片的宽度
        const fheight = height * 0.95;            //背景图片的高度
        this._back = new LAppSprite(x, y, fwidth, fheight, textureInfo.id);  //绘制背景图片
      };

      textureManager.createTextureFromPngFile(  //回掉函数
        resourcesPath + imageName,
        false,
        initBackGroundTexture
      );
    }

    // 齿轮图像初始化 (原来是右上角有一个齿轮的图片,点击齿轮图片切换模型)
    // imageName = LAppDefine.GearImageName;
    // // 齿轮初始化后的回掉函数
    // const initGearTexture = (textureInfo: TextureInfo): void => {
    //   const x = width - textureInfo.width * 0.5;   //出现在右上角
    //   const y = height - textureInfo.height * 0.5;

    //   const fwidth = textureInfo.width;
    //   const fheight = textureInfo.height;
    //   this._gear = new LAppSprite(x, y, fwidth, fheight, textureInfo.id);
    // };

    // textureManager.createTextureFromPngFile(
    //   resourcesPath + imageName,
    //   false,
    //   initGearTexture
    // );

    // 创建阴影
    if (this._programId == null) {
      this._programId = LAppDelegate.getInstance().createShader();
    }
  }

到这里初始化的工作基本完成了

3.2.6 LAppDelegate.getInstance().run() (lappdelegate.ts)

回到main.ts文件接下来就是执行LAppDelegate.getInstance().run()方法,没啥好说的,就是不断循环刷新画布,达到动画的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  // 执行处理
  public run(): void {
    // 主循环
    const loop = (): void => {
      // 确认有无实例
      if (s_instance == null) {
        return;
      }

      // 更新时间
      LAppPal.updateTime();

      // 画面的初始化
      gl.clearColor(0.0, 0.0, 0.0, 1.0);

      // 启动深度测试
      gl.enable(gl.DEPTH_TEST);

      // 附近的物体将远处的物体遮盖起来
      gl.depthFunc(gl.LEQUAL);

      // 清除彩色缓冲区和深度缓冲区  (加上这一句会导致有些浏览器背景变成黑色,而不是透明)
      // gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

      gl.clearDepth(1.0);

      // 透明设置
      gl.enable(gl.BLEND);
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

      // 绘图更新
      this._view.render();

      // 循环递归调用
      requestAnimationFrame(loop);
    };
    loop();
  }

最后打包bundle.js,赋值bundle.js文件和Core核心文件live2dcubismcore.js就可以移植到任何项目了

4成果

在这里插入图片描述
最后的模型是我做的哈哈,比较简单只会摇头眨眼,图片来自于网络,仅供于学习
https://pan.baidu.com/s/1t6GFF-aS00cTLEssMouf5g:83hy