こんにちは。Google のデータ アーツ チームに所属している Michael Chang と申します。先日、近くの星を可視化する Chrome Experiments の 100,000 Stars を完成させました。このプロジェクトは THREE.js と CSS3D で構築されています。このケーススタディでは、検出プロセスについて説明し、プログラミング手法をいくつか紹介し、今後の改善点について説明します。
ここで説明するトピックはかなり広範囲にわたるため、THREE.js の知識が必要になりますが、技術的な事後分析としてお楽しみいただければ幸いです。右側の目次ボタンを使用して、興味のあるセクションに移動してください。まず、プロジェクトのレンダリング部分を示し、次にシェーダー管理、最後に CSS テキストラベルを WebGL と組み合わせて使用する方法を示します。

Space を見つける
Small Arms Globe を完成させた直後、被写界深度を使用した THREE.js パーティクル デモを試していました。適用する効果の量を調整することで、シーンの解釈された「スケール」を変更できることに気づきました。被写界深度効果が極端に強い場合、遠くのオブジェクトは、ティルトシフト写真が顕微鏡で見たような錯覚を与えるのと同じように、非常にぼやけていました。逆に、効果を弱めると、深宇宙を覗き込んでいるように見えました。
私は、粒子の位置を挿入するために使用できるデータを探し始めました。その結果、astronexus.com の HYG データベースにたどり着きました。これは、3 つのデータソース(Hipparcos、Yale Bright Star Catalog、Gliese/Jahreiss Catalog)をまとめたもので、事前に計算された xyz 直交座標が付属しています。では始めましょう


星のデータを 3D 空間に配置するものを 1 時間ほどで作成しました。データセットには 119,617 個の星が含まれているため、各星を粒子で表しても、最新の GPU では問題ありません。個別に識別された星も 87 個あるため、Small Arms Globe で説明したのと同じ手法を使用して、CSS マーカー オーバーレイを作成しました。
この頃、私は Mass Effect シリーズをクリアしたばかりでした。ゲームでは、プレイヤーは銀河を探索し、さまざまな惑星をスキャンして、その惑星に生息していた種、地質学的な歴史など、完全に架空の Wikipedia のような歴史について読みます。
星に関する実際のデータが豊富にあることを知っていれば、同じ方法で銀河に関する実際の情報を提示することも考えられます。このプロジェクトの最終的な目標は、このデータを活用して、視聴者が Mass Effect のように銀河を探索し、星とその分布について学び、宇宙に対する畏敬の念と驚きを感じられるようにすることです。さて、
このケーススタディの残りの部分に入る前に、私は天文学者ではなく、これは外部の専門家からのアドバイスを受けて行われたアマチュアの研究であることをお伝えしておきます。このプロジェクトは、空間のアーティストによる解釈として解釈されるべきです。
構築" id="building_a_galaxy" tabindex="-1">Building a Galaxy
私の計画は、星のデータをコンテキストに配置できる銀河のモデルをプロシージャルに生成し、天の川銀河における私たちの位置を素晴らしい眺めで見られるようにすることでした。

天の川を生成するために、10 万個の粒子を生成し、銀河の腕が形成される方法をエミュレートして、それらを渦巻き状に配置しました。これは数学的モデルではなく表現モデルなので、渦状腕の形成の詳細はあまり気にしませんでした。ただし、渦巻き状の腕の数と回転方向は、ほぼ正しくなるようにしました。
天の川モデルの後のバージョンでは、粒子を伴う銀河の平面画像を使用することで、粒子の使用を抑え、写真のような外観になるようにしました。実際の画像は、約 7, 000 万光年離れたところにある渦巻銀河 NGC 1232 で、天の川銀河のように見えるように加工されています。

私は早い段階で、1 つの GL 単位(基本的に 3D のピクセル)を 1 光年として表現することにしました。これは、視覚化されたすべてのものの配置を統一する慣例でしたが、残念ながら後で深刻な精度の問題が発生しました。
もう 1 つのルールは、カメラを移動するのではなく、シーン全体を回転させることです。これは、他のいくつかのプロジェクトでも採用した手法です。利点の 1 つは、すべてが「ターンテーブル」に配置されるため、マウスを左右にドラッグすると対象のオブジェクトが回転しますが、ズームインは camera.position.z を変更するだけです。
カメラの画角(FOV)も動的です。外側に引っ張ると、視野が広がり、銀河がどんどん映し出されます。星に向かって内側に移動すると、視野が狭くなります。これにより、カメラは近平面のクリッピングの問題に対処することなく、FOV を神のような虫眼鏡のようなものに縮小することで、銀河系と比較して無限小のものを表示できます。

