Twitter
2021年03月06日 - Three.js・WebVR

GPGPUでパーティクル

以前から興味があったので「GPGPUでパーティクルを大量に描く-wgld.org」を参考に、GPGPUでパーティクルアニメーションを試してみました。

GPGPUでパーティクルアニメーション

GPGPUは、General-purpose computing on graphics processing unitsの略で、GPUを画像処理以外の目的で使用する技術のことです。WebGL 1.0では計算結果をテクスチャに書き出し、フラグメントシェーダでその値を読み込むことでGPGPUを実現します。

※WebGL 2.0ではTransformFeedbackという機能が追加され、GPUによる計算結果をバッファに書き出せるようになりました。

● 頂点テクスチャフェッチと浮動小数点数テクスチャ

計算結果をテクスチャに書き出すさい、頂点テクスチャフェッチと浮動小数点数テクスチャを使用します。

通常テクスチャは画像として使用するためフラグメントシェーダで参照しますが、頂点テクスチャフェッチは頂点シェーダでテクスチャを参照します。

テクスチャのRGBAの各要素に格納できる数値は0~255ですが、浮動小数点数テクスチャを使用すると、テクスチャに浮動小数点数を格納できるようになります。

※現時点では、浮動小数点数テクスチャをフレームバッファオブジェクトにバインドするとiOS端末では動作しないようです。(2021年3月6日)

● オフスクリーンレンダリングとフレームバッファ

オフスクリーンレンダリングはスクリーンには表示されないレンダリングのことで、バックグラウンドのメモリ空間上にレンダリングすることができます。

フレームバッファオブジェクトは、カラーバッファ、デプスバッファ、ステンシルバッファなどバッファを統合するオブジェクトのことで、オフスクリーンレンダリングはフレームバッファオブジェクトを使用して行います。

● 行列演算用ライブラリ

minMatrixb.js リファレンス-wgld.org」を参考に、行列演算用ライブラリを読み込みます。

<script src="js/lib/minMatrixb.js"></script>
<script src="js/script.js" type="module"></script>

● script.js

//===============================================================
// GLSL
//===============================================================
//頂点座標の初期位置を格納するシェーダ
const vertexShader =`
	attribute vec3 position;

	void main(void){
		gl_Position = vec4(position,1.0);
	}
`;
const fragmentShader =`
	precision mediump float;

	//フレームバッファの解像度
	uniform vec2 resolution;

	void main(void){

		//頂点座標の初期位置を設定
		vec2 p = (gl_FragCoord.xy / resolution) * 2.0 - 1.0;

		gl_FragColor = vec4(p,0.0,1.0);
	}
`;

//頂点座標をレンダリングするシェーダ
const pointVs =`
	attribute float index;
	uniform vec2 resolution;
	uniform sampler2D texture;
	uniform float pointScale;

	void main(void){

		//参照すべきテクスチャ座標を算出
		vec2 p = vec2(mod(index,resolution.x) / resolution.x, floor(index/resolution.x)/resolution.y);

		//テクスチャの設定
		vec4 t = texture2D(texture,p);

		//ポイントサイズの設定
		gl_PointSize = 0.1 + pointScale;

		//テクスチャのxyが頂点座標
		gl_Position = vec4(t.xy,0.0,1.0);
	}
`;
const pointFs =`
	precision mediump float;

	uniform vec4 ambient;

	void main(void){
		gl_FragColor = ambient;
	}
`;

