Studium przypadku: Znajdź drogę do Oz

Wstęp

„Znajdź drogę do Oz” to nowy eksperyment firmy Disney dostępny w internecie w Google Chrome. Dzięki niej możesz wyruszyć w interaktywną podróż przez cyrk w Kansas, a po powaleniu ogromnej burzy dotrzeć do krainy Oz.

Naszym celem było połączenie bogactwa kinematografii z możliwościami technicznymi przeglądarki w celu stworzenia ciekawego i wciągającego doświadczenia, z którym użytkownicy będą mogli nawiązać silną więź.

Zadanie jest zbyt duże, aby uchwycić je w całości, więc zgłębiliśmy tę historię i wykorzystaliśmy kilka rozdziałów historii technologii, które naszym zdaniem są interesujące. Wyodrębniliśmy kilka samouczków pokazujących rosnący poziom trudności.

Wiele osób włożyło wiele pracy, aby to wydarzenie było możliwe – jest ich zbyt wiele, aby je tu wymienić. Odwiedź tę stronę, aby zobaczyć stronę z informacjami o autorach w sekcji menu, aby przeczytać całą historię.

Rzut oka pod maski

Znajdź drogę do Oz na komputerze to tętniący życiem świat. Korzystamy z technologii 3D i kilku warstw efektów, które są inspiracją do tradycyjnego tworzenia filmów. Łącząc je, tworząc niemal realistyczną scenę. Najbardziej znane technologie to WebGL z Three.js, własne narzędzia do cieniowania i animowane elementy DOM wykorzystujące funkcje CSS3. Oprócz tego interfejs API getUserMedia (WebRTC) umożliwia interaktywne wrażenia i pozwala użytkownikom dodawać obraz bezpośrednio z kamery internetowej i WebAudio, aby uzyskać dźwięk 3D.

Jednak dzięki tak magicznemu doświadczeniu technologicznemu łączy się ono ze sobą. Jest to również jedno z głównych wyzwań: jak połączyć efekty wizualne i interaktywne elementy w jednej scenie, aby uzyskać spójną całość? Trudno było zarządzać tą skomplikowaną wizualną strukturą, co utrudniało określenie, na jakim etapie rozwoju byliśmy w danym momencie.

Aby rozwiązać problem połączonych ze sobą efektów wizualnych i optymalizacji, intensywnie korzystamy z panelu sterowania, który obejmuje wszystkie istotne ustawienia, które wtedy analizowaliśmy. Scenę można dostosować w przeglądarce na żywo pod kątem jasności, głębi ostrości, gamma itd. Każdy użytkownik może spróbować zmodyfikować wartości istotnych parametrów w interfejsie i sprawdzić, co działa najlepiej.

Zanim ujawnimy naszą tajemnicę, chcemy Cię ostrzec, że może dojść do wypadku, tak jakby przebijał się do wnętrza silnika. Upewnij się, że nie masz niczego ważnego, otwórz główny adres URL witryny i dodaj do niego ?debug=on. Poczekaj, aż strona się załaduje, a gdy wejdziesz do środka (naciśnij?) klawisz Ctrl-I. Po prawej stronie pojawi się menu. Jeśli odznaczysz opcję „Zamknij ścieżkę kamery”, możesz swobodnie poruszać się po miejscu za pomocą klawiszy A, W, S i D.

Ścieżka kamery.

Nie będziemy tu omawiać wszystkich ustawień, ale zachęcamy do eksperymentowania: klawisze pozwalają odkrywać różne ustawienia w różnych scenach. W ostatniej sekwencji burzy znajduje się dodatkowy klawisz: Ctrl-A, za pomocą którego możesz przełączać odtwarzanie animacji i latać. W tej scenie naciśnięcie klawisza Esc (wyłączanie blokady myszy) i ponowne naciśnięcie Ctrl-I pozwala uzyskać dostęp do ustawień właściwych dla danej sceny burzy. Rozejrzyj się dookoła i zrób ładne pocztówki, takie jak to poniżej.

Scena burzowa

W tym celu użyliśmy uroczej biblioteki o nazwie dat.gui (tutaj znajdziesz jej wcześniejszy samouczek). Pozwoliło nam to szybko zmienić ustawienia widoczne dla użytkowników witryny.

Obraz podobny do matowego

W wielu klasycznych filmach i animacjach Disneya tworzenie scen wymagało połączenia różnych warstw. Były tam warstwy akcji, animacje w komórkach, a nawet fizyczne scenerie, a na górnych warstwach utworzone przez malowanie na szkle – technikę zwaną malowaniem matowym.

Chociaż struktura stworzonych przez nas treści jest podobna, chociaż niektóre „warstwy” są dużo więcej niż statyczna wizualizacja. Wpływają one na wygląd przy bardziej złożonych obliczeniach. Jednak, jeśli chodzi o ogólną perspektywę, mamy tutaj do czynienia z wyświetleniami, skomponowanymi jedna na drugiej. U góry znajduje się warstwa interfejsu, a pod nią scena 3D złożona z różnych elementów sceny.

Górna warstwa interfejsu została utworzona za pomocą modeli DOM i CSS 3, co oznacza, że edytowanie interakcji może być realizowane na wiele sposobów niezależnie od trybu 3D i komunikacji między nimi zgodnie z wybraną listą zdarzeń. Ta komunikacja wykorzystuje router Backbone Router i zdarzenie HTML5 onHashChange, które określa, który obszar ma być animowany. (źródło projektu: /develop/coffee/router/Router.coffee).

Samouczek: obsługa arkuszy Sprite i Retina

Jedną z ciekawych technik optymalizacji, z której korzystaliśmy w interfejsie, było połączenie wielu obrazów interfejsu w jednym pliku PNG w celu zmniejszenia liczby żądań wysyłanych przez serwer. W tym projekcie interfejs składał się z ponad 70 obrazów (nie licząc tekstur 3D), które były wczytywane z góry, co skróciło czas wczytywania strony. Aktywny arkusz sprite możesz zobaczyć tutaj:

Normalny wyświetlacz – http://findyourwaytooz.com/img/home/interface_1x.png Wyświetlacz Retina – http://findyourwaytooz.com/img/home/interface_2x.png

Oto kilka wskazówek na temat tego, w jaki sposób wykorzystujemy arkusze sprite oraz jak używać ich na urządzeniach z ekranem Retina, aby interfejs był maksymalnie ostry i czytelny.

Tworzenie arkuszy sprite

