100,000 スターを獲得

こんにちは。Google のデータ アート チームの Michael Chang と申します。最近では、付近の星を可視化する Chrome 試験運用版100,000 スターを達成しました。このプロジェクトは THREE.js と CSS3D を使用して作成されました。このケーススタディでは、発見プロセスの概要を説明し、いくつかのプログラミング手法を共有し、将来の改善に向けた考えをまとめます。

ここで取り上げるトピックはかなり広範であり、THREE.js の知識が必要ですが、技術的な事後調査として活用していただければ幸いです。右側の目次ボタンを使用して、関心のある分野に自由に移動できます。まず、プロジェクトのレンダリング部分について説明し、次にシェーダーの管理について説明します。最後に、CSS テキストラベルを WebGL と組み合わせて使用する方法を紹介します。

100,000 スター、Data Arts チームによる Chrome 試験運用
100,000 Stars は THREE.js を使用して天の川の近くの星を可視化

宇宙の発見

Small Arms Globe が完成した直後に、私は被写界深度を使用して THREE.js の粒子デモを試しました。適用する効果の量を調整することで、解釈されるシーンの「スケール」を変更できることに気付きました。被写界深度が極端な場合、遠くの被写体がぼやけてしまいます。これは、チルトシフト写真で、まるで微細なシーンを見ているような錯覚を起こすようなものです。逆に、エフェクトを下げると、ディープ スペースを見つめているように見えます。

そこで、粒子の位置を注入するために使用できるデータを探し始めました。それは、astronexus.com の HYG データベースへの経路で、3 つのデータソース(Hipparcos、Yale Bright Star Catalog、Gliese/Jahreiss Catalog)をまとめたもので、事前に計算された xyz デカルト座標が伴います。では、始めましょう。

星のデータをプロットしています。
最初のステップでは、カタログ内のすべての星を 1 つの粒子としてプロットします。
名前を付けたスター。
カタログ内の一部のスターには、ここにラベル付けされた固有名が付けられています。

スターデータを 3D 空間に配置する何かをハッキングするのに約 1 時間かかりました。データセットにはちょうど 119,617 個の星があるので、最新の GPU では各星を粒子で表現することは問題になりません。また、個別に識別された星は 87 個あるので、「Small Arms Globe」で説明したのと同じ手法で CSS マーカー オーバーレイを作成しました。

ちょうどその頃、ついにマス エフェクト シリーズを終えたところでした。このゲームではプレーヤーは銀河系を探検し、さまざまな惑星をスキャンして、その完全に架空の、ウィキペディアのような歴史的歴史(その惑星で繁栄した種や地質学的歴史など)について読みます。

天体に関する実際のデータが大量にあることから、銀河系に関する実際の情報を同じように表現できると考えられます。このプロジェクトの最終目標は、このデータに命を吹き込み、視聴者が質量効果の銀河を探索できるようにして、星とその分布について学び、宇宙に対する恐怖や感動を刺激することです。さて、

このケーススタディの残りの部分では、まず「私は決して天文学者ではなく、これはアマチュア研究の成果であり、外部の専門家からのアドバイスに裏打ちされたものである」と説明する必要があります。このプロジェクトは、芸術家による空間の解釈と解釈すべきです。

構築" id="building_a_galaxy" tabindex="-1">Galaxy の構築

私の計画は、星のデータをコンテキストに当てはめることができる銀河のモデルを手続きによって生成することでした。そしてできれば、天の川における私たちの位置の素晴らしいビューを提供できればと考えました。

銀河系の初期のプロトタイプ。
天の川素粒子システムの初期のプロトタイプ。

天の川を生成するために、10 万個の粒子を発生させ、銀河の腕の形を模倣して粒子をらせん状に配置しました。スパイラル腕の形が具体的にわかることは、数学ではなく表現モデルだからです。ただし、スパイラルアームの数をある程度正確にし、「正しい方向」に回転させるようにしました。

