Live2D SDK WebGL版で簡易Live2DViewer作ってみました。
Live2Dモデルのファイルをサーバーにアップロードさせたくなかったので、全てHTML5の機能で実装してます。
ファイル選択とファイルのドラッグ&ドロップに対応しています
(テクスチャは複数選択してドラッグ&ドロップして下さい)
フォルダ構成
Live2D SDKのSimpleプロジェクトを元にカスタムしています。
Live2D_DragDrop
│
│ index.html
│
├─framework
│ Live2DFramework.js・・・SDK付属のフレームワーク
│
├─lib
│ live2d.min.js・・・Live2Dのコアライブラリ
│
└─src
Simple.js
ソース
既存のファイル選択ボタンだとダサいので、ダミーボタンを表示してファイル選択処理を呼んでいます
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Live2D Simple</title>
</meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=4.0">
</meta>
<style>
/* 選択したファイルのサムネイル表示 */
.thumb {
height: 75px;
border: 1px solid #000;
margin: 10px 5px 0 0;
}
.drop_area{
border:dashed 2px #ccc;
width:512px;
height:80px;
margin:4px;
background-color:#efefef;
}
.moc_area, .texture_area, .motion_area{
padding-top:10px;
font-size: 18px;
color:blue;
text-align: center;
}
#select_moc, #select_texture, #select_motion{
display:none;
}
#dummy_moc_btn, #dummy_texture_btn, #dummy_motion_btn{
margin-top:10px;
margin-left:150px;
font-size: 12px;
text-decoration: none;
color:#fff;
background-color: #49a9d4;
border:none;
border-radius: 5px;
padding:2px 16px 2px 16px;
}
#dummy_moc_file, #dummy_motion_file{
width:200px;
height: 20px;
font-size: 14px;
border:none;
background-color: #e6e6e6;
}
#displaybtn{
width: 160px;
height:40px;
margin-left:4px;
margin-bottom:4px;
font-size:20px;
font-weight: bold;
color:#fff;
background-color: #49a9d4;
border:none;
border-radius: 5px;
padding:2px 16px 2px 16px;
box-shadow:2px 2px #1a6ea0;
text-shadow:1px 1px #555;
}
#displaybtn:hover{
color:#fff;
background-color: #0F7DAE;
box-shadow: 2px 2px #073E5F;
}
.live2d_area{
position: absolute;
top:4px;
left:528px;
}
#glcanvas{
border:dashed 2px #CCC;
margin:4px;
}
</style>
</head>
<body>
<!-- mocファイル -->
<div class="drop_area" id="drop_moc">
<div class="moc_area">mocをドロップ</div>
<input type="file" id="select_moc" name="files" />
<button id="dummy_moc_btn" onclick="dummy_moc_click()">mocファイル選択</button>
<input type="text" id="dummy_moc_file" readonly>
</div>
<!-- textureファイル -->
<div class="drop_area" id="drop_texture">
<div class="texture_area">texutureをドロップ</div>
<input type="file" id="select_texture" name="files[]" multiple />
<button id="dummy_texture_btn" onclick="dummy_texture_click()">textureファイル選択</button>
</div>
<!-- 選択したしたテクスチャを表示する部分 -->
<output id="texture_list"></output>
<br>
<!-- motionファイル -->
<div class="drop_area" id="drop_motion">
<div class="motion_area">motionをドロップ</div>
<input type="file" id="select_motion" name="files" />
<button id="dummy_motion_btn" onclick="dummy_motion_click()">motionファイル選択</button>
<input type="text" id="dummy_motion_file" readonly>
</div>
<!-- モデル表示ボタン -->
<button id="displaybtn" onclick="Simple()">モデル表示</button>
<div class="live2d_area">
<!-- Live2Dモデル -->
<canvas id="glcanvas" width = "512px" height="512px"></canvas>
<div id="myconsole" style="color:#BBB">---- Log ----</div>
</div>
<!-- Live2D Library -->
<script src="lib/live2d.min.js"></script>
<script src="framework/Live2DFramework.js"></script>
<!-- User's Script -->
<script src="src/Simple.js"></script>
</body>
</html>
元々はLive2Dモデルファイル(.moc、.mtn)はXMLHttpRequestのarraybuffer形式でロードしてました。
今回はファイル選択形式なので、FileReaderでBase64形式にしてロードしてます。
Base64は小さい画像のみ有効かと思ったら、1024px x 1024pxのテクスチャ5枚とかもさくっとロードしてくれました。
/******************** mocファイル ********************/
var binarymoc = null;
// mocファイルのファイル選択ボタン(非表示でダミーボタンで装飾)
var select_moc = document.getElementById('select_moc');
select_moc.addEventListener('change', mocSelect, false);
// mocファイル選択名
var dummy_moc_file = document.getElementById('dummy_moc_file');
// mocファイルのドロップ領域
var drop_moc = document.getElementById('drop_moc');
drop_moc.addEventListener('drop', mocDrop);
drop_moc.addEventListener('dragover', mocDragOver);
drop_moc.addEventListener('dragleave', mocDragLeave);
// dummy用のmocファイル選択ボタンからイベント呼び出す
function dummy_moc_click(){
select_moc.click();
}
// mocファイルドラッグオーバー時
function mocDragOver(event){
drop_moc.style.backgroundColor = "#C1C1C1"; // hover色
event.preventDefault();
}
// mocファイルドラッグリーブ時
function mocDragLeave(event){
drop_moc.style.backgroundColor = "#efefef"; // 元の色
event.preventDefault();
}
// mocファイルドロップ時
function mocDrop(evt){
// ドロップしたファイル情報
var files = evt.dataTransfer.files;
moc_load(files);
evt.preventDefault();
drop_moc.style.backgroundColor = "#efefef"; // 元の色
}
// mocファイル選択時
function mocSelect(evt) {
// 選択したファイル情報
var files = evt.target.files;
moc_load(files);
}
// mocファイルロード処理
function moc_load(files){
// 初期化処理
binarymoc = null;
dummy_moc_file.value = "";
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
// 選択したファイル名をセット
dummy_moc_file.value = files[i].name;
reader.onload = (function(theFile) {
return function(e) {
// moc中身をセット(Base64形式)
binarymoc = e.target.result;
};
})(f);
// arrayBuffer形式
reader.readAsArrayBuffer(f);
}
}
/******************** textureファイル ********************/
var binarytextures = [];
// textureファイルの選択ボタン(非表示でダミーボタンで装飾)
var select_texture = document.getElementById('select_texture');
select_texture.addEventListener('change', TextureSelect, false);
// textureファイルのドロップ領域
var drop_texture = document.getElementById('drop_texture');
drop_texture.addEventListener('drop', textureDrop);
drop_texture.addEventListener('dragover', textureDragOver);
drop_texture.addEventListener('dragleave', textureDragLeave);
// textureファイル選択ボタンからイベント呼び出す
function dummy_texture_click(){
select_texture.click();
}
// textureファイルドラッグオーバー時
function textureDragOver(event){
drop_texture.style.backgroundColor = "#C1C1C1"; // hover色
event.preventDefault();
}
// textureファイルドラッグリーブ時
function textureDragLeave(event){
drop_texture.style.backgroundColor = "#efefef"; // 元の色
event.preventDefault();
}
// textureファイルドロップ時
function textureDrop(evt){
// ドロップしたファイル情報
var files = evt.dataTransfer.files;
texture_load(files);
evt.preventDefault();
drop_texture.style.backgroundColor = "#efefef"; // 元の色
}
// textureファイル選択時
function TextureSelect(evt) {
// 選択したファイル情報
var files = evt.target.files;
texture_load(files);
}
// textureファイルロード処理
function texture_load(files){
// 配列をクリアしておく
binarytextures = [];
// 前回のサムネイルを削除する
var output = document.getElementById('texture_list');
while(output.firstChild){
output.removeChild(output.firstChild);
}
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
// 非同期処理のため、ファイル番号を保持しておく
reader.filenm = i;
reader.onload = (function(theFile) {
return function(e) {
// ロードしたテクスチャのサムネイル表示する
var span = document.createElement('span');
span.innerHTML = ['<img class="thumb" src="', e.target.result,
'" title="', escape(theFile.name), '"/>'].join('');
document.getElementById('texture_list').insertBefore(span, null);
// 変数にBase64エンコード形式で保持
binarytextures[e.target.filenm] = e.target.result;
};
})(f);
// 画像はこちらをつかう
reader.readAsDataURL(f);
}
}
/******************** motionファイル ********************/
var binarymotion = null;
// motionファイルのファイル選択ボタン(非表示でダミーボタンで装飾)
var select_motion = document.getElementById('select_motion');
select_motion.addEventListener('change', motionSelect, false);
// motionファイル選択名
var dummy_motion_file = document.getElementById('dummy_motion_file');
// motionファイルのドロップ領域
var drop_motion = document.getElementById('drop_motion');
drop_motion.addEventListener('drop', motionDrop);
drop_motion.addEventListener('dragover', motionDragOver);
drop_motion.addEventListener('dragleave', motionDragLeave);
// dummy用のmotionファイル選択ボタンからイベント呼び出す
function dummy_motion_click(){
select_motion.click();
}
// motionファイルドラッグオーバー時
function motionDragOver(event){
drop_motion.style.backgroundColor = "#C1C1C1"; // hover色
event.preventDefault();
}
// motionファイルドラッグリーブ時
function motionDragLeave(event){
drop_motion.style.backgroundColor = "#efefef"; // 元の色
event.preventDefault();
}
// motionファイルドロップ時
function motionDrop(evt){
// ドロップしたファイル情報
var files = evt.dataTransfer.files;
motion_load(files);
evt.preventDefault();
drop_motion.style.backgroundColor = "#efefef"; // 元の色
}
// motionファイル選択時
function motionSelect(evt) {
// 選択したファイル情報
var files = evt.target.files;
motion_load(files);
}
// motionファイルロード処理
function motion_load(files){
// 初期化処理
binarymotion = null;
dummy_motion_file.value = "";
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
// 選択したファイル名をセット
dummy_motion_file.value = files[i].name;
reader.onload = (function(theFile) {
return function(e) {
// motion中身をセット(Base64形式)
binarymotion = e.target.result;
Simple();
};
})(f);
// arrayBuffer形式
reader.readAsArrayBuffer(f);
}
}
// JavaScriptで発生したエラーを取得
window.onerror = function(msg, url, line, col, error) {
var errmsg = "file:" + url + "<br>line:" + line + " " + msg;
Simple.myerror(errmsg);
}
var Simple = function() {
/*
* Live2Dモデルのインスタンス
*/
this.live2DModel = null;
/*
* アニメーションを停止するためのID
*/
this.requestID = null;
/*
* モデルのロードが完了したら true
*/
this.loadLive2DCompleted = false;
/*
* モデルの初期化が完了したら true
*/
this.initLive2DCompleted = false;
/*
* WebGL Image型オブジェクトの配列
*/
this.loadedImages = [];
/*
* Live2D モデル設定。
*/
// this.modelDef = {
//
// "type":"Live2D Model Setting",
// "name":"haru",
// "model":"assets/haru/haru.moc",
// "textures":[
// "assets/haru/haru.1024/texture_00.png",
// "assets/haru/haru.1024/texture_01.png",
// "assets/haru/haru.1024/texture_02.png"
// ]
// };
this.motion = null; // モーション
this.motionMgr = null; // モーションマネジャー
// Live2Dの初期化
Live2D.init();
// canvasオブジェクトを取得
var canvas = document.getElementById("glcanvas");
// コンテキストを失ったとき
canvas.addEventListener("webglcontextlost", function(e) {
Simple.myerror("context lost");
loadLive2DCompleted = false;
initLive2DCompleted = false;
var cancelAnimationFrame =
window.cancelAnimationFrame ||
window.mozCancelAnimationFrame;
cancelAnimationFrame(requestID); //アニメーションを停止
e.preventDefault();
}, false);
// コンテキストが復元されたとき
canvas.addEventListener("webglcontextrestored" , function(e){
Simple.myerror("webglcontext restored");
Simple.initLoop(canvas);
}, false);
// Init and start Loop
Simple.initLoop(canvas);
};
/*
* WebGLコンテキストを取得・初期化。
* Live2Dの初期化、描画ループを開始。
*/
Simple.initLoop = function(canvas/*HTML5 canvasオブジェクト*/)
{
//------------ WebGLの初期化 ------------
// WebGLのコンテキストを取得する
var para = {
premultipliedAlpha : true,
// alpha : false
};
var gl = Simple.getWebGLContext(canvas, para);
if (!gl) {
Simple.myerror("Failed to create WebGL context.");
return;
}
// 描画エリアを白でクリア
gl.clearColor( 0.0 , 0.0 , 0.0 , 0.0 );
//------------ Live2Dの初期化 ------------
// mocファイルからLive2Dモデルのインスタンスを生成
// Simple.loadBytes(modelDef.model, function(buf){
// live2DModel = Live2DModelWebGL.loadModel(buf);
// });
live2DModel = Live2DModelWebGL.loadModel(binarymoc);
// テクスチャの読み込み
var loadCount = 0;
// for(var i = 0; i < modelDef.textures.length; i++){
for(var i = 0; i < binarytextures.length; i++){
(function ( tno ){// 即時関数で i の値を tno に固定する(onerror用)
loadedImages[tno] = new Image();
// loadedImages[tno].src = modelDef.textures[tno];
loadedImages[tno].src = binarytextures[i];
loadedImages[tno].onload = function(){
// if((++loadCount) == modelDef.textures.length) {
if((++loadCount) == binarytextures.length) {
loadLive2DCompleted = true;//全て読み終わった
}
}
// loadedImages[tno].onerror = function() {
// Simple.myerror("Failed to load image : " + modelDef.textures[tno]);
// }
})( i );
}
// モーションのロード
motion = new Live2DMotion.loadMotion(binarymotion);
// モーションマネージャのインスタンス化
motionMgr = new L2DMotionManager();
//------------ 描画ループ ------------
(function tick() {
Simple.draw(gl); // 1回分描画
var requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
requestID = requestAnimationFrame( tick , canvas );// 一定時間後に自身を呼び出す
})();
};
Simple.draw = function(gl/*WebGLコンテキスト*/)
{
// Canvasをクリアする
gl.clear(gl.COLOR_BUFFER_BIT);
// Live2D初期化
if( ! live2DModel || ! loadLive2DCompleted )
return; //ロードが完了していないので何もしないで返る
// ロード完了後に初回のみ初期化する
if( ! initLive2DCompleted ){
initLive2DCompleted = true;
// 画像からWebGLテクスチャを生成し、モデルに登録
for( var i = 0; i < loadedImages.length; i++ ){
//Image型オブジェクトからテクスチャを生成
var texName = Simple.createTexture(gl, loadedImages[i]);
live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
}
// テクスチャの元画像の参照をクリア
loadedImages = null;
// OpenGLのコンテキストをセット
live2DModel.setGL(gl);
// 表示位置を指定するための行列を定義する
var s = 2.0 / live2DModel.getCanvasWidth(); //canvasの横幅を-1..1区間に収める
var matrix4x4 = [ s,0,0,0 , 0,-s,0,0 , 0,0,1,0 , -1.0,1,0,1 ];
live2DModel.setMatrix(matrix4x4);
}
// キャラクターのパラメータを適当に更新
// var t = UtSystem.getTimeMSec() * 0.001 * 2 * Math.PI; //1秒ごとに2π(1周期)増える
// var cycle = 3.0; //パラメータが一周する時間(秒)
// PARAM_ANGLE_Xのパラメータが[cycle]秒ごとに-30から30まで変化する
// live2DModel.setParamFloat("PARAM_ANGLE_X", 30 * Math.sin(t/cycle));
// モーションが終了していたらモーションの再生
if(motionMgr.isFinished()){
motionMgr.startMotion(motion);
}
motionMgr.updateParam(live2DModel);
// Live2Dモデルを更新して描画
live2DModel.update(); // 現在のパラメータに合わせて頂点等を計算
live2DModel.draw(); // 描画
};
/*
* WebGLのコンテキストを取得する
*/
Simple.getWebGLContext = function(canvas/*HTML5 canvasオブジェクト*/)
{
var NAMES = [ "webgl" , "experimental-webgl" , "webkit-3d" , "moz-webgl"];
var param = {
alpha : true,
premultipliedAlpha : true
};
for( var i = 0; i < NAMES.length; i++ ){
try{
var ctx = canvas.getContext( NAMES[i], param );
if( ctx ) return ctx;
}
catch(e){}
}
return null;
};
/*
* Image型オブジェクトからテクスチャを生成
*/
Simple.createTexture = function(gl/*WebGLコンテキスト*/, image/*WebGL Image*/)
{
var texture = gl.createTexture(); //テクスチャオブジェクトを作成する
if ( !texture ){
mylog("Failed to generate gl texture name.");
return -1;
}
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); //imageを上下反転
gl.activeTexture( gl.TEXTURE0 );
gl.bindTexture( gl.TEXTURE_2D , texture );
gl.texImage2D( gl.TEXTURE_2D , 0 , gl.RGBA , gl.RGBA , gl.UNSIGNED_BYTE , image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture( gl.TEXTURE_2D , null );
return texture;
};
/*
* ファイルをバイト配列としてロードする
*/
Simple.loadBytes = function(path , callback)
{
var request = new XMLHttpRequest();
request.open("GET", path , true);
request.responseType = "arraybuffer";
request.onload = function(){
switch( request.status ){
case 200:
callback( request.response );
break;
default:
Simple.myerror( "Failed to load (" + request.status + ") : " + path );
break;
}
}
request.send(null);
};
/*
* 画面ログを出力
*/
Simple.mylog = function(msg/*string*/)
{
var myconsole = document.getElementById("myconsole");
myconsole.innerHTML = myconsole.innerHTML + "<br>" + msg;
console.log(msg);
};
/*
* 画面エラーを出力
*/
Simple.myerror = function(msg/*string*/)
{
console.error(msg);
Simple.mylog( "<span style='color:red'>" + msg + "</span>");
};
課題
テクスチャのロードは、0番から正しく順番に読み込まれないと表示が崩れます。
あと、Live2DViewerだとmocやmtnファイルパスを書いたmodel.json1つロードするだけでモデルが表示できます。
HTML5だとセキュリティ上、ファイルを直接選択する必要があるのでmodel.jsonからロードはできない…。
(結局はphpでファイルアップロードさせる方がいいかも)
ただ、Live2DとWebGLで作ったポストエフェクトなどはユーザが作ったモデルで試せるサイトが作れるので、何か面白いもの作ったらこれで公開していこうと思います。