見出し画像

コピペで慣れる生WebGL#02パーティクル

先週の#01ディストーションエフェクトの紹介を行いました。プログラム1つ、プログラムで使用するUniformを1つにするなど非常にシンプルなサンプルでした。

第2回目も非常によく目にするパーティクルについてのサンプルです。

パーティクルは
- 3Dモデルと一緒に使う
- 背景の一部
- シーンの遷移のアニメーション
- メインビジュアルのアニメーション・演出
など使用範囲が非常に広く、自由度が高いです。
個人で実験・スケッチとしてパーティクル用いた作品を発表するのもよく目にします。unityでパーティクルシステムなどあり、どのように使用されるかみていくのも非常に勉強になると思います。

最近目にしたパーティクルを使ったサイトを2つほど紹介します。


画像1

画像2



WebGLの中でパーティクルをどのように動かすかについては3通りあります。(1パーティクルを1ドロー関数してレンダリングする方法はカウントしてません。もっといい方法があれば教えてください。)


パーティクルの動かし方
・VBOで頂点を逐次更新する(WebGLを対応しているブラウザ。)
・GPGPUを使用し頂点情報をテクスチャに更新する(half float textureもしくはfloat texture対応ブラウザのみ使用可能。大半のブラウザは対応していますが、Andoroidの4以下は対応していなかったりするので、確認していくことを勧めます。)
・Transform Feedbackを使用し頂点情報をテクスチャに更新する(WebGL2対応ブラウザのみ使用可能)


下記のサイトでVBO、GPGPU、Transform Feedbackについては詳しく説明しているので、参考にしてください。


今回のサンプルではVBOを逐次更新していく手法を採用します。
3つの手法の中で1番簡単で、javascriptで頂点を更新することはできます。しかし、頂点を更新する際にbufferを更新するので他の方法に比べて遅くなります。




今回作成したパーティクルを使用したサンプルです。

画像3

パーティクルで画像、文字を作成しています。クリックすると画像がアニメーションします。


生WebGLバージョン+コード

three.jsバージョン+コード


1. レンダリングを開始する前に行うパーティクルの初期化
2. 毎フレーム実行するdraw関数などのパーティクルのレンダリング
3. マウスを動かしたり、クリックすることで変化するインタラクション
この大きく3つに分けて、前回と同様にthree.jsと生WebGLのパーティクルの箇所を簡単に説明します。
今回はパーティクル以外にもバックグランドを作成しましたが、バックグランドは前回と同じように作成しましたので割愛します。



初期化

生WebGL・three.jsの共通部分

シェーダー(vertex, fragment)の作成

// パーティクルのシェーダー
const particleVertexShaderSrc = `
precision mediump float;

attribute vec4 position;
attribute vec4 color;

uniform vec2 uWindow;
uniform vec2 uTrans;
uniform float uAngle;

varying vec4 vColor;

vec2 rotate(vec2 v, float a) {
	float s = sin(a);
	float c = cos(a);
	mat2 m = mat2(c, -s, s, c);
	return m * v;
}

void main() {
    vec2 trasformedPos = rotate(vec2(position.x, position.z), uAngle);
    vec3 newPosition = vec3(trasformedPos.x, position.y, trasformedPos.y);
    // newPositionのzの大きさによってscaleを変化させる
    float scale = max(1.0 - newPosition.z / 1800., 0.01);

    gl_Position = vec4( (newPosition.x * scale + uTrans.x) / uWindow.x , (newPosition.y * scale + uTrans.y) /uWindow.y , 0.0, 1.0);
    gl_PointSize = 3. * scale;
    
    vColor = color;
}
`;

const particleFragmentShaderSrc = `
precision mediump float;

varying vec4 vColor;

void main(){
    vec2 diff = gl_PointCoord - vec2(.5, .5);
    if (length(diff) > 0.5) discard; // 簡単な丸にするようしています

    gl_FragColor = vColor;
}
`;

前回と同様シェーダーは生WebGLとthree.jsで同じシェーダーを使用しています。
生WebGLのdraw modeはgl.POINTS、three.jsではTHREE.Pointで作成しています。

パーティクルの大きさをgl_PointSizeで決めることができるので、zの大きさによって変化させるようにしています。(vertexシェーダーのscaleという変数がその役割を担っています。)

