vTuber行业越来越火爆,看着那些形形色色的皮套,忍不住自己实现一遍的冲动,说起2D骨骼动画软件,Spine2D首当其冲。这次就用它来耍一耍。
为啥我不用live2D?毕竟这是日本软件,英语的本地化翻译不知道靠不靠谱,与Spine价格也差不太多,看了一些教程,操作也比较复杂,感觉更倾向于给不熟悉编程的设计人员使用。 为啥不用免费的国产软件龙骨?开始确实是试用了一段时间,软件的基础功能应该满足我的需求,但是程序文档比较简陋,而且似乎已经停止维护,非常可惜。 无奈之下,忍痛花299刀买了Spine。(T_T)
虚拟主播用的动画不知道有没有专业一些的称呼,我暂时就叫“live2D动画”吧~live2D动画的核心是骨骼绑定与蒙皮动画,蒙皮动画的原理和我以前用canvas2D实现的图片变形效果类似:Canvas2D实现对图片进行网格变换 。
首先去网上找了一个已经对角色分件完的PSD模型,为了demo制作方便我找了个比较简单、贴图较少的“人物”,然后用官方的ps插件PhotoshopToSpine.jsx 导出图层,然后用Spine打开导出的工程即可。(psd原件我会放到文末的源码中)
接下里就是再spine里绑定骨骼,刷权重,key帧这些繁琐的工作了,三言两语也说不完,网上的教程还是挺多的,本文还是着重程序实现。我会把Spine工程也在源码中分享出来,如果大家有疑惑的地方我这个生手也许能解答一部分~
程序实现主要用到两个库,pixi.js 和pixi-spine ,用的都是当前(2021-02-28)的最新版,因为我的spine版本是3.8,pixi-spine只有最新版才支持,所以pixi.js也必须是最新版(v5)
虽然spine官方也提供了webgl版的运行时,但是自从上次用了pixi感觉还是相当好使的,pixi也提供了插件,果断就拿来用了。
本次功能主要是三个:①眼球根据鼠标移动位置;②点击角色张嘴;③根据音乐节奏跳动;
功能① 的实现主要依赖spine动画的ik约束,具体实现见官方论坛的这篇博文:眼睛距离限制设置 ,我就拿来现学现卖了,然后程序上只要通过findBone方法找到约束的骨骼,然后移动位置即可。
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 controlEye = skeleton.findBone ('eye' ) controlHead = skeleton.findBone ('head' ) controlEyeX = controlEyeXTarget = controlEye.x controlEyeY = controlEyeYTarget = controlEye.y eyeBasePos.x = controlHead.worldX eyeBasePos.y = controlHead.worldY stage.on ('pointermove' , (e ) => { pageX = e.data .global .x pageY = e.data .global .y controlEyeXTarget = pageX - (basePos.x + eyeBasePos.x ) controlEyeYTarget = -(pageY - (basePos.y + eyeBasePos.y )) })
功能② 则是先在spine中创建一段关键帧动画,然后通过setAnimation方法来进行播放,如果有多个动画可以通过设置BlendMode并控制每段动画轨道的alpha来进行混合,这一块我会以后写个新的demo专门介绍。
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 loader.add ('monster' , 'monster.json' ) .load (function (loader, resources ) { spineMonster = new pixiSpine.Spine (resources.monster .spineData ) state = spineMonster.state }) stage.on ('pointertap' , () => { const entry = state.setAnimation (0 , 'laugh' , false ) })
功能③ 也是通过ik约束和修改关键骨骼的位置来实现,根据音乐节奏跳动的部分不是本文的重点,主要是对音乐的流数据进行采样归并,我也没做深入的研究,目前只是简单调用api的阶段,有兴趣详细了解的小伙伴可以看MDN的入门教程:Using the Web Audio API 。
关键代码:
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 68 69 70 71 72 73 74 75 76 77 78 79 let analyserlet bufferLength = 0 let dataArray = []let now = performance.now ()let last = nowconst loop = function (delta ) { stats.begin () if (gameRunning) { now = performance.now () if (now - last > 100 ) { last = now } if (dancing) { analyser.getByteFrequencyData (dataArray) let i let audioVal = 0 for (i = 0 ; i < bufferLength; i++) { audioVal += dataArray[i] } audioVal /= bufferLength controlBodyYTarget = -bodyBasePos.y + audioVal controlBodyYTarget = controlBodyYTarget * 0.3 controlBodyYTarget = Math .max (30 , Math .min (240 , controlBodyYTarget)) } controlEyeX += (controlEyeXTarget - controlEyeX) * 0.1 controlEyeY += (controlEyeYTarget - controlEyeY) * 0.1 controlBodyX += (controlBodyXTarget - controlBodyX) * 0.1 controlBodyY += (controlBodyYTarget - controlBodyY) * 0.5 controlEye.x = controlEyeX controlEye.y = controlEyeY controlBody.x = controlBodyX controlBody.y = controlBodyY } renderer.render (stage) stats.end () } ticker.add (loop) const $btn = document .getElementById ('play-btn' )const $file = document .getElementById ('file' )let audio = null let timer = -1 $file.onchange = function ( ) { const file = $file.files [0 ] if (!file) return if (audio) audio.pause () audio = new Audio () audio.loop = true audio.addEventListener ('canplay' , event => { audio.play () let flag = false clearInterval (timer) timer = setInterval (function ( ) { flag = !flag controlBodyXTarget = flag ? 50 : -50 }, 1000 ) dancing = true }) audio.src = URL .createObjectURL (file) const audioCtx = new (window .AudioContext || window .webkitAudioContext )() analyser = audioCtx.createAnalyser () analyser.fftSize = 32 const source = audioCtx.createMediaElementSource (audio) source.connect (analyser) analyser.connect (audioCtx.destination ) bufferLength = analyser.frequencyBinCount dataArray = new Uint8Array (bufferLength) }
最终效果如下:
如果文章对你有帮助,还请点个赞 ٩( ‘ω’ )و
完整代码+工程资源+psd源文件
在线演示1 、在线演示2