100 000 gwiazdek

Cześć! Nazywam się Michael Chang i pracuję w zespole Data Arts w Google. Niedawno przeprowadziliśmy 100 000 gwiazdekeksperyment Chrome z wizualizacją pobliskich gwiazd. Projekt został utworzony za pomocą języków THREE.js i CSS3D. W tym studium przypadku przedstawię proces odkrywania treści, przedstawię kilka technik programowania, a na koniec przedstawię kilka pomysłów na przyszłe ulepszenia.

Omówione tu tematy będą dość obszerne i wymagają znajomości języka THREE.js. Mam jednak nadzieję, że zainteresuje Cię ona również w formie post-mortemu technicznego. Możesz przechodzić do interesującej Cię sekcji, klikając przycisk spisu treści po prawej stronie. Najpierw pokażę część projektu: renderowanie, zarządzanie cieniowaniem, a na koniec – jak używać etykiet tekstowych CSS w połączeniu z WebGL.

100 000 gwiazdek, eksperyment w Chrome przeprowadzony przez zespół Data Arts
100 000 gwiazd wykorzystuje THREE.js do wizualizacji pobliskich gwiazd w Drogi Mlecznej.

Odkrywanie kosmosu

Wkrótce po zakończeniu Small Arms Globe poeksperymentować z głębią pola, w wersji demonstracyjnej cząstek THREE.js. Zauważyłem, że mogę zmienić zinterpretowaną „skalę” sceny, dostosowując stopień zastosowanego efektu. Gdy efekt głębi ostrości był naprawdę ekstremalny, odległe obiekty stały się bardzo niewyraźne, podobnie jak w przypadku fotografii z efektem przechylania, która daje wrażenie widoku mikroskopowego. I na odwrót – wyłączenie efektu sprawia wrażenie, jakby użytkownik patrzył w daleki kosmos.

Zacząłem szukać danych, których mógłbym użyć do wstrzyknięcia pozycji cząstek. To ścieżka prowadząca do bazy danych HYG firmy astronexus.com. Jest to kompilacja trzech źródeł danych (Hipparcos, Yale Bright Star Catalog i katalog gliese/Jahreiss Catalog), wraz z wstępnie obliczonymi współrzędnymi xyz kartezjańskimi. Zaczynamy!

Nanoszenie na wykres danych gwiazd.
Pierwszym krokiem jest ułożenie każdej gwiazdy w katalogu jako jednej cząstki.
Gwiazdy nazwane.
Niektóre gwiazdy w katalogu mają nazwy własne, oznaczone tutaj.

Zhakowanie obiektu, dzięki któremu dane o gwiazdach trafiły do przestrzeni 3D, zajęło około godziny. W zbiorze danych jest dokładnie 119 617 gwiazdek, więc reprezentowanie każdej gwiazdy z cząstką nie stanowi problemu w przypadku współczesnych GPU. Istnieje również 87 indywidualnie zidentyfikowanych gwiazd, więc utworzyliśmy nakładkę ze znacznikami CSS przy użyciu tej samej techniki, którą opisałem w witrynie Small Arms Globe.

W tym czasie skończyłam czytać serię Mass Effect. W grze gracz jest zachęcany do odkrywania galaktyki, skanowania różnych planet i poznawania ich całkowicie fikcyjnej, brzmiącej na Wikipedii historii, dotyczącej tego, jakie gatunki rozwinęły się na planecie, jaka była jej historia geologiczna itd.

Znając mnóstwo rzeczywistych danych na temat gwiazd, można w ten sam sposób przedstawić prawdziwe informacje o galaktyce. Ostatecznym celem tego projektu jest ożywienie tych danych, umożliwienie widzowi zbadania galaktyki à la Mass Effect, poznanie gwiazd i ich rozkładu, a także wzbudzenie zachwytu i zachwytu nad kosmosem. Uff...

Na początku tego studium przypadku na koniec stwierdzię, że wcale nie jestem astronomem, a te informacje są prowadzone w ramach badań amatorskich popartych radami zewnętrznych ekspertów. Projekt ten należy rozumieć jako artystyczną interpretację kosmosu.

