読者です 読者をやめる 読者になる 読者になる

Three.js r76 で星空を作ってみた

JavaScript Threejs

リアルな感じではなくGIFアニメっぽい感じです。
完成系はこちらです。

Three.jsはまだ触れ始めたばかりですので手順として誤りなどが含まれているかもしれません。

目次

星を斜めに移動させたいのでカメラを回転させました

SphereGeometryを利用して挑んでいます。

カメラを引いてみると次のようになります。

f:id:nfnoface:20161202172610g:plain

この球体を利用すると地球に例えるなら北極と南極に当たる部分には頂点が密集しています。
この頂点が密集してる部分を利用すると等間隔っぽい並びにならなくなるためカメラに映りこまないようにしたいと考えました。
球体のY軸だけ回せばこの頂点が密集してる部分はカメラに映りこまないので、
斜めに移動しているように見せるためカメラそのもののZ軸を回すことにしました。

テクスチャ画像はjsにBase64エンコードして埋め込んでいます

やり方が悪かったのかデスクトップ上でテクスチャが読まれないので、テクスチャ画像をjsに埋め込みました。

// http://icooon-mono.com/14622-%E6%98%9F%E3%81%AE%E7%84%A1%E6%96%99%E7%B4%A0%E6%9D%907/
var starImg = new Image();
starImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABPElEQVQ4T5WTwU3DQBBF/48FV0IFpANikdyhAtwBOWCLG6GDdADcUMwhdGAqgDtGTirAdJBcg+xBu4sdrxU7ZCQfrJl5uzP/L9ESEgeeSnMwjZrK2JSQ5KaHLP/SeWd9THe23FbbDPgMxhDc6ybijmfTh/0AsZ8CPNFNInMOQ/ffAEn8PjImVoMjLt1wXodQL4o4NSehD0gXYA+A+qqRAqJutQRhQIKFAqjlHLWp0ZJbUT6uz8GOkmlfyAqSe1qFP8kUxIyyOxZwOh7dp7SUUZJRF9mhkupqR/8LnPW48IXlg63br9NqatiAqnmavWuZygbEfgTwsn0EeeUg1G/EmLQSEgey+ZVviIxMFWelK83jKvs2SzRyvhmAPML5mRSLMgs+mAC8Nen8gsPnd+sGZZFIVCTroxjP0KvCfwGZVYAhUmIo4gAAAABJRU5ErkJggg==';
var starTexture = new THREE.Texture();
starTexture.needsUpdate = true;
starTexture.image = starImg;

なお、テクスチャとして画像はこちらのものを利用させていただいています。

画像をjsに埋め込む際Base64エンコードをするため、「画像 base64エンコード」のキーワードでヒットしたサイトを利用しました。

SphereGeometryの頂点座標を活用しPlaneGeometryの星を配置しました

以前作ったThreejs r76で球体&パーティクルの頂点部分が星になれば良いかと考えて、SphereGeometryを利用しています。当初はPointsの個々に貼り付けたテクスチャを回転させることが出来ればと考えましたが、テクスチャを回転させるような手段は用意されていないようで、個々の星はPlaneGeometryを用いてSphereGeometryの頂点部分に貼り付ける形としています。

親の球体に子の星を追加しています

言い回しがあっているかわかりませんが、シーンに直接加えるとグローバル空間上の中心を軸とするようです。
この中心を軸に星々を回転させることは可能でしたが、星々をその場で回転させることは容易でないようです。
そこで、球体を親とし星を子として加えることで子の星はZ軸を回転させるだけでその場で回転させることが行えました。
なお、親を回転させれば子も追随してグローバル空間上を回転するので、やりたいことを達成しました。

  • 球体の頂点が密集している北極と南極に当たる部分はカメラに映りこまないのでその部分は除外するようにしています
if (this.starSphere[this.starSphere.length - 1].geometry.vertices[i].y > 130 || this.starSphere[this.starSphere.length - 1].geometry.vertices[i].y < -130) {
    continue;
}
  • 星になるPlaneGeometryを用意し
