Memberikan 100.000 Bintang

Halo! Nama saya Michael Chang dan saya bekerja dengan Tim Data Arts di Google. Baru-baru ini, kami menyelesaikan 100.000 Bintang, sebuah Eksperimen Chrome yang memvisualisasikan bintang di sekitar. Project ini dibuat dengan THREE.js dan CSS3D. Dalam studi kasus ini, saya akan menguraikan proses penemuan, membagikan beberapa teknik pemrograman, dan mengakhirinya dengan beberapa pemikiran untuk perbaikan di masa mendatang.

Topik yang dibahas di sini cukup luas, dan memerlukan sedikit pengetahuan tentang THREE.js, meskipun kami berharap Anda masih dapat menikmatinya sebagai post-mortem teknis. Jangan ragu untuk melompat ke area minat menggunakan tombol daftar isi di sebelah kanan. Pertama, saya akan menunjukkan bagian rendering dari project, diikuti dengan pengelolaan shader, dan terakhir cara menggunakan label teks CSS yang dikombinasikan dengan WebGL.

100.000 Bintang, Eksperimen Chrome oleh Tim Data Arts
100.000 Bintang menggunakan THREE.js untuk memvisualisasikan bintang terdekat di Bima Sakti

Menemukan Ruang Angkasa

Tak lama setelah kami menyelesaikan Small Arms Globe, saya bereksperimen dengan demo partikel THREE.js dengan kedalaman bidang. Saya perhatikan bahwa saya dapat mengubah "skala" tampilan yang ditafsirkan dengan menyesuaikan jumlah efek yang diterapkan. Ketika efek kedalaman efek medan benar-benar ekstrem, objek jauh menjadi sangat buram mirip dengan cara kerja fotografi tilt-shift dalam memberikan ilusi melihat pemandangan mikroskopis. Sebaliknya, menurunkan efek akan membuatnya tampak seolah-olah Anda sedang melihat ke dalam ruang yang dalam.

Saya mulai mencari data yang dapat saya gunakan untuk menyuntikkan posisi partikel, jalur yang mengarahkan saya ke database HYG astronexus.com, kompilasi dari tiga sumber data (Hipparcos, Katalog Yale Bright Star, dan Katalog Gliese/Jahreiss) disertai dengan koordinat Kartesius xyz yang telah dihitung sebelumnya. Mari kita mulai!

Merencanakan data bintang.
Langkah pertama adalah memetakan setiap bintang di katalog sebagai satu partikel.
Bintang yang telah diberi nama.
Beberapa bintang di katalog memiliki nama diri, yang diberi label di sini.

Perlu waktu sekitar satu jam untuk meretas sesuatu yang menempatkan data bintang di ruang 3D. Tepatnya ada 119.617 bintang dalam {i>dataset<i}, jadi merepresentasikan setiap bintang dengan sebuah partikel bukanlah masalah bagi GPU modern. Ada juga 87 bintang yang diidentifikasi satu per satu, jadi saya membuat overlay penanda CSS menggunakan teknik yang sama seperti yang saya jelaskan di Bola Dunia Kecil.

Selama ini, saya baru saja menyelesaikan seri Mass Effect. Dalam game ini, pemain diajak menjelajahi galaksi dan memindai berbagai planet serta membaca sejarah fiksi mereka yang terdengar seperti wikipedia: spesies apa yang telah berkembang pesat di planet ini, sejarah geologinya, dan sebagainya.

Dengan mengetahui kekayaan data aktual yang ada di luar sana tentang bintang-bintang, seseorang dapat menyajikan informasi nyata tentang galaksi dengan cara yang sama. Tujuan akhir proyek ini adalah untuk menghidupkan data ini, memungkinkan pemirsa menjelajahi galaksi à la Mass Effect, untuk mempelajari bintang dan distribusinya, dan semoga menginspirasi rasa kagum dan rasa ingin tahu tentang luar angkasa. Fiuh!

