试制Live2D 4.x网页插件

发布于 2021-08-19  135 次阅读


记得在大概是3年前(或者是4年前)的时候,好多博客都开始在网站上放置1个Live2D模型作为看板娘。当时用的Live2D Web插件是基于Cubism 2.x SDK的,因此只能加载用Live2D 2.x及更早版本制作的模型。

当时我也跟风改了个Live2D Web插件,一直用到现在。

最近心血来潮突然想自己学学Live2D,并且试着做个模型放在万事屋上。打开官网发现目前(2021年8月)版本已经更新到4.x了,当前在用的插件显然不支持拿最新版软件做的模型。没办法,那就想办法自己拿SDK打包一个吧。

1.准备工作

1.1.安装Node.js

Cubism Web SDK使用的是TypeScript,因此需要先安装Node.js。

偷懒起见,我用的是之前拿来进行测试的一台Ubuntu Server 20.04虚拟机。这次依旧用nodesource源安装。

curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs

用Windows安装Node.js的话,直接去网站上(https://nodejs.org/en/)下载并运行安装包即可。

1.2.下载SDK

最新版的Cubism SDK for Web下载地址是:https://www.live2d.com/download/cubism-sdk/download-web/

勾上“同意使用条款”的复选框之后,就可以下载了。

下载完毕之后,将压缩包解压,启动控制台并移动到Samples/TypeScript/Demo/目录。

在该目录下运行npm install命令安装必要的模块包。

模块包安装完成后,运行npm run build命令将SDK中的TypeScript打包成一个JavaScript文件。命令运行完成后会在目录下生成一个dist/文件夹,里面就是打包完成的bundle.js文件。

1.3.打包JS文件

可以输入npm run serve命令先查看一下默认配置下打包后的效果。在浏览器中访问http://localhost:5000/Samples/TypeScript/Demo/即可看到当前的效果。

不出意外的话,大概能看到这样的画面。

效果看完,按下Ctrl+C结束serve进程。

2.修改TypeScript

刚才打包的那个JS看起来不怎么能实现我想要的效果,所以需要改一下SDKSamples/TypeScript/Demo/目录下的TypeScript文件来实现效果。

……话是这么说,但我其实并不懂TypeScript。_(:з)∠)_

好在在CSDN上找到了一位大佬写的相关笔记(https://blog.csdn.net/weixin_44128558/article/details/104792345),参考着这篇文章的讲解,外加各种Bing一下,最终还是姑且摸出一个能用的JavaScript了。

以下内容记录的是我所作的修改。原本的代码我都是注释掉的,把修改后的片段和原版进行比对,应该就能看出改在哪里了,大概。

2.1.挂载全局方法

根据大佬所写,在通过webpack将TypeScript打包后,TypeScript中所定义的变量及方法都经过了封装而变成内部变量或者内部方法了,所以无法通过外部的JavaScript进行调用。

所以就依葫芦画瓢,也在lappdefine.ts的末尾创建了一个能用外部JavaScript调用的全局方法。

export var ModelSwitchBtn = 'l2d_btn_sw';  // 创建1个变量,用于保存模型切换按钮的ID

// 创建1个全局函数,用于通过外部JS在初始化时定义变量
export const win: any = window
win.initDefine=function(resourcesPath: string, backImageName: string, modelDir: string[], modelSwitchBtn: string){
    ResourcesPath = resourcesPath;
    BackImageName = backImageName;
    ModelDir = modelDir;
    ModelDirSize = modelDir.length;
  ModelSwitchBtn = modelSwitchBtn;
}

2.2.设置手动开关

在默认设置下,Live2D模型将在网页加载时自动进行加载。由于运行Live2D模型所需要的资源还挺高,所以就想把main.ts里面的模型加载触发器改成一个全局方法,通过外部JavaScript来启动。

顺便也要创建一个停止模型运行的方法。起初我是直接使用LAppDelegate.releaseInstance();方法来中止运行的,没想到这么操作之后在网页控制台上直接报错。Live2D模型确实是停止运行了,可是代码似乎崩了。

因此改一下思路,干脆把停止功能改成暂停功能,使用TS方法移除各种事件监听器及动画帧,外部JS方法将画布隐藏。虽然效果一般般,不过总比光把画布隐藏的效果好一点,大概。

首先注释掉onload监听器并创建全局方法l2d_load()

/**
* ブラウザロード後の処理
*/
/*window.onload = (): void => {*/
LAppDefine.win.l2d_load = (): void => {
  if (LAppDelegate.getInstance().initialize() == false) {
    return;
  }

  LAppDelegate.getInstance().run();
};

然后新创建一个全局方法l2d_unload()。(偷懒起见,这个方法里面把画布的ID写死了,如果实际使用时画布ID不是“live2d”的话可以自己改下)

/**
 * 終了時の処理
 */
window.onbeforeunload = (): void => LAppDelegate.releaseInstance();
LAppDefine.win.l2d_unload = (): void => {
  document.getElementById("live2d").onmousedown = null;
  window.onmousemove = null;
  document.getElementById("live2d").onmouseup = null;
  cancelAnimationFrame(loopid);
};

l2d_unload()方法是通过ID来中止动画帧的,所以需要将lappdelegate.ts里面的动画帧保存到变量里面。

lappdelegate.ts文件最上方新建一个变量loopid

export let loopid = 0;

/**
 * アプリケーションクラス。
 * Cubism SDKの管理を行う。
 */

将动画帧保存在变量中。

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);*/
  loopid = requestAnimationFrame(loop);  // 将动画帧ID保存在变量中
};

 

