Merhaba! Ben Michael Chang. Google'daki Data Arts Ekibi'nde çalışıyorum. Kısa süre önce, yakındaki yıldızları görselleştiren bir Chrome denemesi olan 100.000 Yıldız'ı tamamladık. Proje, THREE.js ve CSS3D ile oluşturuldu. Bu örnek olay incelemesinde, keşif sürecini özetleyecek, bazı programlama tekniklerini paylaşacak ve gelecekteki iyileştirmelerle ilgili bazı düşüncelerle bitireceğim.
Burada ele alınan konular oldukça geniş kapsamlı olacak ve THREE.js hakkında biraz bilgi sahibi olmanızı gerektirecek. Ancak bu yazıyı teknik bir analiz olarak yine de keyifle okuyabileceğinizi umuyorum. Sağdaki içindekiler düğmesini kullanarak ilgilendiğiniz bölüme gidebilirsiniz. Öncelikle projenin oluşturma kısmını, ardından gölgelendirici yönetimini ve son olarak CSS metin etiketlerinin WebGL ile birlikte nasıl kullanılacağını göstereceğim.

Uzayı Keşfetme
Small Arms Globe'u bitirdikten kısa bir süre sonra, alan derinliği olan bir THREE.js parçacık demosuyla denemeler yapıyordum. Uygulanan efekt miktarını ayarlayarak sahnenin yorumlanan "ölçeğini" değiştirebildiğimi fark ettim. Alan derinliği efekti çok belirgin olduğunda, uzak nesneler, eğme-kaydırma fotoğrafçılığının mikroskobik bir sahneye bakma yanılsaması yaratmasına benzer şekilde çok bulanık hale geliyordu. Bunun aksine, efekti azaltmak derin uzaya bakıyormuş gibi görünmenize neden oluyordu.
Parçacık konumlarını yerleştirmek için kullanabileceğim verileri aramaya başladım. Bu arayış beni astronexus.com'un HYG veritabanına yönlendirdi. Bu veritabanı, önceden hesaplanmış xyz Kartezyen koordinatlarıyla birlikte üç veri kaynağının (Hipparcos, Yale Bright Star Catalog ve Gliese/Jahreiss Catalog) derlemesidir. Başlayalım!


Yıldız verilerini 3D uzaya yerleştiren bir şey oluşturmak yaklaşık bir saat sürdü. Veri kümesinde tam olarak 119.617 yıldız vardır. Bu nedenle, her yıldızı bir parçacıkla temsil etmek modern bir GPU için sorun teşkil etmez. Ayrıca, tek tek tanımlanmış 87 yıldız var. Bu nedenle, Small Arms Globe'da açıkladığım teknikle aynı olan bir CSS işaretçi katmanı oluşturdum.
Bu sırada Mass Effect serisini yeni bitirmiştim. Oyunda oyuncu, galaksiyi keşfetmeye ve çeşitli gezegenleri tarayıp tamamen kurgusal, Wikipedia tarzı tarihleri hakkında bilgi edinmeye davet edilir. Gezegende hangi türlerin geliştiği, jeolojik tarihi vb. hakkında bilgi verilir.
Yıldızlar hakkında çok fazla gerçek veri olduğunu göz önünde bulundurarak, galaksi hakkında da aynı şekilde gerçek bilgiler sunulabileceği düşünülebilir. Bu projenin nihai hedefi, bu verileri hayata geçirmek, izleyicinin Mass Effect tarzında galaksiyi keşfetmesine, yıldızlar ve dağılımları hakkında bilgi edinmesine ve umarız uzayla ilgili bir hayranlık ve merak duygusu uyandırmasına olanak tanımaktır. Bora
Bu örnek olay incelemesinin geri kalanına başlamadan önce kesinlikle bir astronom olmadığımı ve bunun, harici uzmanların tavsiyeleriyle desteklenen amatör bir araştırma çalışması olduğunu belirtmem gerekiyor. Bu proje kesinlikle uzayın sanatçı yorumu olarak değerlendirilmelidir.
Building a Galaxy
Planım, yıldız verilerini bağlama yerleştirebilecek ve umarım Samanyolu'ndaki yerimizin muhteşem bir görünümünü sunabilecek bir galaksi modeli oluşturmaktı.