天の川モデルの後期バージョンでは、粒子に付随する銀河の平面画像を優先して粒子の使用を強調しないようにしました。これにより、より写真のような外観になることが期待されています。実際の画像は、私たちから約 7, 000 万光年離れたところにある渦巻銀河 NGC 1232 であり、天の川のように画像を加工しています。

銀河の規模を解き明かします。
すべての GL ユニットは光年です。この場合、球体の幅は 110,000 光年で、粒子系を包含しています。

私は当初、1 つの GL ユニット(基本的には 3D の 1 つのピクセル)を 1 光年として表現することに決めました。これは、可視化するすべてのものを一元的に配置するという規則で、残念ながらその後の精度に深刻な問題が生じました。

私が決めたもう一つの慣例は、カメラを動かすのではなく、シーン全体を回転させることでした。これは、他のいくつかのプロジェクトで行ったことです。すべてが「ターンテーブル」上に配置されるため、マウスで左右にドラッグすると対象のオブジェクトが回転しますが、ズームインは camera.position.z を変更するだけでよいという利点があります。

カメラの画角(FOV)も動的に変化します。外に出るにつれ画角が広くなり、銀河系をより多く取り込むことができます。星に向かって内側に移動すると、その逆で画角が狭まります。これにより、画角を神のような虫メガネのようなものまで下げることで、(銀河と比較して)無限に小さいものもカメラで見ることができます。平面に近いクリップの問題を扱う必要はありません。

銀河系をレンダリングするさまざまな方法。
(上)初期の粒子銀河。(下)イメージ プレーンを伴う粒子。

ここから、銀河の核から数単位離れたところに太陽を「配置」できました。また、クイパー クリフの半径をマッピングすることで、太陽系の相対的な大きさを可視化することもできました(最終的にはオールト雲を可視化することに決めました)。この太陽系モデルのモデルでは、簡略化した地球の軌道と、太陽の実際の半径を比較して視覚化することもできます。

太陽系。
カイパーベルトを表す球体と惑星が周回する太陽。

太陽のレンダリングが困難でした。自分が知っている限り、リアルタイム グラフィックスの手法で不正行為をしなければなりませんでした。太陽の表面はプラズマの熱い泡をしており、脈を打ったり、時間の経過とともに変化したりする必要があります。これは、太陽表面の赤外線画像のビットマップ テクスチャによってシミュレートされました。サーフェス シェーダーは、このテクスチャのグレースケールに基づいてカラー ルックアップを行い、別のカラーランプでルックアップを実行します。このルックアップを時間の経過とともにシフトすると、この溶岩のような歪みが生じます。

太陽のコロナにも同様の手法が使用されました。ただし、https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js を使用して、常にカメラの方を向くフラットなスプライト カードを使用します。

Sol をレンダリングしています。
太陽の初期バージョン。

ソーラーフレアは、トーラスに適用された頂点シェーダーとフラグメント シェーダーによって作成され、太陽表面の端の周りを回転します。頂点シェーダーにはノイズ関数があり、これによって粒状に織り込まれます。

このとき、GL の精度による Z ファイティングの問題が発生し始めていました。精度の変数はすべて THREE.js で事前定義されていたため、かなりの作業なしでは現実的に精度を上げることができませんでした。精度の問題は、原点付近ではそれほど悪くありませんでした。しかし、他の星系のモデル化を開始すると、これが問題になりました。

モデルにスターを付ける。
太陽をレンダリングするコードはのちに他の星をレンダリングするために一般化されました。

Z ファイティングを軽減するために採用したハックがいくつかあります。3 つの 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;

}
}

テクスチャのスクロールを簡単に行う方法

『Homeworld』から着想を得たゲームです。
空間内の空間方向性をサポートするデカルト平面

「空間方位面」については、巨大な THREE.CylinderGeometry() が作成され、太陽を中心としています。外側に広がる「光の波」を作成するために、時間の経過とともに次のようにテクスチャ オフセットを変更しました。

mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}

