こんにちは。フロントエンドエンジニアになりたてのぼこです。
今回はThree.jsでモーフィングアニメーションを手軽に実装できる機能を見つけたので、それで遊びたいと思います。モーフィングアニメーションというのは、ある物体が別の物体に切り替わる際に、その過程を補間することでなめらかに変化させるようなアニメーションです。
コードの説明もしていますが、Three.jsのインストールや描画までの基本的な準備の説明は割愛させていただきますので、Three.jsを触ったことがある方向けになります。ご了承ください。
逆に、僕みたいに「Three.jsを触って簡単な立体の描画はできるけど、シェーダーとか意味わかんない、怖い(?)」といった方にはぜひ試していただきたい内容になってます。
目次
今回作るデモ
今回作るデモのCodePenを貼ります。画面内の2つのボタンを押すと、立体がそれぞれの形状になめらかに変化すると思います。
See the Pen
Three.js MorphTarget by Bokoko33 (@bokoko33)
on CodePen.
なにこれどうなっているの
ざっくりとした仕組み
このように立体の形状を変化させるアニメーションを作るには、立体の頂点を移動させて別の立体になるような配置に動かす必要があります(ここでいう「頂点」とは、中学校数学で習うような立方体の8つの角のことではなく、立方体を構成する細かいポリゴンの頂点を意味します)。
今回は「立方体↔︎球体」というアニメーションをさせるため、あらかじめ「立方体になるような頂点データ」と「球体になるような頂点データ」を持っておき、ボタンを押すごとにそれを切り替えるようなイメージになります。
また、ただパッパッと切り替えるだけではモーフィングにならないので、変化をなめらかにする必要があります。
本来はシェーダーを書かないとできない
立体の頂点一つひとつにアクセスしてそれぞれを同時に動かすには、シェーダーを書く必要があります。自分もまだまだ習熟しておりませんので、「シェーダーとは〜」といった概念の詳しい説明は割愛させていただきますが、簡単にいうと3DCGにおける陰影の処理などを行うプログラムのことです。
WebGLではGLSLというJavaScriptとは別の言語で書くので、なかなかハードルが高く苦手意識もあるかと思いますが、Three.jsならそのあたりをライブラリがやってくれたりします。
コードの説明
コード全文は冒頭に載せたCodePenでご確認いただけます。この章では、ポイントになる部分だけ抜き出して少し説明します。
注意事項
今回扱うThree.jsの機能は、執筆時点(2021年9月)では比較的新しいもののようで、古いバージョンでは動かない可能性が高いです。僕がやってみたところr131以上でないと動きませんでした。本記事が公開されて間もない場合は、お使いのThree.jsのバージョンにご注意ください。
メッシュの作成
以下がメッシュ作成の箇所になります。
<script> // ←コードのハイライトの関係でタグを入れてます
const createGeometry = () => {
const size = 150;
const segments = 32;
const geometry = new THREE.BoxBufferGeometry(
size,
size,
size,
segments,
segments,
segments
);
// 変形後の頂点座標を入れておく空の配列
geometry.morphAttributes.position = [];
// オリジナルのジオメトリ(Box)の頂点座標の配列
const positionAttribute = geometry.attributes.position;
// 変形後のジオメトリ(Sphere)の頂点座標の配列
const spherePositions = [];
// 頂点の数だけループを回す
for (let i = 0; i < positionAttribute.count; i++) {
// 立方体の頂点座標を取得
const x = positionAttribute.getX(i);
const y = positionAttribute.getY(i);
const z = positionAttribute.getZ(i);
// 頂点ベクトルを正規化(長さを同じに)して、球形の頂点にする
const vertex = new THREE.Vector3(x, y, z);
const spheredVertex = vertex.normalize().multiplyScalar(size);
spherePositions.push(spheredVertex.x, spheredVertex.y, spheredVertex.z);
}
// ジオメトリの変形先として、計算した座標を登録
geometry.morphAttributes.position[0] = new THREE.Float32BufferAttribute(
spherePositions,
3
);
return geometry;
};
const geometry = createGeometry();
const material = new THREE.MeshPhongMaterial({
color: 0x6b81bd,
flatShading: true,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
</script> // ←コードのハイライトの関係でタグを入れてます
ジオメトリの作成ですが、ただ作成するだけでなくいろいろと手を加えるので、関数にまとめています。
まず、基となる立方体のジオメトリを作成します。そして、ジオメトリに morphAttributes.position というプロパティを生やします。これに変形後(球体)の頂点座標を入れることで、モーフィングできるようになるみたいです。ということで、球体になるような頂点座標を生成する必要があります。以下の部分ですね。
<script> // ←コードのハイライトの関係でタグを入れてます
// オリジナルのジオメトリ(Box)の頂点座標の配列
const positionAttribute = geometry.attributes.position;
// 変形後のジオメトリ(Sphere)の頂点座標の配列
const spherePositions = [];
// 頂点の数だけループを回す
for (let i = 0; i < positionAttribute.count; i++) {
// 立方体の頂点座標を取得
const x = positionAttribute.getX(i);
const y = positionAttribute.getY(i);
const z = positionAttribute.getZ(i);
// 頂点ベクトルを正規化(長さを同じに)して、球形の頂点にする
const vertex = new THREE.Vector3(x, y, z);
const spheredVertex = vertex.normalize().multiplyScalar(size);
spherePositions.push(spheredVertex.x, spheredVertex.y, spheredVertex.z);
}
</script> // ←コードのハイライトの関係でタグを入れてます
立方体の頂点座標を取得し、立方体の中心から各頂点までの長さ(頂点ベクトルの長さ)が均一になるようにします。中心からの距離すべて等しい、つまりそれは球体を表すわけですね。normalize がベクトルの長さを1にするメソッドで、1のままでは小さすぎるので multiplyScalar で同じ数だけかけています。
次に、マテリアルの部分です。
const material = new THREE.MeshPhongMaterial({
color: 0x6b81bd,
flatShading: true,
});
特に変わったことはしていませんが、 flatShading: true として意図的にフラットシェーディングにしています。デフォルトではスムースシェーディングですが、今回の方法だと立方体のときの面がそのまま残るような見た目になってしまうんですよね。詳しい原理はよくわかっていませんが、この部分をコメントアウトして挙動の違いを見てみてください。
アニメーションする
以下がアニメーションの箇所になります。
const animationParam = {
value: 0,
};
const loop = () => {
mesh.morphTargetInfluences[0] = animationParam.value;
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(loop);
};
loop();
const buttonBox = document.querySelector('.js-button-box');
buttonBox.addEventListener('click', () => {
gsap.to(animationParam, {
value: 0,
duration: 0.4,
ease: 'Power2.out',
});
});
const buttonSphere = document.querySelector('.js-button-sphere');
buttonSphere.addEventListener('click', () => {
gsap.to(animationParam, {
value: 1,
duration: 0.4,
ease: 'Power2.out',
});
});
モーフィングアニメーション用に animationParam というオブジェクト変数を用意します。これは自前です。それをボタンのクリックよって発火するGSAPアニメーションで操作しています(GSAPについては過去に解説記事を書いてますので、知らないよという方はぜひそちらを見てみてください)。
そして、その値をなにやら mesh.morphTargetInfluences というプロパティに渡していますね。
morphTargetInfluences[0] となっているのは、今回設定した変形先の頂点座標が一種類(球体のみ)なので、配列の先頭がそれを表している、のだと思われます。
このプロパティに0〜1の値を入れることで、立体を変形させることができます。0のときに完全な立方体、1のときに完全な球体、その間の場合は立方体と球体の中間のような立体となります。
試しに、 morphTargetInfluences[0] = 0.5 としてみましょう。立方体が少し膨らんで、球体になりたがってそうな感じに見えますね。ここから1に近づけることでより球体に近く、0に近づけることで立方体に近い形になります。
このように、ある2つの形状の間を補間して変化させるのがモーフィングです。本来の実装では、シェーダーで線形補間の関数を使って行うことになるかと思いますが、そういった難しそうな部分をThree.jsがカバーしてくれちゃうわけですね。
ちょっと応用
せっかくなので、変形先の図形をもうひとつ追加してみます。「Grid」のボタンを押すと、立方体に穴が空いた格子ような図形に変形します。
See the Pen
Three.js MorphTarget 02 by Bokoko33 (@bokoko33)
on CodePen.
CodePenでソースコードを見ていただくとわかりますが、さっきまで morphTargetInfluences[0] を使って変形していたのを新しく morphTargetInfluences[1] の値を動かすことで、今回追加した「Grid」の変形ができます。
また、2種類の変形を同時に適用することで、2つの図形を組み合わせたかのような形を生成できたりします。「Mix」と書いたボタンを押すと、 morphTargetInfluences[0] と morphTargetInfluences[1] がどちらも1になりますが、そうすると2つの変形を組み合わせたような形になります。これめっちゃ面白いですよね。
今後の課題
今回、変形先の頂点座標を求めるために自分でベクトルの計算を行いましたが、立方体の頂点座標はThree.jsに用意されている BoxBufferGeometry から取ってきました。ということは、球体の頂点座標も SphereBufferGeometry から取れますし、同様にしてThree.jsに用意されている他の立体の頂点座標も、デフォルトのジオメトリから取ってこれるんですよね。
ただ、そうやって取ってきた頂点座標をそのまま変形先として使おうとすると、頂点の数が合わなくて上手くいかなかったり、数を合わせてもおそらく頂点の順番の関係で、あまり理想的な動きにならないんですよね。
これが解決できればいろいろな変形ができてすごく楽しいと思うんですが、今回は断念しました。詳しい方のお知恵をお借りしたいです。
参考にした公式サンプル
今回参考にした公式サンプルはこちらです。
Three.jsの公式サイトには、その機能ごとにさまざまなサンプルが置いてあります。実際にその場で動かして試すことができ、またソースコードも見ることができるのですごく助かっています。
おわりに
今回はThree.jsの機能を使って、自分でシェーダーを書かずにモーフィングアニメーションで遊んでみました。シェーダーは難しくてわからなくても、こんな感じでThree.jsの機能を使って頂点を操作することができるので、ぜひ試してみてください。
でも、ちゃんとシェーダーも勉強しましょうね!(自戒)
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。