phina.jsでLive2Dぬるぬるふわっふわーする

phina.js Advent Calendar 2017の17日目です。

Live2DをWebゲームで使いたいんだよ! アニキ!

時代はLive2Dだ。そうだろ?

なので時代の最先端たるphina.jsでもLive2Dをサポートしていくぜ。

表示したぜ!

というわけで作ったよ。

どうやって使うんだよ!?

材料はこちら。

上記スクリプトはscriptタグで読み込んでおく。

シンプルに表示する

Live2D Cubism3のデータは、「モデルデータ」「テクスチャ」「モーションデータ」等で構成される。

まずはモデルデータ(*.moc3)とテクスチャ(*.png)を読み込もう。

1
2
3
4
5
6
7
8
9
10
11
12
GameApp({
// 中略
assets: {
"live2d.moc": { // モデルデータの読み込み
"MarkModel": "assets/Mark/Mark.moc3",
},
"image": { // テクスチャの読み込み
"MarkTexture": "assets/Mark/Mark.png",
},
},
// 中略
});

続いてLive2Dを表示するための専用レイヤ「phina.live2d.Live2DLayer」をシーンに追加する。

このLive2DLayerは、Live2Dデータを描画するためにWebGLをセットアップしたりしなかったりいろいろしてる。

Live2Dデータはこのレイヤ以外では表示できないので注意。

1
2
3
4
5
6
7
var live2dLayer = phina.live2d.Live2DLayer({
width: 480,
height: 640,
originX: 0,
originY: 0,
});
live2dLayer.addChildTo(this);

最後に「phina.live2d.Live2DSprite」を次のように作成してレイヤに追加する。

1
2
3
4
5
6
var mark = phina.live2d.Live2dSprite({
moc: "MarkModel",
textures: ["MarkTexture"],
});
mark.setPosition(240, 320);
mark.addChildTo(live2dLayer);

これで一応表示できる。

パラメータを操作

Cubism Editor上で作成する「パラメータ」をスクリプトから操作してみる。

 

まずは操作したいパラメータのIDを調べておく。

今回は顔の向き(角度 X)と目の向き(目玉 X)を操作してみたい。

IDはパラメータを右クリックして「パラメータ編集」を開くと参照可能。

 

 

スクリプト側では次のように、Live2DSpriteのparametersというプロパティを介して値を制御出来る。

1
2
mark.parameters["ParamAngleX"] = 30.0;
mark.parameters["ParamEyeBallX"] = 1.0;

また、parametersはphina.app.Elementを継承しているので、Tweenerを使ったイージング処理も可能だ。

1
2
3
4
5
6
7
8
9
10
mark.parameters.tweener
.to({
"ParamAngleX": -30.0,
"ParamEyeBallX": -1,
}, 1000, "easeInOutQuad")
.to({
"ParamAngleX": 30.0,
"ParamEyeBallX": 1,
}, 1000, "easeInOutQuad")
.setLoop(true);

モーションの再生

モーションデータ(*.motion3.json)を扱う場合は、まず次のようにデータを読み込む。

1
2
3
4
5
6
7
8
9
GameApp({
// 中略
assets: {
"live2d.motion": { // モーションデータの読み込み
"MarkMotion": "assets/Mark/Mark.motion3.json",
},
},
// 中略
});

あとはLive2DSpriteのplayメソッドで簡単に再生可能。

1
mark.play("MarkMotion");

stopとかpauseとかresumeといったメソッドも一通り揃えたヨ。

苦労した点

Live2Dではスプライトの形を自由に変形させる必要があるので、WebGLを使って実装した。

わりとサクッとうまくいったのだけど、ちょっと苦労したところもあった。

クリッピング

 

Cubism Editor上で設定できる「クリッピング」を、今回はステンシル処理を使って再現した。

ステンシル処理ってやったことなかったのよね。なのでちょっと迷ったけどまあ最終的には出来てよかったよかった。

楽勝だった点

ぶっちゃけ公式から配布されているpixi.js用のライブラリがあったので、そいつを参考にしたから全体的に楽勝だった。

バイナリデータであるmoc3の読み込みは公式のライブラリに頼っているので苦労はなかった。

パーツの描画処理自体も単純に頂点の位置とUVを変換無しで処理するだけなのでカンタン。