Tworzenie galaktyki

Moim planem było proceduralne wygenerowanie modelu galaktyki, który pozwoli umieścić dane o gwiazdach w kontekście, i mam nadzieję, że uzyska wspaniały widok na naszą Drogę Mleczną.

Wczesny prototyp galaktyki.
Wczesny prototyp układu cząstek Drogi Mlecznej.

Aby wygenerować Drogę Mleczną, udało mi się wygenerować 100 000 cząstek i umieścić je w spirali,naśladując sposób powstawania ramion galaktyki. Nie obawiałem się koncepcji tworzenia spiralnych ramion, ponieważ będzie to model reprezentacyjny, a nie matematyczny. Udało mi się jednak zorientować się, czy liczba ramion spiralnych jest prawidłowa i kręcą się we „właściwym kierunku”.

W późniejszych wersjach modelu Drogi Mlecznej zawęziłem znaczenie cząstek na rzecz planarnego obrazu galaktyki, który towarzyszy cząstkom, mam nadzieję, że nadaje mu bardziej fotograficzny charakter. Rzeczywiste zdjęcie przedstawia galaktykę spiralną NGC 1232, która jest oddalona o około 70 milionów lat świetlnych od nas, a obraz zmanipulowany tak, by wyglądał jak Droga Mleczna.

Określanie skali galaktyki.
Każda jednostka GL jest rokiem świetlnym. W tym przypadku kula ma szerokość 110 tysięcy lat świetlnych i obejmuje układ cząstek.

Wcześnie postanowiłem przedstawić jedną jednostkę GL, czyli piksel w 3D, jako rok świetlny. Był to konwencja ujednoliconego umiejscowienia wszystkich elementów, która spowodowała jednak poważne problemy z dokładnością.

Kolejnym planem, który zdecydowałem, było obrócenie całej sceny zamiast poruszanie kamerą. Zrobiłam to w kilku innych projektach. Jedną z zalet jest to, że wszystko jest umieszczone na statywie obrotowym, dzięki czemu przeciąganie myszą w lewo i w prawo powoduje obrócenie obiektu. Powiększenie to kwestia zmiany wartości Camera.position.z.

Pole widzenia kamery też jest dynamiczne. W miarę odciągania się na zewnątrz pole widzenia się rozszerza, nabierając coraz większą część galaktyki. Przy przesuwaniu się do wewnątrz w kierunku gwiazdy zachodzi odwrotność, a pole widzenia zwęża się. Dzięki temu kamera może rejestrować obiekty, które są nieskończone (w porównaniu z galaktyką) dzięki skierowaniu pola widzenia do powierzchni przypominającej boskie szkło powiększające, bez konieczności rozwiązywania problemów z przycinaniem obrazu w pobliżu płaszczyzny.

Różne sposoby renderowania galaktyki.
(powyżej) Wczesna galaktyka cząstek. (poniżej) cząstki z płaszczyzną obrazu.

Tutaj udało mi się „umieścić” Słońce w odległości określonej liczby jednostek od centrum galaktyki. Udało mi się też zwizualizować względną wielkość Układu Słonecznego, nakreślając promień klifu Kuiper (ostatecznie zdecydowałem się na wizualizację chmury Oort). Ten model Układu Słonecznego pozwolił mi także zwizualizować uproszczoną orbitę Ziemi i porównywany promień Słońca.

Układ Słoneczny.
Słońce okrążane wokół planet i kulę reprezentującą pas Kuipera.

Renderowanie słońca było trudne. Musiałam oszukiwać, korzystając z tylu technik graficznych w czasie rzeczywistym, ile znałam. Powierzchnia Słońca jest gorącą pianą plazmy, która musi pulsować i zmieniać się w czasie. Symulowano to za pomocą tekstury bitmapowej obrazu w podczerwieni na powierzchni Słońca. Narzędzie do cieniowania powierzchni wyszukuje kolor na podstawie skali szarości tej tekstury i sprawdza ją na osobnej ścieżce kolorów. Przesuwanie obrazu powoduje powstanie zniekształceń przypominających lawę.