Samanyolu'nu oluşturmak için 100.000 parçacık oluşturdum ve galaktik kolların oluşum şeklini taklit ederek bunları spiral şeklinde yerleştirdim. Sarmal kol oluşumunun ayrıntılarıyla ilgili çok endişelenmedim çünkü bu, matematiksel bir modelden ziyade temsili bir model olacaktı. Ancak sarmal kolların sayısını az çok doğru ve "doğru yönde" döndürmeye çalıştım.
Samanyolu modelinin sonraki versiyonlarında, parçacıkların kullanımını azaltarak parçacıklara eşlik edecek bir galaksi düzlem görüntüsünü öne çıkardım. Bu sayede, modelin daha çok fotoğraf görünümüne sahip olmasını umdum. Gerçek görüntü, bizden yaklaşık 70 milyon ışık yılı uzaklıkta bulunan NGC 1232 sarmal galaksisine aittir. Bu görüntü, Samanyolu gibi görünecek şekilde değiştirilmiştir.

Başlangıçta bir GL birimini (temelde 3 boyutlu bir piksel) bir ışık yılı olarak temsil etmeye karar verdim. Bu, görselleştirilen her şeyin yerleşimini birleştiren bir yöntemdi ancak maalesef daha sonra ciddi hassasiyet sorunlarına yol açtı.
Kamerayı hareket ettirmek yerine tüm sahneyi döndürmeye karar verdim. Bunu daha önce birkaç projede de yapmıştım. Bir avantajı, her şeyin bir "döner tabla" üzerine yerleştirilmesi ve böylece fareyle sola ve sağa sürüklemenin söz konusu nesneyi döndürmesi, ancak yakınlaştırmanın yalnızca camera.position.z değerini değiştirmekten ibaret olmasıdır.
Kameranın görüş alanı (FOV) da dinamiktir. Biri dışarı doğru çekildiğinde görüş alanı genişler ve galaksinin daha fazla bölümü görünür. Bir yıldıza doğru içe doğru hareket ederken ise görüş alanı daralır. Bu sayede kamera, yakın düzlem kırpma sorunlarıyla uğraşmak zorunda kalmadan görüş alanını (FOV) galaksiye kıyasla sonsuz küçük olan şeyleri görebileceği şekilde, tanrısal bir büyüteç gibi daraltabilir.

