最近没太多事情,想要给群里建一个网站。想来不知道放些什么内容进去,建好站之后姑且先放一个live2d的看板娘。因此产生了这个live2d网页开发的尝试。
第一次尝试,从live2d的官方原生的SDK入手。在我之前已经有很多大佬做成了,这里先放我的成果,再放本人从0开始实现的过程。
一、成品
~浼钤茶会~群内共享站~ (meiqianhime.com)http://www.meiqianhime.com/
前端新手,学了没多久,所以做的网页很烂,轻喷orz
二、过程
开发环境:
Live2D Cubism 4.1 #来自官方,live2d模型的编辑器和查看器。
vscode #官方指定的代码编辑器
node.js TypeScript Webpack #必须环境
python+Flask 本人使用的网页应用框架,可以换其他
1.下载SDK,准备模型
登录官网,下载webSDK。官网需要翻墙,不然会比较慢。
Live2D Cubismhttps://www.live2d.com/
选择【各种下载】-【live2D SDK下载与功能介绍】,
在弹出的页面中找【Live2D Cubism SDK for Web】并下载。
接下来是准备模型。官方提供了几种现成的模型可供下载。本人葱厨,所以下载了初音未来的模型。之后会尝试用自己的模型来做,可能会放在以后的笔记中。
2.运行SDK示例项目
安装vscode、nodejs和TypeScript,并做好相关环境配置。
将下载好的SDK压缩包解压并用vscode打开,在README.md文件中可以看到项目结构的说明。
在终端中cd到Samples/TypeScript/Demo,这是官方的示例项目。
运行指令npm install,根据文件package.json中信息安装项目所需插件。
运行指令npm run build进行项目打包,
完成后会在Demo文件夹下生成dist文件夹,
里面有打包好的bundle.js文件,用于之后的移植开发。
运行指令npm run serve运行服务,打开终端给出的本地链接进入示例页面。
终端中按ctrl-c退出服务。
3.加入自己准备的模型
3.1 模型方面的准备
打开资源文件夹Samples/Recourses,放入准备好的模型。
修改文件夹的名字与里面的cmo3文件相同,双击打开。
这里想要做一个点击交互,点一下miku就会有动作的那种。
点击【建模】--【图形网格】--【创建触碰检测用途的图形网格】,
把红框框套在想要触发点击交互的地方,
然后把左边检查器中的名称改成Body。(不改也行)
然后点上面的【编辑纹理集 】
弹出的窗口中找到【用于触碰检测的图像】,右键添加到纹理集。
完成后ctrl-s保存,然后选择【文件】--【导出运作档】--【导出为moc3文件】,将模型导出。
接下来打开.can3文件进行动画处理。
miku的模型有附带做好的动画,如果是自定义模型,可以看相关教程做一两个简单的动画。
打开后选择【文件】--【导出运作档】--【导出动态文件】,导出所有的动画文件到指定文件夹。
这里创建了新文件夹anime用于存储动画文件。
接下来打开官方的模型查看器CubismViewer4,将模型的moc3文件拖入。
点开左边的【hitAreas】,将定义好的点击框命名为Body(首字母大写)
然后把刚刚的动画文件拖入,选择想要触发的动画,将组名改为TapBody。
然后ctrl-s保存文件,把生成的model3.json文件覆盖原有文件。
处理完之后,模型文件夹应该有以下结构。
3.2 向Demo中加入自己的模型
vscode打开Samples/TypeScript/Demo/src/lappdefine.ts,找到存放模型名的列表。
在列表最末端添加自己的模型的文件夹名。
如果不想显示其他模型,可以只留自己的模型名字。
// モデル定義---------------------------------------------
// モデルを配置したディレクトリ名の配列
// ディレクトリ名とmodel3.jsonの名前を一致させておくこと
//在这里修改,添加'miku'
export const ModelDir: string[] = ['Haru', 'Hiyori', 'Mark', 'Natori', 'Rice', 'miku'];
export const ModelDirSize: number = ModelDir.length;
// 外部定義ファイル(json)と合わせる
export const MotionGroupIdle = 'Idle'; // アイドリング
export const MotionGroupTapBody = 'TapBody'; // 体をタップしたとき
保存之后,使用指令npm run build重新打包,
然后使用指令npm run serve 运行服务。
点击齿轮更换模型,就可以看到自己的模型显示在网页上了= =
到这里大概完成了三分之一,可以稍微喘口气了= =
4. 源码修改
我们最终想要一个id为live2d的canvas画布,当我们在html中创建它时就会显示live2d的内容。
基于此对demo中的源码进行修改。
这里主要参考这几位大佬的方法进行修改。第一篇的大佬有比较详细的讲代码,这里不多说。
(10条消息) 笔记:live2d4.0 sdk 博客园网页动画_weixin_44128558的博客-CSDN博客_live2d生成https://blog.csdn.net/weixin_44128558/article/details/104792345(10条消息) live2d(Web SDK 4.x)Web看板娘进阶_仰望星空的sun的博客-CSDN博客_live2d sdk
https://blog.csdn.net/qq_37735413/article/details/119413744
4.1 lappdefine.ts
将变量ResourcesPath、BackImageName、ModelDir、ModelDirSize的变量声明从const改为let。
// 相対パス
// 相对路径 打完包移植的时候要修改它
export let ResourcesPath = '../../Resources/';
// モデルの後ろにある背景の画像ファイル
// 背景图片 不是很想要,就把它赋了空值。
export let BackImageName = '';
// 歯車
// 画面右上角的齿轮的图片
export let GearImageName = 'icon_gear.png';
// 終了ボタン
// 结束按钮 没见过这东西?
export let PowerImageName = 'CloseNormal.png';
// モデル定義---------------------------------------------
// モデルを配置したディレクトリ名の配列
// ディレクトリ名とmodel3.jsonの名前を一致させておくこと
// 模型定义
// 配置模型的目录名的列表
// 模型的目录名应该与model3.json的文件名一致
// 这里只想要一个模型,所以只留下了miku的模型。
export let ModelDir: string[] = ['miku'];
export let ModelDirSize: number = ModelDir.length;
然后在文件末尾添加代码
export const win: any = window
win.initDefine=function(resourcesPath: string, backImageName: string, modelDir: string[]){
ResourcesPath = resourcesPath;
BackImageName = backImageName;
ModelDir = modelDir;
ModelDirSize = modelDir.length;
}
4.2 main.ts
注释掉window.onresize,否则live2d模型会随着窗口改变大小。
/**
* Process when changing screen size.
*/
// window.onresize = () => {
// if (LAppDefine.CanvasSize === 'auto') {
// LAppDelegate.getInstance().onResize();
// }
// };
4.3 lappdelegate.ts
首先找到getInstance.initialize()进行修改。
public initialize(): boolean {
// キャンバスの作成
// 生成画布
// canvas = document.createElement('canvas');
// if (LAppDefine.CanvasSize === 'auto') {
// this._resizeCanvas();
// } else {
// canvas.width = LAppDefine.CanvasSize.width;
// canvas.height = LAppDefine.CanvasSize.height;
// }
canvas = <HTMLCanvasElement>document.getElementById("live2d"); // index.html中的id为live2d的画布
canvas.width = canvas.width;
canvas.height = canvas.height;
canvas.toDataURL("image/png");
// glコンテキストを初期化
// 初始化gl上下文
// @ts-ignore
gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
alert('不能初始化 WebGL. 浏览器不支持.');
gl = null;
document.body.innerHTML =
'浏览器不支持 <code><canvas></code> 元素.';
// gl初期化失敗
return false;
}
// キャンバスを DOM に追加
// 向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;
window.ontouchmove = onTouchMoved;
canvas.ontouchend = onTouchEnded;
canvas.ontouchcancel = onTouchCancel;
} else {
// マウス関連コールバック関数登録
// 鼠标点击模式下的回调函数
canvas.onmousedown = onClickBegan;
// canvas.onmousemove = onMouseMoved;
window.onmousemove = onMouseMoved;
canvas.onmouseup = onClickEnded;
}
// AppViewの初期化
// 初始化AppView
this._view.initialize();
// Cubism SDKの初期化
// 初始化SDK
this.initializeCubism();
return true;
}
然后找到onMouseMoved函数进行修改。
/**
* マウスポインタが動いたら呼ばれる。
* 鼠标移动时触发
*/
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;
//这里是模型身体随着鼠标摆动的数值
//使用canvas.getBoundingClientRect()读取画布实时位置,加入身体位置的计算,
// 方便移动画布之后模型能依然根据和鼠标的相对位置摆动
let posX: number = e.clientX - canvas.getBoundingClientRect().left;
let posY: number = e.clientY - window.innerHeight + canvas.height + canvas.getBoundingClientRect().top;
// 图像在网页的坐下角,简单处理坐标将超过画布边界坐标就等与边界坐标
posX = (posX > canvas.width) ? canvas.width : posX;
posY = (posY < 0) ? 0 : posY;
LAppDelegate.getInstance()._view.onTouchesMoved(posX, posY);
}
然后找到run()函数,将gl.clear注释掉。
// カラーバッファや深度バッファをクリアする
// 清除彩色缓冲区和深度缓冲区
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
4.4 lappview.ts
找到initializeSprite()函数进行修改。
/**
* 画像の初期化を行う。
*/
public initializeSprite(): void {
const width: number = canvas.width;
const height: number = canvas.height;
const textureManager = LAppDelegate.getInstance().getTextureManager();
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();
}
}
4.5 index.html
在body里插入一个id为live2d的canvas。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1900">
<title>TypeScript HTML App</title>
<style>
html, body {
margin: 0;
overflow: hidden;
}
</style>
<!-- Pollyfill script -->
<script src="https://unpkg.com/core-js-bundle@3.6.1/minified.js"></script>
<!-- Live2DCubismCore script -->
<script src = "../../../Core/live2dcubismcore.js"></script>
<!-- Build script -->
<script src = "./dist/bundle.js"></script>
</head>
<body>
<canvas id="live2d" width="600" height="600" class="live2d"></canvas>
</body>
</html>
全部处理好之后,重新打包并运行。
npm run build
npm run serve
5.打包完成后的移植
完成上述步骤后,开始尝试在普通的网页中加入live2d。
移植只移动模型文件夹和几个js文档,故对移植环境要求不高。
wamp或者flask什么的应该都可以。
这里使用flask的开发环境来移植live2d到原有项目。
需要的东西有:
1.模型文件夹
2.live2dcubismcore.js和live2dcubismcore.js.map,在core文件夹里。
3.bundle.js,在dist文件夹里。
网页名:浼钤茶会
static 存放静态资源文件
css 存放css文件
meiqiancss1.css
src 存放js文件
meiqianjs1.js
resource 存放模型和图片文件
templates 存放网页文件
index.html 登入界面
app.py 用于启动flask网页应用
把模型文件夹扔进resource文件夹,其余三个文件扔进src文件夹。
打开bundle.js,ctrl-f搜索ResourcesPath,找到资源路径并修改为现在的resource。
exports.ResourcesPath = '../static/Resources/';
exports.BackImageName = '';
exports.GearImageName = 'icon_gear.png';
exports.PowerImageName = 'CloseNormal.png';
exports.ModelDir = ['miku'];
然后打开index.html,在head中加入三个js文件的引用。
<!-- Pollyfill script -->
<script src="https://unpkg.com/core-js-bundle@3.6.1/minified.js"></script>
<!-- Live2DCubismCore script -->
<script src = "../static/src/live2dcubismcore.js"></script>
<!-- Build script -->
<script src = "../static/src/bundle.js"></script>
运行app.py,完成。
以下是各种源码。
app.py
from flask import Flask, render_template, request, Response, send_from_directory
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def main():
return render_template('index.html')
if __name__ == '__main__':
app.run()
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>~浼钤茶会~群内共享站~</title>
<link rel="icon" href="../static/resources/favicon.ico">
<link rel="stylesheet" href="../static/css/meiqiancss1.css">
</head>
<body>
<div id="live2d-main">
<canvas id="live2d" height=600 class="live2d"></canvas>
</div>
<div class="header" style="background-image: url('../static/resources/bg2.JPG')">
<h1>浼钤茶会</h1>
<p>尚在建设中的茶会大厅~</p>
</div>
<div class="navbar">
<a href="#" onclick="building()">主页</a>
<a href="#" onclick="building()">花园</a>
<a href="index.html">大厅</a>
<a href="#" onclick="building()">图书馆</a>
<a href="#" onclick="building()">call吧</a>
<a href="#" onclick="building()">浼钤姬的房间</a>
<a href="#" onclick="building()">留言板</a>
<a href="#" class="right" onclick="building()">个人房间</a>
</div>
<div class="row">
<div class="side">
<h1>建设中。。。</h1>
</div>
<div class="main">
<h1>建设中。。。</h1>
</div>
</div>
<div class="footer">
<p>made by 某电饭锅司机</p>
</div>
<!-- Pollyfill script -->
<script src="https://unpkg.com/core-js-bundle@3.6.1/minified.js"></script>
<!-- Live2DCubismCore script -->
<script src = "../static/src/live2dcubismcore.js"></script>
<!-- Build script -->
<script src = "../static/src/bundle.js"></script>
<script src="../static/src/meiqianjs1.js"></script>
</body>
</html>
meiqiancss1.css
* {
box-sizing: border-box;
}
/* body 样式 */
body {
font-family: Arial;
margin: 0;
}
/* 标题 */
.header {
padding: 80px;
text-align: center;
background: #1abc9c;
color: white;
}
/* 标题字体加大 */
.header h1 {
font-size: 40px;
}
/* 导航 */
.navbar {
overflow: hidden;
background-color: #333;
}
/* 导航栏样式 */
.navbar a {
float: left;
display: block;
color: white;
text-align: center;
padding: 14px 20px;
text-decoration: none;
}
/* 右侧链接*/
.navbar a.right {
float: right;
}
/* 鼠标移动到链接的颜色 */
.navbar a:hover {
background-color: #ddd;
color: black;
}
/* 列容器 */
.row {
display: -ms-flexbox; /* IE10 */
display: flex;
-ms-flex-wrap: wrap; /* IE10 */
flex-wrap: wrap;
}
.row div{
min-height: 500px;
max-height: 2000px;
}
/* 创建两个列 */
/* 边栏 */
.side {
-ms-flex: 30%; /* IE10 */
flex: 30%;
background-color: #f1f1f1;
padding: 20px;
}
/* 主要的内容区域 */
.main {
-ms-flex: 70%; /* IE10 */
flex: 70%;
background-color: white;
padding: 20px;
}
/* 测试图片 */
.fakeimg {
background-color: #aaa;
width: 100%;
padding: 20px;
}
/* 底部 */
.footer {
padding: 20px;
text-align: center;
background: #ddd;
}
.footer p{
font-family: '黑体', serif;
}
#live2d-main {
position : absolute;
padding: 10px;
bottom: 5px;
left:5px;
}
#live2d {
/* position : absolute; */
}
/* 响应式布局 - 在屏幕设备宽度尺寸小于 700px 时, 让两栏上下堆叠显示 */
@media screen and (max-width: 700px) {
.row {
flex-direction: column;
}
}
/* 响应式布局 - 在屏幕设备宽度尺寸小于 400px 时, 让导航栏目上下堆叠显示 */
@media screen and (max-width: 400px) {
.navbar a {
float: none;
width: 100%;
}
}
meiqianjs1.js
function building() {
alert("正在建设中。。");
}
//Make the DIV element draggagle:
dragElement(document.getElementById("live2d-main"));
function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (document.getElementById(elmnt.id )) {
/* if present, the header is where you move the DIV from:*/
document.getElementById(elmnt.id ).onmousedown = dragMouseDown;
} else {
/* otherwise, move the DIV from anywhere inside the DIV:*/
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement() {
/* stop moving when mouse button is released:*/
document.onmouseup = null;
document.onmousemove = null;
}
}
项目包:https://pan.baidu.com/s/1IYMhwrQxuCObrPimwyn1KA
提取码:6xqb
--来自百度网盘超级会员V3的分享
三、一些问题
1.首先在运行的时候,发现了这个报错
去提示的位置查看 发现是已经被注释掉的齿轮的相关代码。
将其注释掉,重新打包bundles.js装入,问题解决
2.编写js代码实现了画布的移动,但是当画布离开原来的位置时,点击触发的交互会变得不敏感。
暂时没什么精力处理这个问题,以后再看。
3.使用官方的SDK进行开发,在点击交互的问题上好像只看到了一个Body点击框的情况,在示例中没有看到同时有多个点击框判定不同事件的模型。在实际尝试中,也没能做出想要的结果。之后如果还不行的话,就换个非官方的SDK试试吧。。