Podobną technikę zastosowano do korony Słońca, z tym że była to płaska karta sprite, która zawsze jest skierowana w stronę aparatu i używana jest strona https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.

Renderowanie Sol.
Wczesna wersja usługi Sun.

Flary słoneczne powstały przy użyciu wierzchołków i cieniowań fragmentów zastosowanych na torusa, które wirowały tuż wokół krawędzi powierzchni słonecznej. Szum wierzchołków ma funkcję szumu, która splata się w formie blobów.

To tutaj pojawiły się problemy z konfliktem Z z powodu precyzji GL. Wszystkie zmienne dotyczące precyzji były wstępnie zdefiniowane w tagu THREE.js, więc bez ogromnego nakładu pracy nie udało mi się w realistyczny sposób zwiększyć precyzji. W pobliżu punktu początkowego problemy z dokładnością nie były tak poważne. Jednak gdy zacząłem modelować inne układy gwiazd, problem zaczął się wiązać z tym problemem.

Oznacz model gwiazdką.
Kod do renderowania Słońca został później uogólniony w celu renderowania innych gwiazd.

Było kilka sposobów, aby złagodzić konflikty Z. Właściwość Material.polygonoffset THREE pozwala na renderowanie wielokątów w innej postrzeganej lokalizacji (o ile dobrze rozumiem). Używano go, aby wymusić renderowanie samolotu koronowego na powierzchni Słońca. Poniżej niej wykonano „halo” Słońca, tworząc ostre promienie świetlne oddalające się od kuli.

Innym problemem związanym z precyzją było to, że modele gwiazd zaczynały zniekształcać się, gdy powiększała się scena. Aby to naprawić, musiałem „wyzerować” obrót sceny i oddzielnie obracać model gwiazdy i mapę środowiska, by stworzyć wrażenie, że krążysz wokół gwiazdy.

Tworzę efekt flary w Obiektywie

Ogromna moc wiąże się z wielką odpowiedzialnością.
Z wielką mocą wiąże się wielka odpowiedzialność.

Wizualizacje przestrzenne pozwalają mi uniknąć nadmiernego używania flary. THREE.LensFlare służy do tego celu, wystarczyło dodać kilka anamorficznych sześciokątów i odskoczyć JJ Abrams. Poniższy fragment kodu pokazuje, jak je umieścić w scenie.

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

}
}

Prosty sposób na przewijanie tekstur

Zainspirowane Homeworld.
Płaszczyzna kartezjańska ułatwiająca orientację przestrzenną w przestrzeni.

Dla „płaszczyzny orientacji przestrzennej” utworzono olbrzymią funkcję THREE.CylinderGeometry(), która została wyśrodkowana na Słońcu. Aby utworzyć „falę światła” skierowaną na zewnątrz, zmodyfikowałem w ten sposób odsunięcie jej tekstury w ten sposób:

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

map to tekstura należąca do materiału, dla której otrzymuje się funkcję onUpdate, którą można zastąpić. Ustawienie przesunięcia tekstury powoduje „przewijanie” wzdłuż tej osi, a spamowanie wymagaUpdateUpdate = true wywoła zapętlenie tego działania.

Korzystanie z ramek kolorów

Każda gwiazda ma inny kolor w zależności od „indeksu kolorów”, przypisanego przez astronomów. Czerwone gwiazdy są ogólnie chłodniejsze, a niebieskie/fioletowe – cieplejsze. Gradient zawiera pasek białych i pośrednich pomarańczowych kolorów.

Renderując gwiazdy, chciałem nadać każdej cząstce osobny kolor na podstawie tych danych. Zrobiliśmy to za pomocą „atrybutów” nadanych do cieniowania materiałowi zastosowanemu do cząstek.

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

Wypełnienie tablicy colorIndex nada każdej cząstce niepowtarzalny kolor w cieniowaniu. Normalnie jeden z nich przekazuje się w kolorze vec3, ale w tym przypadku przekazuję wartość pływającą na potrzeby ewentualnego sprawdzenia ramki kolorów.