Buradan, Güneş'i galaktik çekirdekten belirli bir uzaklığa "yerleştirebildim". Ayrıca, Kuiper Kuşağı'nın yarıçapını haritalandırarak güneş sisteminin göreceli boyutunu görselleştirebildim (sonunda Oort Bulutu'nu görselleştirmeyi tercih ettim). Bu model güneş sisteminde, Dünya'nın basitleştirilmiş yörüngesini ve Güneş'in gerçek yarıçapını da karşılaştırmalı olarak görselleştirebiliyordum.

Güneşin oluşturulması zordu. Bildiğim tüm gerçek zamanlı grafik tekniklerini kullanarak hile yapmam gerekti. Güneş'in yüzeyi sıcak bir plazma köpüğüdür ve zaman içinde titreşip değişmesi gerekir. Bu, güneş yüzeyinin kızılötesi görüntüsünün bitmap dokusuyla simüle edildi. Yüzey gölgelendiricisi, bu dokunun gri tonlamasına göre renk araması yapar ve ayrı bir renk rampasında arama gerçekleştirir. Bu arama zaman içinde kaydırıldığında lav benzeri bir bozulma oluşur.
Güneşin koronası için de benzer bir teknik kullanıldı. Ancak bu teknikte, https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js kullanılarak her zaman kameraya dönük düz bir sprite kartı kullanıldı.

Güneş patlamaları, güneş yüzeyinin kenarında dönen bir torusa uygulanan köşe ve parça gölgelendiriciler aracılığıyla oluşturuldu. Köşe gölgelendiricinin, blob benzeri bir şekilde örülmesine neden olan bir gürültü işlevi vardır.
GL hassasiyeti nedeniyle burada bazı Z-fighting sorunları yaşamaya başladım. Kesinlik için tüm değişkenler THREE.js'de önceden tanımlanmıştı. Bu nedenle, çok fazla çalışma yapmadan kesinliği artırmak gerçekçi değildi. Hassasiyet sorunları, başlangıç noktasına yakın yerlerde o kadar kötü değildi. Ancak diğer yıldız sistemlerini modellemeye başladığımda bu durum sorun haline geldi.

Z-fighting'i azaltmak için kullandığım birkaç yöntem vardı. THREE.js'deki Material.polygonoffset, poligonların farklı bir algılanan konumda oluşturulmasına olanak tanıyan bir özelliktir (anladığım kadarıyla). Bu, korona düzleminin her zaman Güneş'in yüzeyinin üzerinde oluşturulmasını sağlamak için kullanıldı. Bunun altında, küreden uzaklaşan keskin ışınlar oluşturmak için bir güneş "halesi" oluşturuldu.
Hassasiyetle ilgili başka bir sorun da sahne yakınlaştırıldığında yıldız modellerin titremeye başlamasıydı. Bu sorunu düzeltmek için sahne dönüşünü "sıfırlamam" ve yıldızın etrafında dönüyormuşsunuz gibi bir yanılsama oluşturmak için yıldız modelini ve ortam haritasını ayrı ayrı döndürmem gerekti.
Lens yansıması oluşturma

Uzay görselleştirmelerinde lens parlamasını aşırı kullanabilirim. THREE.LensFlare bu amaçlara hizmet ediyor. Tek yapmam gereken, biraz anamorfik altıgen ve bir tutam JJ Abrams eklemekti. Aşağıdaki snippet'te, bunları sahnenizde nasıl oluşturacağınız gösterilmektedir.
// 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;
}
}
Doku kaydırmayı kolayca yapma

"Uzamsal yönlendirme düzlemi" için devasa bir THREE.CylinderGeometry() oluşturuldu ve Güneş'in merkezine yerleştirildi. Dışa doğru yayılan "ışık dalgası" oluşturmak için doku kaymasını zaman içinde şu şekilde değiştirdim:
mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}
map
, materyale ait ve üzerine yazabileceğiniz bir onUpdate işlevi alan dokudur. Ofset ayarlandığında doku, bu eksen boyunca "kaydırılır". Spamming needsUpdate = true, bu davranışın döngüye girmesine neden olur.
Renk rampalarını kullanma
Her yıldız, astronomların atadığı "renk indeksine" göre farklı bir renge sahiptir. Genel olarak kırmızı yıldızlar daha soğuk, mavi/mor yıldızlar ise daha sıcaktır. Bu gradyanda beyaz ve ara turuncu renklerden oluşan bir bant bulunur.
Yıldızları oluştururken her parçacığa bu verilere göre kendi rengini vermek istedim. Bu işlem, parçacıklara uygulanan gölgelendirici malzemeye verilen "özellikler" ile yapılıyordu.
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};
colorIndex dizisini doldurmak, gölgelendiricideki her parçacığa benzersiz rengini verir. Normalde bir renk vec3'ü iletilir ancak bu örnekte, nihai renk rampası araması için bir float iletiliyor.

Renk rampası bu şekilde görünüyordu ancak JavaScript'ten bitmap renk verilerine erişmem gerekiyordu. Bu işlemi yapmak için önce resmi DOM'a yükledim, tuval öğesine çizdim ve ardından tuval bit eşlemine eriştim.
// 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;
}
Aynı yöntem, yıldız modeli görünümündeki yıldızları renklendirmek için de kullanılır.