Do utworzenia arkuszy SpriteSheets użyliśmy elementu TexturePacker, który generuje pliki w dowolnym potrzebnym formacie. W tym przypadku wyeksportowaliśmy plik jako EaselJS, co jest bardzo przejrzysty i można go wykorzystać także do tworzenia animowanych sprite’ów.

Korzystanie z wygenerowanego arkusza sprite

Po utworzeniu arkusza sprite powinien pojawić się plik JSON podobny do tego:

{
   "images": ["interface_2x.png"],
   "frames": [
       [2, 1837, 88, 130],
       [2, 2, 1472, 112],
       [1008, 774, 70, 68],
       [562, 1960, 86, 86],
       [473, 1960, 86, 86]
   ],

   "animations": {
       "allow_web":[0],
       "bottomheader":[1],
       "button_close":[2],
       "button_facebook":[3],
       "button_google":[4]
   },
}

Gdzie:

  • Obraz odnosi się do adresu URL arkusza sprite.
  • ramki to współrzędne każdego elementu interfejsu [x, y, szerokość, wysokość]
  • animacje to nazwy poszczególnych zasobów

Pamiętaj, że do utworzenia arkusza sprite użyliśmy obrazów w wysokiej gęstości, a następnie utworzyliśmy wersję standardową, której rozmiar został zmieniony do połowy.

Konkluzja

Teraz, gdy wszystko jest gotowe, wystarczy dodać fragment kodu JavaScript.

var SSAsset = function (asset, div) {
  var css, x, y, w, h;

  // Divide the coordinates by 2 as retina devices have 2x density
  x = Math.round(asset.x / 2);
  y = Math.round(asset.y / 2);
  w = Math.round(asset.width / 2);
  h = Math.round(asset.height / 2);

  // Create an Object to store CSS attributes
  css = {
    width                : w,
    height               : h,
    'background-image'   : "url(" + asset.image_1x_url + ")",
    'background-size'    : "" + asset.fullSize[0] + "px " + asset.fullSize[1] + "px",
    'background-position': "-" + x + "px -" + y + "px"
  };

  // If retina devices

  if (window.devicePixelRatio === 2) {

    /*
    set -webkit-image-set
    for 1x and 2x
    All the calculations of X, Y, WIDTH and HEIGHT is taken care by the browser
    */

    css['background-image'] = "-webkit-image-set(url(" + asset.image_1x_url + ") 1x,";
    css['background-image'] += "url(" + asset.image_2x_url + ") 2x)";

  }

  // Set the CSS to the DIV
  div.css(css);
};

Użyj go w następujący sposób:

logo = new SSAsset(
{
  fullSize     : [1024, 1024],               // image 1x dimensions Array [x,y]
  x            : 1790,                       // asset x coordinate on SpriteSheet         
  y            : 603,                        // asset y coordinate on SpriteSheet
  width        : 122,                        // asset width
  height       : 150,                        // asset height
  image_1x_url : 'img/spritesheet_1x.png',   // background image 1x URL
  image_2x_url : 'img/spritesheet_2x.png'    // background image 2x URL
},$('#logo'));

Aby dowiedzieć się więcej o zmiennych gęstościach pikseli, przeczytaj ten artykuł Borisa Smusa.

Potok treści 3D

Środowisko wykonawcze jest skonfigurowane w warstwie WebGL. Jednym z najtrudniejszych pytań na temat sceny 3D jest sposób, w jaki tworzysz treści, które pozwolą w pełni wykorzystać potencjał modelowania, animacji i efektów. Pod wieloma względami najważniejszym elementem tego zagadnienia jest potok treści, czyli uzgodniony proces tworzenia treści do sceny 3D.

Chcieliśmy tworzyć zachwycający świat, dlatego potrzebujemy solidnego procesu, który umożliwiłby artystom 3D jego tworzenie. Trzeba im dać jak najwięcej wolności w zakresie oprogramowania do modelowania i animacji 3D, a także renderować je na ekranie za pomocą kodu.

Pracowaliśmy nad tym problemem już od jakiegoś czasu, bo za każdym razem, gdy tworzyliśmy witrynę 3D, mieliśmy problemy z dostępnymi narzędziami. Stworzyliśmy więc narzędzie o nazwie 3D Librarian (Biblioteka 3D) – część wewnętrznych badań. i już prawie kwalifikował się do podania w prawdziwej pracy.

To narzędzie miało nieco historię: na początku było przeznaczone dla Flasha i umożliwiało wprowadzenie ważnej sceny Majów w postaci jednego skompresowanego pliku zoptymalizowanego pod kątem środowiska wykonawczego, które zostało rozpakowane. Wynika to z faktu, że rozwiązanie to skutecznie spakowało scenę w zasadniczo tę samą strukturę danych, która jest modyfikowana podczas renderowania i animacji. Po wczytaniu plik nie wymaga przeanalizowania w bardzo niewielkim stopniu. Rozpakowywanie we Flashu było dość szybkie, bo był w formacie AMF, który Flash potrafił rozpakować natywnie. Użycie tego samego formatu w WebGL wymaga więcej pracy z procesorem. Trzeba było odtworzyć warstwę kodu JavaScript rozpakowującą dane, która zasadniczo rozpakowała te pliki i odtworzyła struktury danych niezbędne do działania WebGL. Rozpakowanie całej sceny 3D wymaga trochę pracy procesora. Rozpakowywanie sceny 1 w grze Znajdź drogę do Oz na zaawansowanych komputerach trwa około 2 sekund. Dlatego odbywa się to za pomocą technologii Web Workers w czasie „konfiguracji sceny” (przed uruchomieniem sceny), aby nie zawieszać elementów interfejsu.

To przydatne narzędzie umożliwia importowanie większości sceny 3D: modeli, tekstur i animacji kości. Tworzysz pojedynczy plik biblioteki, który może być następnie wczytany przez mechanizm 3D. Umieszczasz w bibliotece wszystkie modele potrzebne do zaprezentowania danego filmu i gotowe, a potem pojawiasz się w swojej scenie.

Problemem było jednak to, że pracowaliśmy nad WebGL, czyli „nowym chłopakiem”. To było naprawdę trudne dzieciaki, które wyznaczyły standard dla 3D w przeglądarkach. W związku z tym stworzyliśmy doraźną warstwę JavaScript, w której skompresowane pliki scen 3D z biblioteki 3D Librarian zostały przekształcone w format zrozumiały dla WebGL.

Samouczek: Niech zacznie wiatr

Motywem, który powtarzał się w grze „Znajdź drogę do Oz”, był wiatr. Wątek fabuły jest skonstruowany tak, by był crescendo wiatru.