Barwa.
Rampa kolorów używana do wyszukiwania widocznego koloru na podstawie indeksu kolorów gwiazdy.

Ramka kolorów wyglądała tak, ale potrzebowałam dostępu do danych bitmapy o kolorach z JavaScriptu. W ten sposób udało mi się najpierw wczytać obraz do modelu DOM, narysować go w elemencie canvas, a potem otworzyć bitmapę.

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

Ta sama metoda jest następnie stosowana do kolorowania poszczególnych gwiazdek w widoku modelu gwiazdy.

Oczy!
Ta sama technika jest używana do wyszukiwania kolorów w klasie widm gwiazdy.

Gra cienia

W trakcie realizacji projektu odkryłam, że aby uzyskać wszystkie efekty wizualne, muszę pisać coraz więcej narzędzi do cieniowania. Napisałam do tego celu niestandardowy program do cieniowania, ponieważ miałem już dosyć tego, że działają one w 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') );

}
}

Funkcja loadShaders() pobiera listę nazw plików cieniowania (z wyjątkiem nazwy .fsh dla fragmentów i .vsh w przypadku cieniowania wierzchołków), próbuje wczytać ich dane, a następnie zastępuje listę obiektami. Efektem końcowym są uniformy THREE.js, które można przekazać do cieniowania w ten sposób:

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

Możliwe, że użyłem(-am) pliku request.js, choć w tym celu trzeba by było jeszcze raz zmontować kod. Rozwiązanie to, chociaż znacznie łatwiejsze, można byłoby ulepszyć, na przykład nawet jako rozszerzenie THREE.js. Jeśli masz jakieś sugestie lub sugestie, jak to zrobić, daj mi znać.

Etykiety tekstowe CSS na podstawie pliku THREE.js

W ramach naszego ostatniego projektu, Small Arms Globe, skupiłam się na tworzeniu etykiet tekstowych na scenie THREE.js. Używana przeze mnie metoda oblicza bezwzględne położenie modelu, w którym ma pojawić się tekst, następnie ustala pozycję ekranu za pomocą funkcji THREE.Projector(), a na koniec używa funkcji CSS „top” i „left”, aby umieścić elementy CSS w odpowiednim położeniu.

We wczesnych wersjach projektu w ramach tego projektu korzystałam z tej samej techniki, ale mnie swędziało, żeby wypróbować inną metodę opisaną przez Luisa Cruza.

Ogólnie rzecz biorąc, dopasuj przekształcenie macierzy CSS3D do kamery i sceny TRZY. Możesz też „umieścić” elementy CSS w 3D tak, jakby były one na wierzchu TRZY. Istnieją jednak ograniczenia, na przykład nie można umieścić tekstu pod obiektem THREE.js. To działa znacznie szybciej niż próba wykonania układu za pomocą atrybutów CSS „top” i „left” (lewo).

Etykiety tekstowe.
Używanie przekształceń CSS3D do umieszczania etykiet tekstowych na technologii WebGL.

Tutaj znajdziesz prezentację (i kod w widoku źródłowym). Okazało się jednak, że w przypadku THREE.js kolejność macierzy zmieniła się. Zaktualizowana funkcja:

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

Ponieważ wszystko jest przekształcane, tekst nie jest już skierowany w stronę aparatu. Rozwiązaniem było użycie funkcji THREE.Gyroscope(), która wymusza utratę odziedziczonej orientacji ze sceny w obiekcie Object3D. Ta technika to „billboarding”, a do tego idealnie nadaje się do tego żyroskop.

Bardzo miłe jest to, że wszystkie pozostałe elementy DOM i CSS są nadal odtwarzane. Można na przykład najechać kursorem myszy na etykietę tekstową 3D i dotrzeć do niej z cieniem.

Etykiety tekstowe.
Etykiety tekstowe są zawsze skierowane w stronę aparatu. Aby to zrobić, przymocuj je do funkcji THREE.Gyroscope().

