Halo! Nama saya Michael Chang dan saya bekerja di 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 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 harap Anda tetap dapat menikmatinya sebagai post-mortem teknis. Anda dapat langsung membuka area yang diminati 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 yang dikombinasikan dengan WebGL.
Menemukan Ruang
Segera setelah kami menyelesaikan Small Arms Globe, saya bereksperimen dengan demo partikel THREE.js dengan kedalaman bidang. Saya melihat bahwa saya dapat mengubah "skala" scene yang ditafsirkan dengan menyesuaikan jumlah efek yang diterapkan. Jika efek kedalaman bidang sangat ekstrem, objek yang jauh menjadi sangat buram, mirip dengan cara kerja fotografi tilt-shift yang memberikan ilusi seolah-olah melihat pemandangan mikroskopis. Sebaliknya, jika efek dinonaktifkan, Anda akan melihat seolah-olah Anda sedang menatap ruang angkasa yang luas.
Saya mulai mencari data yang dapat digunakan untuk memasukkan 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.
Perlu waktu sekitar satu jam untuk membuat sesuatu yang menempatkan data bintang di ruang 3D. Ada tepat 119.617 bintang dalam set data, sehingga merepresentasikan setiap bintang dengan 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 dengan yang saya jelaskan di Small Arms Globe.
Selama waktu ini, saya baru saja menyelesaikan seri Mass Effect. Dalam game ini, pemain diajak untuk menjelajahi galaksi dan memindai berbagai planet serta membaca sejarahnya yang sepenuhnya fiktif dan terdengar seperti wikipedia: spesies apa yang berkembang di planet tersebut, sejarah geologisnya, dan sebagainya.
Dengan mengetahui kekayaan data sebenarnya yang ada tentang bintang, seseorang dapat menyajikan informasi nyata tentang galaksi dengan cara yang sama. Sasaran utama project ini adalah menghidupkan data ini, memungkinkan penonton menjelajahi galaksi à la Mass Effect, mempelajari bintang dan distribusinya, dan semoga menginspirasi rasa kagum dan keingintahuan tentang luar angkasa. Fiuh!
Saya mungkin harus mengawali studi kasus ini dengan mengatakan bahwa Saya bukan astronom, dan ini adalah hasil riset amatir yang didukung oleh beberapa saran dari pakar eksternal. Project ini harus ditafsirkan sebagai interpretasi ruang oleh seniman.
Membuat Galaxy
Rencana saya adalah membuat model galaksi secara terstruktur yang dapat menempatkan data bintang dalam konteks -- dan semoga memberikan tampilan yang luar biasa tentang tempat kita di Bima Sakti.
Untuk membuat Bima Sakti, saya membuat 100.000 partikel dan menempatkannya dalam spiral dengan mengemulasi cara lengan galaksi terbentuk. Saya tidak terlalu khawatir dengan detail pembentukan lengan spiral karena ini akan menjadi model representasional, bukan model matematika. Namun, saya mencoba membuat jumlah lengan spiral lebih atau kurang benar, dan berputar ke "arah yang benar".
Dalam versi model Bima Sakti yang lebih baru, saya tidak menekankan penggunaan partikel dan lebih memilih gambar galaksi planar untuk menyertai partikel, sehingga diharapkan memberikan tampilan yang lebih fotografis. Gambar sebenarnya adalah galaksi spiral NGC 1232 yang berjarak sekitar 70 juta tahun cahaya dari kita, yang dimanipulasi agar terlihat seperti Bima Sakti.
Saya memutuskan sejak awal untuk merepresentasikan satu unit GL, yang pada dasarnya adalah piksel dalam 3D, sebagai satu tahun cahaya -- sebuah konvensi yang menyatukan penempatan untuk semua hal yang divisualisasikan, dan sayangnya memberi saya masalah presisi yang serius di kemudian hari.
Konvensi lain yang saya putuskan adalah memutar seluruh tampilan, bukan memindahkan kamera, sesuatu yang telah saya lakukan di beberapa project lain. Salah satu keuntungannya adalah semuanya ditempatkan ke "turntable" sehingga menarik mouse ke kiri dan ke kanan akan memutar objek yang dimaksud, tetapi memperbesar hanya mengubah camera.position.z.
Bidang pandang (atau FOV) untuk kamera juga bersifat dinamis. Saat seseorang menariknya ke luar, bidang pandang akan melebar, sehingga mencakup lebih banyak galaksi. 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 menekan FOV ke sesuatu yang mirip kaca pembesar tanpa harus menangani masalah pemotongan bidang dekat.
Dari sini, saya dapat "meletakkan" Matahari pada beberapa unit dari inti galaksi. Saya juga dapat memvisualisasikan ukuran relatif tata surya dengan memetakan radius Kuiper Cliff (akhirnya saya memilih untuk memvisualisasikan Awan Oort). Dalam model tata surya 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 busa plasma panas dan perlu berdenyut serta berubah dari waktu ke waktu. Hal ini disimulasikan melalui tekstur bitmap dari gambar inframerah permukaan matahari. Shader permukaan membuat pencarian warna berdasarkan hitam putih tekstur ini dan melakukan pencarian di gradien warna terpisah. Jika pencarian ini bergeser dari waktu ke waktu, hal ini akan menciptakan distorsi seperti lava.
Teknik serupa digunakan untuk korona Matahari, kecuali bahwa kartu sprite datar yang selalu menghadap kamera menggunakan https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.
Flare matahari dibuat melalui shader verteks dan fragmen yang diterapkan ke torus, yang berputar di sekitar tepi permukaan matahari. Vertex shader memiliki fungsi derau yang menyebabkannya menenun dengan gaya seperti gumpalan.
Di sinilah saya mulai mengalami beberapa masalah z-fighting karena presisi GL. Semua variabel untuk presisi telah ditentukan sebelumnya di THREE.js, sehingga saya tidak dapat meningkatkan presisi secara realistis tanpa banyak pekerjaan. Masalah presisi tidak seburuk di dekat asal. Namun, setelah saya mulai membuat model sistem bintang lainnya, hal ini menjadi masalah.
Ada beberapa hack yang saya gunakan untuk mengurangi z-fighting. Material.polygonoffset THREE adalah properti yang memungkinkan poligon dirender di lokasi yang berbeda (sejauh yang saya pahami). Ini digunakan untuk memaksa bidang korona agar selalu dirender di atas permukaan Matahari. Di bawahnya, "halo" Matahari dirender untuk memberikan sinar cahaya tajam yang menjauh dari bola.
Masalah lain yang terkait dengan presisi adalah model bintang akan mulai bergetar saat tampilan diperbesar. Untuk memperbaikinya, saya harus "mengosongkan" rotasi tampilan dan memutar model bintang dan peta lingkungan secara terpisah untuk memberikan ilusi bahwa Anda mengorbit bintang.
Membuat Lensflare
Visualisasi ruang adalah tempat saya merasa dapat menggunakan lensflare secara berlebihan. THREE.LensFlare memenuhi tujuan ini, yang perlu saya lakukan hanyalah menambahkan beberapa segi enam anamorfik dan sedikit JJ Abrams. Cuplikan di bawah menunjukkan cara membuatnya di tampilan 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 scroll 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 ini:
mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}
map
adalah tekstur yang termasuk dalam material, yang mendapatkan fungsi onUpdate yang dapat Anda tulis ulang. Menetapkan offset-nya akan menyebabkan tekstur "didesak" di sepanjang sumbu tersebut, dan mengirim spam needsUpdate = true akan memaksa perilaku ini untuk berulang.
Menggunakan gradien warna
Setiap bintang memiliki warna yang berbeda berdasarkan "indeks warna" yang ditetapkan oleh astronom. Secara umum, bintang merah lebih dingin dan bintang biru/ungu lebih panas. Terdapat pita warna putih dan oranye sedang dalam gradien ini.
Saat merender bintang, saya ingin memberi setiap partikel warnanya sendiri berdasarkan data ini. Cara melakukannya adalah dengan "atribut" yang diberikan ke material shader yang diterapkan ke 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 memberi setiap partikel warna uniknya di shader. Biasanya, seseorang akan meneruskan vec3 warna, tetapi dalam hal ini, saya meneruskan float untuk pencarian gradien warna akhir.
Ramp warna terlihat seperti ini, tetapi saya perlu mengakses data warna bitmap-nya dari JavaScript. Cara saya melakukannya adalah dengan memuat gambar ke DOM terlebih dahulu, menggambarnya ke 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.
Wrangling shader
Selama project, saya menemukan bahwa saya perlu menulis lebih banyak shader untuk mencapai semua efek visual. Saya menulis loader shader kustom untuk tujuan ini karena saya lelah memiliki shader yang aktif 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 fragmen dan .vsh untuk shader vertex), mencoba memuat datanya, lalu hanya mengganti daftar dengan objek. Hasil akhirnya ada di seragam THREE.js yang dapat Anda teruskan shadernya seperti ini:
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
Saya mungkin bisa menggunakan require.js meskipun hal itu akan memerlukan beberapa penyusunan 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 tampilan THREE.js. Metode yang saya gunakan menghitung posisi model absolut tempat saya ingin teks muncul, lalu me-resolve posisi layar menggunakan THREE.Projector(), dan terakhir menggunakan CSS "top" dan "left" untuk menempatkan elemen CSS di 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 tampilan THREE, dan Anda dapat "meletakkan" elemen CSS dalam 3D seolah-olah berada di atas tampilan THREE. Namun, ada batasan untuk hal ini, misalnya Anda tidak akan dapat menempatkan teks di bawah objek THREE.js. Hal ini masih jauh lebih cepat daripada mencoba melakukan tata letak menggunakan atribut CSS "top" dan "left".
Anda dapat menemukan demo (dan kode dalam sumber tampilan) untuk 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 diwarisi dari tampilan. Teknik ini disebut "billboarding", dan Giroskop sangat cocok untuk melakukannya.
Yang sangat bagus 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 posisi. Mungkinkah hal ini disebabkan oleh kerning dan padding teks? Masalah lainnya adalah teks menjadi pixelated saat diperbesar karena perender DOM memperlakukan teks yang dirender sebagai quad bertekstur, yang harus diperhatikan saat menggunakan metode ini. Seharusnya saya bisa menggunakan teks berukuran font raksasa, dan mungkin ini adalah sesuatu untuk eksplorasi di masa mendatang. Dalam project ini, saya juga menggunakan label teks penempatan CSS "top/left", yang dijelaskan sebelumnya, untuk elemen yang sangat kecil yang menyertai planet di tata surya.
Pemutaran dan pengulangan musik
Musik yang diputar selama 'Peta Galaksi' Mass Effect adalah karya komposer Bioware, Sam Hulick dan Jack Wall, dan musik tersebut memiliki jenis emosi yang ingin saya rasakan oleh pengunjung. Kami ingin menyertakan musik dalam project karena kami merasa musik adalah bagian penting dari atmosfer, yang membantu menciptakan rasa kagum dan keheranan yang kami coba capai.
Produser kami, Valdean Klump, menghubungi Sam yang memiliki banyak musik "cutting floor" dari Mass Effect yang dengan senang hati dia izinkan untuk kami gunakan. Lagu tersebut berjudul "In a Strange Land".
Saya menggunakan tag audio untuk pemutaran musik, tetapi bahkan di Chrome, atribut "loop" tidak dapat diandalkan -- terkadang atribut tersebut gagal diputar berulang. Pada akhirnya, hack tag audio ganda ini digunakan untuk memeriksa akhir pemutaran dan beralih ke tag lain untuk diputar. Yang mengecewakan adalah still ini tidak selalu berulang 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();
Peluang untuk melakukan peningkatan
Setelah menggunakan THREE.js selama beberapa waktu, saya merasa data saya terlalu banyak bercampur dengan kode saya. Misalnya, saat menentukan bahan, tekstur, dan petunjuk geometri secara inline, saya pada dasarnya melakukan "pemodelan 3D dengan kode". Hal ini terasa sangat buruk, dan merupakan area yang dapat ditingkatkan dengan upaya THREE.js di masa mendatang, misalnya menentukan data material dalam file terpisah, sebaiknya dapat dilihat dan diubah dalam beberapa konteks, dan dapat dikembalikan ke project utama.
Kolega kami, Ray McClure, juga menghabiskan waktu untuk membuat beberapa "suara ruang" generatif yang luar biasa yang harus dipotong karena API audio web tidak stabil, sehingga sering kali membuat Chrome error. Hal ini sangat disayangkan… tetapi hal ini membuat kami lebih banyak memikirkan ruang suara untuk pekerjaan mendatang. Saat ini, saya diberi tahu bahwa Web Audio API telah di-patch sehingga mungkin sekarang sudah berfungsi. Hal ini perlu diperhatikan di masa mendatang.
Elemen tipografi yang disambungkan dengan WebGL masih menjadi tantangan, dan saya tidak 100% yakin bahwa apa yang kita lakukan di sini adalah cara yang benar. Masih terasa seperti hack. Mungkin versi THREE mendatang, dengan Renderer CSS yang akan datang, dapat digunakan untuk menggabungkan kedua dunia tersebut dengan lebih baik.
Kredit
Terima kasih kepada Aaron Koblin yang mengizinkan saya untuk mengerjakan project ini. Jono Brandel untuk desain + implementasi UI yang luar biasa, perlakuan jenis, dan implementasi tur. Valdean Klump karena telah memberi nama project dan semua teks. Sabah Ahmed karena telah menyelesaikan banyak hak penggunaan untuk sumber data dan gambar. Clem Wright karena telah menghubungi 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.