Pierwsza scena karnawału jest stosunkowo spokojna. Przemierzając kolejne sceny, użytkownik czuje coraz silniejszy wiatr, którego kulminacją jest burza.

Dlatego ważne było stworzenie immersyjnego efektu wiatru.

W związku z tym zapełniliśmy 3 sceny karnawałowe obiektami, które były miękkie i miały na nie wpływ wiatr, np. namioty, flagi na powierzchni budki fotograficznej i sam balon.

Miękka szmatka.

Obecnie gry komputerowe bazują na mechanizmie fizyki. Jeśli więc konieczne jest zasymulowanie miękkiego obiektu w świecie 3D, przeprowadza się dla niego pełną symulację fizyczną, aby uzyskać wiarygodne miękkie zachowanie.

W WebGL / JavaScript nie mamy (jeszcze) luksusu, aby przeprowadzić w pełni symulację fizyki. W krainie Oz musieliśmy więc znaleźć sposób na uzyskanie efektu wiatru, ale bez jego rzeczywistej symulacji.

Informacje o czułości na wietrze dla każdego obiektu umieściliśmy w modelu 3D. Każdy wierzchołek modelu 3D miał „atrybut wiatru”, który określał, jak bardzo wiatr powinien wpływać na ten wierzchołek. Określiliśmy więc czułość obiektów 3D na wiatr. Potem musieliśmy utworzyć sam wiatr.

W tym celu wygenerowaliśmy obraz zawierający szum Perlin. Ten obraz ma objąć określoną „obszar wiatru”. Dobrym sposobem myślenia o tym jest wyobrażenie sobie takiego jak szum chmur położony na określonym prostokątnym obszarze sceny 3D. Każdy piksel (wartość na poziomie szarości) tego obrazu określa, jak silny jest wiatr w danym momencie „otaczającego go” obszaru 3D.

Aby uzyskać efekt wiatru, obraz przesuwa się w czasie, ze stałą prędkością i w określonym kierunku, czyli w kierunku wiatru. Aby mieć pewność, że „obszar wiatru” nie wpływa na całą scenę, owijamy obraz wiatru wokół krawędzi, ograniczając się do obszaru efektu.

Prosty samouczek wiatru 3D

Utwórzmy teraz efekt wiatru w prostej scenie 3D w języku Three.js.

Zaraz będziemy tworzyć wiatr na prostym „polu trawy procesowej”.

Zacznijmy od utworzenia sceny. Prezentujemy prosty, teksturowany teren. A każdy kawałek trawy zostanie po prostu przedstawiony za pomocą stożka 3D odwróconego do góry nogami.

Teren porośnięty trawą
Teren porośnięty trawą

Oto jak utworzyć tę prostą scenę w języku Three.js za pomocą CoffeeScript.

Najpierw skonfigurujemy plik Three.js i podłączymy go do takich funkcji jak Aparat, kontroler myszy i niektóre światła:

constructor: ->

   @clock =  new THREE.Clock()

   @container = document.createElement( 'div' );
   document.body.appendChild( @container );

   @renderer = new THREE.WebGLRenderer();
   @renderer.setSize( window.innerWidth, window.innerHeight );
   @renderer.setClearColorHex( 0x808080, 1 )
   @container.appendChild(@renderer.domElement);

   @camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 5000 );
   @camera.position.x = 5;
   @camera.position.y = 10;
   @camera.position.z = 40;

   @controls = new THREE.OrbitControls( @camera, @renderer.domElement );
   @controls.enabled = true

   @scene = new THREE.Scene();
   @scene.add( new THREE.AmbientLight 0xFFFFFF )

   directional = new THREE.DirectionalLight 0xFFFFFF
   directional.position.set( 10,10,10)
   @scene.add( directional )

   # Demo data
   @grassTex = THREE.ImageUtils.loadTexture("textures/grass.png");
   @initGrass()
   @initTerrain()

   # Stats
   @stats = new Stats();
   @stats.domElement.style.position = 'absolute';
   @stats.domElement.style.top = '0px';
   @container.appendChild( @stats.domElement );
   window.addEventListener( 'resize', @onWindowResize, false );
   @animate()

Wywołania funkcji initGrass i initTerrain wypełniają scenę odpowiednio trawą i ukształtowaniem terenu:

initGrass:->
   mat = new THREE.MeshPhongMaterial( { map: @grassTex } )
   NUM = 15
   for i in [0..NUM] by 1
       for j in [0..NUM] by 1
           x = ((i/NUM) - 0.5) * 50 + THREE.Math.randFloat(-1,1)
           y = ((j/NUM) - 0.5) * 50 + THREE.Math.randFloat(-1,1)
           @scene.add( @instanceGrass( x, 2.5, y, 5.0, mat ) )

instanceGrass:(x,y,z,height,mat)->
   geometry = new THREE.CylinderGeometry( 0.9, 0.0, height, 3, 5 )
   mesh = new THREE.Mesh( geometry, mat )
   mesh.position.set( x, y, z )
   return mesh

Tworzymy siatkę 15 na 15 fragmentów trawy. Każdej pozycji na trawie dodajemy trochę randomizacji, by nie ustawili się w kolejności jak żołnierze, co mogłoby wyglądać dziwnie.

Jest to płaszczyzna pozioma, umieszczona u podstawy kawałków trawy (y = 2,5).

initTerrain:->
  @plane = new THREE.Mesh( new THREE.PlaneGeometry(60, 60, 2, 2), new THREE.MeshPhongMaterial({ map: @grassTex }))
  @plane.rotation.x = -Math.PI/2
  @scene.add( @plane )

Do tej pory udało nam się więc po prostu utworzyć scenę Three.js i dodać kilka kawałków trawy z wygenerowanych proceduralnie odwróconych stożków oraz prosty teren.

Na razie nic nadzwyczajnego.

Pora zacząć dodawać wiatr. Najpierw chcemy umieścić informacje o czułości na wiatr w modelu 3D trawy.

Te informacje zostaną umieszczone jako atrybut niestandardowy dla każdego wierzchołka modelu 3D trawy. Zastosujemy regułę, według której dolny koniec modelu trawy (czubek stożka) nie ma czułości, ponieważ jest przymocowany do podłoża. Górna część modelu trawy (podstawa stożka) ma maksymalną czułość na wiatr, ponieważ część znajduje się dalej od ziemi.

Oto jak zakodowana jest funkcja instanceGrass w celu dodania czułości na wiatr jako atrybut niestandardowy dla modelu 3D trawy.