Saya mungkin perlu mengawali studi kasus lainnya dengan mengatakan bahwa saya sama sekali bukan ahli astronomi, dan ini adalah pekerjaan penelitian amatir yang didukung oleh beberapa saran dari pakar eksternal. Proyek ini tentu saja harus ditafsirkan sebagai interpretasi seniman tentang ruang angkasa.

Membangun Galaksi

Rencana saya adalah membuat model galaksi secara prosedural yang dapat menempatkan data bintang dalam konteks -- dan semoga dapat memberikan pemandangan mengagumkan dari tempat kita di Bima Sakti.

Prototipe awal galaksi.
Prototipe awal sistem partikel Bima Sakti.

Untuk menghasilkan Bima Sakti, saya menelurkan 100.000 partikel dan menempatkannya dalam bentuk spiral dengan meniru cara pembentukan lengan galaksi. Aku tidak terlalu khawatir tentang spesifikasi pembentukan lengan spiral karena ini akan menjadi model representasional, bukan model matematis. Namun, saya mencoba mendapatkan jumlah lengan spiral kurang lebih tepat, dan berputar ke "arah yang benar".

Pada model Bima Sakti versi terbaru, saya tidak menekankan penggunaan partikel demi gambar galaksi planar yang menyertai partikel tersebut, yang diharapkan dapat memberikan lebih banyak tampilan fotografis. Gambar sebenarnya adalah galaksi spiral NGC 1232 yang berjarak sekitar 70 juta tahun cahaya dari kita, dan dimanipulasi gambar agar terlihat seperti Bima Sakti.

Mencari tahu skala galaksi.
Setiap unit GL adalah tahun cahaya. Dalam kasus ini,bola tersebut memiliki lebar 110.000 tahun cahaya, yang mencakup sistem partikel.

Awalnya saya memutuskan untuk merepresentasikan satu unit GL, pada dasarnya satu piksel dalam 3D, sebagai satu tahun cahaya -- sebuah konvensi yang penempatannya terpadu untuk segala hal yang divisualisasikan, dan sayangnya memberi saya masalah presisi serius di kemudian hari.

Konvensi lain yang saya putuskan adalah memutar seluruh adegan, bukan menggerakkan kamera, sesuatu yang telah saya lakukan di beberapa proyek lain. Salah satu keuntungannya adalah semuanya ditempatkan di "turntable" sehingga tarik mouse ke kiri dan kanan akan memutar objek yang dimaksud, tetapi memperbesar hanya masalah mengubah camera.position.z.

Ruang pandang (atau FOV) untuk kamera juga dinamis. Saat seseorang bergerak ke luar, ruang pandang melebar, mengambil lebih banyak lagi galaksi. Kebalikannya adalah ketika bergerak ke dalam menuju bintang, ruang pandang menyempit. Hal ini memungkinkan kamera melihat objek yang sangat kecil (dibandingkan dengan galaksi) dengan menurunkan FOV menjadi kaca pembesar mirip dewa tanpa harus menangani masalah kliping yang hampir mendekat.

Berbagai cara merender galaksi.
(di atas) Galaksi partikel awal. (di bawah) Partikel yang disertai bidang gambar.

Dari sini saya bisa "menempatkan" Matahari pada beberapa unit yang jauh dari inti galaksi. Saya juga dapat memvisualisasikan ukuran relatif tata surya dengan memetakan radius Kuiper Cliff (akhirnya saya memilih untuk memvisualisasikan Oort Cloud). Dalam model tata surya ini, saya juga bisa memvisualisasikan orbit Bumi yang disederhanakan, dan radius sebenarnya dari Matahari sebagai perbandingan.

Tata surya.
Matahari yang mengorbit oleh planet dan bola yang mewakili Sabuk Kuiper.

Matahari sulit dirender. Saya harus mencoba sebanyak mungkin teknik grafis real-time yang saya tahu. Permukaan Matahari adalah buih plasma panas dan perlu berdenyut dan berubah dari waktu ke waktu. Ini disimulasikan melalui tekstur bitmap dari gambar inframerah permukaan matahari. Shader permukaan membuat pencarian warna berdasarkan hitam putih tekstur ini dan melakukan pencarian di jalur warna terpisah. Ketika pencarian digeser dari waktu ke waktu, distorsi seperti lava ini akan tercipta.

