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文件)
- 打开控制台跳转到Demo文件夹下输入 npm install 命令(安装了淘宝镜像的可以使用cnpm install命令安装的快一些)
install命令会根据package.json 文件中的配置下载安装需要的插件
这里已经安装好了,安装好后在Demo文件夹下回多出一个 node_modules 文件夹 - 运行 npm run build命令
运行完成后会在Demo文件夹下生成一个 dist 文件夹,该文件夹下有一个bundle.js,这个就是集合打包好的js文件,在index.html文件中有引用 - 运行 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中调用到
(注意:这里要将ResourcesPath 、BackImageName 、ModelDir 、ModelDirSize变量声明成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