instanceGrass:(x,y,z,height)->

  geometry = new THREE.CylinderGeometry( 0.9, 0.0, height, 3, 5 )

  for i in [0..geometry.vertices.length-1] by 1
      v = geometry.vertices[i]
      r = (v.y / height) + 0.5
      @windMaterial.attributes.windFactor.value[i] = r * r * r

  # Create mesh
  mesh = new THREE.Mesh( geometry, @windMaterial )
  mesh.position.set( x, y, z )
  return mesh

Teraz używamy niestandardowego materiału windMaterial, zamiast stosowanego wcześniej MeshPhongMaterial. WindMaterial otacza obiekt WindMeshShader, który za chwilę zobaczymy.

Kod w metodzie instanceGrass przechodzi przez wszystkie wierzchołki modelu trawy i do każdego wierzchołka dodaje niestandardowy atrybut wierzchołków o nazwie windFactor. Ten współczynnik wiatru jest ustawiony na 0 dla dolnego końca modelu trawy (tam, gdzie powinien dotykać terenu), i ma wartość 1 dla górnego końca modelu trawy.

Kolejnym potrzebnym nam składnikiem jest dodanie do sceny rzeczywistego wiatru. Jak już mówiliśmy, w tym celu użyjemy szumu Perlin. Wygenerujemy proceduralnie teksturę szumu Perlin.

Aby zwiększyć przejrzystość, przypiszemy tę teksturę do samego terenu zamiast poprzedniej, zielonej tekstury, jaką miał w tym miejscu. Dzięki temu łatwiej będzie Ci zorientować się, co dzieje się na wietrze.

W związku z tym tekstura szumu Perlin pokryje przestrzenną część terenu, a każdy piksel tekstury będzie określać intensywność wiatru na obszarze, w którym pada ten piksel. Prostokąt terenu będzie naszym „obszarem wiatru”.

Szum Perlin jest generowany proceduralnie za pomocą cieniowania o nazwie NoiseShader. Ten program do cieniowania wykorzystuje algorytmy szumu prostego 3D ze strony https://github.com/ashima/webgl-noise . Wersja WebGL została wzniesiona dosłownie na podstawie jednego z przykładów kodu Three.js autorstwa MrDooba dostępnych pod adresem http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.

Funkcja NoiseShader wymaga czasu, skali oraz przesunięcia zestawu parametrów w postaci jednolitych, a następnie otrzymuje ładny rozkład 2D szumu Perlin.

class NoiseShader

  uniforms:     
    "fTime"  : { type: "f", value: 1 }
    "vScale"  : { type: "v2", value: new THREE.Vector2(1,1) }
    "vOffset"  : { type: "v2", value: new THREE.Vector2(1,1) }

...

Użyjemy tego narzędzia Shader do wyrenderowania szumu Perlina na teksturę. Służy do tego funkcja initNoiseShader.

initNoiseShader:->
  @noiseMap  = new THREE.WebGLRenderTarget( 256, 256, { minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat } );
  @noiseShader = new NoiseShader()
  @noiseShader.uniforms.vScale.value.set(0.3,0.3)
  @noiseScene = new THREE.Scene()
  @noiseCameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2,  window.innerHeight / 2, window.innerHeight / - 2, -10000, 10000 );
  @noiseCameraOrtho.position.z = 100
  @noiseScene.add( @noiseCameraOrtho )

  @noiseMaterial = new THREE.ShaderMaterial
      fragmentShader: @noiseShader.fragmentShader
      vertexShader: @noiseShader.vertexShader
      uniforms: @noiseShader.uniforms
      lights:false

  @noiseQuadTarget = new THREE.Mesh( new THREE.PlaneGeometry(window.innerWidth,window.innerHeight,100,100), @noiseMaterial )
  @noiseQuadTarget.position.z = -500
  @noiseScene.add( @noiseQuadTarget )

Powyższy kod służy do skonfigurowania noiseMap jako celu renderowania Three.js, wyposażenia go w obiekt NoiseShader i wyrenderowania go za pomocą aparatu ortograficznego, co pozwoli uniknąć zniekształceń perspektyw.

Jak już wspominaliśmy, ta tekstura będzie teraz używana również jako główna tekstura renderowania terenu. Nie jest to tak naprawdę niezbędne, aby sam efekt wiatru działał. Dobrze jest go jednak mieć, bo dzięki temu możemy lepiej zrozumieć, co dzieje się w wyniku generowania wiatru.

Oto przerobiona funkcja initTerrain, w której jako teksturę wykorzystano NumberMap:

initTerrain:->
  @plane = new THREE.Mesh( new THREE.PlaneGeometry(60, 60, 2, 2), new THREE.MeshPhongMaterial( { map: @noiseMap, lights: false } ) )
  @plane.rotation.x = -Math.PI/2
  @scene.add( @plane )

Skoro tekstura wiatru jest już gotowa, przyjrzyjmy się mechanizmowi WindMeshShader, który odpowiada za odkształcanie modeli trawy pod wpływem wiatru.

Aby utworzyć ten program do cieniowania, zaczęliśmy korzystać ze standardowego narzędzia do cieniowania Three.js MeshPhongMaterial, a następnie go zmodyfikowaliśmy. To dobry sposób na szybkie rozpoczęcie pracy z działającym cieniem, który nie wymaga zaczynania od zera.

Nie będziemy kopiować tutaj całego kodu do cieniowania (możesz to sprawdzić w pliku z kodem źródłowym), ponieważ większość będzie repliką programu do cieniowania MeshPhongMaterial. Przyjrzyjmy się jednak zmodyfikowanym elementom związanym z wiatrem w Vertex Shader.

vec4 wpos = modelMatrix * vec4( position, 1.0 );
vec4 wpos = modelMatrix * vec4( position, 1.0 );

wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
vWindForce = texture2D(tWindForce,windUV).x;

float windMod = ((1.0 - vWindForce)* windFactor ) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;

mvPosition = modelViewMatrix *  pos;

Program do cieniowania używa najpierw współrzędnych wyszukiwania tekstury windUV na podstawie 2D, xz (poziomego) wierzchołka. Ta współrzędna UV służy do wyszukiwania siły wiatru vWindForce w tekście wiatru, szumu Perlin.

Ta wartość vWindForce jest skomponowana z omówionym powyżej atrybutem niestandardowym windFactor specyficznym dla wierzchołków, aby obliczyć stopień odkształcenia potrzebnego wierzchołkowi. Mamy też globalny parametr windScale do kontrolowania ogólnej siły wiatru oraz windDirection, czyli wektor, który określa, w którym kierunku ma następować odkształcenie wiatru.

