この記事は、Three.js Advent Calendar 2016 9日目の記事です。
たまたま空いていたので、まんがタイムきらら Advent Calendar 2016 9日目の記事でもありますが、きらら成分はステラのまほうのアニメ(しかもエンディングだけ)となっていますので気をつけてください。
✗目次
仕事の疲れをステラのまほうで癒やしているとき、エンディングのヨナカジカルを見ているとWebGLでやれそうだなぁ...となったので仕事の息抜きにやってみた話です。
普段からWebGL触っている系の職種ではないため過度な期待はしないで下さい。
結果として以下のようなものが出来上がります。
Demo => Yonakajikaru
Repo => yymm/Yonakajikaru
何でThree.js?
WebGLレンダリングもでき、APIがわかりやすいという印象を受けたので採用しました。
はじめの一歩
基本的に公式のドキュメントを参考にして作っていきました。
three.js - documentation - Manual - Creating a scene
上記のドキュメントの最後に書いてあるhtmlとjavascript部分をちょっといじってファイルにしたものです。
<html>
<head>
<title>My first Three.js app</title>
<style>
body { margin: 0; }
canvas { width: 100%; height: 100% }
</style>
</head>
<body>
<script src="js/three.js"></script>
<script src="js/app.js"></script>
</body>
</html>
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
camera.position.z = 5;
const render = function () {
requestAnimationFrame( render );
cube.rotation.x += 0.1;
cube.rotation.y += 0.1;
renderer.render(scene, camera);
};
render();
index.htmlをブラウザで見てみると緑のcubeがぐるぐるしているのがみえるはずです。
このサンプルコードをベースに作っていきます。
四角を表示する
表示したいのはただの四角形なので四角形をドキュメントから探します。
図形はGeometriesにあり、PlaneGeometoryかPlaneBufferGeometryを使えば四角形を表示できるようです。
three.js - documentation - Reference - PlaneGeometry
PlaneGeometry(width, height, widthSegments, heightSegments)
width — Width along the X axis.
height — Height along the Y axis.
widthSegments — Optional. Default is 1.
heightSegments — Optional. Default is 1.
BoxGeometryをPlaneGeometryに差替えます、引数は必要なものだけBoxGeometryを同じ値で設定します。
- const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+ const geometry = new THREE.PlaneGeometry( 1, 1 );
引数はwidth,heightなので1×1の四角になります。
普通のGeometryかBufferGeometryのどちらを使えばいいのか判断するためBufferGeometryのドキュメントを読みました。
three.js - documentation - Reference - BufferGeometry
BufferGeometryはGeometryよりも効率的だけど扱いが大変で、シェーダーなど使う場合はBufferGeomerty一択になるようです。
今回は複雑な使い方しないの+いっぱい四角を表示するのでGPU効率の良いBufferGeometryを使うことにします。
四角を増やす
愚直にfor文をぶん回す方法で増やてみます、ひとつひとつ図形(Mesh:geometryとmaterial)を作ってSceneに追加していく方法です。
図形の位置はMeshのpositionを変更することで移動できます。positionのデフォルト値は(x,y,z)=(0,0,0)です。
for (let i = 0; i < 3; ++i) {
const geometry = new THREE.PlaneBufferGeometry( 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const plane = new THREE.Mesh( geometry, material );
plane.position.x = i;
plane.position.y = i;
scene.add( plane );
}
増えましたが、効率悪そうです。
three.js製パーティクルシステムの実装 | 技術コラム | つみきブログ
上記のブログにあるようにgeometryで四角いパーティクルを作ってシェーダーで色や動きをつけていくほうが効率的なように思えますが今後の課題とします。(パフォーマンスの測定方法とかも知らないのでこれも課題)
ランダムにいっぱいの四角を表示する
まず表示する範囲について考えます。
エンディングでカメラが動いた時に横にも四角が広がっているように見えるので、見えている範囲より広め(表示範囲の2倍くらい)に分布するようにします。
各話でエンディングムービーが若干異なりますが一話のエンディングの感じだと相当奥まで多くの四角が広がっているのでz方向が深めにとったほうが良さそうです。
以上を踏まえてxy方向、z方向についてカメラの範囲と位置や四角の大きさ、表示範囲を調整します。
const scene = new THREE.Scene();
const far = 10000;
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, far );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
function getRandomInt(min, max) {
return Math.floor( Math.random() * (max - min + 1) ) + min;
}
const w = window.innerWidth * 2;
const h = window.innerHeight * 2;
for (let i = 0; i < 1000; i++) {
const geometry = new THREE.PlaneBufferGeometry( 50, 50 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const plane = new THREE.Mesh( geometry, material );
plane.position.x = getRandomInt(-w, w);
plane.position.y = getRandomInt(-h, h);
plane.position.z = getRandomInt(0, far);
scene.add( plane );
}
camera.position.z = far;
const render = function () {
requestAnimationFrame( render );
renderer.render(scene, camera);
};
render();
いい感じです、コードの説明をします。
まずカメラですが、ドキュメントを確認すると引数は以下のようになっています。
PerspectiveCamera( fov, aspect, near, far )
fov — Camera frustum vertical field of view.
aspect — Camera frustum aspect ratio.
near — Camera frustum near plane.
far — Camera frustum far plane.
fov(画角)を広げるか、frustum(円錐)を広げるか迷いますが、frustumのfarを変更するほうが直感的に分かりやすかったのでfovはサンプルコードの初期値のままにしました。
あと、cameraのz方向が元のサンプルコードのままだと近すぎて何も見えないので設定したfar程度に離します。
(fovやfrustumなど全く聞き慣れない単語なので検索ないとよくわからなかったです。画角 - Wikipediaや6. カメラ(processing 3D入門) | Yasushi Noguchi Classなどを参考にしました。)
四角の位置はランダムに配置するようにします、x,y,z方向で範囲が違うので任意の範囲で乱数生成しています。サイズは1のままだと近いものしか見えないので適度な大きさ(50)にしています。個数は1000個にしてますがカメラをもっと離して個数をもっと増やすと賑やかしくなりそうです。
(ちなみに、はじめのうちは調整する感覚がつかめず真っ黒な画面になることが多かったです、値を変更しながらイメージして試行錯誤してこの値になってます。)
カラフルにする
色などはmaterialで設定します。
- three.js - documentation - Reference - MeshBasicMaterial
- three.js - documentation - Reference - Material
透過も一緒に設定します。
const color = "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);});
const material = new THREE.MeshBasicMaterial( { color: color, transparent: true, opacity: 0.6 } );
それっぽい感じになってきました。ちょっと変更するだけでそれっぽくなってテンションが上がります。
fovを2倍、個数を5倍にして賑やかしさも上げました、PCのファンも回り出します。
HEXを作っているコードはstackoverflowから拾ってきたものです(URLは忘れてしまいました...)
カメラを動かす
アニメーションは最後の方にあるrenderの関数内に記述します。
とりあえず、cameraのz方向を小さくしていくと近づいていきます。
const render = function () {
requestAnimationFrame( render );
camera.position.z -= 4.0;
renderer.render(scene, camera);
};
xy方向はカメラが回転している感じに動いているので、mousemoveイベントを追加してカメラを連動させてみます。
document.addEventListener( 'mousemove', function(event) {
const x = window.innerWidth / 2 - event.clientX;
const y = window.innerHeight / 2 - event.clientY;
camera.rotation.x = y / 500;
camera.rotation.y = x / 500;
} );
画面中心を(0,0)としてマウスの方向に連動(x,yの指定が逆)して動くように設定しています。
rotationは角度(rad)で指定するため大きい数で割らないとぶっ飛んで行くので注意です。
カメラを回転して気づくことですが、通り過ぎていった四角を後ろから見ようとすると見えません何故か消えてなくなっています。これはMaterialのsideがデフォルトでTHREE.FrontSideになっており表しか見えなかったためです。T
HREE.DoubleSideを指定すると通り過ぎていった四角も見えるようになります。
three.js - documentation - Reference - Material
const material = new THREE.MeshBasicMaterial( { color: color, side: THREE.DoubleSide, transparent: true, opacity: 0.8 } );
マウスに連動してカメラがぐるぐるしている様子です。
微調整
- 回転済みの四角作成
- スピードチェンジ機能
- その他微調整
以下これまでの内容のapp.jsです。
const scene = new THREE.Scene();
const far = 20000;
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, far );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
function getRandomInt(min, max) {
return Math.floor( Math.random() * (max - min + 1) ) + min;
}
function getRandom(min, max) {
return Math.random() * (max - min) + min;
}
const w = window.innerWidth * 2.5;
const h = window.innerHeight * 2.5;
for (let i = 0; i < 5000; i++) {
const geometry = new THREE.PlaneBufferGeometry( 50, 50 );
const color = "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);});
const material = new THREE.MeshBasicMaterial( { color: color, side: THREE.DoubleSide, transparent: true, opacity: getRandom( 0.1, 0.9 ) } );
const plane = new THREE.Mesh( geometry, material );
plane.position.x = getRandomInt(-w, w);
plane.position.y = getRandomInt(-h, h);
plane.position.z = getRandomInt(0, far);
if (plane.position.z % 5 == 0) {
plane.rotation.y = getRandom( - Math.PI / 2.0, Math.PI / 2.0 );
}
if (plane.position.z % 7 == 0) {
plane.rotation.x = getRandom( - Math.PI / 2.0, Math.PI / 2.0 );
}
scene.add( plane );
}
camera.position.z = far * 3 / 4;
let isMouseDown = false;
let zSpeed = 5;
document.addEventListener( 'mousedown', function(event) {
isMouseDown = true;
} );
document.addEventListener( 'mouseup', function(event) {
isMouseDown = false;
} );
console.log(camera.rotation.order);
document.addEventListener( 'mousemove', function(event) {
const x = window.innerWidth / 2 - event.clientX;
const y = window.innerHeight / 2 - event.clientY;
if (isMouseDown) {
zSpeed = 50;
} else {
zSpeed = 5;
}
camera.rotation.x = y / 500;
camera.rotation.y = x / 500;
} );
const render = function () {
requestAnimationFrame( render );
camera.position.z -= zSpeed;
renderer.render(scene, camera);
};
render();
左ドラッグするとギューンと近づきます。すごいそれっぽい。
透過と角度をランダムに指定しているのでキラキラ感が増した気がします。
背景アニメーション
背景の色はrendererのsetClearColorで変更できます。
three.js - documentation - Reference - WebGLRenderer
いい方法が思いつかないので非常に雑に白にしてみます。
const fadeColor = [ 0xffffff, 0xdddddd, 0xbbbbbb, 0x555555, 0x333333, 0x111111 ];
const render = function () {
requestAnimationFrame( render );
camera.position.z -= zSpeed;
if (camera.position.z == 14800) { renderer.setClearColor(fadeColor[0], 1.0); }
if (camera.position.z == 14810) { renderer.setClearColor(fadeColor[1], 1.0); }
if (camera.position.z == 14820) { renderer.setClearColor(fadeColor[2], 1.0); }
if (camera.position.z == 14830) { renderer.setClearColor(fadeColor[3], 1.0); }
if (camera.position.z == 14840) { renderer.setClearColor(fadeColor[4], 1.0); }
if (camera.position.z == 14850) { renderer.setClearColor(fadeColor[5], 1.0); }
renderer.render(scene, camera);
};
雑ですが動いたのでok.....
最後に
サンプルコードをベースにして、コードを書く側としては無理なく作ってみました(ブラウザやGPUやCPUは無理してるかもしれません)。
普段ビジュアル的なプログラミングをしていない自分でも手軽にリッチな表現を扱えるThree.jsは面白いと感じました。
(欲を言えば音楽や星形や図形のアニメーションもやりたかったのですが仕事の息抜きの範疇を超え始めるので断念しました無念。)
余裕があれば最適なコードを勉強していきたい所存なので、有識者の方アドバイスあれば是非に。
もともとまんがタイムきらら Advent Calendar 2016の参加予定はなかったのですが、たまたま空いていたので急遽入れてみました。
きららアニメには毎期癒やされていますが、ステラのまほうは癒やし効果とプログラミングのモチベーション向上効果があって最高です、みなさん癒やされてプログラミングがんばりましょう。
Show comments