ここから、銀河の中心から一定の距離に太陽を「配置」することができました。また、カイパー崖の半径をマッピングして、太陽系の相対的な大きさを可視化することもできました(最終的には、オールトの雲を可視化することにしました)。このモデルの太陽系では、地球の簡略化された軌道と、太陽の実際の半径を比較して視覚化することもできました。

太陽のレンダリングが難しかった。私が知っているリアルタイム グラフィック技術をできるだけ多く使って、ごまかす必要がありました。太陽の表面は熱いプラズマの泡で、脈動し、時間とともに変化する必要がありました。これは、太陽表面の赤外線画像のビットマップ テクスチャを使用してシミュレートされました。サーフェス シェーダーは、このテクスチャのグレースケールに基づいてカラー ルックアップを行い、別のカラーランプでルックアップを実行します。このルックアップが時間とともにシフトすると、溶岩のような歪みが生まれます。
太陽のコロナにも同様の手法が使用されました。ただし、https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js を使用して、常にカメラの方向を向くフラットなスプライト カードが使用されました。

太陽フレアは、太陽表面の端の周りを回転するトーラスに適用された頂点シェーダーとフラグメント シェーダーによって作成されました。頂点シェーダーには、ブロブのような形で織り込むノイズ関数があります。
ここで、GL の精度が原因で z ファイティングの問題が発生し始めました。精度のすべての変数は THREE.js で事前に定義されているため、膨大な作業なしに精度を上げることは現実的ではありませんでした。原点付近では精度に関する問題はそれほど深刻ではありませんでした。しかし、他の星系のモデリングを始めると、これが問題になりました。

z ファイティングを軽減するために、いくつかのハックを適用しました。THREE の Material.polygonoffset は、ポリゴンを別の認識された位置でレンダリングできるようにするプロパティです(私の理解では)。これは、コロナ面を太陽の表面の上に常にレンダリングするために使用されました。その下には、球体から離れていく鋭い光線を表す太陽の「ハロー」がレンダリングされています。
精度に関連する別の問題として、シーンを拡大すると星モデルがジッターし始めることがありました。この問題を解決するために、シーンの回転をゼロに設定し、星のモデルと環境マップを個別に回転させて、星の周りを回っているような錯覚を生み出す必要がありました。
レンズフレアの作成

宇宙のビジュアリゼーションでは、レンズフレアを過剰に使用しても許されるような気がします。THREE.LensFlare はこの目的を果たします。必要なのは、アナモルフィックな六角形と JJ Abrams の要素を少し加えることだけでした。次のスニペットは、シーン内でそれらを構築する方法を示しています。
// This function returns a lesnflare THREE object to be .add()ed to the scene graph
function addLensFlare(x,y,z, size, overrideImage){
var flareColor = new THREE.Color( 0xffffff );
lensFlare = new THREE.LensFlare( overrideImage, 700, 0.0, THREE.AdditiveBlending, flareColor );
// we're going to be using multiple sub-lens-flare artifacts, each with a different size
lensFlare.add( textureFlare1, 4096, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
// and run each through a function below
lensFlare.customUpdateCallback = lensFlareUpdateCallback;
lensFlare.position = new THREE.Vector3(x,y,z);
lensFlare.size = size ? size : 16000 ;
return lensFlare;
}
// this function will operate over each lensflare artifact, moving them around the screen
function lensFlareUpdateCallback( object ) {
var f, fl = this.lensFlares.length;
var flare;
var vecX = -this.positionScreen.x _ 2;
var vecY = -this.positionScreen.y _ 2;
var size = object.size ? object.size : 16000;
var camDistance = camera.position.length();
for( f = 0; f < fl; f ++ ) {
flare = this.lensFlares[ f ];
flare.x = this.positionScreen.x + vecX * flare.distance;
flare.y = this.positionScreen.y + vecY * flare.distance;
flare.scale = size / camDistance;
flare.rotation = 0;
}
}
テクスチャ スクロールを簡単に行う方法

「空間オリエンテーション平面」には、巨大な THREE.CylinderGeometry() が作成され、太陽を中心として配置されました。光の波が外側に広がる効果を作成するため、テクスチャ オフセットを時間とともに次のように変更しました。
mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}
map
はマテリアルに属するテクスチャです。このテクスチャは、オーバーライド可能な onUpdate 関数を取得します。オフセットを設定すると、テクスチャがその軸に沿って「スクロール」されます。spamming needsUpdate = true を設定すると、この動作が強制的にループします。
カラー ランプの使用
各星には、天文学者が割り当てた「カラー インデックス」に基づいて異なる色が付けられています。一般的に、赤い星は温度が低く、青や紫の星は温度が高いです。このグラデーションには、白と中間的なオレンジ色の帯があります。
星をレンダリングする際に、このデータに基づいて各パーティクルに独自の色を付けたいと考えました。これを行うには、パーティクルに適用されるシェーダー マテリアルに「属性」を指定します。
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};
colorIndex 配列に値を入力すると、シェーダー内の各パーティクルに固有の色が割り当てられます。通常はカラー vec3 を渡しますが、ここでは最終的なカラーランプのルックアップ用に float を渡しています。