//テクスチャを参照し頂点座標を更新するシェーダ
const velocityVs =`
	attribute vec3 position;

	void main(void){
		gl_Position = vec4(position,1.0);
	}
`;
const velocityFs =`
	precision mediump float;

	uniform vec2 resolution;
	uniform sampler2D texture;
	uniform vec2 mouse;
	uniform bool mouseFlag;
	uniform float velocity;
	const float SPEED = 0.05;

	void main(void){
		//テクスチャ座標を計算
		vec2 p = gl_FragCoord.xy / resolution;

		//前のフレームの頂点座標の読み込み
		vec4 t = texture2D(texture,p);

		//カーソル位置へのベクトル
		vec2 v = normalize(mouse - t.xy) * 0.1;

		//向きを補正
		vec2 w = normalize(v + t.zw);

		//テクスチャから読み込んだ値は、xyが頂点座標、zwが進行方向ベクトル
		vec4 destColor = vec4(t.xy + w * SPEED * velocity,w);

		if(!mouseFlag){

			//前のフレームの進行方向を維持
			destColor.zw = t.zw;
		}
		gl_FragColor = destColor;
	}
`;

//===============================================================
// Init & Redering
//===============================================================
window.addEventListener('load',function(){
	init();
  	rendering();
});

let canvas,gl;
let prg,attLocation,attStride,uniLocation;
let pPrg,pAttLocation,pAttStride,pUniLocation;
let vPrg,vAttLocation,vAttStride,vUniLocation;
let position,vVBOList,planeVBOList;
let vertices,resolution,ambient;
let backBuffer,frontBuffer,flip;
let velocity = 0.0;
let mouseFlag = false;
let mousePositionX = 0.0;
let mousePositionY = 0.0;
let count = 0;

//テクスチャの幅と高さ
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;

function init(){
	canvas = document.getElementById('webgl-canvas');
	canvas.width = Math.min(window.innerWidth,window.innerHeight);
	canvas.height = canvas.width;

	gl = canvas.getContext('webgl');

	//頂点テクスチャフェッチが利用可能か確認
	const vtf = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
	if(vtf > 0){
		//console.log('max vertex texture image unit:' + vtf);
	}else{
		console.log('VTF not supported');
		return;
	}

	//浮動小数点数テクスチャが利用可能か確認
	const ext = gl.getExtension('OES_texture_float') || gl.getExtension('OES_texture_half_float');
	if(ext == null){
		console.log('float texture not supported');
		return;
	}

	//頂点座標の初期位置を格納するシェーダ
	const vShader = createShader(gl.VERTEX_SHADER,vertexShader);
	const fShader = createShader(gl.FRAGMENT_SHADER,fragmentShader);
	prg = createProgram(vShader,fShader);

	attLocation = [];
	attLocation[0] = gl.getAttribLocation(prg,'position');
	attStride = [];
	attStride[0] = 3;

	uniLocation = [];
	uniLocation[0] = gl.getUniformLocation(prg,'resolution');

	//頂点座標をレンダリングするシェーダ
	const vShader2 = createShader(gl.VERTEX_SHADER,pointVs);
	const fShader2 = createShader(gl.FRAGMENT_SHADER,pointFs);
	pPrg = createProgram(vShader2,fShader2);

	pAttLocation = [];
	pAttLocation[0] = gl.getAttribLocation(pPrg,'index');
	pAttStride = [];
	pAttStride[0] = 1;

	pUniLocation = [];
	pUniLocation[0] = gl.getUniformLocation(pPrg,'resolution');
	pUniLocation[1] = gl.getUniformLocation(pPrg,'texture');
	pUniLocation[2] = gl.getUniformLocation(pPrg,'pointScale');
	pUniLocation[3] = gl.getUniformLocation(pPrg,'ambient');

	//テクスチャを参照し頂点座標を更新するシェーダ
	const vShader3 = createShader(gl.VERTEX_SHADER,velocityVs);
	const fShader3 = createShader(gl.FRAGMENT_SHADER,velocityFs);
	vPrg = createProgram(vShader3,fShader3);

	vAttLocation = [];
	vAttLocation[0] = gl.getAttribLocation(vPrg,'position');
	vAttStride = [];
	vAttStride[0] = 3;

	vUniLocation = [];
	vUniLocation[0] = gl.getUniformLocation(vPrg,'resolution');
	vUniLocation[1] = gl.getUniformLocation(vPrg,'texture');
	vUniLocation[2] = gl.getUniformLocation(vPrg,'mouse');
	vUniLocation[3] = gl.getUniformLocation(vPrg,'mouseFlag');
	vUniLocation[4] = gl.getUniformLocation(vPrg,'velocity');

	//テクスチャの幅と高さ
	resolution = [TEXTURE_WIDTH,TEXTURE_HEIGHT];

	//頂点配列
	vertices = Array(TEXTURE_WIDTH * TEXTURE_HEIGHT);

	//頂点のインデックスを連番で設定
	for(let i = 0, j = vertices.length; i < j; i++){
		vertices[i] = i;
	}

	//頂点情報をからVBOを生成
	const vIndex = createVbo(vertices);
	vVBOList = [vIndex];

	//四角形ポリゴン
	position = [
		-1.0,1.0,0.0,
		-1.0,-1.0,0.0,
		1.0,1.0,0.0,
		1.0,-1.0,0.0
	];

	const vPlane = createVbo(position);
	planeVBOList = [vPlane];

	//フレームバッファの生成
	backBuffer = createFramebuffer(TEXTURE_WIDTH,TEXTURE_WIDTH,gl.FLOAT);
	frontBuffer = createFramebuffer(TEXTURE_WIDTH,TEXTURE_WIDTH,gl.FLOAT);
	flip = null;

	gl.disable(gl.BLEND);
	gl.blendFunc(gl.ONE,gl.ONE);

	//フレームバッファをバインド
	gl.bindFramebuffer(gl.FRAMEBUFFER,backBuffer.f);
	gl.viewport(0,0,TEXTURE_WIDTH,TEXTURE_HEIGHT);

	gl.clearColor(0.0,0.0,0.0,0.0);
	gl.clear(gl.COLOR_BUFFER_BIT);

	//プログラムオブジェクトの選択
	gl.useProgram(prg);

	//テクスチャへ頂点情報をレンダリング
	setAttribute(planeVBOList,attLocation,attStride);
	gl.uniform2fv(uniLocation[0],resolution);
	gl.drawArrays(gl.TRIANGLE_STRIP,0,position.length/3);

	ambient = [];

	window.addEventListener('mousedown',mouseDown,true);
	window.addEventListener('mouseup',mouseUp,true);
	window.addEventListener('mousemove',mouseMove,true);
}

