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-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 mengakhiri dengan beberapa pemikiran untuk peningkatan di masa mendatang.
Topik yang dibahas di sini akan cukup luas, dan memerlukan beberapa pengetahuan tentang THREE.js, meskipun saya berharap Anda tetap dapat menikmatinya sebagai post-mortem teknis. Anda dapat langsung membuka area yang diinginkan menggunakan tombol daftar isi di sebelah kanan. Pertama, saya akan menunjukkan bagian rendering project, diikuti dengan pengelolaan shader, dan terakhir cara menggunakan label teks CSS bersama dengan WebGL.

Menjelajahi Ruang
Segera setelah kami menyelesaikan Small Arms Globe, saya bereksperimen dengan demo partikel THREE.js dengan kedalaman bidang. Saya menyadari bahwa saya dapat mengubah "skala" adegan yang diinterpretasikan dengan menyesuaikan jumlah efek yang diterapkan. Saat efek kedalaman bidang sangat ekstrem, objek yang jauh menjadi sangat buram, mirip dengan cara kerja fotografi tilt-shift yang memberikan ilusi melihat adegan mikroskopis. Sebaliknya, mengurangi efeknya membuat Anda seolah-olah sedang menatap ruang angkasa yang dalam.
Saya mulai mencari data yang dapat saya gunakan untuk menyuntikkan posisi partikel, sebuah jalur yang mengarahkan saya ke database HYG astronexus.com, kompilasi dari tiga sumber data (Hipparcos, Yale Bright Star Catalog, dan Gliese/Jahreiss Catalog) yang disertai dengan koordinat Kartesius xyz yang telah dihitung sebelumnya. Mari kita mulai!


Saya memerlukan waktu sekitar satu jam untuk merakit sesuatu yang menempatkan data bintang di ruang 3D. Ada tepat 119.617 bintang dalam set data, jadi merepresentasikan setiap bintang dengan partikel tidak menjadi masalah bagi GPU modern. Ada juga 87 bintang yang diidentifikasi secara terpisah, jadi saya membuat overlay penanda CSS menggunakan teknik yang sama dengan yang dijelaskan di Small Arms Globe.
Saat itu, saya baru saja menyelesaikan seri Mass Effect. Dalam game, pemain diundang untuk menjelajahi galaksi dan memindai berbagai planet serta membaca tentang sejarahnya yang sepenuhnya fiktif dan terdengar seperti Wikipedia: spesies apa yang pernah berkembang di planet tersebut, sejarah geologinya, dan sebagainya.
Dengan mengetahui banyaknya data sebenarnya yang ada tentang bintang, seseorang dapat menyajikan informasi nyata tentang galaksi dengan cara yang sama. Tujuan utama proyek ini adalah menghidupkan data ini, memungkinkan penonton menjelajahi galaksi seperti dalam Mass Effect, mempelajari bintang dan distribusinya, serta diharapkan dapat menginspirasi rasa kagum dan takjub terhadap luar angkasa. Fiuh!
Saya mungkin harus mengawali studi kasus ini dengan mengatakan bahwa saya sama sekali bukan astronom, dan ini adalah hasil penelitian amatir yang didukung oleh beberapa saran dari pakar eksternal. Project ini jelas harus ditafsirkan sebagai interpretasi ruang oleh seorang seniman.
Membangun Galaksi
Rencana saya adalah membuat model galaksi secara prosedural yang dapat menempatkan data bintang dalam konteks -- dan semoga memberikan tampilan yang mengagumkan tentang tempat kita di Bima Sakti.