色傾斜は次のようになりますが、JavaScript からビットマップの色データにアクセスする必要がありました。この処理を行うには、まず画像を DOM に読み込み、キャンバス要素に描画してから、キャンバスのビットマップにアクセスします。
// make a blank canvas, sized to the image, in this case gradientImage is a dom image element
gradientCanvas = document.createElement('canvas');
gradientCanvas.width = gradientImage.width;
gradientCanvas.height = gradientImage.height;
// draw the image
gradientCanvas.getContext('2d').drawImage( gradientImage, 0, 0, gradientImage.width, gradientImage.height );
// a function to grab the pixel color based on a normalized percentage value
gradientCanvas.getColor = function( percentage ){
return this.getContext('2d').getImageData(percentage \* gradientImage.width,0, 1, 1).data;
}
この同じメソッドは、スターモデル ビューで個々の星に色を付けるためにも使用されます。

シェーダーのラングリング
プロジェクトを進めるうちに、すべての視覚効果を実現するには、シェーダーをさらに多く記述する必要があることがわかりました。シェーダーが index.html に存在することにうんざりしていたため、この目的のためにカスタム シェーダー ローダーを作成しました。
// list of shaders we'll load
var shaderList = ['shaders/starsurface', 'shaders/starhalo', 'shaders/starflare', 'shaders/galacticstars', /*...etc...*/];
// a small util to pre-fetch all shaders and put them in a data structure (replacing the list above)
function loadShaders( list, callback ){
var shaders = {};
var expectedFiles = list.length \* 2;
var loadedFiles = 0;
function makeCallback( name, type ){
return function(data){
if( shaders[name] === undefined ){
shaders[name] = {};
}
shaders[name][type] = data;
// check if done
loadedFiles++;
if( loadedFiles == expectedFiles ){
callback( shaders );
}
};
}
for( var i=0; i<list.length; i++ ){
var vertexShaderFile = list[i] + '.vsh';
var fragmentShaderFile = list[i] + '.fsh';
// find the filename, use it as the identifier
var splitted = list[i].split('/');
var shaderName = splitted[splitted.length-1];
$(document).load( vertexShaderFile, makeCallback(shaderName, 'vertex') );
$(document).load( fragmentShaderFile, makeCallback(shaderName, 'fragment') );
}
}
loadShaders() 関数は、シェーダー ファイル名のリスト(フラグメント シェーダーには .fsh、頂点シェーダーには .vsh を想定)を受け取り、データの読み込みを試みて、リストをオブジェクトに置き換えます。最終的な結果は THREE.js のユニフォームにあります。次のようにシェーダーを渡すことができます。
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
require.js を使用することもできましたが、この目的のためだけにコードを再構築する必要がありました。このソリューションははるかに簡単ですが、THREE.js 拡張機能として改善できると思います。この件に関してご提案やより良い方法がございましたら、お知らせください。
THREE.js の上に CSS テキストラベルを表示する
前回のプロジェクト「Small Arms Globe」では、THREE.js のシーンの上にテキストラベルを表示することを試しました。私が使用していたメソッドでは、テキストを表示したい場所の絶対モデル位置を計算し、THREE.Projector() を使用して画面位置を解決し、最後に CSS の「top」と「left」を使用して CSS 要素を目的の位置に配置します。
このプロジェクトの初期のイテレーションでは同じ手法を使用していましたが、Luis Cruz 氏が説明している別の方法を試してみたいと思っていました。
基本的な考え方: CSS3D の行列変換を THREE のカメラとシーンに合わせると、THREE のシーンの上に CSS 要素を「配置」できます。ただし、これには制限があります。たとえば、THREE.js オブジェクトの下にテキストを表示することはできません。これは、CSS の「top」属性と「left」属性を使用してレイアウトを実行しようとするよりもはるかに高速です。