Teknik serupa digunakan untuk korona Matahari, hanya saja kartu tersebut akan berupa kartu sprite datar yang selalu menghadap ke kamera menggunakan https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.

Sol. Rendering
Versi awal Matahari.

Suar matahari dibuat melalui shader verteks dan fragmen yang diterapkan pada torus, yang berputar di sekitar tepi permukaan matahari. Shader verteks memiliki fungsi derau yang menyebabkannya menenun dengan cara seperti blob.

Di sinilah saya mulai mengalami beberapa masalah perkelahian {i>z<i} karena presisi GL. Semua variabel untuk presisi sudah ditentukan sebelumnya di THREE.js, jadi saya tidak bisa meningkatkan presisi secara realistis tanpa upaya besar. Masalah presisi tidak terlalu buruk di dekat asalnya. Namun, setelah saya mulai membuat model sistem bintang lainnya, hal ini menjadi masalah.

Model bintang.
Kode untuk merender Matahari kemudian digeneralisasi untuk merender bintang lainnya.

Ada beberapa cara yang saya lakukan untuk memitigasi pemberantasan z. Material.polygonoffset dari TIGA adalah properti yang memungkinkan poligon dirender di lokasi yang berbeda yang dirasakan (sejauh yang saya pahami). Hal ini digunakan untuk memaksa bidang korona agar selalu dirender di atas permukaan Matahari. Di bawahnya, "halo" Matahari dirender untuk memberikan sinar cahaya tajam yang bergerak menjauh dari bola.

Masalah lain yang terkait dengan presisi adalah model bintang akan mulai bergetar saat adegan diperbesar. Untuk memperbaikinya, saya harus "menghilangkan" rotasi ruang dan memutar model bintang dan peta lingkungan secara terpisah untuk memberikan ilusi bahwa Anda sedang mengorbit bintang.

Membuat Lensflare

Bersama dengan kekuatan besar 
datanglah tanggung jawab yang besar.
Dengan kekuatan besar, tanggung jawab yang besar juga muncul.

Visualisasi ruang angkasa adalah di mana saya merasa seperti bisa menghindari penggunaan {i>lenflare<i} yang berlebihan. THREE.LensFlare memenuhi tujuan ini, yang perlu saya lakukan hanyalah menampilkan beberapa segi enam anamorfik dan tanda hubung JJ Abrams. Cuplikan di bawah ini menunjukkan cara membuatnya dalam scene Anda.

// 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 &lt; 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;

}
}

Cara mudah untuk melakukan scroll tekstur

Terinspirasi oleh Homeworld.
Bidang kartesius untuk membantu orientasi spasial di ruang angkasa.

Untuk "bidang orientasi spasial", sebuah THREE.CylinderGeometry() raksasa dibuat dan berpusat di Matahari. Untuk membuat "gelombang cahaya" yang menyebar ke luar, saya mengubah offset teksturnya dari waktu ke waktu seperti ini:

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

map adalah tekstur milik material, yang mendapatkan fungsi onUpdate yang dapat Anda tulis secara berlebih. Menyetel offset-nya menyebabkan tekstur "di-scroll" di sepanjang sumbu tersebut, dan spamming needUpdate = true akan memaksa perilaku ini untuk diulang.

Menggunakan jalur warna

Setiap bintang memiliki warna yang berbeda berdasarkan "indeks warna" yang telah ditetapkan oleh para astronom. Secara umum, bintang merah lebih dingin dan bintang biru/ungu lebih panas. Pita warna putih dan oranye menengah ada dalam gradien ini.

Saat merender bintang-bintang, saya ingin memberikan warna sendiri untuk setiap partikel berdasarkan data ini. Cara melakukannya adalah dengan "atribut" yang diberikan ke material shader yang diterapkan pada partikel.

var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};

Mengisi array colorIndex akan memberikan warna unik pada setiap partikel dalam shader. Biasanya satu akan meneruskan vec3 warna, tetapi dalam hal ini saya meneruskan float untuk pencarian tanjakan warna akhirnya.