Podczas powiększania widzę, że problemy z pozycjonowaniem powodują skala typografii. Być może jest to spowodowane kerningiem i dopełnieniem tekstu? Innym problemem było to, że po powiększeniu tekst uległ pikselizacji, ponieważ mechanizm renderowania DOM traktuje renderowany tekst jako teksturowany czworokąt, o czym trzeba pamiętać w przypadku tej metody. Patrząc wstecz, mogłem użyć gigantycznego tekstu o rozmiarze czcionki, który może przydać się w przyszłości. W tym projekcie użyłem też opisanych wcześniej etykiet tekstowych miejsc docelowych CSS „top/left” dla bardzo małych elementów towarzyszących planetom w Układzie Słonecznym.

Odtwarzanie muzyki i zapętlenie

Muzyką zagraną podczas „Map Galaktycznej” grupy Mass Effect były kompozytorzy bioware, Sam Hulick i Jack Wall, i wywołał wzruszenie, które chciałem wzbudzić w widzach. Chcieliśmy dodać do naszego projektu trochę muzyki, ponieważ czuliśmy, że jest ona ważną częścią atmosfery, i powstała w ten sposób wzbudzanie zachwytu i zachwytu, do którego dążyliśmy.

Nasz producent Valdean Klump skontaktował się z Samem, który przekazał nam sporo muzyki z kanału Mass Effect, którą z przyjemnością nam wykorzystał. Utwór nosi tytuł „In a strange Land”.

Użyłem tagu audio do odtwarzania muzyki, ale nawet w Chrome atrybut „loop” był niestabilny – czasami nie zapętlał się. Na koniec wykorzystaliśmy ten trik z podwójnym tagiem audio, aby sprawdzić, czy odtwarzanie się zakończyło, i przejść do drugiego tagu, aby rozpocząć odtwarzanie. Bardzo mnie rozczarował, ponieważ ten nadal nie był zapętlony przez cały czas. Niestety wydaje mi się, że to było najlepsze, co udało mi się zrobić.

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

Możliwość poprawy

Po jakimś czasie pracy z THREE.js mam wrażenie, że moje dane za bardzo mieszają się z kodem. Na przykład podczas definiowania szczegółowych instrukcji materiałów, tekstur i geometrii używaliśmy zasadniczo „modelowania 3D za pomocą kodu”. Było to bardzo niekorzystne i w tym obszarze można znacznie ulepszyć przyszłe przedsięwzięcia wykorzystujące THREE.js, na przykład określić dane materiałowe w osobnym pliku, które najlepiej będzie wyświetlać i dostosowywać w jakimś kontekście, i można je przywrócić do głównego projektu.

Nasz współpracownik, Ray McClure, również spędził trochę czasu na tworzeniu niezwykłych generatywnych „szumów kosmosu”, które trzeba było usunąć z powodu niestabilności internetowego interfejsu API dźwięku, co często powoduje awarię Chrome. Przykre... ale z pewnością skłoniło nas to do zastanowienia się nad przyszłością pracy w tej branży. Wiemy, że wprowadziliśmy poprawki do interfejsu Web Audio API, więc być może problem działa już teraz. Na co warto zwracać uwagę w przyszłości.

Elementy typograficzne połączone z WebGL wciąż stanowią wyzwanie i nie jestem w 100% pewny, co tu robimy. Nadal wygląda to jak haker. Być może przyszłe wersje THREE, z nadchodzącym mechanizmem renderowania CSS, mogłyby zostać wykorzystane do lepszego połączenia tych dwóch światów.

Środki

Dziękuję Aaronowi Koblinowi za ten projekt. Jono Brandel za świetny projekt użytkownika, implementacja, opracowanie typu i implementacja wycieczki. Valdean Klump za nadanie projektowi nazwy i całej kopii. Sabah Ahmed za wyjaśnienie mnóstwo praw do użytkowania źródeł danych i obrazów. Clem Wright za kontakt z właściwymi osobami w celu publikacji. Doug Fritz za doskonałość techniczną. Jerzy Brower, który nauczył mnie JS i CSS. I oczywiście pan Doob dla THREE.js.

Źródła