デモ(およびソースコード)はこちらでご覧いただけます。ただし、THREE.js の行列の順序は変更されていることがわかりました。更新した関数は次のとおりです。
/_ Fixes the difference between WebGL coordinates to CSS coordinates _/
function toCSSMatrix(threeMat4, b) {
var a = threeMat4, f;
if (b) {
f = [
a.elements[0], -a.elements[1], a.elements[2], a.elements[3],
a.elements[4], -a.elements[5], a.elements[6], a.elements[7],
a.elements[8], -a.elements[9], a.elements[10], a.elements[11],
a.elements[12], -a.elements[13], a.elements[14], a.elements[15]
];
} else {
f = [
a.elements[0], a.elements[1], a.elements[2], a.elements[3],
a.elements[4], a.elements[5], a.elements[6], a.elements[7],
a.elements[8], a.elements[9], a.elements[10], a.elements[11],
a.elements[12], a.elements[13], a.elements[14], a.elements[15]
];
}
for (var e in f) {
f[e] = epsilon(f[e]);
}
return "matrix3d(" + f.join(",") + ")";
}
すべてが変換されるため、テキストはカメラの方向を向かなくなります。解決策は THREE.Gyroscope() を使用することでした。これにより、Object3D はシーンから継承された向きを「失う」ことになります。この手法は「ビルボード」と呼ばれ、ジャイロスコープはこれに最適です。
通常の DOM と CSS はすべて引き続き機能します。たとえば、3D テキストラベルにマウスオーバーすると、ドロップ シャドウで光ります。

ズームインすると、タイポグラフィのスケーリングによって位置指定に問題が生じていることがわかりました。テキストのカーニングとパディングが原因でしょうか?もう 1 つの問題は、DOM レンダラがレンダリングされたテキストをテクスチャ付きの四角形として扱うため、ズームインするとテキストがピクセル化されることです。この方法を使用する場合は、この点に注意してください。振り返ってみると、巨大なフォントサイズのテキストを使用するだけでよかったかもしれません。これは今後の検討課題です。このプロジェクトでは、前述の「top/left」CSS 配置テキストラベルを、太陽系の惑星に付随する非常に小さな要素にも使用しました。
音楽の再生とループ
Mass Effect の「銀河マップ」で流れる音楽は、Bioware の作曲家である Sam Hulick と Jack Wall によるもので、私が来場者に感じてほしいと思っていた感情を表現していました。音楽は、私たちが目指す畏敬の念と驚きを生み出すうえで重要な要素だと考え、プロジェクトに音楽を取り入れたいと思いました。
プロデューサーの Valdean Klump が Sam に連絡を取り、Mass Effect の「カッティング フロア」の音楽をたくさん提供していただきました。トラックのタイトルは「In a Strange Land」です。
音楽の再生に audio タグを使用しましたが、Chrome でも「loop」属性が信頼できませんでした。ループが失敗することがありました。最終的に、このデュアル音声タグのハックは、再生の終了を確認し、再生のために他のタグに切り替えるために使用されました。残念ながら、この静止画は常に完璧にループするわけではありませんでしたが、これが私のベストを尽くした結果だと思います。
var musicA = document.getElementById('bgmusicA');
var musicB = document.getElementById('bgmusicB');
musicA.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playB = function(){
musicB.play();
}
// make it wait 15 seconds before playing again
setTimeout( playB, 15000 );
}, false);
musicB.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playA = function(){
musicA.play();
}
// otherwise the music will drive you insane
setTimeout( playA, 15000 );
}, false);
// okay so there's a bit of code redundancy, I admit it
musicA.play();
改善の余地あり
THREE.js をしばらく使用していると、データとコードが混在しすぎていると感じるようになりました。たとえば、マテリアル、テクスチャ、ジオメトリの指示をインラインで定義するときは、基本的に「コードによる 3D モデリング」を行っていました。これは非常に残念なことであり、THREE.js を使用した今後の取り組みで大幅に改善できる分野です。たとえば、マテリアル データを別のファイルで定義し、できれば何らかのコンテキストで表示して調整できるようにし、メイン プロジェクトに戻すことができます。
同僚の Ray McClure は、素晴らしい生成「宇宙ノイズ」の作成に時間を費やしましたが、ウェブ音声 API が不安定で、Chrome が頻繁にクラッシュするため、カットせざるを得ませんでした。残念でしたが、今後の作品で音響分野についてもっと考えるきっかけになりました。この記事の執筆時点で、Web Audio API が修正されたため、現在は動作している可能性があると報告されています。今後の動向に注意する必要があります。
タイポグラフィ要素と WebGL の組み合わせは依然として課題であり、ここで実施していることが正しい方法であるかどうかは 100% 確信できません。ハックのように感じられます。CSS レンダラが搭載される予定の THREE の将来のバージョンでは、この 2 つの世界をより適切に統合できるかもしれません。
クレジット
このプロジェクトを自由にやらせてくれた Aaron Koblin に感謝します。優れた UI デザインと実装、タイポグラフィ、ツアーの実装を担当した Jono Brandel に感謝します。プロジェクトの名前とすべてのコピーを提供してくれた Valdean Klump に感謝します。データと画像ソースの使用権限を大量にクリアしてくれた Sabah Ahmed に感謝します。公開に向けて適切な担当者を紹介してくれた Clem Wright に感謝します。技術的な卓越性に対して Doug Fritz。JS と CSS を教えてくれた George Brower に感謝します。そして、もちろん THREE.js の Mr. Doob にも感謝します。