Merhaba! Adım Michael Chang. Google'ın Veri Sanatları Ekibi'nde çalışıyorum. Yakın yıldızları görselleştiren bir Chrome denemesi olan 100.000 Yıldız'ı kısa süre önce tamamladık. Proje, THREE.js ve CSS3D ile oluşturulmuştur. Bu örnek olay incelemesinde keşif sürecini özetleyeceğim, bazı programlama tekniklerini paylaşacağım ve gelecekte yapılacak iyileştirmelerle ilgili bazı düşüncelerimi paylaşacağım.
Burada ele alınacak konular oldukça geniş kapsamlı olacak ve THREE.js hakkında bilgi sahibi olmanız gerekecek. Yine de teknik bir inceleme olarak bu makaleden keyif alacağınızı umuyorum. Sağdaki içindekiler bölümünü kullanarak ilgilendiğiniz bir alana atlayabilirsiniz. Öncelikle projenin oluşturma bölümü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 tamamladıktan kısa bir süre sonra, alan derinliği olan bir THREE.js parçacık denemesi yapıyordum. Uygulanan efektin miktarını ayarlayarak sahnenin yorumlanmış "ölçeğinin" değiştirilebileceğini fark ettim. Alan derinliği efekti gerçekten uç noktalara ulaştığında uzaktaki nesneler, mikroskobik bir manzaraya bakıyormuş hissi veren tilt-shift fotoğrafçılığa benzer şekilde gerçekten bulanıklaşıyordu. Bunun aksine, efekti azalttığınızda derin uzaya bakıyormuşsunuz gibi görünür.
Parçacık konumlarını beslemek için kullanabileceğim verileri aramaya başladım. Bu arayış beni, önceden hesaplanmış xyz Kartezyen koordinatlarının eşlik ettiği üç veri kaynağının (Hipparcos, Yale Bright Star Catalog ve Gliese/Jahreiss Catalog) bir derlemesi olan astronexus.com'un HYG veritabanına yönlendirdi. 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ıklarla temsil etmek modern bir GPU için sorun teşkil etmez. Ayrıca, tek tek tanımlanmış 87 yıldız da var. Bu nedenle, Small Arms Globe'da açıkladığım tekniği kullanarak bir CSS işaretçi yer paylaşımı oluşturdum.
Bu sırada Mass Effect serisini yeni bitirmiştim. Oyunda oyuncu, galaksiyi keşfetmeye ve çeşitli gezegenleri taramaya ve tamamen kurgusal, Vikipedi'ye benzeyen tarihlerini okumaya davet edilir: Gezegende hangi türlerin geliştiği, jeolojik geçmişi vb.
Yıldızlarla ilgili gerçek verilere sahip olan biri, galaksi hakkında gerçek bilgileri de aynı şekilde sunabilir. Bu projenin nihai hedefi, bu verileri canlandırmak, izleyicinin Mass Effect tarzında galaksiyi keşfetmesine, yıldızlar ve dağılımlarıyla ilgili bilgi edinmesine olanak tanımak ve umarım uzaya karşı hayranlık ve merak duygusu uyandırmaktır. Bora
Bu örnek çalışmanın geri kalanına başlamadan önce, astronom olmadığımı ve bu çalışmanın, harici uzmanlardan alınan bazı tavsiyelerle desteklenen amatör bir araştırma olduğunu belirtmek isterim. Bu proje, sanatçının mekanı yorumlaması olarak değerlendirilmelidir.
Galaksi oluşturma
Planım, yıldız verilerini bağlama yerleştirebilecek ve 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 galaksi kollarının oluşum şeklini taklit ederek bunları spiral şeklinde yerleştirdim. Bu matematiksel bir model yerine temsili bir model olacağından, spiral kol oluşumunun ayrıntıları konusunda çok endişelenmedim. Ancak sarmal kol sayısını aşağı yukarı doğru yapmaya ve "doğru yönde" dönmeye çalıştım.
Samanyolu modelinin sonraki sürümlerinde, parçacıklara eşlik edecek bir galaksinin düzlemsel resmini kullanarak parçacıkların kullanımını azalttım. Böylece, daha fotoğrafik bir görünüm elde etmeyi umuyorum. Gerçek resim, yaklaşık 70 milyon ışık yılı uzaklıktaki NGC 1232 sarmal galaksisine ait olup Samanyolu'na benzeyecek şekilde düzenlenmiştir.
Başlangıçta, temelde 3D'de bir piksel olan bir GL birimini bir ışık yılı olarak göstermeye karar verdim. Bu, görselleştirilen her şeyin yerleşimini birleştiren ve maalesef daha sonra ciddi hassasiyet sorunları yaşamama neden olan bir kuraldı.
Kamerayı hareket ettirmek yerine sahnenin tamamını döndürmeye karar verdim. Bunu birkaç projede daha yapmıştım. Bu yaklaşımın avantajlarından biri, her şeyin bir "döner tablaya" yerleştirilmesidir. Böylece, fareyi sola ve sağa sürükleyerek söz konusu nesneyi döndürebilirsiniz. Ancak yakınlaştırmak için tek yapmanız gereken camera.position.z değerini değiştirmek.
Kameranın görüş alanı da dinamiktir. Dışa doğru çekildikçe görüş alanı genişler ve galaksinin daha fazlasını kapsar. Bir yıldıza doğru yaklaşırken tam tersi olur, görüş alanı daralır. Bu sayede kamera, yakın plan kırpma sorunlarıyla uğraşmak zorunda kalmadan FOV'yi bir tür tanrısal büyüteç boyutuna indirerek galaksiye kıyasla son derece küçük olan nesneleri görüntüleyebilir.
Buradan Güneş'i, galaktik çekirdekten belirli bir mesafeye "yerleştirebildim". Kuiper Uçurumu'nun yarıçapını haritalayarak güneş sisteminin göreceli boyutunu da görselleştirebildim (Sonunda bunun yerine 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ştirebiliyorum.
Güneşi oluşturmak zordu. Bildiğim tüm gerçek zamanlı grafik tekniklerini kullanarak hile yapmak zorunda kaldım. Güneş'in yüzeyi, sıcak bir plazma köpüğüdür ve zaman içinde nabız gibi atması ve değişmesi gerekir. Bu, güneş yüzeyinin kızılötesi görüntüsünün bir bitmap dokusuyla simüle edildi. Yüzey gölgelendirici, bu dokunun gri tonlamasına göre bir renk araması yapar ve ayrı bir renk rampasında arama yapar. 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ı. Tek fark, https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js kullanılarak her zaman kameraya bakan düz bir sprite kartı olmasıydı.
Güneş parlamaları, güneş yüzeyinin hemen kenarında dönen bir torus'a uygulanan köşe ve parça gölgelendiricileri aracılığıyla oluşturuldu. Köşe düğümü gölgelendiricisinde, nokta benzeri bir şekilde örülmesine neden olan bir gürültü işlevi vardır.
GL hassasiyeti nedeniyle z-kavga sorunları yaşamaya başladığım yer burasıydı. Hassasiyetle ilgili tüm değişkenler THREE.js'de önceden tanımlanmıştı. Bu nedenle, çok fazla çalışma yapmadan hassasiyeti gerçekçi bir şekilde artıramadım. Hassasiyet sorunları, orijine yakın yerlerde o kadar kötü değildi. Ancak diğer yıldız sistemlerini modellemeye başladığımda bu sorun ortaya çıktı.
Z-kavgalarını azaltmak için birkaç hile kullandım. THREE'ün Material.polygonoffset özelliği, poligonların farklı bir algılanan konumda oluşturulmasına olanak tanır (anladığım kadarıyla). Bu, korona düzlemindeki görüntünün 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 ışık ışınları vermek için bir güneş "halası" oluşturuldu.
Hassasiyetle ilgili farklı bir sorun da, sahne yakınlaştırıldıkça yıldız modellerinin titremeye başlamasıydı. Bu sorunu düzeltmek için sahne dönme ayarını "sıfırlamalı" ve yıldızın etrafında dönüyormuşsunuz izlenimi vermek için yıldız modelini ve ortam haritasını ayrı ayrı döndürmem gerekiyordu.
Lens parlaması oluşturma
Uzay görselleştirmelerinde, lens parlaması efektini aşırı kullanabileceğimi düşünüyorum. THREE.LensFlare bu amaç için ideal. Tek yapmam gereken, birkaç anamorfik altıgen ve biraz JJ Abrams eklemekti. Aşağıdaki snippet'te, bunların sahnenizde nasıl oluşturulacağı 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ırma işlemini kolayca yapma
"Uzamsal yön düzlemi" için devasa bir THREE.CylinderGeometry() oluşturuldu ve Güneş'in ortasına yerleştirildi. Dışa doğru yayılan "ışık dalgası"nı oluşturmak için zaman içinde doku ofsetini şu şekilde değiştirdim:
mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}
map
, malzemeye ait doku olup üzerine yazabildiğiniz bir onUpdate işlevi alır. Ofsetinin ayarlanması, dokunun bu eksen boyunca "kaydırılmasına" neden olur ve needsUpdate = true değerinin spam'lenmesi bu davranışın döngüye girmesine neden olur.
Renk aralıkları kullanma
Her yıldızın, astronomların atadığı "renk dizini"ne göre farklı bir rengi vardır. 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 turuncu renklerin bir bandı vardır.
Yıldızları oluştururken her parçacığa bu verilere göre kendi rengini vermek istedim. Bunu yapmanın yolu, parçacıklara uygulanan gölgelendirici malzemeye verilen "özellikler"di.
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};
colorIndex dizisini doldurmak, her parçacığa gölgelendiricide benzersiz bir renk verir. Normalde bir renk vec3 iletilir ancak bu örnekte, nihai renk rampası araması için bir float iletiyorum.
Renk rampası buna benziyordu ancak JavaScript'ten bitmap renk verilerine erişmem gerekiyordu. Bunu yapma şeklim, önce resmi DOM'a yüklemek, bir kanvas öğesine çizmek ve ardından kanvas bitmap'ine erişmek oldu.
// 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;
}
Ardından, yıldız modeli görünümündeki yıldızları tek tek renklendirmek için aynı yöntem kullanılır.
Gölgelendiriciyi düzenleme
Proje boyunca, tüm görsel efektleri oluşturmak için daha fazla gölgelendirici yazmam gerektiğini fark ettim. Shader'ların index.html dosyasında yer almasından bıktığım için bu amaç için özel bir gölgelendirici yükleyici yazdım.
// 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 dosya adlarının listesini alır (parçacık gölgelendiricileri için .fsh ve köşe gölgelendiricileri için .vsh beklenir), verilerini yüklemeye çalışır ve ardından listeyi nesnelerle değiştirir. Sonuç olarak, THREE.js üniformalarınıza şu şekilde gölgelendiriciler iletebilirsiniz:
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
Muhtemelen require.js'yi kullanabilirdim ancak bunun için kodların yeniden derlenmesi gerekirdi. Bu çözüm çok daha kolay olsa da bence geliştirilebilir. Hatta THREE.js uzantısı olarak bile geliştirilebilir. Bu konuda önerileriniz veya daha iyisini yapmanın yolları varsa lütfen bana bildirin.
THREE.js'in üstüne CSS metin etiketleri ekleme
Son projemiz olan Small Arms Globe'da, bir THREE.js sahnesinin üstünde metin etiketleri göstermeyi denedim. 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 "üst" ve "sol"u kullanır.
Bu projenin ilk iterasyonlarında aynı teknik kullanıldı ancak Luis Cruz tarafından açıklanan bu diğer yöntemi denemek için can atıyordum.
Temel fikir: CSS3D'nin matris dönüşümünü THREE'ün kamerası ve sahnesiyle eşleştirin. Böylece CSS öğelerini 3D'de THREE'ün sahnesinin üzerindeymiş gibi "yerleştirebilirsiniz". Bununla birlikte, bu işlemin bazı sınırlamaları vardır. Örneğin, metni bir THREE.js nesnesinin altına yerleştiremezsiniz. Bu yöntem, "üst" ve "sol" CSS özelliklerini kullanarak sayfa düzeni oluşturmaya çalışmaktan çok daha hızlıdır.
Bu demoyu (ve kaynak görüntülemedeki kodu) burada bulabilirsiniz. Ancak THREE.js için matris sırasının o zamandan beri 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üğü için metin artık kameraya bakmaz. Çözüm, bir Object3D'yi sahneden devralınan yönelimini "kaybetmeye" zorlayan THREE.Gyroscope() işlevini kullanmaktı. Bu tekniğe "billboarding" adı verilir ve Jiroskop bu işlem için mükemmel bir araçtır.
En güzeli de normal DOM ve CSS'nin tümünün çalışmaya devam etmesiydi. Örneğin, fareyle 3D metin etiketinin üzerine geldiğinizde etiketin gölgelerle parlaması gibi.
Yakınlaştırdığımda, yazı tipinin ölçeklendirilmesinin konumlandırmayla ilgili sorunlara neden olduğunu fark ettim. Bu durum, metnin kerning ve dolgu ayarlarından kaynaklanıyor olabilir mi? Bir diğer sorun da DOM oluşturma aracı, oluşturulan metni dokulu bir dörtgen olarak ele aldığından, metne yakınlaştırıldığında metnin piksel piksel olmasıydı. Bu yöntemi kullanırken dikkat edilmesi gereken bir noktadır. Şimdi geriye dönüp baktığımda, dev yazı tipi boyutunda metin kullanabilirdim. Belki de bunu gelecekte deneyeceğim. Bu projede, güneş sistemindeki gezegenlere eşlik eden çok küçük öğeler için daha önce açıklanan "üst/sol" CSS yerleşimi metin etiketlerini de kullandım.
Müzik çalma ve döngüye alma
Mass Effect'in "Galactic Map" bölümünde çalan parça, Bioware'ın bestecileri Sam Hulick ve Jack Wall tarafından yazılmıştı. Bu parça, ziyaretçilerin hissetmesini istediğim duyguyu yansıtıyordu. Projemizde müzik kullanmak istedik çünkü müziğin, hedeflediğimiz hayranlık ve merak duygusunu yaratmaya yardımcı olacak, atmosferin önemli bir parçası olduğunu düşünüyorduk.
Yapımcımız Valdean Klump, Sam ile iletişime geçti. Sam, Mass Effect'ten bir grup "kesme aşaması" müziğine sahipti ve bu müzikleri kullanmamıza çok nazik bir şekilde izin verdi. Parçanın adı "In a Strange Land".
Müzik çalma için ses etiketini kullandım ancak Chrome'da bile "döngü" özelliği güvenilir değildi. Bazen döngü oluşturma işlemi başarısız oluyordu. Sonunda, oynatmanın sona erip ermediğini kontrol etmek ve oynatmak için diğer etikete geçmek üzere bu ikili ses etiketi hilesi kullanıldı. Bu sabit görüntü her zaman mükemmel şekilde dönmedi. Maalesef elimden geleni 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ığı bir noktaya ulaştığımı hissediyorum. Örneğin, satır içi olarak malzemeleri, dokuları ve geometri talimatlarını tanımlarken aslında "kodla 3D modelleme" yapıyordum. Bu durum gerçekten kötüydü ve THREE.js ile gelecekteki çalışmaların çok daha iyi olabileceği bir alandır. Örneğin, malzeme verilerini ayrı bir dosyada tanımlayabilir, tercihen bazı bağlamlarda görüntülenebilir ve düzenlenebilir hale getirebilir ve ana projeye geri getirebilirsiniz.
İş arkadaşımız Ray McClure da bir süre boyunca harika üretken "uzay sesleri" oluşturdu. Ancak web ses API'sinin kararsız olması ve Chrome'u zaman zaman kilitlemesi nedeniyle bu seslerin kesilmesi gerekti. Bu durum talihsiz bir durum olsa da gelecekteki çalışmalarda ses konusunda daha fazla düşünmemizi sağladı. Bu e-postayı yazarken Web Audio API'ye bir düzeltme uygulandığı bildirildi. Bu nedenle, bu sorun şu anda düzeltilmiş olabilir. Gelecekte bu konuyu takip edebilirsiniz.
WebGL ile birlikte kullanılan yazım öğeleri hâlâ sorun olmaya devam ediyor ve burada yaptığımız şeyin doğru yöntem olduğundan% 100 emin değilim. Hâlâ bir saldırı gibi geliyor. Gelecek vaat eden CSS Oluşturucusu ile THREE'ün gelecekteki sürümleri, iki dünyayı daha iyi birleştirmek için kullanılabilir.
Kredi
Bu projede özgürce çalışmama izin verdiği için Aaron Koblin'a teşekkürler. Mükemmel kullanıcı arayüzü tasarımı ve uygulaması, yazı tipi kullanımı ve tur uygulaması için Jono Brandel. Valdean Klump'a projeye bir ad ve tüm metni verdiği için teşekkür ederiz. Veri ve resim kaynakları için tonlarca kullanım hakkı sorununu çözen Sabah Ahmed'e teşekkürler. Yayınlamayla ilgili olarak doğru kişilere ulaşan Clem Wright. Teknik mükemmellik için Doug Fritz. George Brower'a JS ve CSS'yi öğrettiği için. Ve elbette THREE.js için Bay Doob.