map はマテリアルに属するテクスチャで、上書き可能な onUpdate 関数を取得します。このオフセットを設定すると、テクスチャがその軸に沿って「スクロール」され、スパム送信に needUpdate = 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,
/_..._/
});

おそらく need.js を使用できたでしょうが、この目的のためだけにコードを再アセンブルする必要があったでしょう。このソリューションはかなり簡単ですが、おそらく THREE.js 拡張機能としても改善できると思います。ご提案やより良い方法がありましたら、ぜひお聞かせください。

THREE.js 上の CSS テキストラベル

前回のプロジェクトである Small Arms Globe では、THREE.js のシーンの上にテキストラベルを表示することにしました。私が使用しているメソッドでは、テキストを表示する場所の絶対モデル位置を計算し、THREE.Projector() を使用して画面の位置を解決し、最後に CSS の「top」と「left」を使用して CSS 要素を目的の位置に配置します。

このプロジェクトの初期のイテレーションでもこれと同じ手法が使用されていましたが、Luis Cruz が説明した別の手法を試してみたいと思っていました。

基本的な考え方は、CSS3D の行列変換を 3 つのカメラとシーンに一致させることで、CSS 要素を 3D のシーンの上に重ねるように 3D で「配置」できます。ただし、これには制限があります。たとえば、THREE.js オブジェクトの下にテキストを配置することはできません。それでも「top」と「left」の CSS 属性を使用してレイアウトを実行するよりもはるかに高速です。

テキストラベル。
CSS3D 変換を使用して、WebGL の上にテキストラベルを配置します。

こちらのデモ(およびビューソース内のコード)をご覧ください。しかし、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 はシーンから継承した向きを強制的に「失います」。この手法は「ビルボード」と呼ばれ、ジャイロスコープはこの目的に最適です。

実際に優れているのは、3D テキストラベルにマウスカーソルを合わせるとドロップ シャドウによって光り輝くような、通常の DOM と CSS はそのまま再生されることです。

テキストラベル。
テキストラベルを常にカメラに向けるには、THREE.Gyroscope() にテキストラベルを付けます。

ズームインすると、タイポグラフィのスケーリングが原因で配置に問題が生じていることがわかりました。これは、テキストのカーニングとパディングが原因でしょうか?もう 1 つの問題は、ズームインするとテキストがモザイクになることでした。これは、DOM レンダラがレンダリングされたテキストをテクスチャ付きのクワッドとして扱うためです。この方法を使用する場合は注意が必要です。振り返ってみると、大きなフォントサイズのテキストを使用できたかもしれないので、これは今後検討すべき内容かもしれません。このプロジェクトでは、前述のように、太陽系の惑星に付随する非常に小さな要素に対して「左上/左」の CSS プレースメント テキストラベルも使用しました。

音楽の再生とループ再生

「Mass Effect」の「Galactic Map」で演奏された曲は、バイオウェア作曲家の Sam Hulick と Jack Wall によるもので、訪問者に体験してもらいたいような感情を持っていました。私たちが求めていたのは、雰囲気の中で重要な部分であり、自分たちの尊敬と驚きの気持ちを生み出すのに役立つと感じたからです。

プロデューサーの Valdean Klump が、Mass Effect の「カッティング フロア」音楽をたくさん受け取ったサムに連絡しました。サムはとても優しく使用してもらいました。トラックのタイトルは「不思議な国」です。

音楽の再生には 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 に感謝します。Jono Brandel は、優れた UI デザインと実装、型処理、ツアーの実装について貢献しました。Valdean Klump: プロジェクトに名前とコピーのすべてを与えてくれました。Sabah Ahmed 博士は、データと画像ソースに関する大量の使用権を消去しました。Clem Wright 氏: 出版関係者にご連絡を差し上げた。Doug Fritz 氏は、卓越した技術力を高く評価しています。George Brower が JS と CSS について教えてくれました。そしてもちろん、THREE.js では Doob 先生です。

リファレンス