今後どうするんだよ!

今回作ったのはあくまで仮実装って感じなので、紹介したインタフェースはコロコロ変わるかもしれない。

その辺注意して使って欲しい。

TODO

  • 物理
  • ユーザーデータ
  • クリッピングの完全再現

phina.js + three.jsでモーションコントローラー作った

JavaScriptで本格的な3Dゲームが作りたいんだ!アニキ!

そろそろJSでも3Dゲームの時代ってことで、俺が愛してやまないphina.jsとそれほどでもないthree.jsでゲームを作ってる。

モーションを再生したいんだぜ!

3Dでゲームつったらやっぱボーン入れてモーションつけて動かしたいわけです。

three.jsにはblenderからデータを読み込む仕組みがあるので、これを利用してちょっとしたモーションコントローラーを作ってみた。

Unityのやつを参考にしたのでわりと似ている。

上記のサンプルはblenderで作成したモーションを読み込んで動かしてる。

使い方はこんな感じで。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 敵キャラをロードしてシーンに配置
let enemy = SkinnedMesh("enemy3").addChildTo(this);
// アイドルモーション
let motionIdle = enemy.getMotion("idle");
// アイドルモーションはループON
motionIdle.setLoop(true);
// 攻撃モーション
let motionAttack = enemy.getMotion("attack0");
// アイドルモーションにトランジションを追加。
// タップフラグが立ったら攻撃モーションへ移行する。
motionIdle.addTransition(() => tapFlag, motionAttack, 10);
// 攻撃モーションにトランジションを追加。
// モーションが「完了」したらアイドルモーションへ移行する。
motionAttack.addTransition("finished", motionIdle, 10);
// 敵キャラのモーションをアイドルモーションにスイッチ。
enemy.motionController.switchMotion(motionIdle);

まず、SkinnedMeshというクラスを用意した。

これはthree.jsのBlendCharacterというクラスを単純にラップしたものだ。phina.js のシーングラフに追加できるようにphina.app.Elementを継承している。

さらにMotionControllerというクラスを実装した。こちらはSkinnedMeshに付与できるアクセサリだ。

SkinnedMeshオブジェクトからはgetMotionメソッドでモーションオブジェクトを取り出すことが出来る。

1
2
// アイドルモーション
let motionIdle = enemy.getMotion("idle");
1
2
// 攻撃モーション
let motionAttack = enemy.getMotion("attack0");

指定しているモーション名はblender上で名付けたやつだね。

このモーションオブジェクトにはaddTransitionというメソッドが生えていて、他のモーションへの移行を条件とともに登録することができる。

1
2
3
// アイドルモーションにトランジションを追加。
// タップフラグが立ったら攻撃モーションへ移行する。
motionIdle.addTransition(() => tapFlag, motionAttack, 10);
1
2
3
// 攻撃モーションにトランジションを追加。
// モーションが「完了」したらアイドルモーションへ移行する。
motionAttack.addTransition("finished", motionIdle, 10);

この条件の監視やモーションの移行を司っているのがMotionControllerというわけ。

目玉としてはこのモーションのトランジションにはdurationを指定できて、モーションとモーションの間をなめらかにつなぐことが出来る。この機能には当然、みんな大好きphina.jsのTweenerを使っている。

今後どうすんだよ!

とりあえず10月くらいまでにゲームを完成させたい。

んで、出来た機能はまとめてphina.js用のプラグインとしてリリースしたいと思っている。

ソースとか

サンプルのソースはこちら

three.jsのCubeTexture 画像の指定順

覚え書き。

X軸プラス方向を東と見なし、

  1. 西

の順に指定する。

1
2
3
4
5
6
7
8
9
let skyboxTexture = new THREE.CubeTexture([
"DaylightBox_Front",
"DaylightBox_Back",
"DaylightBox_Top",
"DaylightBox_Bottom",
"DaylightBox_Left",
"DaylightBox_Right",
].map(name => AssetManager.get("image", name).domElement));
skyboxTexture.needsUpdate = true;
1
2
3
4
5
6
7
8
9
let skyboxTexture = new THREE.CubeTexture([
"clouds1_east",
"clouds1_west",
"clouds1_up",
"clouds1_down",
"clouds1_north",
"clouds1_south",
].map(name => AssetManager.get("image", name).domElement));
skyboxTexture.needsUpdate = true;

