three.jsサンプルを解析してみた。風ではためく布表現

今回からthree.jsの大量にあるサンプルを、気楽に解析していきたいと思います。今回はこちらのサンプル

風ではためく布のような表現方法です。再利用しやすいように、サンプルよりも機能はシンプルにまとめて、簡易なサンプルを作成しました。

20160406_img

ソースコードは以下より参照ください。

再利用しやすいように、ES6のクラスにまとめています。まだES6に不慣れなところがありますが、変な記述していたらすいません。

└── src : ソースファイルディレクトリ。同一階層のbuildディレクトリに公開用ソールを出力します。
    ├── babel
    │   ├── Main.es6 : Clothクラスを実行したり、three.jsの基本処理を行うメインのスクリプト
    │   └── cloth
    │       └── Cloth.es6 : 布表現の機能を有したClothクラス
    └── jade
        └── index.jade

幾つかのポイントに分けて説明します。

パラメトリック曲面

旗のジオメトリにパラメトリック曲面を利用しています。パラメトリック曲面は2つのパラメータによって表現される曲面です。パラメトリック曲面は、パラメトリック曲線を組み合わせることで構成できます。

パラメトリック曲面

とは言ってもこのサンプルの場合、表現上必要な分だけ分割した平面を生成するためだけにパラメトリック曲面のジオメトリを利用していますので、曲面を描いているわけではありません。そのためパラメトリック曲面に渡す式は非常にシンプルです。

src/babel/Main.es6 (Line: 38)

var cloth = new Cloth(SEGMENTS_X, SEGMENTS_Y, DISTANCE, function(u, v){
  var x = (u - 0.5) * DISTANCE * SEGMENTS_X;
  var y = (v + 0.5) * DISTANCE * SEGMENTS_Y;
  var z = 0;
  return new THREE.Vector3(x, y, z);
});

「Main.es6」から渡された関数を元に「Cloth.es6」のClothクラスにて、パラメトリック曲面を生成しています。

src/babel/cloth/Cloth.es6 (Line: 15)

this.geometry = new THREE.ParametricGeometry(this.paramFunc, this.segmentX, this.segmentY);

風のシミュレーション

「Cloth.es6」のClothクラスwindSimulate関数で、時間を引数として、風のシミュレーションを行っています。三角関数で風のベクトルを決定し、this.windForceに格納、面の頂点毎に風のシミュレーションを行います

src/babel/cloth/Cloth.es6 (Line: 68)

this.windForce.set(Math.sin(time / 2000), Math.cos(time / 3000), Math.sin(time / 1000)).normalize().multiplyScalar(200);

以下の記述で法線と頂点を自動的に再計算させ更新します。

src/babel/cloth/Cloth.es6 (Line: 73)

this.geometry.computeFaceNormals();
this.geometry.computeVertexNormals();
this.geometry.normalsNeedUpdate = true;
this.geometry.verticesNeedUpdate = true;

旗をポールに固定していないと、旗が飛んで行ってしまいますので、一番右端の頂点を常に移動しないように、初期位置に戻します。

src/babel/cloth/Cloth.es6 (Line: 91)

for (i = 0, max = this.particles.length; i < max; i = i + 1) {
    if ((i % (this.segmentX + 1)) == 0) {
        this.particles[i].position.copy(this.particles[i].original);
        this.particles[i].previous.copy(this.particles[i].original);
    }
}

風のシミュレーションを行うwindSimulate関数の全体は以下になります。実際にどのように風のシミュレーションを行っているのかは、ソースを参考に丁寧に読み解けば、わかるかと思います。ちなみに僕はこれ以上読み解いてないのでわかりません。

src/babel/cloth/Cloth.es6 (Line: 63)

windSimulate(time) {
    if (!this.lastTime) {
        this.lastTime = time;
        return;
    }
    this.windForce.set(Math.sin(time / 2000), Math.cos(time / 3000), Math.sin(time / 1000)).normalize().multiplyScalar(200);
    var i = 0, max;
    for (i = 0, max = this.particles.length; i < max; i = i + 1) {
        this.geometry.vertices[i].copy(this.particles[i].position);
    }
    this.geometry.computeFaceNormals();
    this.geometry.computeVertexNormals();
    this.geometry.normalsNeedUpdate = true;
    this.geometry.verticesNeedUpdate = true;
    var faces = this.geometry.faces;
    for (i = 0, max = faces.length; i < max; i = i + 1) {
        this.tmpForce.copy(faces[i].normal).normalize().multiplyScalar(faces[i].normal.dot(this.windForce));
        this.particles[faces[i].a].addForce(this.tmpForce);
        this.particles[faces[i].b].addForce(this.tmpForce);
        this.particles[faces[i].c].addForce(this.tmpForce);
    }
    for (i = 0, max = this.particles.length; i < max; i = i + 1) {
        this.particles[i].addForce(new THREE.Vector3(0, -(981 * 1.4), 0).multiplyScalar(0.1));
        this.particles[i].integrate((18 / 1000) * (18 / 1000));
    }
    for (i = 0, max = this.constrains.length; i < max; i = i + 1) {
        this.satisifyConstrains_(this.constrains[i][0], this.constrains[i][1], this.constrains[i][2]);
    }
    for (i = 0, max = this.particles.length; i < max; i = i + 1) {
        if ((i % (this.segmentX + 1)) == 0) {
            this.particles[i].position.copy(this.particles[i].original);
            this.particles[i].previous.copy(this.particles[i].original);
        }
    }
};

まとめ

はじめは、どうやってるんだろう。きっと便利なクラスがあるんだろうと思っていましたが、そんなに甘くはないんですね。頂点毎に自力で計算させて、変形させているんですね。布表現においては、分割数次第では負荷が高いと思いますので、気をつけてご利用ください。引き続きサンプルを気楽に解析していきます。