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上で名付けたやつだね。

blenderのAction

このモーションオブジェクトには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で出力したものを変換します。

COLLADAで出力 テクスチャも中に含めちゃおう

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

Collada 2 glTF

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は非常に便利なんだけど、こいつで作ったデータは頂点数が無駄に多くなっちゃうのだ。

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

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

普通にOBJ形式でエクスポートしたデータ。頂点数は2,605

最適化方針!

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

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

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

PLY形式でエクスポート

Blenderにインポート。

Blenderにインポート

重複した頂点を削除する

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

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

UV展開

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

Smart UV Projectでスマァ~トにUV展開 Smart UV Projectでスマァ~トにUV展開

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

Screen Layoutを「UV Editing」に。

Screen Layoutを「UV Editing」に

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

「New」のところを押して新規にテクスチャを作成

Screen Layoutを「default」に戻す。

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

「Vertex Colors」をBake

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

焼きこまれた

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

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

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

Remove Vertex Color

不要な頂点を削減

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

Decimateモディファイアを追加

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

Planer

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

出来た!

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

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

顔にらくがきしてみたり 顔にらくがきしてみたり

まとめ

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

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

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

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

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

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

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

概要

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

どんなプレイを?

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

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

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

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

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

トワ様サーバーの日々

- - - - - - - - -

MagicaVoxel0.97.3のアップデート内容

2016年6月5日にバージョン0.97.3へのアップデートが行われていたようです。

https://ephtracy.github.io/

主にレンダリング関係の更新のようです。

1. New Metal/Plastic Material with GGX NDF

従来からあった金属っぽい質感にくわえ、プラスチックっぽいレンダリングが出来るようになりました。

金属っぽく

ss1.png

プラスチックっぽく

ss2.png

2. Post Effect

レンダリング画像にポストエフェクトをかけられるようになりました。

Exposure

暗いすぎる画像を明るく。

ss3.png

Vignette

トイカメラっぽいアレ。

ss4.png

Gamma Correction

ガンマ値を設定可能に。

ss5.png ss6.png

3. Pick up Sun/Sky Color from Palette

SUNライトとSKYライトに対し、光源色を設定可能になりました。

ss7.png ss8.png

4. Better Quality of Soft Shadow

ソフトシャドウがよりクオリティアップしたようです。

5. Sample->GI : on the top right, enable stochastic sampling for better indirect illumination

グローバルイルミネーションを使えるようになりました。

…らしいんですが機能してるんかなコレ?

ss9.png ss10.png