phina.jsとWebGLでいろいろ

phina.js Advent Calendar 2016の18日目です。大遅刻!

今回のアレ

うごくやつはこちら→http://github.dev7.jp/phinajsadvcal20161218/

WebGLでいろいろしたいぜ!

去年の記事で紹介したように、phina.jsはレイヤーを使ってWebGLの描画結果を自分のシーングラフに追加することが出来る。

このしくみを使っていろいろうまいことをやってしまおうというのが今回の趣旨。

WebGLといえば3DCGなわけだが、それだけではない。GPUあまりある計算能力を2Dゲームへ応用すれば色々と面白いことを実現することが出来る。

インスタンシングを使って大量に表示するぜ!

WebGLにはインスタンシングという拡張機能があって、それを使えば同じ種類のオブジェクトを大量かつ高速に描画することが出来る。

そんなわけなので、四角いポリゴン板に画像を貼ったメッシュをたくさん描けば超弾幕が可能なのだ。

ポストプロセッシングでいろいろフィルターをかけるぜ!

一旦描画した結果をテクスチャとして取り込み、シェーダーを使ってさらにいろいろと効果をかけることもできる。

どうやってやるんだよ?

以前からちょっとずつ作っている俺専用WebGLライブラリであるphigl.jsというのがあって、今回はそれを使ってみた。

phigl.jsはphina.jsと一緒に使うことを前提とした薄~いWebGLライブラリ。

高度な機能はないけど、ちょっとしたことをやりたい時は便利かもしれない。

phigl.jsに現在備わっている機能は次のような感じ。

  • シェーダーソースをAssetManagerで読み込む機能
  • attribute変数やuniform変数をサクッ扱うための機能
  • オフスクリーンレンダリングのためのバッファをカンタンに用意する機能
  • インスタンシングを扱う機能
  • VAOを扱う機能
  • テクスチャを扱う機能

今回作ったもの

うごくやつ

github (download-zip)

phigl.js - phina.jsでWebGLを使うための俺専用スーパー最強ライブラリ

GLBoostで俺のキャラを動かす

GLBoost Advent Calendar 2016の4日目です。

Blenderで作ったモーション付きのモデルをGLBoostで動かすよ!

動作サンプル

GLBoostではglTFっつー形式のデータが読み込めるらしいです。

Blenderからは直接glTFのデータを出力することが出来ない(一応プラグインはあるみたいですが、なんか動かん…)ので、一度COLLADAで出力したものを変換します。

出力したデータ(*.dae)はオンラインの変換サービスでお手軽に変換しましょう。

Collada 2 glTF

作ったglTFデータを読み込むぜ

GLBoostのexampleフォルダからスキニングアニメーションのサンプルを探して適当に改造します。

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
var canvas = document.getElementById('canvas');
var context = new GLBoost.GLBoostMiddleContext(canvas);
var renderer = context.createRenderer({ clearColor: { red: 0.25, green: 0.25, blue: 0.25, alpha: 1 } });
var scene = context.createScene();
var light1 = context.createPointLight(new GLBoost.Vector3(1.25, 1.25, 1.25));
light1.translate = new GLBoost.Vector3(10, 10, 10);
scene.addChild(light1);
var light2 = context.createPointLight(new GLBoost.Vector3(1.0, 1.0, 1.0));
light2.translate = new GLBoost.Vector3(-10, 10, -10);
scene.addChild(light2);
var camera = context.createPerspectiveCamera({
eye: new GLBoost.Vector3(5, 3, 5),
center: new GLBoost.Vector3(0.0, 2, 0.0),
up: new GLBoost.Vector3(0.0, 1.0, 0.0),
}, {
fovy: 45.0,
aspect: 1.0,
zNear: 0.1,
zFar: 1000,
});
scene.addChild(camera);
var expression = context.createExpressionAndRenderPasses(1);
expression.renderPasses[0].scene = scene;
expression.prepareToRender();
var render = function() {
scene.setCurrentAnimationValue('time', (Date.now() % 1300) / 1000);
renderer.clearCanvas();
renderer.draw(expression);
requestAnimationFrame(render);
};
render();
var glTFLoader = GLBoost.GLTFLoader.getInstance();
glTFLoader
.loadGLTF(context, 'kick.gltf', 1, null)
.then(function(group) {
group.scale = new GLBoost.Vector3(1, 1, 1);
scene.addChild(group);
expression.prepareToRender();
});

