創下 100,000 顆星

Michael Chang
Michael Chang

你好!我是 Google 資料藝術團隊的 Michael Chang。我們最近完成了 100,000 顆星星,這項 Chrome 實驗功能看到了附近的星星。這個專案是以 THREE.js 和 CSS3D 建構。在本個案研究中,我會概述探索程序、分享一些程式設計技巧,並在最後分享一些未來改善的想法。

本文討論的主題可能很廣泛,且需要對 THREE.js 的瞭解,雖然我希望您能繼續享受這份深入的諮詢服務。您可以使用右側的目錄按鈕,跳到感興趣的部分。首先,我們會顯示專案的轉譯部分,以及著色器管理,最後說明如何搭配使用 CSS 文字標籤與 WebGL。

100,000 顆星星,Data Arts 團隊的 Chrome 實驗功能
100,000 顆星星使用 THREE.js,在銀河系中繪製附近星星

探索太空

我們完成 Small Arms Globe 後,隨即進行了實驗深度 THREE.js 粒子示範的效果。我注意到我能夠調整套用的效果,來改變場景的解譯「比例」。景深的效果非常極端時,遠處物體變得非常模糊,就像傾斜攝影的運作方式,是看微鏡場景的錯覺。相反地,將效果降低後,效果會像是你潛入深遠的空間。

我開始尋找可用來插入粒子位置的資料,透過路徑引導我前往 astronexus.com 的 HYG 資料庫,其中有三種資料來源 (Hipparcos、Yale Bright Star Catalog 和 Gliese/Jahreiss Catalog),以及預先計算的 xyz 購物車座標。那我們開始吧!

正在繪製星星資料。
首先,請將目錄中的每個星星繪製成一顆粒子。
命名的星星。
目錄中有些星號的名稱會加上專有名稱,並標示在這裡。

組織花了大約一小時的時間,才能將星星資料放入 3D 空間。資料集裡剛好是 119,617 顆星,因此使用粒子表示每個星星並不適用於現代 GPU。此外,還有 87 個各自識別的星號,因此我採用在小型 Arms Globe 中描述的技術,建立了 CSS 標記疊加層。

在這段期間,我剛看完了「質量效應」系列。在遊戲中,玩家邀請探索銀河系,掃描各種星球並閱讀相關的完全虛構的根據維基百科歷史:哪些物種在地球上蓬勃發展、其地質歷史等等。

掌握星座上的大量實際資料後,人們就可能以相同的方式呈現星系的真實資訊。這項專案的最終目標是實際運用這些資料,讓觀眾探索銀河系、瞭解星星和其分佈關係,進而為太空有所啟發,並探索宇宙的奧秘。呼!

我應該在本個案研究的其餘部分談到「我是天文學家,這是由外部專家提供的意見支持的業餘研究成果」。本專案應解釋為藝術家對太空的解釋。

打造銀河

我計劃一開始就產生一個星系模型,把星星資料應用在背景中,希望能帶給我們美麗的銀河美景。

銀河的早期原型。
銀河粒子系統初期原型。

為了產生銀河系,我累積了 10 萬顆粒子,然後模擬銀河手臂的組成方式,將這些粒子放在螺旋內。我不太擔心螺旋形形成的具體細節,因為這是一個代表性模型,而不是數學。但我確實嘗試讓螺旋數量較不正確,並且朝「正確方向」旋轉。

在較新版本的銀河模型中,我強調使用粒子,並放上星系的平面圖,希望能為粒子放上更生動的影像。實際圖像為螺旋星系 NGC 1232 距離我們約 7, 000 萬光年數,並經過操縱,看起來像銀河系。

繪製銀河系的比例。
每 GL 單位都是光年。在本案例中,球體的寬度為 110,000 光年,涵蓋粒子系統。

我很早就決定代表 3D 中的像素 (基本上就是 3D 中的像素),因為這項慣例統一排列所有視覺元素,但很遺憾地,我日後會遇到嚴重的精確度問題。

我決定採用另一種慣例,就是要旋轉整個場景,而不是移動攝影機。不過,我其他的功用是做到這點。一個優點是一切都放在「可轉折」上,然後左右拖曳滑鼠即可旋轉出有問題的物件,但是放大僅用於變更鏡頭。position.z

攝影機的視野 (或 FOV) 也是動態的,如果有人向外擴張,視野就會擴大,並且吸收更多銀河系。反之則朝著星星移動時,視野就會縮小。如此一來,相機只需將 FOV 往下壓到某個像地殼的放大鏡,就能看見無限的物體 (相對於銀河系),而不用處理近乎飛機的夾扣問題。

以不同方式呈現銀河系。
(上方) 初期粒子星系。(下圖) 旁邊有一顆圖像。

在這裡,我可以將太陽「放置」到遠離銀河核心數的地方。我還能繪製 Kuiper Cliff 的半徑範圍 (最後選擇以視覺化方式呈現 Oort Cloud),以視覺化方式呈現太陽系的相對大小。在這個模型的太陽能系統中,我還能繪製一個簡化的地球軌域,並與太陽的實際半徑進行比較。

太陽系。
太陽環繞著行星,還有代表庫柏帶的球體。

太陽難以算繪。我所熟悉的即時圖像技巧都無法滿足他們的需求。太陽的表面是電漿的熱水,因此需要一段時間才能脈搏並改變。透過太陽表面紅外線圖片的點陣圖紋理模擬。表面著色器會根據此紋理的灰階進行色彩查詢,並在獨立的色彩漸層中執行外觀。當這個向上擴充檢視長期下來時,會產生類似熔岩的扭曲。

像太陽的眼鏡一樣採用類似的技巧,差別在於它是一律會面對相機 (使用 https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js) 的平面 Sprite 卡。