Powoduje to odkształcenie kawałków trawy spowodowane przez wiatr. To jednak nadal nie koniec. Odkształcenie jest statyczne i nie przekazuje efektu wietrznego obszaru.

Jak już wspomnieliśmy, będziemy musieli przesuwać teksturę szumu w całym obszarze wiatru, żeby nasze szkło mogło falować.

Jest to możliwe dzięki przesuwaniu w czasie uniformu vOffset, który jest przekazywany do NoiseShader. Jest to parametr vec2, który pozwoli nam określić przesunięcie szumu wzdłuż określonego kierunku (naszego kierunku wiatru).

Robimy to w funkcji render, która jest wywoływana w każdej klatce:

render: =>
  delta = @clock.getDelta()

  if @windDirection
      @noiseShader.uniforms[ "fTime" ].value += delta * @noiseSpeed
      @noiseShader.uniforms[ "vOffset" ].value.x -= (delta * @noiseOffsetSpeed) * @windDirection.x
      @noiseShader.uniforms[ "vOffset" ].value.y += (delta * @noiseOffsetSpeed) * @windDirection.z
...

To wszystko. Właśnie stworzyliśmy scenę z „trawą zwyczajową” dotkniętą przez wiatr.

Dodawanie pyłu do miksu

Spróbujmy nieco urozmaicić naszą scenę. Dodajmy trochę kurzu powietrznego, aby urozmaicić scenę.

Dodawanie pyłu
Dodawanie pyłu

Na pył powinien wpływać przecież wiatr, więc to świetnie, gdy wiatr unosi się na naszej planecie.

Funkcja initDust pełni funkcję systemu cząstek.

initDust:->
  for i in [0...5] by 1
      shader = new WindParticleShader()
      params = {}
      params.fragmentShader = shader.fragmentShader
      params.vertexShader   = shader.vertexShader
      params.uniforms       = shader.uniforms
      params.attributes     = { speed: { type: 'f', value: [] } }

      mat  = new THREE.ShaderMaterial(params)
      mat.map = shader.uniforms["map"].value = THREE.ImageUtils.loadCompressedTexture("textures/dust#{i}.dds")
      mat.size = shader.uniforms["size"].value = Math.random()
      mat.scale = shader.uniforms["scale"].value = 300.0
      mat.transparent = true
      mat.sizeAttenuation = true
      mat.blending = THREE.AdditiveBlending
      shader.uniforms["tWindForce"].value      = @noiseMap
      shader.uniforms[ "windMin" ].value       = new THREE.Vector2(-30,-30 )
      shader.uniforms[ "windSize" ].value      = new THREE.Vector2( 60, 60 )
      shader.uniforms[ "windDirection" ].value = @windDirection            

      geom = new THREE.Geometry()
      geom.vertices = []
      num = 130
      for k in [0...num] by 1

          setting = {}

          vert = new THREE.Vector3
          vert.x = setting.startX = THREE.Math.randFloat(@dustSystemMinX,@dustSystemMaxX)
          vert.y = setting.startY = THREE.Math.randFloat(@dustSystemMinY,@dustSystemMaxY)
          vert.z = setting.startZ = THREE.Math.randFloat(@dustSystemMinZ,@dustSystemMaxZ)

          setting.speed =  params.attributes.speed.value[k] = 1 + Math.random() * 10
          
          setting.sinX = Math.random()
          setting.sinXR = if Math.random() < 0.5 then 1 else -1
          setting.sinY = Math.random()
          setting.sinYR = if Math.random() < 0.5 then 1 else -1
          setting.sinZ = Math.random()
          setting.sinZR = if Math.random() < 0.5 then 1 else -1

          setting.rangeX = Math.random() * 5
          setting.rangeY = Math.random() * 5
          setting.rangeZ = Math.random() * 5

          setting.vert = vert
          geom.vertices.push vert
          @dustSettings.push setting

      particlesystem = new THREE.ParticleSystem( geom , mat )
      @dustSystems.push particlesystem
      @scene.add particlesystem

Tutaj powstaje 130 cząstek pyłu. Pamiętaj, że każdy z nich jest wyposażony w specjalny WindParticleShader.

Teraz w każdej klatce będziemy się trochę poruszać wokół cząstek, używając CoffeeScriptu, niezależnie od wiatru. Oto kod.

moveDust:(delta)->

  for setting in @dustSettings

    vert = setting.vert
    setting.sinX = setting.sinX + (( 0.002 * setting.speed) * setting.sinXR)
    setting.sinY = setting.sinY + (( 0.002 * setting.speed) * setting.sinYR)
    setting.sinZ = setting.sinZ + (( 0.002 * setting.speed) * setting.sinZR) 

    vert.x = setting.startX + ( Math.sin(setting.sinX) * setting.rangeX )
    vert.y = setting.startY + ( Math.sin(setting.sinY) * setting.rangeY )
    vert.z = setting.startZ + ( Math.sin(setting.sinZ) * setting.rangeZ )

Oprócz tego będziemy przesuwać każdą pozycję cząstek odpowiednio do wiatru. Odbywa się to w narzędziu WindParticleShader. Szczególnie dotyczy to programu do cieniowania wierzchołków.

Kod tego cieniowania to zmodyfikowana wersja ParticleMaterial Three.js. Jej rdzeń wygląda tak:

vec4 mvPosition;
vec4 wpos = modelMatrix * vec4( position, 1.0 );
wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
float vWindForce = texture2D(tWindForce,windUV).x;
float windMod = (1.0 - vWindForce) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;

mvPosition = modelViewMatrix *  pos;

fSpeed = speed;
float fSize = size * (1.0 + sin(time * speed));

#ifdef USE_SIZEATTENUATION
    gl_PointSize = fSize * ( scale / length( mvPosition.xyz ) );
#else,
    gl_PointSize = fSize;
#endif

gl_Position = projectionMatrix * mvPosition;

Ten cieniowanie wierzchołków nie różni się tak bardzo od tego, który mieliśmy podczas odkształcania trawy przez wiatr. Jako dane wejściowe pobiera teksturę szumu Perlin i w zależności od położenia pyłu sprawdza wartość vWindForce. Następnie używa tej wartości do zmiany pozycji cząstki pyłu.

Jeźdźcy w burzy

Najbardziej ekscytująca scena WebGL była prawdopodobnie ostatnia scena, którą możesz zobaczyć, przelatując przez balon w stronę tornada, aby dotrzeć do końca przygody w witrynie. Dostępny jest też wyjątkowy film z nadchodzącej premiery.

Scena lotu balonem