function rendering(){
	gl.disable(gl.BLEND);

	//フレームバッファをバインド
	gl.bindFramebuffer(gl.FRAMEBUFFER,frontBuffer.f);

	gl.viewport(0,0,TEXTURE_WIDTH,TEXTURE_HEIGHT);

	gl.clearColor(0.0,0.0,0.0,0.0);
	gl.clear(gl.COLOR_BUFFER_BIT);

	//プログラムオブジェクトの選択
	gl.useProgram(vPrg);

	//テクスチャとしてバックバッファをバインド
	gl.bindTexture(gl.TEXTURE_2D,backBuffer.t);

	//テクスチャへ頂点情報をレンダリング
	setAttribute(planeVBOList,vAttLocation,vAttStride);
	gl.uniform2fv(vUniLocation[0],resolution);
	gl.uniform1i(vUniLocation[1],0);
	gl.uniform2fv(vUniLocation[2],[mousePositionX,mousePositionY]);
	gl.uniform1i(vUniLocation[3],mouseFlag);
	gl.uniform1f(vUniLocation[4],velocity);
	gl.drawArrays(gl.TRIANGLE_STRIP,0,position.length/3);

	//パーティクル色を設定
	count ++;
	ambient = hsva(count % 360,1.0,0.8,1.0);

	gl.enable(gl.BLEND);

	gl.viewport(0,0,canvas.width,canvas.height);
	gl.bindFramebuffer(gl.FRAMEBUFFER,null);
	gl.clearColor(0.0,0.0,0.0,0.0);
	gl.clear(gl.COLOR_BUFFER_BIT);

	//プログラムオブジェクトの選択
	gl.useProgram(pPrg);

	//テクスチャとしてフロントバッファをバインド
	gl.bindTexture(gl.TEXTURE_2D,frontBuffer.t);

	//頂点を描画
	setAttribute(vVBOList,pAttLocation,pAttStride);
	gl.uniform2fv(pUniLocation[0],resolution);
	gl.uniform1i(pUniLocation[1],0);
	gl.uniform1f(pUniLocation[2],velocity);
	gl.uniform4fv(pUniLocation[3],ambient);
	gl.drawArrays(gl.POINTS,0,vertices.length);

	gl.flush();

	//加速度の調整
	if(mouseFlag){
		velocity = 1.0;
	}else{
		velocity *= 0.95;
	}

	//フレームバッファをフリップ
	flip = backBuffer;
	backBuffer = frontBuffer;
	frontBuffer = flip;

	requestAnimationFrame(rendering);
}