出来たー(/・ω・)/

今回作ったもの

動作サンプル

githubリポジトリ

phina.jsでbulletml.jsを使う!

phina.js Advent Calendar 2016の2日目です。

ネタがねえんだよ! アニキ!

ネタがない(時間もない)ので持ちネタでいきます。

BulletMLってなんだよ!?

BulletMLとは弾幕記述言語であります。くわしくはこちら

このBulletMLをJavaScriptで使えるようにしたのが拙作bulletml.jsってわけです。

phina.jsでBulletMLを使うぜ!

それではphina.jsでbulletml.jsを使ってみましょう。

main.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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
var SCREEN_WIDTH = 640;
var SCREEN_HEIGHT = 960;
phina.globalize();
phina.define("MainScene", {
superClass: "DisplayScene",
init: function() {
this.superInit();
this.fromJSON({
children: {
// 自機
player: {
className: "TriangleShape",
x: this.gridX.span(8),
y: this.gridY.span(14),
update: function(app) {
// ポインティングデバイスで操作するよー
var p = app.pointer;
if (p.getPointing()) {
this.position.add(p.deltaPosition.mul(1.7));
}
},
},
// 敵
enemy: {
className: "RectangleShape",
x: this.gridX.span(8),
y: this.gridY.span(4),
runner: null,
update: function() {
if (this.runner) {
this.runner.x = this.x;
this.runner.y = this.y;
this.runner.update();
}
},
},
},
});
// bulletmlエンジンに対する設定
var bulletConfig = {
// 敵が狙うターゲット
target: this.player,
// 弾が発射された時に呼ばれる関数
createNewBullet: function(runner) {
// シーンのfireBulletイベントを発火する
this.flare("fireBullet", { runner: runner });
}.bind(this),
};
// fireBulletイベントのハンドラ
this.on("fireBullet", function(e) {
// 弾オブジェクトを生成してシーンに追加
Bullet(e.runner).addChildTo(this);
});
// 攻撃パターンアセット
var xmlAsset = AssetManager.get("xml", "attackPattern");
// 敵のrunnerプロパティに攻撃パターンをセット
this.enemy.runner = bulletml.buildXML(xmlAsset.data).createRunner(bulletConfig);
},
});
// 敵弾クラス
phina.define("Bullet", {
superClass: "CircleShape",
init: function(runner) {
this.superInit({ radius: 10 });
this.runner = runner;
// 初期位置セット
this.x = runner.x;
this.y = runner.y;
},
update: function() {
this.runner.update();
this.x = this.runner.x;
this.y = this.runner.y;
if (this.x < 0 || SCREEN_WIDTH < this.x || this.y < 0 || SCREEN_HEIGHT < this.y) {
this.remove();
}
}
});
GameApp({
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
startLabel: "main",
// attackPattern.xmlをアセットとして読み込むよー
assets: {
xml: {
attackPattern: "attackPattern.xml",
},
},
}).run();

今回のポイント!!

XMLをアセットとして読み込む

BulletMLはXMLで記述されるので、今回はXMLファイルとして用意しています。

phina.jsにはXMLファイルをアセットとして読み込む機能があるので、それを利用しています。

1
2
3
4
5
assets: {
xml: {
attackPattern: "attackPattern.xml",
}
}

Runner

bulletml.jsにはRunnerという概念があります。

このオブジェクトが「敵」や「弾」の動きを制御します。

今回のサンプルでは敵のRunnerはアセットとして読み込んだxmlファイルを元に作成しセットしています。

1
2
// 敵のrunnerプロパティに攻撃パターンをセット
this.enemy.runner = bulletml.buildXML(xmlAsset.data).createRunner(bulletConfig);

また、各弾のRunnerはcreateNewBullet関数に引数として渡ってきますので、それを敵弾クラスにセットしています。