Jalan warna.
Lampu warna yang digunakan untuk mencari warna yang terlihat dari indeks warna bintang.

Area warnanya tampak seperti ini, tetapi saya perlu mengakses data warna bitmap-nya dari JavaScript. Cara saya melakukannya adalah dengan memuat gambar terlebih dahulu ke DOM, menggambarnya ke dalam elemen kanvas, lalu mengakses bitmap kanvas.

// 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;
}

Metode yang sama ini kemudian digunakan untuk mewarnai masing-masing bintang dalam tampilan model bintang.

Mataku!
Teknik yang sama digunakan untuk melakukan pencarian warna untuk kelas spektrum bintang.

Shader wrangling

Sepanjang project, saya menyadari bahwa saya perlu menulis lebih banyak shader untuk mencapai semua efek visual. Saya menulis loader shader khusus untuk tujuan ini karena saya lelah memiliki shader yang berada di 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&lt;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') );

}
}

Fungsi loadShaders() mengambil daftar nama file shader (mengharapkan .fsh untuk fragmen dan .vsh untuk shader verteks), mencoba memuat datanya, kemudian mengganti daftar dengan objek. Hasil akhirnya adalah dalam uniform THREE.js, Anda dapat meneruskan shader seperti ini:

var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});

Saya mungkin dapat menggunakan arrange.js meskipun hal itu memerlukan beberapa kode yang dirakit ulang hanya untuk tujuan ini. Solusi ini, meskipun jauh lebih mudah, dapat ditingkatkan menurut saya, bahkan mungkin sebagai ekstensi THREE.js. Jika Anda memiliki saran atau cara untuk melakukan hal ini dengan lebih baik, harap beri tahu kami.

Label Teks CSS di atas THREE.js

Pada project terakhir kami, Small Arms Globe, saya bermain-main dengan membuat label teks muncul di atas tampilan THREE.js. Metode yang saya gunakan menghitung posisi model absolut tempat saya ingin teks muncul, kemudian me-resolve posisi layar menggunakan THREE.Projector(), dan terakhir menggunakan CSS "top" dan "left" untuk menempatkan elemen CSS pada posisi yang diinginkan.

Iterasi awal pada project ini menggunakan teknik yang sama, tetapi saya sangat ingin mencoba metode lain yang dijelaskan oleh Luis Cruz.

Ide dasarnya: cocokkan transformasi matriks CSS3D ke kamera dan adegan TIGA, dan Anda dapat "menempatkan" elemen CSS dalam 3D seolah-olah berada di atas adegan TIGA. Namun ada batasan untuk hal ini, misalnya Anda tidak akan dapat menempatkan teks di bawah objek THREE.js. Ini masih jauh lebih cepat daripada mencoba melakukan tata letak menggunakan atribut CSS "top" dan "left".

Label teks.
Menggunakan transformasi CSS3D untuk menempatkan label teks di atas WebGL.

Anda dapat menemukan demonya (dan kode dalam tampilan sumber) untuk laporannya di sini. Namun, saya menemukan bahwa urutan matriks telah berubah untuk THREE.js. Fungsi yang telah saya perbarui:

/_ 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(",") + ")";
}

Karena semuanya berubah, teks tidak lagi menghadap kamera. Solusinya adalah menggunakan THREE.Gyroscope() yang memaksa Object3D "hilang" orientasi yang diwarisinya dari scene. Teknik ini disebut "billboard", dan Giroskop sangat cocok untuk melakukannya.

Yang paling menyenangkan adalah semua DOM dan CSS normal masih bisa dimainkan, seperti dapat mengarahkan mouse ke label teks 3D dan membuatnya bersinar dengan drop shadow.

Label teks.
Pastikan label teks selalu menghadap ke kamera dengan melampirkannya ke THREE.Gyroscope().