//===============================================================
// Controll
//===============================================================
function mouseDown(event){
	mouseFlag = true;
}
function mouseUp(event){
	mouseFlag = false;
}
function mouseMove(event){
	if(mouseFlag){
		const cw = canvas.width;
		const ch = canvas.height;
		mousePositionX = (event.clientX - canvas.offsetLeft - cw / 2.0) / cw * 2.0;
		mousePositionY = -(event.clientY - canvas.offsetTop - ch / 2.0) / ch * 2.0;
	}
}

//===============================================================
// Function
//===============================================================
//シェーダを生成
function createShader(shaderType,shaderText){
	const shader = gl.createShader(shaderType);
	gl.shaderSource(shader,shaderText);
	gl.compileShader(shader);
	return shader;
}

//プログラムオブジェクトを生成しシェーダをリンク
function createProgram(vs,fs){
	const program = gl.createProgram();
	gl.attachShader(program, vs);
	gl.attachShader(program, fs);
	gl.linkProgram(program);
	gl.useProgram(program);
	return program;
}

//VBOを生成
function createVbo(data){
	const vbo = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER,vbo);
	gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data),gl.STATIC_DRAW);
	gl.bindBuffer(gl.ARRAY_BUFFER,null);
	return vbo;
}

//VBOのバインド、登録
function setAttribute(vbo,attL,attS){
	for(let i in vbo){
		gl.bindBuffer(gl.ARRAY_BUFFER,vbo[i]);
		gl.enableVertexAttribArray(attL[i]);
		gl.vertexAttribPointer(attL[i],attS[i],gl.FLOAT,false,0,0);
	}
}

//フレームバッファをオブジェクトとして生成
function createFramebuffer(width,height,format){
	let textureFormat = null;
	if(!format){
		textureFormat = gl.UNSIGNED_BYTE;
	}else{
		textureFormat = format;
	}

	const frameBuffer = gl.createFramebuffer();
	gl.bindFramebuffer(gl.FRAMEBUFFER,frameBuffer);

	const depthRenderBuffer = gl.createRenderbuffer();
	gl.bindRenderbuffer(gl.RENDERBUFFER,depthRenderBuffer);

	gl.renderbufferStorage(gl.RENDERBUFFER,gl.DEPTH_COMPONENT16,width,height);

	gl.framebufferRenderbuffer(gl.FRAMEBUFFER,gl.DEPTH_ATTACHMENT,gl.RENDERBUFFER,depthRenderBuffer);

	const fTexture = gl.createTexture();
	gl.bindTexture(gl.TEXTURE_2D,fTexture);

	gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,width,height,0,gl.RGBA,textureFormat,null);

	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

	gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,fTexture, 0);

	gl.bindTexture(gl.TEXTURE_2D,null);
	gl.bindRenderbuffer(gl.RENDERBUFFER,null);
	gl.bindFramebuffer(gl.FRAMEBUFFER,null);

	return {f:frameBuffer,d:depthRenderBuffer,t:fTexture};
}

完成したデモになります。GPGPUでパーティクルアニメーションを試してみました。
※現時点では、iOS端末では動作しません。(2021年3月6日)

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

WebGLでハーフトーンシェーディング

次の記事へ

WebGLでシェーダ(GLSL)入門