Tworząc tę scenę, wiedzieliśmy, że potrzebujemy głównej funkcji, która będzie miała znaczenie. Wirujące tornado stanowiłoby centralny element, a warstwy innych materiałów ukształtowałyby tę funkcję i wywoływały dramatyczny efekt. Aby to osiągnąć, zbudowaliśmy coś, co byłoby odpowiednikem studia filmowego wokół tego dziwnego cienia.

Do utworzenia realistycznego kompozytu zastosowaliśmy różne podejście. Niektóre były efektami wizualnymi, np. efektami flary w postaci świateł lub kropli deszczu, które animowały się jako warstwy na tle oglądanej sceny. W innych przypadkach płaskie powierzchnie sprawiają wrażenie poruszających się, na przykład nisko unoszące się chmury, które poruszają się zgodnie z kodem systemu cząstek. Podczas gdy fragmenty gruzu krążące wokół tornada były warstwami w scenie 3D, które przemieszczały się przed tornadem i za nim.

Głównym powodem, dla którego musieliśmy stworzyć scenę w ten sposób, było zapewnienie nam wystarczającej ilości GPU do obsługi cieniowania tornado, zrównoważonego z innymi zastosowanymi efektami. Początkowo mieliśmy duże problemy z równoważeniem GPU, ale później ta scena została zoptymalizowana i stała się lżejsza niż główna scena.

Samouczek: The Storm Shader

Do utworzenia ostatecznej sekwencji burzy połączyliśmy wiele różnych technik, ale najważniejszym elementem tego projektu był niestandardowy cieniowanie GLSL, który wygląda jak tornado. Próbowaliśmy wielu różnych technik, od cieniowania wierzchołków do stworzenia interesujących geometrycznych wirów, animacji opartych na cząstkach, a nawet animacji 3D z zakręconymi kształtami geometrycznymi. Żaden z nich nie oddawał atmosfery tornada i nie wymagał zbyt wielu procesów przetwarzania.

Ostatecznie odpowiedź dała nam zupełnie inny projekt. Prowadzony przez Max Planck Institute (brainflight.org) projekt równoległy z udziałem gier naukowych, który tworzy mapę mózgu myszy, wygenerował ciekawe efekty wizualne. Udało nam się stworzyć filmy przedstawiające wnętrze neuronu myszy przy użyciu niestandardowego cieniowania wolumetrycznego.

Wnętrze neuronu myszy za pomocą niestandardowego cieniowania objętościowego
Wnętrze neuronu myszy za pomocą niestandardowego cieniowania objętościowego

Odkryliśmy, że wnętrze komórki mózgu wygląda jak lejek tornada. A ponieważ stosowaliśmy technikę wolumetryczną, to wiedzieliśmy, że w kosmosie możemy obserwować go ze wszystkich stron. Moglibyśmy połączyć renderowanie programu z cieniowaniem ze sceną burzy, szczególnie wtedy, gdy model jest ukryty na tle spektakularnych chmur.

Technika do cieniowania polega na wykorzystaniu pojedynczego cieniowania GLSL do renderowania całego obiektu za pomocą uproszczonego algorytmu renderowania zwanego „promieniem marszowym” z polem odległości. W tej technice tworzony jest program do cieniowania pikseli, który szacuje najbliższą odległość od powierzchni dla każdego punktu na ekranie.

Dobre informacje o algorytmie znajdziesz w opisie na stronie iq: Rendering Worlds With Two Triangles – Iñigo Quilez. W galerii programów do cieniowania na stronie glsl.heroku.com znajdziesz wiele przykładów tej techniki, z którą możesz poeksperymentować.

Serce cieniowania zaczyna się od jego głównej funkcji, po czym kamera zmienia się i wprowadza pętlę, która wielokrotnie ocenia odległość do powierzchni. Wywołanie RaytraceFoggy( wektor_kierunku, maksymalne_iteracje, kolor, mnożnik_koloru ) służy do obliczania marszu promieniowego.

for(int i=0;i < number_of_steps;i++) // run the ray marching loop
{
  old_d=d;
  float shape_value=Shape(q); // find out the approximate distance to or density of the tornado cone
  float density=-shape_value;
  d=max(shape_value*step_scaling,0.0);// The max function clamps values smaller than 0 to 0

  float step_dist=d+extra_step; // The point is advanced by larger steps outside the tornado,
  //  allowing us to skip empty space quicker.

  if (density>0.0) {  // When density is positive, we are inside the cloud
    float brightness=exp(-0.6*density);  // Brightness decays exponentially inside the cloud

    // This function combines density layers to create a translucent fog
    FogStep(step_dist*0.2,clamp(density, 0.0,1.0)*vec3(1,1,1), vec3(1)*brightness, colour, multiplier); 
  }
  if(dist>max_dist || multiplier.x < 0.01) { return;  } // if we've gone too far stop, we are done
  dist+=step_dist; // add a new step in distance
  q=org+dist*dir; // trace its direction according to the ray casted
}

Chodzi o to, że w miarę zbliżania się do kształtu tornada regularnie dodajemy kolor do ostatecznej wartości koloru piksela, a także wpływamy na przezroczystość promieniowania. W ten sposób powstaje miękka, warstwowa powłoka, która pokrywa teksturę tornada.

Kolejnym podstawowym aspektem tornada jest sam kształt utworzony przez utworzenie szeregu funkcji. Początkowo jest to stożek, który składa się z hałasu, aby utworzyć organiczną chropowatą krawędź, a następnie skręca się wzdłuż głównej osi i obraca w czasie.

mat2 Spin(float angle){
  return mat2(cos(angle),-sin(angle),sin(angle),cos(angle)); // a rotation matrix
}

// This takes noise function and makes ridges at the points where that function crosses zero
float ridged(float f){ 
  return 1.0-2.0*abs(f);
}

// the isosurface shape function, the surface is at o(q)=0 
float Shape(vec3 q) 
{
    float t=time;

    if(q.z < 0.0) return length(q);

    vec3 spin_pos=vec3(Spin(t-sqrt(q.z))*q.xy,q.z-t*5.0); // spin the coordinates in time

    float zcurve=pow(q.z,1.5)*0.03; // a density function dependent on z-depth

    // the basic cloud of a cone is perturbed with a distortion that is dependent on its spin 
    float v=length(q.xy)-1.5-zcurve-clamp(zcurve*0.2,0.1,1.0)*snoise(spin_pos*vec3(0.1,0.1,0.1))*5.0; 

    // create ridges on the tornado
    v=v-ridged(snoise(vec3(Spin(t*1.5+0.1*q.z)*q.xy,q.z-t*4.0)*0.3))*1.2; 

    return v;
}