gl.POINTSは1つの頂点で1つの点をドローします。頂点情報の入ったバッファーを小さくことができます。しかし、gl.TRIANGLESについて比べ自由度は減ります。

今回のサンプルでは共通してParticleクラスParticleManagerクラスを使用し、パーティクルのレンダリングを行っています。

ParticleクラスParticleManagerクラスはコードの行数が長くなるので、簡単に何をしているのかを説明します。

Particleクラス

・生WebGLとthree.jsでコードは共通
・現在の色情報、位置情報を保持
・画像、テキストの色情報、位置情報を保持
ParticleManagerクラス

・ 生WebGLではバッファーやプログラムのプロパーティ、three.jsではmesh、material、bufferGeometry、bufferAttributeのプロパーティクラス
・共通してParticle配列をクラス
・updateメソッドでparticle配列の値を更新し、位置情報のpositionBufferと色情報のcolorBufferに代入する。
・ 生WebGLではdrawメソッド保持。webglのdraw関数を実行する


サンプルはcodepenでも見れますが、見にくいと思い別途ParticleMangerクラスとParticleクラスを別のファイルにgistにアップしています。


ParticleManagerの_createProgramメソッドと_createBuffer()はWebGL実行するのに必要なprogram生成、バッファの初期化を行っています。

_createProgram() {
    // purogramの初期化
    this._program = createProgram(
        particleVertexShaderSrc,
        particleFragmentShaderSrc
    );

    // uniformのロケーションを予め取得しておく
    this._uniformWindowLocation = gl.getUniformLocation(
        this._program,
        'uWindow'
    );
    this._uniformAngleLocation = gl.getUniformLocation(
        this._program,
        'uAngle'
    );
    this._uniformTransLocation = gl.getUniformLocation(this._program, 'uTrans');
}

_createBuffer() {
    // positionバッファの初期化
    this._positionBuffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, this._positionBuffer);
    gl.bufferData(
        gl.ARRAY_BUFFER,
        this._particlePositionsArray,
        gl.STATIC_DRAW
    );
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    this._positionLocation = gl.getAttribLocation(this._program, "position");

    // colorバッファの初期化
    this._colorBuffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, this._colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this._particleColorsArray, gl.STATIC_DRAW);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    this._colorLocation = gl.getAttribLocation(this._program, "color");
}


three.jsではBufferGoemetryをgeometryとして、RawShaderMaterialをmaterialとして使用し、meshではなくpointを使用します。
生WebGLで使用したbufferはthree.jsではBufferAttributeとして使用でき、BufferGeometryのattributeとして追加できます。

_createMesh() {
    this.geometry = new THREE.BufferGeometry();
    this.material = new THREE.RawShaderMaterial({
        uniforms: {
            uWindow: {
                value: new THREE.Vector2(windowWid, windowHig)
            },
            uTrans: {
                value: new THREE.Vector2(this._transX, this._transY)
            },
            uAngle: {
                value: 0
            },
        },
        vertexShader: particleVertexShaderSrc,
        fragmentShader: particleFragmentShaderSrc,
    });
    this.material.transparent = true;

    this.points = new THREE.Points(this.geometry, this.material);
}

_createBufferAttribute() {

    this._positionAttribute = new THREE.BufferAttribute(this._particlePositionsArray, 3);
    this._colorAttributes = new THREE.BufferAttribute(this._particleColorsArray, 4);

    this.geometry.addAttribute('position', this._positionAttribute);
    this.geometry.addAttribute('color', this._colorAttributes);

}


生WebGL

ParticleManagerのインスタンスを生成

function createParticleManager() {
   steakParticleManager = new ParticleManager("left");
   sushiParticleManager = new ParticleManager("right");
   particleManagerArr.push(steakParticleManager, sushiParticleManager);
}

テキスト、画像ファイルのローディング終了したら、テキスト、画像の色、位置情報をparticleManagerに代入する。

steakParticleManager.setTextData(textData['steak']);
steakParticleManager.setPicturData(steakImgDataArr);
sushiParticleManager.setTextData(textData['sushi']);
sushiParticleManager.setPicturData(sushiImgDataArr);


three.js

ParticleManagerのインスタンスを生成とプロパティのpointsをsceneにaddする。