轉譯程式庫。
太陽的早期版本。

太陽能火焰是由套用至土地的頂點和片段著色器所建立,在太陽能表面邊緣正旋轉。頂點著色器具有雜訊函式,它會以類似 blob 的方式編織。

我突然開始遇到一些因 GL 精確度而導致的多面向衝突問題。所有精準度變數都是在 THREE.js 中定義,所以我不需要投入大量心力,才能確實提高精確度。精確度問題離原點較差。但是,一旦開始模擬其他星星系統,就會發生問題。

星形模型
用於呈現太陽的程式碼後來經過一般化,以算繪其他星星。

我利用了幾個駭客手法來減少多面向衝突問題。THREE 的 Material.polygonoffset 是一種屬性,可讓多邊形在不同的感知位置 (如我所理解) 算繪。用來強制讓冠狀動植物永遠顯示在太陽表面的上方。在這個圖中,它算繪出太陽的「光暈」,讓光明的光線從球體中移出。

精確度的另一個問題是,星號模型隨著場景放大後會出現抖動。為了修正這個問題,我必須「完全退出」場景旋轉,並分別旋轉星星模型和環境地圖,使之製造你繞過星星的假象。

正在建立鏡頭耀光

權力越大,責任越重。
權力越大,責任越重。

從太空視覺圖中,如果過度使用鏡頭耀眼效果,就讓我覺得很失望。THREE.LensFlare 的效用是,我只需使用一些變形的六角形和一連串 JJ Abram 的物品。以下程式碼片段說明如何在場景中建構這些元件。

// 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,但在這個範例中,我會傳遞一個浮點值,供最終色彩坡度查詢使用。

色彩漸層。
用於從星號色彩索引查詢可見色彩的色階。

色彩漸層看起來像這樣,但是我需要從 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,
/_..._/
});

我可能已經使用 requirements.js,不過其實需要為了這個目的而重新組合程式碼。這個解決方案雖然簡單得多,但也許是 THREE.js 副檔名。如有任何建議或改善方式,歡迎與我聯絡!

THREE.js 上的 CSS 文字標籤

在最後一個專案「Ssmall Arms Globe」中,我用文字標籤顯示在 THREE.js 場景的上方。我使用的方法會計算希望文字顯示位置的絕對模型位置,然後使用 THREE.Projector() 解析螢幕位置,最後再使用 CSS「top」和「left」將 CSS 元素放在所需位置。

這項專案的早期疊代都採用相同的技巧,但我忍不住想嘗試採用 Luis Cruz 所述的其他方法

基本概念:比對 CSS3D 的矩陣轉換到 THREE 的相機和場景,您可以將 CSS 元素「放置在」3D 中,就像是在 THREE 的場景上方一樣。不過,此功能有其限制,例如,您無法將文字放在 THREE.js 物件的下方。相較於嘗試使用「頂端」和「左側」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「遺失」從場景沿用的方向。這項技巧稱為「滑板」,而陀螺儀是最適合的拍攝方式。

更棒的是,所有正常的 DOM 和 CSS 仍然在播放,例如可以將滑鼠遊標移到 3D 文字標籤上,讓它在投射陰影中變亮。

文字標籤。
將文字標籤附加至 THREE.Gyroscope(),就能讓文字一律面對相機鏡頭。

放大時,我發現字體排版縮放導致位置定位問題。也許是因為文字的結構和邊框間距造成問題?另一個問題是,由於 DOM 轉譯器將轉譯的文字視為紋理四方,因此文字在放大時出現象素化的問題,使用這個方法時需要留意的事項。追溯到光景,我可能剛剛用巨型字型文字,那可能是日後要探索的東西。在這個專案中,我也使用如先前所述的「上方/左」CSS 位置文字標籤,針對伴隨太陽系行星的非常小元素使用。

音樂播放和循環播放

Mass Effect 的「銀河地圖」由生物軟體作曲家 Sam Hulick 和 Jack Wall 出手的配樂,呈現我希望訪客體驗的一種情緒。我們當初在專案中加入一些音樂,是因為我們認為音樂是大氣中不可或缺的一環,有助於營造出我們想要追求的美感與奇觀感。

我們的製作人 Valdean Klump 與 Sam 聯絡,他擁有大量 Mass Effect 的「剪刀地板」音樂,他非常慶幸讓我們自由使用。曲目的名稱是「In a Strange Land」。

我使用音訊標記播放音樂,但是 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,例如在單獨檔案中定義 Material 資料,特別是在某些情況下,最好能檢視及調整這些資料,並且再回到主要專案。

我們也的同事 Ray McClure 花了些時間創造出令人讚嘆的生成式「空間噪音」,因為網路音訊 API 不穩定,導致 Chrome 經常當機。很不幸...不過,這確實讓我們決定未來在音域中加入更多思維,以供日後工作時使用。截至本文時,我們通知您 Web Audio API 已經過修補,因此可能可以正常運作,但日後有需要注意的地方。

與 WebGL 配對的打字元素仍然是一大挑戰,我不能 100% 能完全確定我們所做的一切都正確無誤。這就像個駭客入侵。也許日後推出的 THREE 版本 (包含即將推出和即將推出的 CSS 轉譯器) 都能用來更妥善地加入這兩個世界。

抵免額

感謝 Aaron Koblin 參與這項專案,幫助我走訪城鎮。Jono Brandel 提供卓越的 UI 設計 + 導入、類型處理和導覽實作。Valdean Klump 為專案命名,並為所有副本命名。Sabah Ahmed 清除資料和圖片來源的大量使用權利。Clem Wright 讓我們觸及合適的發布對象。為卓越技術而 Doug Fritz。給 George Brower 教授 JS 和 CSS。當然還有 THREE.js 先生的 Doob。

參考資料