Help us understand the problem. What is going on with this article?

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

この記事は、Three.js Advent Calendar 2016 7日目の記事です。

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

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

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

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

完成品サンプルはこちら
zipでくれ

一番近いサンプルは?

今回の実装に一番近いthree.jsのサンプルは、instanced circle 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、えっといくつだ、たくさん!そんな配列が一度に、しかも複数きても難なく処理してくれます。バケモノですね。

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

//パーティクルを初期化する
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 です。

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の深い沼の中、GPUで暖をとる通路に繋がる階段が目の前に現れました。さぁ恐れず登ってみましょう。
ですが、ちゃんとthree.jsに帰ってきてくださいね。約束ですよ・・・

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

adrs2002
読み方は「じゃいあん」です。巻き舌気味でお願いします。日本の秘境「トチギー」の僻地で、 Three.jsとWebGL使って同人ゲーム作ろうとしてます。  本業はゲームとは関係ないC#いじってます。
http://www001.upp.so-net.ne.jp/adrs2002/teststage.html
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away