2.3.去掉画布生成代码

在默认设置下,呈现Live2D模型的画布通过lappdelegate.ts进行生成。由于实际使用中已经在外部创建好了相应画布,所以需要注释掉相关代码,并且将外部画布赋值给canvas变量。

同上,这里也把画布的ID直接写进去了。注意根据需要进行修改。

// キャンバスの作成
// 注释掉画布生成代码
/*canvas = document.createElement('canvas');
if (LAppDefine.CanvasSize === 'auto') {
  this._resizeCanvas();
} else {
  canvas.width = LAppDefine.CanvasSize.width;
  canvas.height = LAppDefine.CanvasSize.height;
}*/

// 通过ID获取HTML中的画布
canvas = <HTMLCanvasElement>document.getElementById("live2d");
canvas.width = canvas.width;
canvas.height = canvas.height;
canvas.toDataURL("image/png");

向DOM添加画布这句代码也要注释掉。

// キャンバスを DOM に追加
/*document.body.appendChild(canvas);*/

由于自行创建的画布并非全屏大小,所以需要将resizeCanvas()方法相关的内容注释掉。

public onResize(): void {
  /*this._resizeCanvas();*/
  this._view.initialize();
  this._view.initializeSprite();
}
/*private _resizeCanvas(): void {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}*/

最后还要注释这句清除颜色及深度缓冲区的代码,否则在浏览器中可能会出现画布背景变黑的情况。

// カラーバッファや深度バッファをクリアする
/*gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);*/

2.4.修改监听事件

在默认设置下,切换模型是通过画面右上角的齿轮来实现的,而那个齿轮也是通过TypeScript进行生成的。实际使用中一般都是通过一个按钮进行切换,所以需要新创建一个对模型切换按钮的监听事件并且注释齿轮相关的代码。

之前已经在lappdefine.ts中创建了1个用于存放按钮ID的变量,通过该变量来对按钮进行绑定即可。

在“初始化gl上下文”这句注释前添加以下内容。

var switchBtn = <HTMLLIElement>document.getElementById(LAppDefine.ModelSwitchBtn);

// glコンテキストを初期化

在以下位置添加监听事件。

if (supportTouch) {
  // タッチ関連コールバック関数登録
  // 触屏板子压箱底,暂时没打算调整触屏模式下的监听事件,干脆全部注释掉
  /*canvas.ontouchstart = onTouchBegan;
  canvas.ontouchmove = onTouchMoved;
  canvas.ontouchend = onTouchEnded;
  canvas.ontouchcancel = onTouchCancel;*/
} else {
  // マウス関連コールバック関数登録
  canvas.onmousedown = onClickBegan;
  /*canvas.onmousemove = onMouseMoved;*/
  window.onmousemove = onMouseMoved;  // 自行创建的画布没有布满整个窗口,因此将鼠标移动事件改成全窗口监听
  canvas.onmouseup = onClickEnded;
  
  // 创建模型切换按钮的监听事件
  switchBtn.onmousedown = (): void => {
    const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance();
    live2DManager.nextScene();
  };
}

由于自行创建的画布没有布满整个屏幕,所以需要对所有鼠标事件中获取的坐标进行相应的修改。(触屏相关的我直接注释掉了)

/**
 * クリックしたときに呼ばれる。
 */
function onClickBegan(e: MouseEvent): void {
  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }
  /*LAppDelegate.getInstance()._captured = true;
  const posX: number = e.pageX;
  const posY: number = e.pageY;*/
  
  // 获取鼠标按下时的坐标
  let rect = canvas.getBoundingClientRect();
  let posX: number = e.clientX - rect.left;
  let posY: number = e.clientY - rect.top;
  // 对超出画布范围的坐标进行转换
  posX = (posX < 0) ? 0 : (posX > canvas.width) ? canvas.width : posX ;
  posY = (posY < 0) ? 0 : (posY > canvas.height) ? canvas.height : posY;

  LAppDelegate.getInstance()._view.onTouchesBegan(posX, posY);
}

/**
 * マウスポインタが動いたら呼ばれる。
 */
