three.jsはjavascriptで簡単に3D表現を行うためのライブラリです。
今回はこれを使って、立方体を転がすアニメーションを作っていきます。
※説明の都合上GIFアニメを多用するためご注意ください。
作成したもの
はじめに
使用するライブラリ
- three.js(r86)
-
OrbitControls.js(three.jsのプラグイン)
- CDNには上がってなかったのでGistから直接参照
- es6-tween(v3.6.0)
1. 初期設定
以下の記事を参考にThree.jsの初期設定を行います。
【Qiita】three.jsで3Dを作りたい!
以降は、今回追加する設定です。
グリッド配置
cubeのサイズに合わせてグリッド(床部分)を表示します。
var cube_size = 1; // cubeのサイズ
var grid_count = 10; // グリッドの分割数
var grid_size = grid_count * cube_size;
var grid = new THREE.GridHelper(grid_size, grid_count);
grid.material.color = new THREE.Color(0xaaaaaa);
scene.add(grid);
面ごとに色を変える
回転が分かるよう、立方体の対面ごとに違う色をつけます。
var geometry = new THREE.BoxGeometry(cube_size, cube_size, cube_size);
// 6面分のmaterialを設定
var materials = [
new THREE.MeshLambertMaterial({color: 0xE9546B}), // right
new THREE.MeshLambertMaterial({color: 0xE9546B}), // left
new THREE.MeshLambertMaterial({color: 0x00A95F}), // front
new THREE.MeshLambertMaterial({color: 0x00A95F}), // back
new THREE.MeshLambertMaterial({color: 0x187FC4}), // top
new THREE.MeshLambertMaterial({color: 0x187FC4}), // bottom
];
var cube = new THREE.Mesh( geometry, materials );
cubes.push(cube);
軸線を表示
回転や移動方向を明確にするため、cubeとgridに軸線を表示します。
// X軸: red, Y軸: green, Z軸: blue
var line_size = cube_size;
var add_line = (obj, end_pos, color) => {
var start_pos = new THREE.Vector3(0, 0, 0);
var g = new THREE.Geometry();
g.vertices.push(start_pos);
g.vertices.push(end_pos);
var material = new THREE.LineBasicMaterial({linewidth: 4, color: color});
var line = new THREE.Line(g, material);
obj.add(line);
}
// gridに表示
var grid_half = grid_size / 2;
add_line(grid, new THREE.Vector3( grid_half, 0, 0 ), "#ff0000");
add_line(grid, new THREE.Vector3( 0, grid_half, 0 ), "#00ff00");
add_line(grid, new THREE.Vector3( 0, 0, grid_half ), "#0000ff");
// cubeに表示
add_line(cube, new THREE.Vector3( line_size, 0, 0 ), "#ff0000");
add_line(cube, new THREE.Vector3( 0, line_size, 0 ), "#00ff00");
add_line(cube, new THREE.Vector3( 0, 0, line_size ), "#0000ff");
TWEEN.autoPlay(true)
es6-tweenのアニメーションを有効にするためTWEEN.autoPlay(true)
を実行します。
// render() の後に以下を実行
TWEEN.autoPlay(true);
ここまでの成果物
2. tweenを使った単純な移動と回転
まずは、tweenを使用して単純な横移動と回転を行ってみます。
横(x軸)方向の移動
// TWEEN.autoPlayの後に以下を実行
var origin_pos = cube.position.clone();
var move_axis = 'x'; // 移動方向
var move_offset = cube_size; // 移動距離
var from_param = {x: 0}; // tween開始時の値
var to_param = {x: 1}; // tween終了時の値
var duration = 1000; // 単位はミリ秒
var tween = new TWEEN.Tween(from_param)
.to(to_param, duration)
.easing(TWEEN.Easing.Linear)
.on('update', ({x}) => {
// xには0~1までの値がカウントアップしながら渡されてくる
cube.position[move_axis] = origin_pos[move_axis] + (x * cube_size * move_offset);
})
tween.start();
Quaternionによる回転
three.jsでは回転の制御に「Rotation」と「Quaternion」が使用できます。
Rotationの場合、三軸(x, y, z)全てについて回転を加えた時に予期せぬ動きとなるため、
今回はQuaternionを使用します。
【参考】【THREE.jsの基礎】Quaternionでローカル座標の回転を取り扱う
var origin_pos = cube.position.clone();
// tween.on('update')内に以下を追記
var axis = new THREE.Vector3(0, 0, -1); // 回転軸
var rad90 = Math.PI / 2; // ラジアン90°
var rad = rad90 * x;
var new_q = origin_quaternion.clone()
var target = new THREE.Quaternion();
// 指定した軸に対して回転を加える
target.setFromAxisAngle(axis, rad);
new_q.multiply(target);
cube.quaternion.copy(new_q)
補足
three.jsでオブジェクトを回転させる場合、
**回転軸を奥に向けて時計回りが「プラスの回転」**となります。
このため、例えば次の2つの実行結果は同じです。
// z軸の負の方向に向かって時計回りに回転
var axis = new THREE.Vector3(0, 0, -1)
var rad = rad90 * x;
// z軸の正の方向に向かって半時計回りに回転
var axis = new THREE.Vector3(0, 0, 1)
var rad = -rad90 * x;
ここまでの成果物
3. もっと自然な動きにしたい
一応動いているように見えますが、床から下にはみ出しているなど少し不自然です。
これを修正するため、まずは理想の動作を考えます。
理想の動作
面の上で立方体を綺麗に転がした場合、
ある頂点を基準に回転するような動きになるはずです。
これを見ると、cubeの中心位置は円弧を描くように移動することが分かります。
しかし、現状はオブジェクトの高さを変更していないため、
そこに理想との乖離がある状態だと想定できます。
では、この問題を解決していきましょう。
高さの算出
三角関数を使い、オブジェクトが移動する時の高さを求めていきます。
そんなの覚えてない!という方はこちらを見て一緒に思い出しましょう。(全く覚えてなかった)
【参考】進研ゼミ高校講座 sin,cos,tanの値の覚え方
まず、角度θにおける高さをyとした時、sinθの定義から以下の式を算出できます。
移動距離の算出
高さと同様、実は横方向の移動距離もズレてしまっています。
これは角度θが1°増加した時、横方向の移動距離が一定ではないことが原因です。
θの変化に合わせて適切に移動させるため、移動距離をxと置き
cosθの定義を使って以下を算出しましょう。
移動動作を実装する
上記で求めた式を元に、移動の動きを実装してみたものがこちらです。
.on('update', ({x}) => {
var r = cube_half * Math.sqrt(2); // 中心の回転半径
var center_angle = 45 + (90 * x); // 現在の中心位置の角度(移動開始時は45°)
var center_rad = center_angle * Math.PI / 180; // 角度をラジアンに変換
// 移動中のcube中心の高さ (sinθ * r)
// cubeサイズの半分を引くことで、移動開始時点からの増加分だけを取得する
var current_height = Math.sin(center_rad) * r - cube_half;
// 移動中のcube中心の位置 (α - (cosθ * r))
// 移動方向の逆転を考慮してmove_offset(1 or -1)を掛け合わせる
var current_move = (cube_half - (Math.cos(center_rad) * r)) * move_offset;
// 移動前のpositionを基準に計算
cube.position[move_axis] = origin_pos[move_axis] + current_move;
cube.position.y = origin_pos.y + current_height;
var axis = new THREE.Vector3(0, 0, -1); // 回転軸
var new_q = origin_quaternion.clone()
var target = new THREE.Quaternion();
var rad90 = Math.PI / 2; // ラジアン90°
var rad = rad90 * x;
target.setFromAxisAngle(axis, rad);
new_q.multiply(target);
cube.quaternion.copy(new_q)
})
ここまでの成果物
4. 連続動作させるための調整
これまでに、1方向に回転し続けることができるようになりました。
以降はもっと色んな方向の回転に挑戦してみます。
x軸で回転した後、z軸で回転させたい
例えば一度右に移動し、次は手前に転がしたいという場合、単純に考えると
- x方向に移動する時はz軸で回転
- z方向に移動する時はx軸で回転
と書いておけば良さそうです。
まずはこれを実装してみましょう。
// move_axisとrot_axisの定義を変更
var move_axis = Math.random() > 0.5 ? 'x' : 'z'; // 移動方向を50%の確率で切替
var rot_axis = move_axis == 'x' ? 'z' : 'x'; // 移動方向に応じて回転軸設定
しかし、これだけでは以下のような結果になります。
このことから、cubeオブジェクトに回転を加えると、「空間全体の軸(world軸)」ではなく
「cubeオブジェクトのx軸(local軸)」を使った回転が行われるという事が分かりました。
ローカル回転軸の計算
今回は、解決方法として「cubeオブジェクトの中心から空間全体のx方向に1移動したworld座標」を
「cubeオブジェクトのlocal座標に変換する」という方法を使います。
(もっと素直な方法があれば教えてください。)
// 移動方向に対する回転軸を求める
var normal_unit = 1;
var rot_axis_v = new THREE.Vector3();
rot_axis_v[rot_axis] = normal_unit;
var nextr = origin_pos.clone().add(rot_axis_v); // Cubeの中心から、求めたい方向に1移動した座標を算出
// world座標をlocalの座標に変換
// ※ worldToLocalが正常に動かない(?)事があるため、normalizeは必須
var rot_vec = cube.worldToLocal(nextr.clone()).normalize();
// rot_vecからlocal軸(x, y, z)を取得
// 本来なら(1, 0, 0)や(0, 0, -1)のような値が出てくる想定だが、
// 何度も回転させるとなぜか(1, 0.49999999, 0)のような値になるので
// 絶対値がnormal_unitと同じ方向を正として扱う
var local_rot_axis;
if(Math.abs(rot_vec.x) == normal_unit) local_rot_axis = 'x';
if(Math.abs(rot_vec.y) == normal_unit) local_rot_axis = 'y';
if(Math.abs(rot_vec.z) == normal_unit) local_rot_axis = 'z';
var axis= new THREE.Vector3();
axis[local_rot_axis] = normal_unit;
// .on('update')の中にある「var axis」の定義は削除する
上記ではworldToLocal
を使っていますが、何度も回転させていくと
[1, 0, 0]
や [0, 0, -1]
のような綺麗な値ではなく
[1, 0.49999999, 0]
という値になってしまいます。
(使い方が悪いのか、単純に不具合なのかは不明です)
そのため、絶対値が1になっている方向が正しいとして計算を進めると、
一応上手く状態になりました。
回転方向を計算する
回転軸を正常に取るとこのような動きになりました。
逆回転してしまっている部分もありますが、ここまで来ればあと少し。
状況を整理すると、以下の動きになっているようです。
- x方向に移動する際、プラス移動(→)の時は正常で、マイナス移動(←)するときに逆回転になっている
- z方向に移動する際、プラス移動(↓)する時が逆回転で、マイナス移動(↑)する時は正常に動いている
移動方向によって逆回転するタイミングが異なっていますが、
これは回転軸の関係性による事象です。
これに対処するため、以下の処理を加えます。
// 回転軸の向きと移動方向から、回転するべき方向(時計回り, 半時計回り)を判定
var axis_dir = rot_vec[local_rot_axis];
var rot_dir = 1;
if((move_axis == 'x' &&
((axis_dir > 0 && move_offset > 0) ||
(axis_dir < 0 && move_offset < 0))
) ||
(move_axis == 'z' &&
((axis_dir > 0 && move_offset < 0) ||
(axis_dir < 0 && move_offset > 0))
)
) {
rot_dir = -rot_dir;
}
// .on('update')を変更
// radを求める時にrot_dirを掛け合わせる
var rad = rad90 * x * rot_dir;
ここまでの成果物
やっとまともに動くようになりましたね!
5. おまけ
この後cubeを複数個配置したり、端まで転がった時に
Gridからはみ出さないよう設定したりしたものが冒頭のURLの実装です。
アニメーションをループさせるのにsetIntervalを使用した所、
Chromeで別タブを開いて戻ってきた時に動作がおかしくなる不具合が発生しています。
恐らくtween.repeatを使うべきなのかなぁと思いますが、
今は修正する元気がないのでこのままにしておきます。
さいごに
3D空間をいじるのは非常に楽しいです。
でも一度詰まると対応方法が分からず
数日返ってこれなくなることがあるので注意が必要です。
余談
この記事を書くにあたり、Microsoft Office365 Soloを契約してPowerPoint2016で製図してみました。
図形の合成で半円を作れたり、標準図形の「円弧」が汎用的だったりと最高に便利だったので、
皆さんもぜひ使ってみてください。
それでは。