Untuk membuat Bima Sakti, saya memunculkan 100.000 partikel dan menempatkannya dalam spiral dengan meniru cara terbentuknya lengan galaksi. Saya tidak terlalu mengkhawatirkan spesifikasi pembentukan lengan spiral karena ini akan menjadi model representasional, bukan model matematika. Namun, saya mencoba mendapatkan jumlah lengan spiral yang kurang lebih benar, dan berputar ke "arah yang benar".
Dalam versi model Bima Sakti selanjutnya, saya mengurangi penekanan pada penggunaan partikel dan lebih memilih gambar galaksi planar untuk menemani partikel, dengan harapan memberikan tampilan yang lebih mirip foto. Gambar sebenarnya adalah galaksi spiral NGC 1232 yang berjarak sekitar 70 juta tahun cahaya dari kita, yang dimanipulasi agar terlihat seperti Bima Sakti.

Sejak awal, saya memutuskan untuk merepresentasikan satu unit GL, yang pada dasarnya adalah piksel dalam 3D, sebagai satu tahun cahaya -- sebuah konvensi yang menyatukan penempatan untuk semua yang divisualisasikan, dan sayangnya menimbulkan masalah presisi yang serius di kemudian hari.
Konvensi lain yang saya putuskan adalah memutar seluruh adegan, bukan menggerakkan kamera, yang telah saya lakukan di beberapa proyek lain. Salah satu keuntungannya adalah semua ditempatkan di atas "meja putar" sehingga penarikan 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 ditarik ke luar, bidang pandang akan melebar, sehingga semakin banyak galaksi yang terlihat. Sebaliknya, saat bergerak ke dalam menuju bintang, bidang pandang akan menyempit. Hal ini memungkinkan kamera melihat hal-hal yang sangat kecil (dibandingkan dengan galaksi) dengan mengecilkan FOV menjadi seperti kaca pembesar dewa tanpa harus berurusan dengan masalah kliping bidang dekat.

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

Matahari sulit dirender. Saya harus menggunakan sebanyak mungkin teknik grafis real-time yang saya ketahui. Permukaan Matahari adalah buih plasma panas yang perlu berdenyut dan berubah seiring waktu. Hal ini disimulasikan melalui tekstur bitmap gambar inframerah permukaan matahari. Shader permukaan membuat pencarian warna berdasarkan skala abu-abu tekstur ini dan melakukan pencarian di ramp warna terpisah. Saat pencarian ini bergeser dari waktu ke waktu, akan tercipta distorsi seperti lava ini.
Teknik serupa digunakan untuk korona Matahari, kecuali bahwa korona tersebut akan menjadi kartu sprite datar yang selalu menghadap kamera menggunakan https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.

Flare surya dibuat melalui shader verteks dan fragmen yang diterapkan pada torus, yang berputar tepat di sekitar tepi permukaan matahari. Shader vertex memiliki fungsi noise yang menyebabkan shader tersebut bergerak seperti gumpalan.
Di sinilah saya mulai mengalami beberapa masalah z-fighting karena presisi GL. Semua variabel untuk presisi telah ditentukan sebelumnya di THREE.js, jadi saya tidak dapat meningkatkan presisi secara realistis tanpa banyak pekerjaan. Masalah presisi tidak terlalu buruk di dekat titik asal. Namun, setelah saya mulai memodelkan sistem bintang lain, hal ini menjadi masalah.

