出たら消える！実用的？パーティクル実装例

この記事は、[Three.js Advent Calendar 2016](http://qiita.com/advent-calendar/2016/threejs) 7日目の記事です。

#消えたり動いたするパーティクルを作ろう！

パーティクル皆さん好きですか。
色々なwebglのサンプルにてパーティクルは公開されています。どれもキレイですね。一番なんて決めなくていいですね。世界で一つだけの粒です。  

さて。その数々のキレイなサンプルですが、「じゃあ、自分のサイトにも実装してみよう！」となると、うーん？となること、ありませんかね。自分はなりました。
何故かというと、パーティクルって、沢山の粒が、【点いたり消えたり】して、もっとこう、パーって動くものだと思うのですよ。 
多くのサンプルが、いわば【点いたまま】なんです。消えろよと。点いたり消えたりしろよと。

という感じで無駄に敵を増やしたところで、点いたり消えたりするパーティクルを実装したいと思います。

[完成品サンプルはこちら](http://www001.upp.so-net.ne.jp/adrs2002/JenParticle/particleTest2.html)。
[zipでくれ](http://www001.upp.so-net.ne.jp/adrs2002/jenParticle2/jenParticle2.zip)

###一番近いサンプルは？
今回の実装に一番近いthree.jsのサンプルは、[instanced circle billboards](https://threejs.org/examples/#webgl_buffergeometry_instancing_billboards)   になります。このサンプルで行われていることを紐解いていきましょう。

##サンプルの構成

まず、THREE.InstancedBufferGeometry　と　THREE.CircleBufferGeometry が宣言されています。THREE.CircleBufferGeometry(x,y) は、半径xの y角形を、中心点を持った形で作成してくれるものです。サンプルでは6角形になっています。
次に、これをInstancedBufferGeometryにしています。いんすたんすどばっふぁじおめとりとは耳慣れない言葉です。バッファは、たまに横文字大好きな人が「緩衝」という意味で使うようですが、ここでのイメージは「せき止めておいて、まとめて流す」と考えてくれればいいかなと。

ジオメトリをInstancedBufferGeometryとして割り当てると、「割り当てた数分、同じ図形を表示しろ！」と、GPUに命令をします。ベースとなったサンプルでは、particleCount の数＝7万5千個も一気に表示しろ！となります。

何それ？そんなことをしても、全部同じ位置に同じものが表示されるだけじゃねーの？ってお思いでしょう。普通にやったらそうなりますよね。
そこで、この機能を意味のあるものにするために、シェーダーと、それに渡すための「InstancedBufferAttribute」というものがでてきます。

##InstancedBufferAttributeとは
InstancedBufferAttributeの宣言方法は、奇妙な形になっているかと思います。これが分かりづらい要因ですね。ここは、three.jsよりも、生のwebglにとても近いところを触っているからです。

今更なのですが、three.jsは、javascriptのライブラリです。何を今更です。
もう使いすぎて忘れることもあるのですが、Vector3という【型】は、javascriptにはありません。three.js独自の拡張です。
何故こんなことを今更書くかというと、webglでは、「厳密な型宣言」が必要になるからです。
そのため、「3次元の位置情報、xyzだオラァ」とは、そのままではwebglは認識してくれません。
そのため、データ的には、Vector3型を一回忘れて、全て分解された単なるfloat型の配列として投げられ、後から、**「あ、今のデータ、3つで1組だからヨロシク！」と、シェーダー側で解釈をしてくれるようにする必要があります。**これが156行目の引数の「3」の意味です。
これが、7万5千個をまとめて表示しても、重ならず意味を持って表示されるカラクリです。これ最初に考えた人は、変態に限りなく近い天才だったのでしょう。
ということで、GPUは7万5千＊3、えっといくつだ、たくさん！そんな配列が一度に、しかも複数きても難なく処理してくれます。バケモノですね。


この仕組みさえ分かってしまえば、パーティクルを自由自在に操ることができます。

```js
//パーティクルを初期化する
function makeParticle()
{
	//重要なのはココから
	//円形オブジェクトの作成
	geometry = new THREE.InstancedBufferGeometry();
	geometry.copy( new THREE.CircleBufferGeometry( 1, 6 ) );

	//入れ物を初期化していく
	//シェーダに渡すには明確に【型】が決まってる必要があるため、こういうことを行う
	translateArray = new Float32Array( particleCount * 3); //Vector3を入れるため、float型が3つ
	colArray = new Float32Array( particleCount * 3);     //Vector3を入れるため、float型が3つ
	vectArray = new Float32Array( particleCount * 3);    //Vector3を入れるため、float型が3つ
	scaleArray = new Float32Array( particleCount * 1);
	timeArray = new Float32Array( particleCount * 1);

	//カウンタでぶん回してガンガン初期化
	for ( var i = 0; i < particleCount; i ++) {
		//いわば、パーティクルの初期位置に該当。どうせ書き換わるから気にするな
		translateArray[i * 3 + 0] = 1.0;
		translateArray[i * 3 + 1] = 1.0;
		translateArray[i * 3 + 2] = 1.0;

		//パーティクルの大きさをセットする入れ物
		scaleArray[i * 3 + 0] = 0.0;

		//【色】を管理する入れ物
		colArray[i * 3 + 0] = 0.0;
		colArray[i * 3 + 1] = 1.0;
		colArray[i * 3 + 2] = 0.0;

		//出現してからの時間管理の入れ物
		timeArray[i * 3 + 0] = 0.0;

	}


	//用意した「入れ物」をシェーダー側（VS)に渡すための宣言。
	//ここで重要なのは、JSコード側「のみ」で管理が必要なものは、渡す必要はない、ということ。
	//そしてもちろん、VS側のattribute に、コレと同じ変数が用意されていることが大前提となる
	geometry.addAttribute( "translate", new THREE.InstancedBufferAttribute( translateArray, 3, 1 ) );
	geometry.addAttribute( "col", new THREE.InstancedBufferAttribute( colArray, 3, 1 ) );
	geometry.addAttribute( "movevect", new THREE.InstancedBufferAttribute( vectArray, 3, 1 ) );
	geometry.addAttribute( "scale", new THREE.InstancedBufferAttribute( scaleArray, 1, 1 ) );
	geometry.addAttribute( "time", new THREE.InstancedBufferAttribute( timeArray, 1, 1 ) );

	//シェーダー。困ったらコピペでイケる
	material = new THREE.RawShaderMaterial( {
		uniforms: {
			map: { value: new THREE.TextureLoader().load( "textures/sprites/circle.png" ) },
			time: { value: 0.0 }
		},
		vertexShader: document.getElementById( 'vshader' ).textContent,
		fragmentShader: document.getElementById( 'fshader' ).textContent,
		depthTest: true,
		depthWrite: false,
		transparent:true,
		blending:THREE.AdditiveBlending
	} );

	//
	mesh = new THREE.Mesh( geometry, material );
	mesh.scale.set( 1, 1, 1 );
	scene.add( mesh );

}
```


##罠！アップデートを忘れるな！

さて、ここまで読んで「もう分かった楽勝。後は読まなくていいや」と思った貴方。おかえりなさい。おそらく貴方が書いたソースは間違っていなくても、きっと「見た目が更新されない」という事態が待っていたでしょう。
これはthree.jsの仕様で、「頂点やマテリアルは、一度宣言した後には、基本的には変わらないことを前提にしている」ということがあるっぽく、変更したら、「今、変わったから！」と明示的に教える必要があります。それが  needsUpdate です。

```js
function updateParticle(_dulTime)
{
	var onCount = 0;
	for(var i=0; i < particleCount;i++){
		if(timeArray[i] > 0.0){
			onCount++;
			timeArray[i] += _dulTime;
			if(timeArray[i] > 1000){
					//1秒経過していたら、消滅させる。
					timeArray[i] = 0.0;
					scaleArray[i] = 0.0;
				}
		}
	}

	console.log(onCount);

	geometry.attributes.translate.needsUpdate = true;
	geometry.attributes.col.needsUpdate = true;
	geometry.attributes.movevect.needsUpdate = true;
	geometry.attributes.scale.needsUpdate = true;			
	geometry.attributes.time.needsUpdate = true;

}
```

ということで、出現管理＆情報更新管理ができたら、最後にこの呪文を唱えて完了です。
このneedsUpdate は、メッシュ頂点やいたるところに必要になることがあるので、何か更新されずにおかしいな？と思ったら、ひとまずやってみる、という勢いでもいいかもしれません。んで、メソッドが無いって怒られたら消す。と。


#まとめ

頭に「なんでサンプルのパーティクルは動かないのばっかりなんだよ！意味ねーだろ！」と言いましたが、きっと理由は、three.jsから離れた内容になるから、だったのかもしれません。っていうか今更ですが、これthree.jsのカレンダー記事で大丈夫ですかね。

ここまで来て理解できてしまった貴方には、[生webglの深い沼](http://qiita.com/advent-calendar/2016/webgl)の中、[GPUで暖をとる通路に繋がる階段](http://qiita.com/advent-calendar/2016/glsl)が目の前に現れました。さぁ恐れず登ってみましょう。
ですが、ちゃんとthree.jsに帰ってきてくださいね。約束ですよ・・・


それが、彼が残した、最期の言葉だった。