function onMouseMoved(e: MouseEvent): void {
  // 注释掉鼠标左键按下判定
  /*if (!LAppDelegate.getInstance()._captured) {
    return;
  }*/

  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }

  /*const rect = (e.target as Element).getBoundingClientRect();
  const posX: number = e.clientX - rect.left;
  const posY: number = e.clientY - rect.top;*/
  
  // 获取鼠标移动时的坐标
  let rect = canvas.getBoundingClientRect();
  let posX: number = e.clientX - rect.left;
  let posY: number = e.clientY - rect.top;
  // 对超出画布范围的坐标进行转换
  posX = (posX < 0) ? 0 : (posX > canvas.width) ? canvas.width : posX ;
  posY = (posY < 0) ? 0 : (posY > canvas.height) ? canvas.height : posY;


  LAppDelegate.getInstance()._view.onTouchesMoved(posX, posY);
}

/**
 * クリックが終了したら呼ばれる。
 */
function onClickEnded(e: MouseEvent): void {
  LAppDelegate.getInstance()._captured = false;
  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }

  /*const rect = (e.target as Element).getBoundingClientRect();
  const posX: number = e.clientX - rect.left;
  const posY: number = e.clientY - rect.top;*/
  
  // 获取鼠标松开时的坐标
  let rect = canvas.getBoundingClientRect();
  let posX: number = e.clientX - rect.left;
  let posY: number = e.clientY - rect.top;
  // 对超出画布范围的坐标进行转换
  posX = (posX < 0) ? 0 : (posX > canvas.width) ? canvas.width : posX ;
  posY = (posY < 0) ? 0 : (posY > canvas.height) ? canvas.height : posY;
  

  LAppDelegate.getInstance()._view.onTouchesEnded(posX, posY);
}

//  触摸相关的方法暂时全部注释掉了
/**
 * タッチしたときに呼ばれる。
 */
/*function onTouchBegan(e: TouchEvent): void {
  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }

  LAppDelegate.getInstance()._captured = true;

  const posX = e.changedTouches[0].pageX;
  const posY = e.changedTouches[0].pageY;

  LAppDelegate.getInstance()._view.onTouchesBegan(posX, posY);
}*/

/**
 * スワイプすると呼ばれる。
 */
/*function onTouchMoved(e: TouchEvent): void {
  if (!LAppDelegate.getInstance()._captured) {
    return;
  }

  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }

  const rect = (e.target as Element).getBoundingClientRect();

  const posX = e.changedTouches[0].clientX - rect.left;
  const posY = e.changedTouches[0].clientY - rect.top;

  LAppDelegate.getInstance()._view.onTouchesMoved(posX, posY);
}*/

/**
 * タッチが終了したら呼ばれる。
 */
/*function onTouchEnded(e: TouchEvent): void {
  LAppDelegate.getInstance()._captured = false;

  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }

  const rect = (e.target as Element).getBoundingClientRect();

  const posX = e.changedTouches[0].clientX - rect.left;
  const posY = e.changedTouches[0].clientY - rect.top;

  LAppDelegate.getInstance()._view.onTouchesEnded(posX, posY);
}*/

/**
 * タッチがキャンセルされると呼ばれる。
 */
/*function onTouchCancel(e: TouchEvent): void {
  LAppDelegate.getInstance()._captured = false;

  if (!LAppDelegate.getInstance()._view) {
    LAppPal.printMessage('view notfound');
    return;
  }

  const rect = (e.target as Element).getBoundingClientRect();

  const posX = e.changedTouches[0].clientX - rect.left;
  const posY = e.changedTouches[0].clientY - rect.top;

  LAppDelegate.getInstance()._view.onTouchesEnded(posX, posY);
}*/

2.5.禁用齿轮

在默认设置下,画面右上角的齿轮是通在lappview.ts中初始化的。注释掉相关内容即可彻底告别齿轮。

// 歯車画像初期化
/*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._gear.isHit(pointX, pointY)) {
  live2DManager.nextScene();
}*/

2.6.加载跨域纹理

如果要把模型放到对象存储之类的地方时,可能会出现跨域纹理加载错误的情况。在lapptexturemanager.ts中给图片添加CrossOrigin属性即可解决。

// データのオンロードをトリガーにする
const img = new Image();
img.crossOrigin = '';
img.onload = (): void => {

3.构建JS

将控制台cd到SDK目录下Samples/TypeScript/Demo/目录,使用npm run build命令将TypeScript打包成dist/目录下的bundle.js

把这个JS文件与SDK目录下Core/目录内的live2dcubismcore.min.js以及live2dcubismcore.js.map文件一同拷贝至网页的JavaScript文件夹,并在HTML内引用前2个JS文件,就能在网页上加载Live2D模型啦~

4.Demo

往GitHub传了个Demo:https://denpaya.github.io/Live2Dv4-Web-Demo/(仓库里的许可证是直接复制的SDK许可证)

试着访问了下,果然直连GitHub Pages速度有点感人……还是更推荐下载下来进行测试。