Gölgeleyici ayırma
Proje boyunca tüm görsel efektleri elde etmek için giderek daha fazla gölgelendirici yazmam gerektiğini fark ettim. Bu amaçla özel bir gölgelendirici yükleyici yazdım çünkü gölgelendiricilerin index.html'de bulunmasından yorulmuştum.
// 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() işlevi, gölgelendirici dosyası adlarının bir listesini alır (parça için .fsh ve köşe gölgelendiriciler için .vsh beklenir), verilerini yüklemeye çalışır ve ardından listeyi nesnelerle değiştirir. Sonuç olarak, THREE.js üniformalarınızda gölgelendiricileri şu şekilde iletebilirsiniz:
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
Bu amaç için biraz kod yeniden düzenlemesi gerektirecek olsa da muhtemelen require.js'yi kullanabilirdim. Bu çözüm çok daha kolay olsa da geliştirilebileceğini düşünüyorum. Belki de THREE.js uzantısı olarak bile geliştirilebilir. Bu işlemi daha iyi yapmanın yolları veya önerileriniz varsa lütfen bize bildirin.
THREE.js üzerinde CSS metin etiketleri
Son projemiz Small Arms Globe'da, metin etiketlerinin bir THREE.js sahnesinin üzerinde görünmesiyle ilgili denemeler yaptım. Kullandığım yöntem, metnin görünmesini istediğim yerin mutlak model konumunu hesaplar, ardından THREE.Projector() kullanarak ekran konumunu çözer ve son olarak CSS öğelerini istenen konuma yerleştirmek için CSS "top" ve "left" özelliklerini kullanır.
Bu projenin ilk yinelemelerinde aynı teknik kullanıldı ancak Luis Cruz'un açıkladığı bu diğer yöntemi denemek için sabırsızlanıyorum.
Temel fikir: CSS3D'nin matris dönüşümünü THREE'nin kamerası ve sahnesiyle eşleştirin. Böylece, CSS öğelerini THREE'nin sahnesinin üzerindeymiş gibi 3D olarak"yerleştirebilirsiniz". Ancak bu konuda sınırlamalar vardır. Örneğin, metinleri bir THREE.js nesnesinin altına yerleştiremezsiniz. Bu yöntem, "top" ve "left" CSS özelliklerini kullanarak düzen oluşturmaya çalışmaktan çok daha hızlıdır.

Bununla ilgili demoyu (ve kaynak kodunu) burada bulabilirsiniz. Ancak THREE.js için matris sırasının değiştiğini fark ettim. Güncellediğim işlev:
/_ 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(",") + ")";
}
Her şey dönüştürüldüğünden metin artık kameraya dönük değildir. Çözüm olarak, bir Object3D'nin sahneden devraldığı yönünü "kaybetmesini" sağlayan THREE.Gyroscope() kullanıldı. Bu tekniğe "billboarding" adı verilir ve Gyroscope bu işlem için idealdir.
Normal DOM ve CSS'nin tümünün çalışmaya devam etmesi (ör. 3D metin etiketinin üzerine fareyle gelindiğinde gölgeyle parlaması) gerçekten çok güzel.

