WebGLで大量のパーティクルを

WebGLでパーティクルを大量に使いたい時


javascriptで全部の点の座標を指定していたら重くて数を増やせないはず。そんなときに使える技。
頂点が沢山あるVBOを作って、点の位置を全部シェーダで決定させればいい
OpenGLのPointSpriteを使えたら良いけど見つからなかったから正三角形のビルボード(常に視点を向くように調整した平板なポリゴン)を使う事にする。

サンプル
WebGLの使えるブラウザでどうぞ
http://www.geocities.jp/flyinpng/particle20120417/sample.html
on/off切り替えてから回転させて見てみると分かりやすいかも

VertexShader

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform float size,time;
attribute vec2 delta;
attribute vec3 random0,random1,random2,random3;
varying vec2 texcoord;
varying vec3 color;
void main(){
	texcoord=delta;
	//3頂点で共通の乱数を使いパーティクルの位置を決定
	vec3 pos=random0*time+random1*time*time+random2*time*time*time;
	//正三角形になるようdeltaだけ座標をずらす
	gl_Position=projectionMatrix*(modelViewMatrix*vec4(pos,1)+vec4(delta,0,0)*size);
	color=(vec3(1,1,1)+random3)/2.;
}

FragmentShader

#ifdef GL_ES
precision mediump float;
#endif
varying vec2 texcoord;
varying vec3 color;
void main(void){
	float r2=dot(texcoord,texcoord);
	if(r2>1.)discard;//正三角形の内接円外部は描画しない
	gl_FragColor=vec4(color*(1.-r2)*(1.-r2),1);
}

VBO初期化

var numParticle=1000;
var arrayBufferDelta,arrayBufferRandom=[];
function initArrayBuffers(){
	var deltaArr=[],sqrt3=Math.sqrt(3);
	//正三角形(ビルボードの形)の頂点座標
	for(var i=0;i<numParticle;i++){deltaArr.push(-1.7320508,-1);deltaArr.push(1.7320508,-1);deltaArr.push(0,2);}
	gl.bindBuffer(gl.ARRAY_BUFFER,arrayBufferDelta=gl.createBuffer());
	gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(deltaArr),gl.STATIC_DRAW);
	
	for(var i=0;i<5;i++){
		var rndArr=[];
		for(var j=0;j<numParticle;j++){
			var s=2*Math.random()-1,t=2*Math.PI*Math.random(),r=Math.sqrt(1-s*s);
			var x=r*Math.cos(t),y=r*Math.sin(t),z=s;
			//3頂点で共通の乱数値 これを使ってパーティクルの位置をVertexShaderで決定する
			rndArr.push(x,y,z);rndArr.push(x,y,z);rndArr.push(x,y,z);
		}
		gl.bindBuffer(gl.ARRAY_BUFFER,arrayBufferRandom[i]=gl.createBuffer());
		gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(rndArr),gl.STATIC_DRAW);
	}
}

描画

function render(size,time){
	gl.clear(gl.COLOR_BUFFER_BIT);
	gl.enable(gl.BLEND);
	gl.disable(gl.DEPTH_TEST);
	gl.blendFunc(gl.ONE, gl.ONE);
	
	gl.uniformMatrix4fv(gl.getUniformLocation(gl.program,"modelViewMatrix"),modelViewMatrix);
	gl.uniformMatrix4fv(gl.getUniformLocation(gl.program,"projectionMatrix"),projectionMatrix);
	gl.uniform1f(gl.getUniformLocation(gl.program,"size"),size);
	gl.uniform1f(gl.getUniformLocation(gl.program,"time"),time);
	gl.enableVertexAttribArray(0);
	gl.bindBuffer(gl.ARRAY_BUFFER,arrayBufferDelta);
	gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
	for(var i=0;i<4;i++){
		gl.enableVertexAttribArray(i+1);
		gl.bindBuffer(gl.ARRAY_BUFFER,arrayBufferRandom[i]);
		gl.vertexAttribPointer(i+1,3,gl.FLOAT,false,0,0);
	}
	gl.drawArrays(gl.TRIANGLES,0,3*numParticle);
}

javascript側では描画時に時間を指定しているだけで重い処理は無い。
つまりGPUが十分速ければパーティクルが100万個あっても動く!!

2段階で動きを変化させた花火っぽいサンプル
http://www.geocities.jp/flyinpng/particle20120417/fireworks.html#N=100000
うちのPCだと8万粒くらいが60fps出せる限界

線で軌跡を描画するサンプル
http://www.geocities.jp/flyinpng/particle20120417/sample2.html#N=10000

制約と誓約

  • シェーダで時間から位置を直接計算できないといけない
    • particle.x+=dt*f(particle.x,particle.y,particle.z);みたいな処理は出来ない。
    • ローレンツアトラクタとかやりたくても出来ない(VertexShaderFetchとかを使えば可能かも)。
  • その代わりべらぼうに速い