Edited at

Three.jsでGPUTrails


概要

パーティクル飛ばすと次に、やってみたくなりますよね。Trail。

今回はThree.js & GPUComputationRendererでGPUを使ったヌルヌルトレイルを実装して見たいと思います。

Gitはこちら

デモはこちら


方針

GPUComputationRendererでは計算結果の保存にテクスチャを使用します。

トレイルではトレイルの個数とそれぞれの長さ分の頂点座標を保持する必要があります。

そのためGPUComputationRendererは以下の図のようにトレイル一本の長さxトレイルの本数で初期化します。

image.png

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


実装


GPUComputationRendererの作成


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();
}


位置保存テクスチャと速度保存テクスチャを作成します。


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;
}
}
}

位置テクスチャをランダムな位置で初期化します。

テクスチャの初期値は0なので速度は初期化しなくて大丈夫です。

追記(2019/05/30)

初期値は0ではありませんでした!!

申し訳ありません...

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

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ジオメトリの作成


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に見せています。(多分、ダメ。絶対。)


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;
}



trails.fs

varying vec4 vColor;

void main() {
gl_FragColor = vColor;
}

トレイルのシェーダーです。


コンピュートシェーダーを作成


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 );
}


速度の計算です。一番左のピクセルの時のみ、新たな速度を計算しています。

ノイズは毎度ながらこちらのnoise4Dを利用させていただきました。


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);
}
}


位置更新のシェーダーです。

一番左のピクセルの時は速度テクスチャから速度を取得して座標に加算しています。

それ以外は一つ左のピクセルの座標をそのまま上書きしています。


コンピュート!!


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に値を入れてコンピュートレンダラーを実行してします。


いざ生成


MainScene.js

        this.trails = new Trails(this.renderer,2000,30);

this.scene.add(this.trails.obj);

sceneに長さ30頂点のトレイルを2000本生成します。


まとめ

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