Yazı tipi ölçeklendirmesinin, yakınlaştırma sırasında konumlandırmayla ilgili sorunlara neden olduğunu fark ettim. Belki de bu durum, metnin karakter aralığı ve dolgusundan kaynaklanıyordur. Başka bir sorun da DOM oluşturucu, oluşturulan metni dokulu bir dörtgen olarak işlediğinden bu yöntemi kullanırken dikkat edilmesi gereken bir nokta olarak metnin yakınlaştırıldığında pikselli hale gelmesiydi. Geriye dönüp baktığımda, devasa yazı tipi boyutunda metin kullanabileceğimi görüyorum. Belki bu, gelecekte keşfedilmesi gereken bir şeydir. Bu projede, güneş sistemindeki gezegenlere eşlik eden çok küçük öğeler için daha önce açıklanan "top/left" CSS yerleştirme metin etiketlerini de kullandım.
Müzik çalma ve döngüye alma
Mass Effect'in "Galactic Map" bölümünde çalınan müzik, Bioware bestecileri Sam Hulick ve Jack Wall'a aitti ve ziyaretçinin yaşamasını istediğim duyguyu yansıtıyordu. Atmosferin önemli bir parçası olduğunu düşündüğümüz için projemizde müzik kullanmak istedik. Müzik, hedeflediğimiz hayranlık ve merak duygusunu yaratmamıza yardımcı olacaktı.
Yapımcımız Valdean Klump, Mass Effect'ten bir sürü "kullanılmayan" müzik parçası olan Sam ile iletişime geçti. Sam, bu parçaları kullanmamıza izin verdi. Parçanın adı "In a Strange Land" (Garip Bir Ülkede).
Müzik çalmak için ses etiketini kullandım ancak Chrome'da bile "loop" özelliği güvenilir değildi. Bazen döngü oluşturma işlemi başarısız oluyordu. Sonuç olarak, bu çift ses etiketi hack'i, oynatmanın sonunu kontrol etmek ve oynatma için diğer etikete geçmek üzere kullanıldı. Hayal kırıklığı yaratan nokta, bu görüntünün her zaman mükemmel bir şekilde döngüye girmemesiydi. Ancak bu konuda elimden gelenin en iyisini yaptığımı düşünüyorum.
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();
İyileştirme fırsatları
Bir süredir THREE.js ile çalıştıktan sonra verilerimin kodumla çok fazla karıştığı noktaya geldiğimi hissediyorum. Örneğin, malzemeleri, dokuları ve geometri talimatlarını satır içi olarak tanımlarken aslında "kodla 3D modelleme" yapıyordum. Bu durum gerçekten kötüydü ve THREE.js ile gelecekteki çalışmaların büyük ölçüde iyileştirebileceği bir alan. Örneğin, malzeme verilerini tercihen bir bağlamda görüntülenebilir ve ayarlanabilir şekilde ayrı bir dosyada tanımlayarak ana projeye geri getirebilirsiniz.
Meslektaşımız Ray McClure da bir süre harcayarak harika üretken "uzay sesleri" oluşturdu. Ancak web audio API'nin kararsız olması ve Chrome'un zaman zaman kilitlenmesi nedeniyle bu sesler kesilmek zorunda kaldı. Bu durum üzücü olsa da gelecekteki çalışmalarımızda ses alanında daha fazla düşünmemizi sağladı. Bu makalenin yazıldığı sırada Web Audio API'nin düzeltildiği bildirildi. Bu nedenle, bu özellik şu anda çalışıyor olabilir. Gelecekte bu konuya dikkat etmenizi öneririz.
Tipografik öğelerin WebGL ile eşleştirilmesi hâlâ zorlu bir süreç ve burada yaptığımızın doğru yöntem olduğundan% 100 emin değilim. Hâlâ bir hack gibi hissediyorum. Belki de yakında kullanıma sunulacak CSS oluşturucusu ile THREE'nin gelecekteki sürümleri, iki dünyayı daha iyi birleştirmek için kullanılabilir.
Kredi
Bu projede istediğim gibi çalışmama izin verdiği için Aaron Koblin'e teşekkür ederim. Mükemmel kullanıcı arayüzü tasarımı + uygulama, yazı tipi düzenlemesi ve tur uygulaması için Jono Brandel'e. Projeye adını ve tüm metinleri veren Valdean Klump'a Veri ve resim kaynaklarının kullanım haklarını temizlediği için Sabah Ahmed'e teşekkür ederiz. Yayın için doğru kişilere ulaştığı için Clem Wright'a teşekkür ederiz. Doug Fritz, teknik mükemmellik için. JS ve CSS'yi bana öğrettiği için George Brower'a. Ayrıca THREE. js için Mr.Doob'a teşekkür ederiz.