1
2
3
4
5
// 弾が発射された時に呼ばれる関数
createNewBullet: function(runner) {
// シーンのfireBulletイベントを発火する
this.flare("fireBullet", { runner: runner });
}.bind(this)
1
2
// 弾オブジェクトを生成してシーンに追加
Bullet(e.runner).addChildTo(this);

Runnerはupdateメソッドを実行するたびに内部値(x, y, direction)を変化させたり、新たな弾を生成したりします。

ですので、phina.jsのupdateメソッド内でRunnerのupdateメソッドを呼び出すことで弾幕パターンを進行していくわけですね。

まとめ

というわけで持ちネタでした。

本AdventCaldendarにはあと何回か寄稿しようと思っていますので、次はもうちょっとちゃんとしたネタを書きますね。

MagicaVoxelで作ったデータの頂点数を減らす

頂点数が多すぎるんだよ!アニキ!

MagicaVoxelは非常に便利なんだけど、こいつで作ったデータは頂点数が無駄に多くなっちゃうのだ。

凝った模様の入った複雑なデータを作っちゃうと、ゲームが重くなってコリャイカンって感じになる。

なんとかして最適化する方法はないだろうか。

最適化方針!

  1. 頂点カラー付きの形式でエクスポート
  2. Blenderで重複頂点削除・UV展開
  3. 頂点カラーをテクスチャに焼きこむ
  4. 頂点数を削減

頂点カラー付きの形式でエクスポート

MagicaVoxelからPLY形式でエクスポートする。PLY形式は頂点カラー情報の入ったフォーマット。

Blenderにインポート。

重複した頂点を削除する

「Remove Doubles」で重複した頂点を削除する。

UV展開

Smart UV Projectでスマァ~トにUV展開する。または必要に応じて手動でUV展開する。

頂点カラーをテクスチャに焼く

Screen Layoutを「UV Editing」に。

「New」のところを押して新規にテクスチャを作成。名前を「vc」とかにしとく。

Screen Layoutを「default」に戻す。

PropertiesウインドウのRenderタブ内のBakeパネルを開き、Bake Modeを「Vertex Colors」にした上でBakeボタンを押す。

テクスチャに頂点カラーが焼き込まれる。

モデルに新規にマテリアルをアタッチし、作成されたテクスチャを貼る。

テクスチャに色情報を取り込んだので、頂点カラーはもう必要ない。削除しよう。

モデルの頂点をすべて選択して「Remove Vertex Color」。

不要な頂点を削減

モデルにDecimateモディファイアを追加する。

Planerを選択し、Angle Limitを5°とかにしてApplyする。結果が変になったら調整。

あとは手動で余計な頂点やエッジを削除する。

出来た!

平面部分から余計な頂点が削除され、かつ元の色情報はテクスチャによって再現される。

あとはテクスチャを画像ファイルとして保存して編集したり、モデルにいい感じの関節を追加したりしていこう。

まとめ

以上のやり方で頂点数を最低限まで減らせるはずだ。

平面が多く、色がたくさん使われたボクセルモデルであれば効果が顕著に出ると思う。

ただ、お手軽にモデルデータを用意できるという理由でMagicaVoxelを採用しているはずなのに、こんなやり方だと手間がかかりすぎてしまうのも事実。

もっと楽にやる方法があればいいなあ。

Minecraftサーバーを運営している件

実は俺はMinecraftサーバーを運営している。

それなりに長いことやっているので、プレイヤー募集を兼ねて紹介したい。

概要

  • アドレスは mctowa.dev7.jp:25565
  • 通称トワ様サーバー
  • 稼働しているのは週末だけ(金曜21:00~月曜3:00)
  • 現在のバージョンは1.10.2
  • Modは一切なしのバニラ
  • 難易度Hard
  • 20歳未満の未成年は立ち入り非推奨
  • 今んとこ、定期的にログインしているのは4人程度
  • 破壊行為・他プレイヤーの殺傷行為を行うプレイヤーは予告なくBANする場合あり

どんなプレイを?

エンダードラゴン討伐済み。

近いうちにウィザー戦も行いたい。

また、初期リスポーン地点(通称トワ様王国)周辺の開発は一通り完了している。

現在は各プレイヤーがそれぞれ僻地で自分の国を開発中。

いずれは各拠点を鉄道などの交通網で結んでいきたい。

トワ様サーバーの日々