Ada beberapa solusi yang saya gunakan untuk mengurangi z-fighting. Material.polygonoffset THREE adalah properti yang memungkinkan poligon dirender di lokasi yang berbeda (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 menjauhi bola.
Masalah lain terkait presisi adalah model bintang akan mulai bergetar saat adegan diperbesar. Untuk memperbaikinya, saya harus "menghilangkan" rotasi adegan dan memutar model bintang serta peta lingkungan secara terpisah untuk memberikan ilusi bahwa Anda mengorbit bintang.
Membuat Kilauan Lensa

Visualisasi ruang adalah tempat di mana saya merasa bisa menggunakan efek cahaya lensa secara berlebihan. THREE.LensFlare memenuhi tujuan ini. Yang perlu saya lakukan hanyalah menambahkan beberapa heksagon anamorfik dan sedikit sentuhan JJ Abrams. Cuplikan di bawah menunjukkan cara membuatnya di adegan 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 < 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 scrolling tekstur

Untuk "bidang orientasi spasial", THREE.CylinderGeometry() raksasa dibuat dan dipusatkan pada Matahari. Untuk membuat "gelombang cahaya" yang menyebar ke luar, saya mengubah offset teksturnya dari waktu ke waktu seperti berikut:
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 timpa. Menetapkan offsetnya akan menyebabkan tekstur "di-scroll" di sepanjang sumbu tersebut, dan mengirim spam needsUpdate = true akan memaksa perilaku ini untuk berulang.
Menggunakan ramp 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. Garis warna putih dan oranye di antara keduanya ada dalam gradien ini.
Saat merender bintang, saya ingin memberikan warna sendiri untuk setiap partikel berdasarkan data ini. Cara melakukannya adalah dengan "atribut" yang diberikan ke materi 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 untuk setiap partikel dalam shader. Biasanya, orang akan meneruskan vec3 warna, tetapi dalam contoh ini, saya meneruskan float untuk pencarian tampilan ramp warna pada akhirnya.

Ramp warna terlihat seperti ini, tetapi saya perlu mengakses data warna bitmapnya 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 setiap bintang dalam tampilan model bintang.

Pengelolaan shader
Selama project ini, saya menemukan bahwa saya perlu menulis lebih banyak shader untuk menyelesaikan semua efek visual. Saya menulis pemuat shader kustom untuk tujuan ini karena saya sudah bosan memiliki shader 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<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 shader fragmen dan .vsh untuk shader verteks), mencoba memuat datanya, lalu mengganti daftar dengan objek. Hasil akhirnya ada di uniform THREE.js Anda. Anda dapat meneruskan shader ke sana seperti ini:
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
Saya mungkin bisa menggunakan require.js, meskipun hal itu memerlukan beberapa perakitan ulang kode hanya untuk tujuan ini. Solusi ini, meskipun jauh lebih mudah, menurut saya dapat ditingkatkan, bahkan mungkin sebagai ekstensi THREE.js. Jika Anda memiliki saran atau cara untuk melakukannya dengan lebih baik, beri tahu kami.
Label Teks CSS di atas THREE.js
Pada project terakhir kami, Small Arms Globe, saya mencoba membuat label teks muncul di atas adegan THREE.js. Metode yang saya gunakan menghitung posisi model absolut tempat saya ingin teks muncul, lalu menyelesaikan posisi layar menggunakan THREE.Projector(), dan terakhir menggunakan "top" dan "left" CSS untuk menempatkan elemen CSS pada posisi yang diinginkan.
Iterasi awal pada project ini menggunakan teknik yang sama, tetapi saya ingin mencoba metode lain yang dijelaskan oleh Luis Cruz.
Ide dasarnya: cocokkan transformasi matriks CSS3D dengan kamera dan adegan THREE, dan Anda dapat "menempatkan" elemen CSS dalam 3D seolah-olah berada di atas adegan THREE. Namun, ada batasan untuk hal ini, misalnya Anda tidak akan dapat membuat teks berada di bawah objek THREE.js. Cara ini masih jauh lebih cepat daripada mencoba melakukan tata letak menggunakan atribut CSS "top" dan "left".

Anda dapat menemukan demo (dan kode dalam tampilan sumber) untuk hal ini 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 diubah, teks tidak lagi menghadap kamera. Solusinya adalah menggunakan THREE.Gyroscope() yang memaksa Object3D untuk "kehilangan" orientasi yang diwariskan dari adegan. Teknik ini disebut "billboarding", dan Gyroscope sangat cocok untuk melakukannya.
Yang sangat menarik adalah semua DOM dan CSS normal masih berfungsi, seperti dapat mengarahkan kursor ke label teks 3D dan membuatnya bersinar dengan drop shadow.

