元々 OpenGL を使っていたことがあって 3D の基礎についてはある程度わかっていたので WebGL には本当にすんなり入ることができた。WebGL は OpenGL ES の仕様を元にしているので当然といえば当然なのですが、他の OpenGL のプラットフォームと比べて WebGL はテキストエディタとブラウザさえあれば開発環境が整うという手軽さ、さらに WebGL 界隈ではとても有名な three.js というライブラリがあるおかげで、さらに敷居が下がってる。ということで、前々から WebGL で作ってみたかった綺麗でしなやかなパーティクルの演出を作ってみました。
※ちなみに XSEEDS というのは、有志で集まったエンジニアがお酒飲みながらワイワイ開発やってる集団です。
作ってみたので、せっかくなので WebGL ってこんなことができるよ、こうやって作れるんだよ、という少し実装よりの話を紹介してみようと思います。three.js を使うの前提です。ちなみに OpenGL については以前ブログを書いたことがあって、興味のある方はそちらも読んでみてください。
独学で 1 ヶ月間 OpenGL を学んで得た基礎知識のまとめ
※シリーズ物になっていて全6回です。
はじめに
こんな人が読むと面白いかも。ただの目安です。
- WebGL に興味がある
- 3D なとなくわかるんだけど実際にモノを作ったことがない
- なんでもいいから WebGL を使って簡単なものを作ってみたい
それから three.js を使うことを前提としてます。また、three.js の基礎知識の解説はしません。他に良い解説サイトがあると思います。基本的な シーン、カメラ、レンダラ、ジオメトリ、マテリアル、オブジェクト といった概念に対する知識はある前提として話が進みます。
パーティクルで文字を作る
まずはこういうのを作ります。
テクスチャの生成
まず文字を構成するパーティクルオブジェクトに貼り付けるテクスチャを作ってしまいます。具体的にはこういうの。
さて、こういう画像を準備してそれをテクスチャとして読み込んでもいいのですが、同じテクスチャが張られたオブジェクトばっかり並んでいると単調でつまらないものになってしまいます(というか最初そのようにしてて「これじゃなんかつまらないな」と思っていました)。なので少しゆらぎを持たせるために canvas を使って複数個ランダムに丸い画像を描きます。
function createParticleTexture() {
// 128pxのサイズのcanvasを作ります。サイズは別に128pxじゃなくても
// いいですが、テクスチャとして利用するため2のべき乗のサイズにして
// おいた方が良いです。(32pxとか64pxとか256pxとか)
var canvas = document.createElement('canvas');
var SIZE = 128;
var HALF = SIZE / 2;
var CENTER = SIZE / 2;
canvas.width = SIZE;
canvas.height = SIZE;
// パーティクルの色にゆらぎを持たせるために色にランダム要素を与えます。
// 基本色は青色にしたいのでHSLカラー空間で考えて
// - 色相 : 200〜230の間
// - 彩度 : 40〜60の間
// - 輝度 : 50〜70の間
// でランダムに色を作ります。
var color = new THREE.Color();
var h = 200 + 30 * Math.random();
var s = 40 + 20 * Math.random();
var l = 50 + 20 * Math.random();
color.setHSL(h / 360, s / 100, l / 100);
// 円を書いて中身を先ほど作った色で塗りつぶします。ただし、全てその色
// で塗りつぶすわけではなく、暗闇にぼんやりと浮かぶ球のような演出をし
// たいので中心から外側に向かってだんだん暗くなるような円形グラデーシ
// ョンをかけます。
var context = canvas.getContext('2d');
var grad = context.createRadialGradient(CENTER, CENTER, 0, CENTER, CENTER, HALF);
grad.addColorStop(0, color.getStyle());
grad.addColorStop(1, '#000000');
context.lineWidth = 0;
context.beginPath();
context.arc(CENTER, CENTER, HALF, 0, 2 * Math.PI, false);
context.fillStyle = grad;
context.fill();
context.closePath();
return canvas;
}
// 基本色が青系なランダムな色を持つテクスチャを20個読み込みます。
var materials = [];
for (var i = 0; i < 20; i++) {
var material = new THREE.MeshPhongMaterial({
map: new THREE.Texture(createCircleCanvas()),
blending: THREE.AdditiveBlending,
transparent: true
});
material.map.needsUpdate = true;
materials.push(material);
}
すると、こんな風に20個のキャンバス上にそれぞれ球が描画されます。
文字画像からパーティクルの生成
テクスチャを作ったので次はテキスト描画用のパーティクルオブジェクトを生成していきます。これは普通にテキストを描画しているわけではなく、文字を構成する1ドット1ドットがパーティクルになっています。やり方としては
- canvasを用意してそこに描画したいテキストを描く
- canvasから画像データをRGBAの配列として取り出す
- 配列を1ピクセル分ずつ走査していって色があればその座標にパーティクルを生成する
という手順でパーティクルテキストを作ることができます。以下の URL でも同じような手法を使ってパーティクルテキストを作っていますが、今回はこれを参考にしました。
http://cssdeck.com/labs/cool-canvas-particles-text-effect
// 1. canvasを用意してそこに描画したいテキストを描く
// -------------------------------------------------
// canvasにテキストを描画するにはgetContextで2Dのコンテキストを取得後
// にそれに対してフォントとテキストを指定した後でfillTextメソッドを呼
// び出します。
var canvas = document.createElement('canvas');
var context = canvas.getContext("2d");
context.font = '30px "Open sans"';
context.fillStyle = '#ffffff';
context.textAlign = 'center';
context.fillText("xseeds", WIDTH / 2, 30);
// 2. canvasから画像データをRGBAの配列として取り出す
// -------------------------------------------------
// コンテキストのgetImagetDataメソッドを使って画像データのRGBA配列を取
// り出します。getImageDataメソッドの戻り値はオブジェクトになっていて
// - imageData.width
// - imageData.height
// - imageData.data
// という3つのプロパティが参照できます。今回はRGBA配列が欲しいので.data
// だけ取り出しています。
var data = context.getImageData(0, 0, WIDTH, HEIGHT).data;
// 3. 配列を1ピクセル分ずつ走査していって色があればその座標にパーティクルを生成する
// --------------------------------------------------------------------------------
// 取り出したRGBA配列は4バイトで1ピクセルを表しています。つまり
// - data[ 0] => 1ピクセル目のR(赤)
// - data[ 1] => 1ピクセル目のG(緑)
// - data[ 2] => 1ピクセル目のB(青)
// - data[ 3] => 1ピクセル目のA(アルファ値)
// - data[1 * 4 + 0] => 2ピクセル目のR(赤)
// - data[1 * 4 + 1] => 2ピクセル目のG(緑)
// - data[1 * 4 + 2] => 2ピクセル目のB(青)
// - data[1 * 4 + 3] => 2ピクセル目のA(アルファ値)
// - data[2 * 4 + 0] => 3ピクセル目のR(赤)
// - data[2 * 4 + 1] => 3ピクセル目のG(緑)
// - data[2 * 4 + 2] => 3ピクセル目のB(青)
// - data[2 * 4 + 3] => 3ピクセル目のA(アルファ値)
// という風にならんでいます。先に紹介したサンプルURLではピクセルが白
// でない場合にその座標にドットがあるとみなしていますが、ピクセルのア
// ルファ値明度が0の場合、つまりdata[i * 4 + 3]が0であれば、そこには
// ドットがないとみなすこともできます。
// そのようにチェックして透明ではないピクセルのxとyをオブジェクトの座
// 標に指定してあげばパーティクルで描くテキストの完成です。オブジェク
// トに貼り付けるテクスチャは、先に20個作っておいたものからランダムで
// 選択します。
for (var x = 0; x < WIDTH; x++) {
for (var y = 0; y < HEIGHT; y++) {
if (data[(x + y * WIDTH) * 4 + 3] == 0) {
continue;
}
var geometry = new THREE.PlaneBufferGeometry(5, 5);
var material = materials[Math.floor(materials.length * Math.random())];
var mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0);
particles.add(mesh);
}
}
これでテキストをパーティクルで描くことができます。あとはこのパーティクルの手前に PointLight を設置すれば、駅の電光掲示板のように、パーティクル 1 つ 1 つが光を発しているかのように見えます。
空間に浮かぶチリやホコリ
次にこういうのを作ってみます。
パーティクルシステム
この白いチリやホコリのようなもの、どうやって作っているのかというとまずそれ用のテクスチャを用意します。中心に白を置いて外側に向かうにつれてだんだん透明になっていく円形グラデーションをかけた球の画像を用意します。さっきのテキスト用パーティクルと同じ理屈で、暗闇にぼんやり浮かんだような演出ができます。
このテクスチャを使ってチリ、ホコリを表現するわけですが、three.js には手軽に大量のパーティクルを描画してくれる PointCloud
という仕組みがあります。これはパーティクルの頂点の位置だけ指定すると GL_POINTS
を使って点を描画してくれるというものです。この仕組みのお陰で、大量の(それこそ数千、数万)のパーティクルを描画しても実用的に使うことができます。
// 5000個のパーティクルを画面上にくまなくランダムに配置します。通常は
// 透視投影用カメラを使うと思うので、奥行き(z方向)もランダムに配置する
// ことで、各パーティクルのサイズの違いもうまれて、よりランダムに見え
// ます。
var geometry = new THREE.Geometry();
for (var i = 0; i < 5000; i++) {
geometry.vertices.push(new THREE.Vector3(
300 * (Math.random() - 0.5),
300 * (Math.random() - 0.5),
100 * (Math.random() / 2) + 100
));
}
// PointCloud用のマテリアルを準備。先ほど紹介した画像をテクスチャとし
// て利用します。またパーティクルのサイズも指定できます。
var material = new THREE.PointCloudMaterial({
map: THREE.ImageUtils.loadTexture('images/p.png'),
size: 4,
blending: THREE.AdditiveBlending,
transparent: true
});
// PointCloudオブジェクトを生成します。
var dustClound = new THREE.PointCloud(geometry, material);
フォグ
さらに良い演出をするためにフォグをつけてみます。霧ですね。遠くにあるオブジェクト程、どんどん背景と同化してみえなくなっていきます。three.js はフォグを扱うのもとても簡単で、シーンに対してフォグを設定するだけです。というかコードでいうと 1 行だけ。
scene.fog = new THREE.FogExp2('#140066', 0.035);
左側がフォグなし、右側がフォグあり、です。右側のほうがなんとなく全体的にぼやけてるように見えます。
動かしてみる
このままだとパーティクルが止まったままで特におもしろみがないので、これを動かしてみます。具体的にはフレームループ内で PointCloud
オブジェクトの座標を少しずつずらしていくだけです。
// これフレームループです。
(function() {
requestAnimationFrame(arguments.callee);
// オブジェクトの(x, y)座標をフレーム毎に0.01ずつ増やしていいきます。
// これでチリやホコリがだんだん右上の方に動いていっているように見え
// ます。パーティクルのz座標は初期生成時にランダムに指定されているた
// め、手前にいるパーティクルの方が早く動き、奥にいるパーティクルの
// 方が遅く動いているように見え、結果的に奥行きがあることが強調され
// ています。
if (dustClound) {
dustClound.position.x += 0.01;
dustClound.position.y += 0.01;
}
renderer.render(scene, camera);
})();
綺麗なアニメーション
パーティクルでの演出のポイントとして、パーティクルそれ自体の綺麗さももちろんありますが、もう一つの要素としてどういうアニメーションをするのか、というものがあると思います。どういう軌道でアニメーションさせるかはそれを作る人の腕次第なところがあると思いますが、アニメーションさせる方法については誰でもできる簡単な TweenMax という JavaScript のライブラリがあるので、それを使っています。(使用方法はリンク先のドキュメントを参照してください…)
これは three.js とも親和性が高く、CDN も提供されているので簡単に導入することができます。たとえば 10 秒間で左下から右上に移動しながらだんだん薄くなって透明になっていくサンプルは次のように書けます。
// object変数にはthree.jsの何かしらのオブジェクトが入っています。
TweenMax.to(object, 10, {
x: 100,
y: 100,
opacity: 0,
startAt {
x: 0,
y: 0,
opacity: 1
}
});
ただ、これだと直線の移動しか表現できません。パーティクルの美しいアニメーションにはやはり曲線の軌道があったほうが良いです。なのでベジェ曲線を使います。ベジェ曲線だとすごく綺麗な曲線の軌道を作ることができます。先ほどの TweenMax のページにも ベジェ曲線を使ったデモ (赤いキャラクターが曲線の軌道にそって動いていくやつ) があるので、それを見てみると雰囲気がつかみやすいと思います。
テキストのアニメーション
では TweenMax を使ってアニメーションしてる部分について。実際には TimelineMax というライブラリを使っています。こういうコードで、左下手前から始まって、奥の方にビューーンとパーティクルが飛んでいった後に、急カーブしてこっちに戻ってくる軌道をしてくれます。
// ベジェ曲線の制御点を指定します。The XSEEDSのアニメーションでは
// 1. 初期位置は画面右下(隠れている)
// 2. 画面の上奥の方
// 3. 画面の左下の方
// 4. 最後はこちらに向かってくるように
// という軌道でアニメーションします。そのような曲線を描く制御点を指定
// して、それをTimelineMaxに渡します。制御点の指定は慣れないと難しくて
// 実際は自分も何度も試行錯誤しました。
// particlesにはテキストを構成するパーティクルの配列、positionには最終
// 的な位置の座標が入っているものとします。
var z = 140;
var t = new TimelineMax();
for (var i = 0; i < particles.length; i++) {
var mesh = particles[i];
var curvePoint = [{
x: 30,
y: -25,
z: 140
}, {
x: 0 + (20 * (Math.random() - 0.5)),
y: 100 + (20 * (Math.random() - 0.5)),
z: z / 10
}, {
x: -120 + (100 * (Math.random() - 0.5)),
y: -60 + (100 * (Math.random() - 0.5)),
z: 0
}, {
x: position.x / 10 + 400,
y: position.y / 10 + 400,
z: camera.position.z + 200
}];
t.to (mesh.position, DURATION, { bezier: curvePoint, ease: Expo.easeNone }, delay);
}
ロゴのアニメーション
最後にロゴが表示されてクルッと回転するアニメーションがありますが、これも TimelineMax を使っています。といっても three.js のオブジェクトの rotation
プロパティをいじっているだけです。
// このコードで1秒間でy軸に沿って1回転するアニメーションをしてくれます。
var t = new TimelineMax();
t.to(logo.rotation, 1.0, { y: 2 * Math.PI }, DELAY);
キラキラ光るアニメーション
パーティクルの動くアニメーションが終わって最後にロゴが表示されたあと、全てのアニメーションは終わりになります。ただ、そうするとアニメーションが全部終わった後に何も動きがない(当たり前だけど)。ちょっと素っ気ないなーと思って、最後はテキストを構成するパーティクルがそれぞれキラキラと光っているような演出をしてみました。これは、各パーティクルに設定しているマテリアルの透明度と色を段階的に変化させるというアニメーションをリピートしています。
// 透明度を1〜0.5くらいまでの間で、さらに色相を青の付近でゆらがせなが
// ら、それを0.8秒間隔でリピートさせます。これによって見た目的には、
// パーティクルがキラキラと光っているように見えます。
var t = new TimelineMax();
for (var i = 0; i < materials.length; i++) {
t.to(materials[i], 0.8, {
opacity: 0.2 * Math.random() + 0.4,
startAt: { opacity: 1 },
repeat: -1,
yoyo: true,
onUpdate: function(material, hsl) {
material.color.setHSL(hsl.h + (this.progress() / 5), hsl.s, hsl.l);
},
onUpdateParams: [ materials[i], materials[i].color.getHSL() ]
}, DELAY + Math.random());
まとめ
three.js を使うと僕みたいな素人でも結構簡単にいろいろ出来ます。世の中には After Effects などを使って製作された綺麗なエフェクトがインターネットにいっぱい転がっているので、そういうのを見てるとインスピレーションが湧いてきます。
試しになんか作ってみたらどうでしょうか。WebGL 楽しい。