var plane = new THREE.Mesh(
 new THREE.PlaneGeometry(starSize, starSize),
  material.clone()
);
  • 親となる球体this.starSphere[this.starSphere.length - 1]の頂点座標を活用して
plane.position.set(
    this.starSphere[this.starSphere.length - 1].geometry.vertices[i].x + ((variability * Math.random()) - (variability / 2)),
    this.starSphere[this.starSphere.length - 1].geometry.vertices[i].y + ((variability * Math.random()) - (variability / 2)),
    this.starSphere[this.starSphere.length - 1].geometry.vertices[i].z
);
  • addしています
this.starSphere[this.starSphere.length - 1].add(plane);
  • 子を親に加えた後は親のオブジェクトをシーンに加えます
this.scene.add(this.starSphere[this.starSphere.length - 1]);

球体はあくまでも軸としているだけなのでMaterialは不要と思っていましたが。

やり方の問題かもしれませんが、子の星を追加していない状態ならば何もレンダリングされないようなのですが、Material未指定のPointsを利用したところ球体の頂点が青白い点でレンダリングされてしまいました。

this.starSphere.push(new THREE.Points(
    new THREE.SphereGeometry(200, particleSize, particleSize)
));
f:id:nfnoface:20161202174239g:plain

透明のMaterialを利用することで回避しました。

this.starSphere.push(new THREE.Points(
    new THREE.SphereGeometry(200, particleSize, particleSize),
    new THREE.PointsMaterial({transparent: true, opacity: 0})
));

Raycasterでマウスに反応するようにしてみました

マウスに流れ星以外の星が重なると色が変わり回転が速くなります。

f:id:nfnoface:20161202173345g:plain
  • マウスが動いたら位置を記録
this.renderer.domElement.addEventListener('mousemove',function(e){
    this.mouseVector2.x = (e.clientX / this.renderer.domElement.width) * 2 - 1;
    this.mouseVector2.y = -(e.clientY / this.renderer.domElement.height) * 2 + 1;
}.bind(this), false);
  • マウスに重なった星intersectsを取り出して処理

※この際、個々の星の色を変えるためにmaterialclone()しています。同じマテリアルだと同じマテリアルを利用している星全ての色が変わってしまいます。

f:id:nfnoface:20161202180403g:plain
var checkMouseMove = (this.mouseVector2.x >= -1 && this.mouseVector2.x <= 1 && this.mouseVector2.y >= -1 && this.mouseVector2.y <= 1);
if (checkMouseMove) {
    this.raycaster.setFromCamera(this.mouseVector2, this.camera);
}

for (var i = this.starSphere.length - 1; i >= 0; --i) {if (checkMouseMove) {
        var intersects = this.raycaster.intersectObjects(this.starSphere[i].children);
        if (intersects.length > 0){
            for (var j = intersects.length - 1; j >= 0; --j) {
                var add = this.localAddZ[intersects[j].object.geometry.id];
                intersects[j].object.rotation.z += add > 0 ? 0.15 : -0.15;
                intersects[j].object.material.color = this.starMaterialMouseenterColor;
                intersects[j].object.scale.set(1.5, 1.5, 1);
            }
        }
    }
}

Raycasterの使い方についてはドキュメントにあるExampleを参考にしています。
https://threejs.org/docs/api/core/Raycaster.html

フレームレートは 30 fps にしています

Firefox 50.0.2 だと次のような警告が出力されます。

Error: WebGL: texImage2D: Incurred CPU-side conversion, which is very slow.
Error: WebGL: texImage2D: Incurred CPU pixel conversion, which is very slow.
Error: WebGL: texImage2D: Chosen format/type incurred an expensive reformat: 0x1908/0x1401

Firefoxの問題のようです。
https://bugzilla.mozilla.org/show_bug.cgi?id=1246410

requestAnimationFrame()を利用すると 60 fps になるそうですが、処理の負荷軽減になるかと思い 30 fps になるようにしました。

if (this.frame++ % 2 === 0) {
    this.render();
}

こちらのサイトを参考にしました。