Saat memperbesar, saya menemukan bahwa penskalaan tipografi menyebabkan masalah pada penempatan. Mungkin hal ini disebabkan oleh kerning dan padding teks? Masalah lain adalah teks menjadi berpiksel saat diperbesar karena perender DOM memperlakukan teks yang dirender sebagai quad bertekstur, sesuatu yang perlu diperhatikan saat menggunakan metode ini. Jika dipikir-pikir, saya bisa saja menggunakan teks berukuran font raksasa, dan mungkin ini bisa menjadi sesuatu yang bisa dieksplorasi di masa mendatang. Dalam project ini, saya juga menggunakan label teks penempatan CSS "atas/kiri", yang dijelaskan sebelumnya, untuk elemen yang sangat kecil yang menyertai planet dalam tata surya.
Pemutaran dan pengulangan musik
Musik yang diputar selama 'Galactic Map' Mass Effect dibuat oleh komposer Bioware, Sam Hulick dan Jack Wall, dan memiliki emosi yang ingin saya rasakan oleh pengunjung. Kami ingin menyertakan musik dalam proyek kami karena kami merasa musik adalah bagian penting dari suasana, yang membantu menciptakan rasa kagum dan keheranan yang ingin kami capai.
Produser kami, Valdean Klump, menghubungi Sam yang memiliki banyak musik "cutting floor" dari Mass Effect yang dengan sangat baik hati mengizinkan kami menggunakannya. Lagu ini berjudul "In a Strange Land".
Saya menggunakan tag audio untuk pemutaran musik, tetapi bahkan di Chrome, atribut "loop" tidak dapat diandalkan -- terkadang gagal melakukan loop. Pada akhirnya, trik tag audio ganda ini digunakan untuk memeriksa akhir pemutaran dan beralih ke tag lain untuk diputar. Yang mengecewakan adalah still ini tidak selalu berputar dengan sempurna, tetapi saya merasa ini adalah yang 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();
Aspek yang perlu ditingkatkan
Setelah menggunakan THREE.js selama beberapa waktu, saya merasa data saya terlalu tercampur dengan kode. Misalnya, saat menentukan petunjuk materi, tekstur, dan geometri secara inline, pada dasarnya saya melakukan "pemodelan 3D dengan kode". Hal ini terasa sangat buruk, dan merupakan area yang dapat ditingkatkan secara signifikan pada upaya mendatang dengan THREE.js, misalnya menentukan data materi dalam file terpisah, yang sebaiknya dapat dilihat dan disesuaikan dalam beberapa konteks, serta dapat dibawa kembali ke project utama.
Rekan kami, Ray McClure, juga meluangkan waktu untuk membuat beberapa "suara luar angkasa" generatif yang luar biasa, tetapi harus dihilangkan karena API audio web tidak stabil dan sering kali membuat Chrome mengalami error. Sayang sekali… tetapi hal ini membuat kami lebih memikirkan ruang suara untuk pekerjaan mendatang. Saat ini, saya mendapat informasi bahwa Web Audio API telah di-patch sehingga kemungkinan API ini berfungsi sekarang. Hal ini perlu diperhatikan pada masa mendatang.
Elemen tipografi yang dipadukan dengan WebGL masih menjadi tantangan, dan saya tidak 100% yakin bahwa yang kita lakukan di sini adalah cara yang benar. Masih terasa seperti peretasan. Mungkin versi THREE mendatang, dengan CSS Renderer yang akan segera hadir, dapat digunakan untuk menggabungkan kedua dunia dengan lebih baik.
Kredit
Terima kasih kepada Aaron Koblin yang telah mengizinkan saya mengerjakan project ini. Jono Brandel atas desain + implementasi UI, penanganan huruf, dan implementasi tur yang luar biasa. Valdean Klump yang telah memberi nama project dan semua teksnya. Sabah Ahmed karena telah mengurus izin penggunaan untuk sumber data dan gambar yang jumlahnya berton-ton. Clem Wright karena telah menghubungi orang yang tepat untuk publikasi. Doug Fritz atas keunggulan teknisnya. George Brower yang telah mengajari saya JS dan CSS. Dan tentu saja Mr. Doob untuk THREE.js.