Three.jsでGPUTrails

#概要
パーティクル飛ばすと次に、やってみたくなりますよね。Trail。
今回はThree.js & GPUComputationRendererでGPUを使ったヌルヌルトレイルを実装して見たいと思います。

Gitは[こちら](https://github.com/ukonpower/glsl-graphics/tree/master/src/gl16/js/utils/Trails)
デモは[こちら](https://oregl.ukon.dev/gl/GPUTrails/)

#方針
GPUComputationRendererでは計算結果の保存に**テクスチャ**を使用します。
トレイルでは**トレイルの個数**とそれぞれの**長さ**分の頂点座標を保持する必要があります。
そのためGPUComputationRendererは以下の図のように**トレイル一本の長さxトレイルの本数**で初期化します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/c1a65f68-ecbd-af79-5741-d9062c46bd63.png)

また、計算自体は最初の頂点一つだけ行い、他の頂点は一つ隣の座標をそのままコピーすれば良いでしょう。
#実装
## GPUComputationRendererの作成

```javascript:Trails.js
initComputeRenderer(){        
        this.computeRenderer = new GPUComputationRenderer(this.length,this.num,this.renderer);
        
        let initPositionTex = this.computeRenderer.createTexture();
        let initVelocityTex = this.computeRenderer.createTexture();

        this.initPosition(initPositionTex);
    
        this.comTexs.position.texture = this.computeRenderer.addVariable("texturePosition",comShaderPosition,initPositionTex);
        this.comTexs.velocity.texture = this.computeRenderer.addVariable("textureVelocity",comShaderVelocity,initVelocityTex);

        this.computeRenderer.setVariableDependencies( this.comTexs.position.texture, [ this.comTexs.position.texture, this.comTexs.velocity.texture] );
        this.comTexs.position.uniforms = this.comTexs.position.texture.material.uniforms;

        this.computeRenderer.setVariableDependencies( this.comTexs.velocity.texture, [ this.comTexs.position.texture, this.comTexs.velocity.texture] );  
        this.comTexs.velocity.uniforms = this.comTexs.velocity.texture.material.uniforms;
        this.comTexs.velocity.uniforms.time =  { type:"f", value : 0};

        this.computeRenderer.init();
    }
```
位置保存テクスチャと速度保存テクスチャを作成します。

```javascript:Trails.js
initPosition(tex){
        var texArray = tex.image.data;
        let range = new THREE.Vector3(10,10,10);
        for(var i = 0; i < texArray.length; i += this.length * 4){
            let x = Math.random() * range.x - range.x / 2;
            let y = Math.random() * range.y - range.y / 2;
            let z = Math.random() * range.z - range.z / 2;
            for(let j = 0; j < this.length * 4; j += 4){
                texArray[i + j + 0] = x;
                texArray[i + j + 1] = y;
                texArray[i + j + 2] = z;
                texArray[i + j + 3] = 0.0;
            }
        }
    }
```
位置テクスチャをランダムな位置で初期化します。
~~テクスチャの初期値は０なので速度は初期化しなくて大丈夫です。~~

**追記(2019/05/30)**
初期値は0ではありませんでした！！
申し訳ありません...

今回のGPUTrailsでは必要ありませんが、テクスチャの使い道によっては以下のように初期化をします。

```javascript
initVelocity(tex) {
    var texArray = tex.image.data;
    let range = new THREE.Vector3(10,10,10);
    for (var i = 0; i < texArray.length; i += 4) {
        let x = Math.random() * range.x - range.x / 2;
        let y = Math.random() * range.y - range.y / 2;
        let z = Math.random() * range.z - range.z / 2;
        texArray[i + 0] = x;
        texArray[i + 1] = y;
        texArray[i + 2] = z;
        texArray[i + 3] = 0.0;
    }
}
```

## Trailジオメトリの作成

```javascript:Trails.js
createTrails(){
        let geo = new THREE.BufferGeometry();

        let pArray = new Float32Array(this.num * this.length * 3);
        let indices = new Uint32Array((this.num * this.length - 1) * 3);
        let uv = new Float32Array(this.num * this.length * 2);

        let max = this.length * this.n;

        for(let i = 0; i < this.num; i++){
            for(let j = 0; j < this.length; j++){
                let c = i * this.length + j;
                let n = (c) * 3;
                pArray[n] = 0;
                pArray[n + 1] = 0;
                pArray[n + 2] = 0;

                uv[c * 2] = j / this.length;
                uv[c * 2 + 1] = i / this.num;

                indices[n] = c;
                indices[n + 1] = Math.min(c + 1,i * this.length + this.length - 1);
                indices[n + 2] = Math.min(c + 1,i * this.length + this.length - 1);
            }
        }
        
        geo.addAttribute('position', new THREE.BufferAttribute( pArray, 3 ) );
        geo.addAttribute('uv', new THREE.BufferAttribute( uv, 2 ) );
        geo.setIndex(new THREE.BufferAttribute(indices,1));

        this.uni = {
            texturePosition : {value: null},
            textureVelocity : {value: null},
        }

        let mat = new THREE.ShaderMaterial({
            uniforms: this.uni,
            vertexShader: vert,
            fragmentShader: frag,
        });
        mat.wireframe = true;

        this.obj = new THREE.Mesh(geo,mat);
        this.obj.matrixAutoUpdate = false;
        this.obj.updateMatrix();
    }
```
Trailのジオメトリを作成します。
THREE.Lineだと線が切り離せなかったため、indexを使ってMeshをつぎはぎしてLineに見せています。(多分、ダメ。絶対。)

```glsl:trails.vs
#include <common>
uniform sampler2D texturePosition;
varying vec4 vColor;

void main() {
    vec3 pos = texture2D( texturePosition, uv ).xyz;
    
    vec3 c = vec3(uv.y,sin(uv.y * 3.0),1.0); 
    vColor = vec4(c,1.0);

    vec4 mvPosition = modelViewMatrix * vec4( pos + position, 1.0 );
    gl_Position = projectionMatrix * mvPosition;
}
```

```glsl:trails.fs
varying vec4 vColor;
void main() {
    gl_FragColor = vColor;
}
```
トレイルのシェーダーです。

##コンピュートシェーダーを作成
```javascript:computeVelocity.glsl
void main() {
    if(gl_FragCoord.x >= 1.0) return;    
    
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec3 pos = texture2D( texturePosition, uv ).xyz;
    vec3 vel = texture2D( textureVelocity, uv ).xyz;
    float idParticle = uv.y * resolution.x + uv.x;

    vel.xyz += 40.0 * vec3(
      snoise( vec4( 0.1 * pos.xyz, 7.225 + 0.5 * time ) ),
      snoise( vec4( 0.1 * pos.xyz, 3.553 + 0.5 * time ) ),
      snoise( vec4( 0.1 * pos.xyz, 1.259 + 0.5 * time ) )
    ) * 0.2;
    vel += -pos * length(pos) * 0.1;
    vel.xyz *= 0.9 + abs(sin(uv.y * 9.0)) * 0.03;

    gl_FragColor = vec4( vel.xyz, 1.0 );
}
```
速度の計算です。一番左のピクセルの時のみ、新たな速度を計算しています。
ノイズは毎度ながら[こちら](https://github.com/ashima/webgl-noise/blob/master/src/noise4D.glsl)のnoise4Dを利用させていただきました。

```javascript:computePosition.glsl
void main() {
    if(gl_FragCoord.x <= 1.0){
        vec2 uv = gl_FragCoord.xy / resolution.xy;
        vec3 pos = texture2D( texturePosition, uv ).xyz;
        vec3 vel = texture2D( textureVelocity, uv ).xyz;

        pos += vel * 0.01;
        gl_FragColor = vec4(pos,1.0);
        
    }else{
        vec2 bUV = (gl_FragCoord.xy - vec2(1.0,0.0)) / resolution.xy;
        vec3 bPos = texture2D( texturePosition, bUV ).xyz;  
        gl_FragColor = vec4(bPos,1.0);
    }
}
```
位置更新のシェーダーです。
一番左のピクセルの時は速度テクスチャから速度を取得して座標に加算しています。
それ以外は一つ左のピクセルの座標をそのまま上書きしています。

##コンピュート！！
```javascript:Trails.js
    update(){
        this.time += this.clock.getDelta();

        this.comTexs.velocity.uniforms.time.value = this.time;
        this.uni.texturePosition.value = this.computeRenderer.getCurrentRenderTarget(this.comTexs.position.texture).texture;
        this.uni.textureVelocity.value = this.computeRenderer.getCurrentRenderTarget(this.comTexs.velocity.texture).texture;
        this.computeRenderer.compute();
    }
```
uniformsに値を入れてコンピュートレンダラーを実行してします。

##いざ生成
```javascript:MainScene.js
        this.trails = new Trails(this.renderer,2000,30);
        this.scene.add(this.trails.obj);
```
sceneに長さ30頂点のトレイルを2000本生成します。

##まとめ
今度はMesh貼ってリッチなトレイル作りたいです。