Utworzenie takiego programu do cieniowania jest trudne. Oprócz problemów związanych z abstrakcją wykonywanych operacji pojawiają się też poważne problemy związane z optymalizacją i zgodnością między platformami, które trzeba wyśledzić i rozwiązać, zanim będzie można wykorzystać utwór w produkcji.

Pierwsza część problemu: optymalizacja tego cienia pod kątem naszej sceny. Aby sobie z tym radzić, potrzebna była „bezpieczna” metoda na wypadek, gdyby program do cieniowania był zbyt ciężki. W tym celu skomponowaliśmy cieniowanie tornado w innej rozdzielczości próbkowania niż reszta sceny. Dane pochodzą z pliku burzTest.coffee (tak, to był test).

Zaczynamy od parametru renderTarget, który pasuje do szerokości i wysokości sceny, dzięki czemu możemy niezależnie od rozdzielczości cieniowania tornada. Następnie ustalamy zmniejszenie rozdzielczości cieniowania burzowego w zależności od otrzymywanej liczby klatek.

...
Line 1383
@tornadoRT = new THREE.WebGLRenderTarget( @SCENE_WIDTH, @SCENE_HEIGHT, paramsN )

... 
Line 1403 
# Change settings based on FPS
if @fpsCount > 0
    if @fpsCur < 20
        @tornadoSamples = Math.min( @tornadoSamples + 1, @MAX_SAMPLES )
    if @fpsCur > 25
        @tornadoSamples = Math.max( @tornadoSamples - 1, @MIN_SAMPLES )
    @tornadoW = @SCENE_WIDTH  / @tornadoSamples // decide tornado resWt
    @tornadoH = @SCENE_HEIGHT / @tornadoSamples // decide tornado resHt

Na koniec renderujemy tornado w celu odfiltrowania przy użyciu uproszczonego algorytmu sal2x (aby uniknąć efektu blokowego) @line 1107 w pliku burzTest.coffee. Oznacza to, że w gorszych przypadkach widok tornada jest bardziej niewyraźny, ale przynajmniej działa ono bez odbierania użytkownikowi kontroli.

Następny etap optymalizacji wymaga zagłębienia się w algorytm. Czynnik obliczeniowy w programie do cieniowania to iteracja wykonywana na każdym pikselu w celu oszacowania odległości funkcji powierzchni, czyli liczby iteracji pętli marszowej. Używając większego rozmiaru schodków, można było oszacować powierzchnię tornada z mniejszą liczbą powtórzeń, gdy przebywaliśmy poza jego chmurą. Po włączeniu funkcji zmniejszmy rozmiar kroku, aby zapewnić precyzję i mieliśmy możliwość mieszania wartości w celu uzyskania efektu mgły. Dodatkowo utworzenie walca ograniczającego w celu uzyskania oszacowania głębokości odrzucanego promienia zapewniło dobrą prędkość.

Kolejnym problemem było dopilnowanie, aby program cieniowania działał na różnych kartach wideo. Za każdym razem przeprowadzaliśmy kilka testów i zaczęliśmy myśleć, jakie problemy ze zgodnością mogą wystąpić. Nie możemy zrobić nic lepiej niż intuicja, bo nie zawsze można było uzyskać dobre informacje na temat debugowania błędów. Typowy scenariusz to po prostu błąd GPU, niewiele zostało do zakończenia lub nawet awaria systemu.

Problemy ze zgodnością tablic wideo miały podobne rozwiązania: upewnij się, że stałe statyczne zostały wprowadzone precyzyjnego typu danych zgodnie z definicją, IE: 0,0 dla liczby zmiennoprzecinkowej i 0 dla int.Zachowaj ostrożność podczas pisania dłuższych funkcji. Lepiej dzielić obiekty na kilka prostszych funkcji i zmiennych tymczasowych, ponieważ kompilatory wydawały się nie obsługiwać poprawnie niektórych przypadków. Dopilnuj, aby tekstury miały wartość potęgową 2, a nie za duże. Zachowaj ostrożność podczas przeglądania danych tekstur w pętli.

Największe problemy ze zgodnością wiązały się z efektem oświetlenia podczas burzy. Użyliśmy gotowej tekstury owiniętej wokół tornada, aby pokolorować jego pasy. To było niesamowite efekty – można było łatwo skomponować tornado w kolorach sceny, ale znalezienie go na innych platformach zajęło dużo czasu.

tornado

Witryna mobilna

Wersja na komputery nie mogła być zwykłym tłumaczeniem w wersji na urządzenia mobilne, ponieważ wymagania dotyczące technologii i przetwarzania były po prostu zbyt skomplikowane. Musieliśmy utworzyć coś nowego, skierowanego specjalnie do użytkowników telefonów komórkowych.

Pomyśleliśmy, że dobrze byłoby mieć fotobudkę z karnawału na komputerze jako aplikację internetową dla urządzeń mobilnych, która używa aparatu urządzenia mobilnego użytkownika. Coś, czego nie widzieliśmy do tej pory.

W celu urozmaicenia zakodowaliśmy przekształcenia 3D w CSS3. Dzięki połączeniu urządzenia z żyroskopem i akcelerometrem mogliśmy wprowadzić więcej szczegółów w tej usłudze. Strona reaguje na sposób przytrzymywania, poruszania się i patrzenia na telefon.

Podczas pisania tego artykułu uznaliśmy, że warto dać Ci kilka wskazówek, które pomogą Ci sprawnie przeprowadzić proces tworzenia aplikacji mobilnych. Gotowe! Śmiało i przekonaj się, czego możesz się z niego dowiedzieć.

Porady i wskazówki dotyczące urządzeń mobilnych

Moduł wstępnego ładowania to coś, co jest potrzebne i nie należy unikać takich działań. Wiemy, że czasami może się zdarzyć. Wynika to głównie z tego, że w miarę rozwoju projektu musisz utrzymywać listę wstępnie wczytywanych elementów. Co gorsza, nie wiadomo, jak obliczyć postęp wczytywania, gdy pobierasz różne zasoby i wiele z nich jednocześnie. Tutaj przydaje się nasza niestandardowa, bardzo ogólna abstrakcyjna klasa „Task”. Głównym założeniem jest umożliwienie niekończącej się zagnieżdżonej struktury, w której zadanie może mieć własne zadania podrzędne, które mogą mieć swoje itd. Poza tym każde zadanie oblicza jego postęp w odniesieniu do postępu realizacji poszczególnych podzadań (ale nie postępów nadrzędnych). Łącząc wszystkie elementy MainPreloadTask, AssetPreloadTask i TemplatePreFetchTask z poziomu Task, utworzyliśmy strukturę podobną do tej:

Moduł wstępnego ładowania

Dzięki takiemu podejściu i klasie Task możemy z łatwością poznać globalny postęp (MainPreloadTask) lub tylko postęp zasobów (AssetPreloadTask) albo postęp wczytywania szablonów (TemplatePreFetchTask). a nawet postęp konkretnego pliku. Aby zobaczyć, jak to działa, zapoznaj się z klasą Task (w języku angielskim) w pliku /m/javascripts/raw/util/Task.js, a opisem implementacji zadań na stronie /m/javascripts/preloading/task. Na przykład jest to fragment skonfigurowanego przez nas klasy /m/javascripts/preloading/task/MainPreloadTask.js, która jest naszym najlepszym opakowaniem wstępnego wczytywania:

Package('preloading.task', [
  Import('util.Task'),
...

  Class('public MainPreloadTask extends Task', {

    _public: {
      
  MainPreloadTask : function() {
        
    var subtasks = [
      new AssetPreloadTask([
        {name: 'cutout/cutout-overlay-1', ext: 'png', type: ImagePreloader.TYPE_BACKGROUND, responsive: true},
        {name: 'journey/scene1', ext: 'jpg', type: ImagePreloader.TYPE_IMG, responsive: false}, ...
...
      ]),

      new TemplatePreFetchTask([
        'page.HomePage',
        'page.CutoutPage',
        'page.JourneyToOzPage1', ...
...
      ])
    ];
    
    this._super(subtasks);

      }
    }
  })
]);

W klasie /m/javascripts/preloading/task/subtask/AssetPreloadTask.js, oprócz informacji o tym, jak komunikuje się ona z elementem MainPreloadTask (przez wspólną implementację zadania), warto też zwrócić uwagę na sposób ładowania zasobów zależnych od platformy. Zasadniczo mamy cztery rodzaje obrazów. Standard mobilny (.ext, gdzie ext to rozszerzenie pliku, zwykle .png lub .jpg), retina mobilna (-2x.ext), standard dla tabletów (-tab.ext) i retina dla tabletów (-tab-2x.ext) Zamiast wykrywać w komponencie MainPreloadTask i kodowaniu na stałe cztery tablice zasobów, mówimy po prostu, jaka jest nazwa i rozszerzenie zasobu do wstępnego wczytania oraz czy zasób zależy od platformy (elastyczny = prawda / fałsz). Następnie AssetPreloadTask wygeneruje dla nas nazwę pliku:

resolveAssetUrl : function(assetName, extension, responsive) {
  return AssetPreloadTask.ASSETS_ROOT + assetName + (responsive === true ? ((Detection.getInstance().tablet ? '-tab' : '') + (Detection.getInstance().retina ? '-2x' : '')) : '') + '.' +  extension;
}

W niższej części łańcucha klas rzeczywisty kod, który przeprowadza wstępne wczytywanie zasobów, wygląda tak (/m/javascripts/raw/util/ImagePreloader.js):

loadUrl : function(url, type, completeHandler) {
  if(type === ImagePreloader.TYPE_BACKGROUND) {
    var $bg = $('<div>').hide().css('background-image', 'url(' + url + ')');
    this.$preloadContainer.append($bg);
  } else {
    var $img= $('<img />').attr('src', url).hide();
    this.$preloadContainer.append($img);
  }

  var image = new Image();
  this.cache[this.generateKey(url)] = image;
  image.onload = completeHandler;
  image.src = url;
}

generateKey : function(url) {
  return encodeURIComponent(url);
}

Samouczek: Photo Booth HTML5 (iOS6/Android)

Opracowując OZ Mobile, odkryliśmy, że spędzamy mnóstwo czasu, bawiąc się w budce fotograficznej, zamiast pracować :D. A to dlatego, że tak się bawisz. Przygotowaliśmy więc demonstrację, którą możesz wypróbować.

Mobilna budka fotograficzna
Komórka mobilna

Prezentację na żywo możesz zobaczyć tutaj (uruchom ją na iPhonie lub telefonie z Androidem):

http://u9html5rocks.appspot.com/demos/mobile_photo_booth

Aby ją skonfigurować, potrzebujesz bezpłatnej instancji aplikacji Google App Engine, w której możesz uruchomić backend. Kod interfejsu nie jest złożony, ale można go rozwiązać na kilka sposobów. Omówmy je teraz:

  1. Dozwolony typ pliku graficznego Chcemy, aby użytkownicy mogli przesyłać tylko obrazy (ponieważ jest to budka fotograficzna, a nie wideo). Teoretycznie możesz określić filtr w kodzie HTML w ten sposób: input id="fileInput" class="fileInput" type="file" name="file" accept="image/*" Jednak to działa tylko na iOS, więc po wybraniu pliku musimy dodać kolejną kontrolę względem wyrażeń regularnych:
   this.$fileInput.fileupload({
          
   dataType: 'json',
   autoUpload : true,
   
   add : function(e, data) {
     if(!data.files[0].name.match(/(\.|\/)(gif|jpe?g|png)$/i)) {
      return self.onFileTypeNotSupported();
     }
   }
   });
  1. Anulowanie przesyłania lub wyboru pliku Kolejną niespójnością, którą zaobserwowaliśmy podczas programowania, jest to, że różne urządzenia powiadamiają o anulowanym wyborze pliku. Telefony i tablety z iOS nie wykonują żadnych działań – w ogóle nie powiadamiają. W tym przypadku nie trzeba więc nic specjalnego robić, jednak telefony z Androidem i tak uruchamiają funkcję add(), nawet jeśli nie wybrano żadnego pliku. Oto jak to zaspokoić:
    add : function(e, data) {

    if(data.files.length === 0 || (data.files[0].size === 0 && data.files[0].name === "" && data.files[0].fileName === "")) {
            
    return self.onNoFileSelected();

    } else if(data.files.length > 1) {

    return self.onMultipleFilesSelected();            
    }
    }

Reszta działa płynnie na różnych platformach. Baw się dobrze!

Podsumowanie

Ze względu na ogromny rozmiar aplikacji Znajdź drogę do Oz i szeroki wybór różnych technologii w tym artykule mogliśmy omówić tylko kilka z zastosowanych podejść.

Jeśli chcesz poznać całą enchiladę, zapoznaj się z pełnym kodem źródłowym strony Znajdź swoją drogę do Oz.

Środki

Aby wyświetlić pełną listę środków, kliknij tutaj.

Źródła