Saat memperbesar, saya menemukan bahwa penskalaan tipografi menyebabkan masalah pada pemosisian. Mungkin ini karena kerning dan padding teks? Masalah lainnya adalah teks menjadi pikselasi saat diperbesar karena perender DOM memperlakukan teks yang dirender sebagai segi empat bertekstur, hal yang perlu diketahui saat menggunakan metode ini. Kalau diingat, saya bisa saja menggunakan teks berukuran {i>font<i} yang sangat besar, dan mungkin ini adalah sesuatu untuk eksplorasi masa depan. Dalam project ini, saya juga menggunakan label teks penempatan CSS "atas/kiri", yang dijelaskan sebelumnya, untuk elemen yang sangat kecil yang menyertai planet di tata surya.

Pemutaran dan perulangan musik

Karya musik yang diputar selama 'Galactic Map' Mass Effect dibuat oleh komposer Bioware, Sam Hulick dan Jack Wall, dan memiliki emosi seperti yang saya inginkan untuk dialami pengunjung. Kami menginginkan musik dalam proyek ini karena kami merasa musik itu adalah bagian penting dari suasana, yang membantu menciptakan rasa kagum dan kagum yang ingin kami capai.

Produser kami Valdean Klump menghubungi Sam yang memiliki banyak musik "cutting floor" dari Mass Effect yang dengan sangatnya dia izinkan untuk kami gunakan. Lagunya berjudul "In a Strange Land".

Saya menggunakan tag audio untuk pemutaran musik, namun bahkan di Chrome atribut "loop" tidak dapat diandalkan -- terkadang hanya gagal untuk diulang. Pada akhirnya, peretasan tag audio ganda ini digunakan untuk memeriksa akhir pemutaran dan beralih ke tag lain untuk diputar. Yang mengecewakan adalah fitur ini masih tidak diulang dengan sempurna sepanjang waktu, sayangnya saya merasa ini adalah hal terbaik yang bisa saya lakukan.

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();

Ruang untuk perbaikan

Setelah bekerja dengan THREE.js untuk beberapa waktu, saya merasa seperti sampai pada titik di mana data saya terlalu banyak tercampur dengan kode saya. Misalnya saat mendefinisikan petunjuk material, tekstur, dan geometri secara inline, pada dasarnya saya melakukan "pemodelan 3D dengan kode". Hal ini terasa sangat buruk, dan merupakan area tempat upaya mendatang dengan THREE.js dapat ditingkatkan secara signifikan, misalnya menentukan data material dalam file terpisah, sebaiknya dilihat dan diubah dalam konteks tertentu, dan dapat dikembalikan ke proyek utama.

Kolega kami, Ray McClure, juga meluangkan waktu untuk membuat beberapa "suara luar angkasa" generatif yang luar biasa, yang harus dipotong karena API audio web yang tidak stabil, sehingga sering kali membuat Chrome error. Sayang sekali... tapi hal itu jelas membuat kami lebih memikirkan peluang kerja berikutnya. Saat tulisan ini dibuat, kami mengetahui bahwa Web Audio API telah di-patch, jadi mungkin saja API ini sudah berfungsi sekarang dan perlu diwaspadai di masa mendatang.

Elemen tipografi yang dipasangkan dengan WebGL masih menjadi sebuah tantangan, dan saya tidak 100% yakin apa yang kami lakukan di sini adalah cara yang benar. Masih terasa seperti peretasan. Mungkin versi THREE mendatang, dengan Perender CSS yang akan datang, dapat digunakan untuk menggabungkan dua dunia dengan lebih baik.

Kredit

Terima kasih kepada Aaron Koblin yang telah mengizinkan saya pergi ke kota dengan proyek ini. Jono Brandel untuk desain UI + implementasi, jenis perlakuan, dan implementasi tur yang luar biasa. Valdean Klump yang telah memberi nama proyek dan semua salinannya. Sabah Ahmed karena telah mendapatkan ton metrik hak penggunaan untuk sumber data dan gambar. Clem Wright karena telah menjangkau orang yang tepat untuk publikasi. Doug Fritz untuk keunggulan teknis. George Brower karena telah mengajari saya JS dan CSS. Dan tentu saja Mr. Doob untuk THREE.js.

Referensi