function createParticleManager() {
    steakParticleManager = new ParticleManager("left");
    sushiParticleManager = new ParticleManager("right");
    particleManagerArr.push(steakParticleManager, sushiParticleManager);

    scene.add(steakParticleManager.points);
    scene.add(sushiParticleManager.points);
}

生WebGL同様、テキスト、画像ファイルのローディング終了したら、テキスト、画像の色、位置情報をparticleManagerに代入する。

steakParticleManager.setTextData(textData['steak']);
steakParticleManager.setPicturData(steakImgDataArr);
sushiParticleManager.setTextData(textData['sushi']);
sushiParticleManager.setPicturData(sushiImgDataArr);



レンダリング

生WebGLとthree.jsは共に毎フレームloopという関数を実行しています。


生WebGL

function loop() {
    // WebGLを初期化する
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0, 0, 0, 1);
    gl.clearDepth(1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // 背景の描画
    drawBg();

    particleManagerArr.forEach(particleManager => {
        particleManager.update(mouseX);
        particleManager.draw();
    });

    loopId = requestAnimationFrame(loop);
}

particleのdraw部分はgl.drawArraysというドロー関数を実行しています。

// particleMnagerクラスのdrawメソッド部分

draw() {
    gl.useProgram(this._program);

    gl.bindBuffer(gl.ARRAY_BUFFER, this._positionBuffer);
    if (this._needsUpdate) {
        gl.bufferSubData(gl.ARRAY_BUFFER, 0, this._particlePositionsArray);
        // gl.bufferData(gl.ARRAY_BUFFER, this._particlePositionsArray, gl.STATIC_DRAW); <-この手法でも可能。Bufferが初期化されるので、最適ではない。
    }
    gl.vertexAttribPointer(this._positionLocation, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(this._positionLocation);

    gl.bindBuffer(gl.ARRAY_BUFFER, this._colorBuffer);
    if (this._needsUpdate) {
        gl.bufferSubData(gl.ARRAY_BUFFER, 0, this._particleColorsArray);
        // gl.bufferData(gl.ARRAY_BUFFER, this._particleColorsArray, gl.STATIC_DRAW); <-この手法でも可能。Bufferが初期化されるので、最適機ではない。
    }
    gl.vertexAttribPointer(this._colorLocation, 4, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(this._colorLocation);

    gl.uniform2f(this._uniformWindowLocation, windowWid, windowHig);
    gl.uniform2f(this._uniformTransLocation, this._transX, this._transY);
    gl.uniform1f(this._uniformAngleLocation, this._rot);

    // ブレンドモードを設定する
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.enable(gl.BLEND);

    // draw関数を実行する
    gl.drawArrays(gl.POINTS, 0, this._particleSize);
    this._needsUpdate = false;
}

_needsUpdataがtrueの場合bufferの配列を更新します。buffer自体を更新するのは時間かかりやすいので、必要な時のみ更新するようにします。

three.js

function loop() {
   mat.uniforms.uTrans.value = obj.loadTrans;
   particleManagerArr.forEach(particleManager => {
       particleManager.update(mouseX);
   });
   renderer.render(scene, camera);
   requestAnimationFrame(loop);
}

前回と同様three.jsのWebGLRendererのrenderメソッドは以下から確認することができます。



インタラクション


document.body.addEventListener('mousemove', (event) => {
   mouseX = event.clientX / windowWid;
});
document.body.addEventListener('mouseleave', (event) => {
   mouseX = 9999;
});
document.body.addEventListener('click', (event) => {
   for (let ii = 0; ii < particleManagerArr.length; ii++) {
       particleManagerArr[ii].click();
   }
});

共通して同じ関数を使っています。mouseXの値を毎フレームparticleManagerのupdateメソッドに代入しています。



今回のデモから複数のプログラムを使い、少しコードが長くなりました。(生WebGL 791行 / three.js 701行)細々としたところやparticleクラスの動作に関わるメソッドですぐに増えますね。。

共通する部分が多いので、生WebGLからthree.jsの書き換えは1時間もあればできました。(次回はthree.jsから生WebGLの書き換えをやってみたいと思います。)



コピペで慣れるWebGL

#00 はじめに
#01 ディストーションエフェクト
#02パーティクル



この記事が気に入ったらサポートをしてみませんか?