Studium przypadku: Znajdź drogę do Oz

Wprowadzenie

„Find Your Way to Oz” to nowy eksperyment Google Chrome, który Disney udostępnił w internecie. W ramach tej gry możesz odbyć interaktywną podróż po cyrku w Kanzasie, która po potężnej burzy zaprowadzi Cię do krainy Oz.

Naszym celem było połączenie bogatego świata kina z technicznymi możliwościami przeglądarki, aby stworzyć przyjemne i angażujące wrażenia, które użytkownicy mogą docenić.

To zbyt obszerny temat, aby można było go w pełni przedstawić w tym artykule, dlatego wybraliśmy kilka interesujących nas rozdziałów z historii technologii. W międzyczasie stworzyliśmy kilka samouczków o zwiększającym się stopniu trudności.

Wiele osób pracowało nad tym, aby to było możliwe: zbyt wiele, aby wymienić je wszystkie. Odwiedź stronę, aby w sekcji menu zapoznać się z pełną historią.

Jak to działa

Find Your Way to Oz na komputerze to bogaty, wciągający świat. Używamy efektów 3D i kilku warstw efektów inspirowanych tradycyjnym filmowaniem, które razem tworzą niemal realistyczną scenę. Najpopularniejsze technologie to WebGL z Three.js, niestandardowe shadery i animowane elementy DOM za pomocą funkcji CSS3. Ponadto interfejs getUserMedia API (WebRTC) umożliwia interakcję z użytkownikiem, pozwalając mu dodawać obraz bezpośrednio z kamery internetowej, a interfejs WebAudio – dźwięk 3D.

Jednak magia takiej technologii polega na tym, jak wszystko się ze sobą łączy. Jest to też jedno z głównych wyzwań: jak połączyć efekty wizualne i elementy interaktywne w jednej scenie, aby stworzyć spójną całość? Ta złożoność wizualna była trudna do zarządzania: trudno było określić, na jakim etapie rozwoju jesteśmy w danym momencie.

Aby rozwiązać problem wzajemnie powiązanych efektów wizualnych i optymalizacji, intensywnie korzystaliśmy z panelu sterowania, który rejestrował wszystkie istotne ustawienia, które sprawdzaliśmy w danym momencie. Scena może być dostosowywana na żywo w przeglądarce pod kątem różnych parametrów, takich jak jasność, głębia ostrości czy gama. Każdy może spróbować zmienić wartości istotnych parametrów i sprawdzić, co działa najlepiej.

Zanim zdradzimy nasz sekret, chcemy Cię ostrzec, że może się on nie udać, tak jak w przypadku grzebania w silniku samochodowym. Upewnij się, że nie masz otwartych żadnych ważnych stron, a potem otwórz główną stronę witryny i dodaj do adresu parametr ?debug=on. Poczekaj, aż strona się załaduje, a gdy się zalogujesz (naciśnij klawisz Ctrl-I), po prawej stronie pojawi się menu. Jeśli odznaczysz opcję „Wyjdź z ścieżki kamery”, możesz używać klawiszy A, W, S, D i myszy, aby swobodnie poruszać się po przestrzeni.

Ścieżka kamery.

Nie będziemy tutaj omawiać wszystkich ustawień, ale zachęcamy do eksperymentowania: klucze odsłaniają różne ustawienia w różnych scenach. W ostatniej sekwencji burzy jest dodatkowy klawisz: Ctrl-A, za pomocą którego możesz włączać i wyłączać odtwarzanie animacji oraz przemieszczać się po ekranie. W tej scenie, jeśli naciśniesz Esc (aby zamknąć funkcję blokady myszy) i ponownie naciśniesz Ctrl-I, uzyskasz dostęp do ustawień związanych z tą sceną. Rozejrzyj się i zrób kilka ładnych zdjęć, takich jak to poniżej.

Scena burzy

Aby to osiągnąć i zapewnić sobie wystarczającą elastyczność, użyliśmy świetnej biblioteki o nazwie dat.gui (tutaj znajdziesz samouczek na temat jej używania). Dzięki temu mogliśmy szybko zmieniać ustawienia widoczne dla użytkowników witryny.

trochę jak rysunek wstępny,

W wielu klasycznych filmach i animowanych produkcjach Disneya tworzenie scen polegało na łączeniu różnych warstw. Były tam warstwy z udziałem aktorów, animacje ręczne, a nawet fizyczne plany zdjęciowe. Na wierzchu znajdowały się warstwy stworzone przez malowanie na szkle, czyli techniką zwaną matte-painting.

Struktura stworzonej przez nas usługi jest w wielu aspektach podobna, choć niektóre „warstwy” to coś więcej niż statyczne obrazy. W istocie wpływają one na wygląd obiektów zgodnie ze złożonymi obliczeniami. Jednak przynajmniej na poziomie ogólnym mamy do czynienia z widokami złożonymi jeden na drugim. U góry widać warstwę interfejsu, a pod nią scenę 3D, która składa się z różnych elementów sceny.

Górna warstwa interfejsu została utworzona za pomocą DOM i CSS 3, co oznacza, że można było edytować interakcje na wiele sposobów niezależnie od interfejsu 3D, z komunikacją między nimi zgodnie z wybraną listą zdarzeń. Ta komunikacja korzysta z Backbone Router + zdarzenia HTML5 onHashChange, które kontroluje, która część ma się animować. (źródło projektu: /develop/coffee/router/Router.coffee).

Samouczek: obsługa arkuszy sprite’ów i wyświetlaczy Retina

Jedną z ciekawych technik optymalizacji, na której polegaliśmy w przypadku interfejsu, było połączenie wielu obrazów nakładki interfejsu w jeden plik PNG, aby zmniejszyć liczbę żądań wysyłanych do serwera. W tym projekcie interfejs składał się z ponad 70 obrazów (nie licząc tekstur 3D), które zostały załadowane z góry, aby skrócić czas oczekiwania na stronie. Tutaj możesz zobaczyć arkusz sprite’ów na żywo:

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

Oto kilka wskazówek dotyczących korzystania z arkuszy sprite’ów, ich zastosowania na urządzeniach z wyświetlaczami retina oraz uzyskiwania jak najostrzejszego i najbardziej przejrzystego interfejsu.

Tworzenie arkuszy sprite

Do tworzenia arkuszy sprite’ów użyliśmy programu TexturePacker, który umożliwia zapisywanie danych w dowolnym formacie. W tym przypadku wyeksportowaliśmy plik w formacie EaselJS, który jest bardzo przejrzysty i można go wykorzystać do tworzenia animowanych sprite’ów.

Korzystanie z wygenerowanego arkusza sprite

Po utworzeniu arkusza sprite’ów powinieneś zobaczyć 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:

  • image 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 komponentów

Pamiętaj, że do utworzenia arkusza sprite użyliśmy obrazów o wysokiej gęstości, a potem utworzyliśmy normalną wersję, zmniejszając rozmiar do połowy.

Konkluzja

Teraz, gdy wszystko jest gotowe, potrzebujemy tylko fragmentu 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);
};

Oto jak go używać:

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 zmiennej gęstości pikseli, przeczytaj ten artykuł Borisa Smusa.

Ścieżka przetwarzania treści 3D

Środowisko jest konfigurowane na warstwie WebGL. Jeśli chodzi o sceny 3D, jednym z najtrudniejszych pytań jest to, jak zapewnić maksymalny potencjał wyrazu w modelowaniu, animacji i efektach. W wielu aspektach podstawą tego problemu jest łańcuch treści: uzgodniony proces tworzenia treści do sceny 3D.

Chcieliśmy stworzyć zachwycający świat, więc potrzebowaliśmy solidnego procesu, który umożliwiłby jego tworzenie przez artystów 3D. Musimy dać im jak największą swobodę w modelowaniu 3D i oprogramowaniu do animacji, a następnie musimy renderować to na ekranie za pomocą kodu.

Pracowaliśmy nad tym problemem przez jakiś czas, ponieważ za każdym razem, gdy w przeszłości tworzyliśmy witrynę 3D, natrafialiśmy na ograniczenia w dostępnych narzędziach. Stworzyliśmy narzędzie o nazwie 3D Librarian, które powstało w ramach wewnętrznych badań. I właśnie miało zostać zastosowane w praktyce.

To narzędzie ma już pewną historię: pierwotnie było przeznaczone do Flasha i pozwalało wstawić dużą scenę Maya jako jeden skompresowany plik zoptymalizowany pod kątem rozpakowywania w czasie działania. Było to optymalne rozwiązanie, ponieważ scena została efektywnie skompresowana w ramach tej samej struktury danych, którą można manipulować podczas renderowania i animacji. Po załadowaniu pliku trzeba przeanalizować tylko niewielką jego część. Rozpakowywanie w Flash przebiegało dość szybko, ponieważ plik był w formacie AMF, który Flash mógł rozpakować natywnie. Używanie tego samego formatu w WebGL wymaga nieco więcej pracy procesora. Musieliśmy odtworzyć warstwę kodu JavaScripta służącą do rozpakowywania danych, która miałaby dekompresować te pliki i odtwarzać struktury danych potrzebne do działania WebGL. Rozpakowywanie całej sceny 3D to operacja, która w niewielkim stopniu obciąża procesor: rozpakowanie sceny 1 w grze Find Your Way To Oz zajmuje około 2 sekund na komputerze średniej lub wysokiej klasy. Dlatego korzystamy z technologii Web Workers w momencie „konfigurowania sceny” (przed jej uruchomieniem), aby nie powodować zawieszania aplikacji.

To przydatne narzędzie może importować większość sceny 3D: modele, tekstury, animacje kości. Utwórz pojedynczy plik biblioteki, który można następnie wczytać w silniku 3D. W tej bibliotece umieszczasz wszystkie modele, których potrzebujesz w scenie, i voilà, modele pojawiają się w scenie.

Problem polegał na tym, że mieliśmy do czynienia z WebGL: nowością na rynku. To było trudne zadanie: wyznaczanie standardów w obszarze 3D w przeglądarce. Dlatego utworzyliśmy specjalną warstwę JavaScript, która pobiera skompresowane pliki 3D z biblioteki 3D Librarian i odpowiednio je przekształca w format zrozumiały dla WebGL.

Samouczek: Niech stanie się wiatr

W filmie „Find Your Way To Oz” często pojawiał się wiatr. Wątek fabuły jest skonstruowany tak, aby stanowił crescendo wiatru.

Pierwsza scena karnawału jest stosunkowo spokojna. W trakcie oglądania różnych scen użytkownik doświadcza coraz silniejszego wiatru, który w ostatniej scenie przeradza się w burzę.

Dlatego ważne było, aby zapewnić realistyczny efekt wiatru.

Aby to zrobić, wypełniliśmy 3 sceny karnawałowe obiektami, które były miękkie i w związku z tym miały być podatne na działanie wiatru, takimi jak namioty, flagi na powierzchni kabiny do zdjęć i sam balon.

miękką szmatką.

Obecnie gry na komputery są zwykle tworzone na podstawie podstawowego silnika fizyki. Dlatego gdy trzeba symulować miękki obiekt w świecie 3D, przeprowadzana jest pełna symulacja fizyki, która tworzy wiarygodne zachowanie miękkiego obiektu.

W WebGL / JavaScript nie mamy (jeszcze) luksusu, jakim jest pełna symulacja fizyki. W przypadku Oz musieliśmy znaleźć sposób na stworzenie efektu wiatru bez jego symulowania.

W przypadku każdego obiektu w modelu 3D zapisaliśmy informacje o „czułości na wiatr”. Każdy wierzchołek modelu 3D miał „atrybut wiatru”, który określał, w jakim stopniu wierzchołek miał być poddawany działaniu wiatru. Ta specyfikacja określa wrażliwość na wiatr obiektów 3D. Potem musieliśmy stworzyć wiatr.

W tym celu wygenerowaliśmy obraz zawierający szum Perlin. Ten obraz ma obejmować pewien „obszar wiatru”. Możesz sobie wyobrazić, że obraz chmury jest jak szum rozłożony na określonym prostokątnym obszarze sceny 3D. Każdy piksel, czyli wartość szarości na tym obrazie, określa, jak silny jest wiatr w określonym momencie w obszarze 3D „otaczającym” ten piksel.

Aby uzyskać efekt wiatru, obraz jest przesuwany w czasie z ustaloną prędkością w określonym kierunku, czyli w kierunku wiatru. Aby „obszar wiatru” nie wpływał na wszystko w scenie, otaczamy obraz wiatru wokół krawędzi, ograniczając go do obszaru efektu.

Prosty samouczek dotyczący wiatru 3D

Stwórzmy teraz efekt wiatru w prostatej scenie 3D w Three.js.

Utworzymy wiatr w prostym „proceduralnym polu trawy”.

Najpierw utwórz scenę. Będziemy mieć prostą, płaską rzeźbę terenu z teksturą. Każdy kawałek trawy będzie reprezentowany przez odwrócony stożek 3D.

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

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

Najpierw skonfigurujemy Three.js i podłączymy go do Camera, Mouse controller i Some Light:

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 initGrassinitTerrain wypełniają odpowiednio scenę trawą i terenem:

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

Tutaj tworzymy siatkę 15 kawałek trawy na 15. Do każdej pozycji trawy dodajemy trochę losowości, aby nie były one ułożone jak żołnierze, co wyglądałoby dziwnie.

Ten teren to tylko pozioma płaszczyzna umieszczona u podstaw źdźbła 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 stworzyliśmy tylko scenę Three.js i dodaliśmy kawałek trawy utworzony z odwróconych stożków generowanych proceduralnie oraz prosty teren.

Nic specjalnego.

Teraz czas zacząć dodawać wiatr. Po pierwsze chcemy umieścić informacje o czułości na wiatr w modelu 3D trawy.

Te informacje zostaną zakodowane jako atrybuty niestandardowe w przypadku każdego wierzchołka modelu 3D trawy. Użyjemy reguły, że dolna część modelu trawy (wierzchołek stożka) ma zerową czułość, ponieważ jest przymocowana do podłoża. Górna część modelu trawy (podstawa stożka) ma maksymalną czułość na wiatr, ponieważ jest to część, która jest najdalej od ziemi.

Oto jak funkcja instanceGrass została ponownie zakodowana, aby dodać czułość na wiatr jako atrybut niestandardowy 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

Zamiast używanego wcześniej materiału MeshPhongMaterial używamy teraz materiału niestandardowego windMaterial. WindMaterial zawiera WindMeshShader, który za chwilę zobaczysz.

Kod w funkcji instanceGrass przechodzi przez wszystkie wierzchołki modelu trawy i dla każdego z nich dodaje niestandardowy atrybut wierzchołka o nazwie windFactor. Ten parametr ma wartość 0 w przypadku dolnej części modelu trawy (gdzie ma ona stykać się z terenem), a wartość 1 w przypadku górnej części modelu trawy.

Drugim składnikiem, którego potrzebujemy, jest dodanie do sceny prawdziwego wiatru. Jak już ustaliliśmy, użyjemy do tego szumu Perlin. Wygenerujemy proceduralnie teksturę szumu Perlin.

Dla jasności przypiszemy tę teksturę do samego terenu zamiast poprzedniej zielonej tekstury. Dzięki temu łatwiej będzie Ci określić, co dzieje się z wiatrem.

Ta tekstura Perlin noise będzie przestrzennie pokrywać rozszerzenie naszego terenu, a każdy piksel tekstury będzie określać natężenie wiatru w obszarze terenu, w którym znajduje się ten piksel. Prostokąt na rzeźbie terenu będzie „obszarem wiatru”.

Szum Perlina jest generowany proceduralnie za pomocą shadera o nazwie NoiseShader. Ten shader używa algorytmów szumu 3D simplex z adresu https://github.com/ashima/webgl-noise . Wersja WebGL została zaczerpnięta dosłownie z jednego z plików przykładowych Three.js na stronie http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.

NoiseShader przyjmuje jako uniformy czas, skalę i zbiór parametrów offsetu, a na wyjściu generuje ładną 2D-wy rozkład 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 shadera do renderowania szumu Perlin na potrzeby tekstury. Jest to wykonywane w ramach funkcji 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 )

Kod ten powoduje skonfigurowanie noiseMap jako celu renderowania Three.js, wyposażenie go w NoiseShader, a następnie renderowanie za pomocą kamery ortogonalnej, aby uniknąć zniekształceń perspektywy.

Zgodnie z porozumieniem będziemy teraz używać tej tekstury również jako głównej tekstury renderowania dla terenu. Nie jest to konieczne, aby efekt wiatru działał. Ale jest to przydatne, ponieważ pozwala nam lepiej zrozumieć, co dzieje się z produkcją energii wiatrowej.

Oto zmieniona funkcja initTerrain, która używa mapy hałasu jako tekstury:

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 )

Teraz, gdy mamy już teksturę wiatru, przyjrzyjmy się shaderowi WindMeshShader, który odpowiada za deformowanie modeli trawy zgodnie z wiatrem.

Aby utworzyć ten shader, zaczęliśmy od standardowego shadera MeshPhongMaterial w Three.js i go zmodyfikowaliśmy. To szybki i prosty sposób na rozpoczęcie pracy z działającym shaderem bez konieczności zaczynania od zera.

Nie będziemy tu kopiować całego kodu shadera (możesz go zobaczyć w pliku kodu źródłowego), ponieważ większość z niego to kopia shadera MeshPhongMaterial. Przyjrzyjmy się jednak zmodyfikowanym fragmentom związanym z wiatrem w shaderze wierzchołkowym.

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;

Ten shader najpierw oblicza współrzędną wyszukiwania tekstury windUV na podstawie pozycji 2D (poziomej) wierzchołka xz. Ta współrzędna UV jest używana do wyszukiwania siły wiatru (vWindForce) w teksturze wiatru Perlin.

Ta wartość vWindForce jest łączona z atrybutem niestandardowym windFactor (omówionym powyżej) wierzchołka, aby obliczyć, jak duża deformacji potrzebuje wierzchołek. Mamy też globalny parametr windScale, który służy do kontrolowania ogólnej siły wiatru, oraz wektor windDirection, który określa, w jakim kierunku ma nastąpić deformacja wiatru.

W ten sposób tworzymy deformację traw na podstawie działania wiatru. Nie spoczywamy jednak na laurach. Obecnie ta deformacja jest statyczna i nie będzie odzwierciedlać efektu wiatru.

Jak już wspomnieliśmy, musimy przesuwać teksturę szumu w czasie w obszarze wiatru, aby szkło mogło się poruszać.

Dzieje się to przez przesuwanie w czasie uniformu vOffset przekazywanego do NoiseShader. Jest to parametr wektorowy, który pozwoli nam określić przesunięcie szumu w określonym kierunku (kierunek 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 utworzyliśmy scenę z „proceduralną trawą” nawiewaną przez wiatr.

Dodanie efektu pyłu

Teraz nieco urozmaicimy naszą scenę. Dodajmy trochę unoszącego się kurzu, aby scena była bardziej interesująca.

Dodawanie kurzu
Dodawanie kurzu

W końcu wiatr powinien oddziaływać na kurz, więc ma on latać w scenie z wiatrem.

Pył jest konfigurowany w funkcji initDust jako system 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ąsteczek kurzu. Pamiętaj, że każda z nich jest wyposażona w specjalny WindParticleShader.

Teraz w każdej klatce cząsteczki będą się trochę poruszać za pomocą CoffeeScript, 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 )

Dodatkowo przesuniemy każdą pozycję cząsteczki zgodnie z kierunkiem wiatru. Jest to realizowane w WindParticleShader. W szczególności w shaderze wierzchołka.

Kod tego shadera to zmodyfikowana wersja ParticleMaterial z Three.js. Oto jego główna część:

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 shader wierzchołkowy nie różni się zbytnio od tego, który mieliśmy w przypadku deformacji trawy na podstawie wiatru. Jako dane wejściowe przyjmuje teksturę szumu Perlin i w zależności od pozycji pyłu w świecie wyszukiwania w teksturze szumu odwołuje się do wartości vWindForce. Następnie używa tej wartości do modyfikowania położenia cząsteczki kurzu.

Riders On The Storm

Najbardziej ryzykowna z naszych scen WebGL była prawdopodobnie ostatnia. Możesz ją zobaczyć, jeśli klikniesz balon, a potem wleziesz do oka trąby powietrznej, aby dotrzeć do końca swojej podróży w witrynie. Dostępne jest też ekskluzywne wideo z nadchodzącej aktualizacji.

Scena lotu balonem

Podczas tworzenia tej sceny wiedzieliśmy, że musi ona zawierać centralny element, który będzie miał duży wpływ na rozgrywkę. Wirujące tornado byłoby centralnym elementem, a inne warstwy treści tworzyłyby efekt dynamizmu. Aby to osiągnąć, stworzyliśmy coś na kształt studia filmowego wokół tego niezwykłego shadera.

Aby uzyskać realistyczny efekt, zastosowaliśmy podejście mieszane. Niektóre z nich to sztuczki wizualne, takie jak kształty światła, które dają efekt flary, czy krople deszczu, które animują się jako warstwy na wierzchu sceny. W innych przypadkach rysowaliśmy płaskie powierzchnie, które miały sprawiać wrażenie poruszających się, jak np. chmury nisko nad ziemią poruszające się zgodnie z kodem systemu cząstek. Podczas gdy kawałki gruzu krążące wokół tornada były warstwami w scenie 3D uporządkowanymi tak, aby poruszały się przed i za tornadem.

Głównym powodem, dla którego musieliśmy zbudować scenę w taki sposób, było zapewnienie wystarczającej mocy GPU do obsługi shadera tornada w stosunku do innych stosowanych przez nas efektów. Początkowo mieliśmy duże problemy z balansem GPU, ale później ta scena została zoptymalizowana i stała się lżejsza niż główne sceny.

Samouczek: shader Storm

Aby utworzyć ostatnią sekwencję burzy, połączyliśmy wiele różnych technik, ale centralnym elementem tego projektu był niestandardowy shader GLSL, który wygląda jak tornado. Wypróbowaliśmy wiele różnych technik, od shaderów wierzchołkowych po tworzenie ciekawych geometrycznych wirów, animacji opartych na cząsteczkach, a nawet animacji 3D skręconych kształtów geometrycznych. Żaden z efektów nie odzwierciedlał wrażenia wywoływanego przez tornado ani nie wymagał zbyt wielu zasobów przetwarzania.

Odpowiedź na to pytanie przyniosła nam zupełnie inna praca. Równolegle z tym instytut Maxa Plancka (brainflight.org) prowadził projekt gier dla nauki, którego celem było zmapowanie mózgu myszy. Wygenerował on ciekawe efekty wizualne. Udało nam się utworzyć filmy z wewnętrznego neuronu myszy za pomocą niestandardowego shadera objętościowego.

Neuron myszy w technologii niestandardowego cieniowania objętościowego
Wewnątrz neuronu myszy z wykorzystaniem niestandardowego shadera objętościowego

Okazało się, że wnętrze komórki mózgowej przypomina trochę lej tornada. Ponieważ używaliśmy techniki wolumetrycznej, wiedzieliśmy, że shader będzie widoczny ze wszystkich kierunków. Możemy ustawić renderowanie shadera tak, aby łączył się ze sceną burzy, zwłaszcza jeśli jest umieszczony między warstwami chmur i nad dramatycznym tłem.

Technika shadera polega na zastosowaniu triku, który wykorzystuje jeden shader GLSL do renderowania całego obiektu za pomocą uproszczonego algorytmu renderowania o nazwie renderowanie marszczące promienie z polem odległości. W ramach tej techniki tworzony jest shader pikseli, który szacuje najbliższą odległość do powierzchni dla każdego punktu na ekranie.

Informacje o tym algorytmie znajdziesz w artykule Rendering Worlds With Two Triangles – Iñigo Quilez. Warto też przejrzeć galerię shaderów na stronie glsl.heroku.com, gdzie można znaleźć wiele przykładów tej techniki, z którymi można eksperymentować.

Serce shadera zaczyna się od funkcji głównej, która konfiguruje przekształcenia kamery i wchodzi w pętlę, która wielokrotnie ocenia odległość do powierzchni. Główne obliczenia marszrutyzacji promieniowej odbywają się w nazwie RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ).

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
}

W miarę zbliżania się do środka tornada regularnie dodajemy do końcowej wartości koloru piksela udziały koloru i przezroczystości wzdłuż promienia. Dzięki temu tekstura tornada będzie miała warstwową, miękką jakość.

Kolejnym kluczowym aspektem tornada jest jego kształt, który powstaje przez złożenie kilku funkcji. Na początku jest to stożek, który jest komponowany z szumem w celu stworzenia organicznego, szorstkiego brzegu, a następnie jest skręcany wzdłuż głównej osi i obracany 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;
}

Tworzenie tego typu shaderów jest trudne. Oprócz problemów związanych z abstrukcją tworzonych operacji występują poważne problemy z optymalizacją i zgodnością między platformami, które musisz zidentyfikować i rozwiązać, zanim zaczniesz używać tej wersji w produkcji.

Pierwsza część problemu: optymalizacja tego shadera pod kątem naszej sceny. Aby temu zaradzić, musieliśmy zastosować „bezpieczne” podejście na wypadek, gdyby shader okazał się zbyt ciężki. W tym celu zmiksowaliśmy shader torna w innej rozdzielczości niż reszta sceny. To jest z pliku stormTest.coffee (tak, to był test).

Zaczynamy od renderTarget, który pasuje do szerokości i wysokości sceny, dzięki czemu możemy uzyskać niezależność rozdzielczości shadera torna od sceny. Następnie decydujemy o obniżeniu rozdzielczości cieniowania burzy w dynamiczny sposób w zależności od uzyskiwanej częstotliwości 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 na ekranie za pomocą uproszczonego algorytmu sal2x (aby uniknąć blokowego wyglądu) w wierszu 1107 w pliku stormTest.coffee. Oznacza to, że w najgorszym przypadku tornado będzie bardziej rozmyte, ale przynajmniej użytkownik będzie mógł nad nim kontrolować.

Następny krok optymalizacji wymaga zagłębienia się w algorytm. Głównym czynnikiem obliczeniowym w shaderze jest iteracja wykonywana na każdym pikselu w celu przybliżenia się do odległości funkcji powierzchni: liczba iteracji pętli raymarching. Dzięki większemu krokowi mogliśmy uzyskać szacowaną powierzchnię tornada przy mniejszej liczbie iteracji, gdy byliśmy poza jej chmurną powierzchnią. W pomieszczeniu zmniejszyliśmy rozmiar kroku, aby uzyskać większą precyzję i móc mieszać wartości, aby uzyskać efekt mglistości. Utworzenie też ograniczającego cylindra, aby uzyskać szacowany wymiar głębokości dla rzutowanego promienia, znacznie przyspieszyło działanie.

Kolejnym problemem było upewnienie się, że shader będzie działać na różnych kartach graficznych. Za każdym razem przeprowadzaliśmy testy i staraliśmy się przewidzieć, jakie problemy ze zgodnością mogą wystąpić. Nie udało nam się uzyskać lepszych wyników niż przy użyciu intuicji, ponieważ nie zawsze mogliśmy uzyskać dobre informacje o błędach na potrzeby debugowania. Typowym scenariuszem jest błąd GPU, który nie ma większego znaczenia, lub nawet awaria systemu.

Problemy z kompatybilnością z płytką z filmami miały podobne rozwiązania: upewnij się, że statyczne stałe są wprowadzane z dokładnym typem danych zgodnie z definicją, np. 0.0 dla typu float i 0 dla typu int. Zachowaj ostrożność podczas pisania dłuższych funkcji; lepiej jest podzielić je na kilka prostszych funkcji i półkowych zmiennych, ponieważ kompilatory najwyraźniej nie obsługują niektórych przypadków prawidłowo. Upewnij się, że wszystkie tekstury są potęgą 2, nie są zbyt duże i w każdym razie zachowujesz „ostrożność” podczas wyszukiwania danych tekstury w pętli.

Największe problemy ze zgodnością wynikały z efektu świetlnego burzy. Użyliśmy gotowej tekstury owiniętej wokół tornada, aby pokolorować jego smugi. To był wspaniały efekt, który ułatwiał łączenie torna z kolorami sceny, ale wymagał długiego czasu na próby uruchomienia na innych platformach.

tornado

Witryna internetowa na urządzenia mobilne

Wersja mobilna nie mogła być prostym przełożeniem wersji na komputer, ponieważ wymagania technologiczne i przetwarzania były zbyt duże. Musieliśmy stworzyć coś nowego, co będzie kierowane do użytkowników urządzeń mobilnych.

Pomyśleliśmy, że fajnie byłoby mieć aplikację mobilną, która korzystałaby z aparatu w telefonie i zastąpiłaby program Carnival Photo-Booth na komputery. Coś, czego do tej pory nie widzieliśmy.

Aby dodać nieco smaku, zaimplementowaliśmy w CSS3 przekształcenia 3D. Po połączeniu go z żyroskopem i akcelerometrem mogliśmy znacznie zwiększyć realizm. Witryna reaguje na to, jak trzymasz, poruszasz i patrzysz na telefon.

Podczas pisania tego artykułu uznaliśmy, że warto podać kilka wskazówek dotyczących płynnego przeprowadzania procesu tworzenia aplikacji mobilnej. Oto one. Sprawdź, czego możesz się z niego nauczyć.

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

Preloader jest potrzebny, a nie czymś, czego należy unikać. Wiemy, że czasami zdarza się to drugie. Wynika to głównie z tego, że wraz z rozwojem projektu musisz stale aktualizować listę elementów do wstępnego wczytania. Co gorsza, nie jest jasne, jak obliczyć postęp wczytywania, jeśli pobierasz różne zasoby, a na dodatek wiele z nich jednocześnie. Właśnie w takich sytuacjach przydaje się niestandardowa, bardzo ogólna abstrakcyjna klasa „Task”. Głównym założeniem jest umożliwienie tworzenia nieskończonej struktury zagnieżdżonej, w której zadanie może mieć własne podzadania, które mogą mieć swoje podzadania itd. Ponadto każde zadanie oblicza swój postęp z uwzględnieniem postępu podzadań (ale nie postępu zadania nadrzędnego). Aby wszystkie zadania MainPreloadTask, AssetPreloadTask i TemplatePreFetchTask pochodziły z Task, utworzyliśmy strukturę o takiej postaci:

Moduł wstępnego ładowania

Dzięki temu podejściu i klasie Task możemy łatwo poznać ogólny postęp (MainPreloadTask), postęp w wczytywaniu zasobów (AssetPreloadTask) lub postęp w wczytywaniu szablonów (TemplatePreFetchTask). Nawet postępy w przypadku konkretnego pliku. Aby zobaczyć, jak to zrobić, zapoznaj się z klasą Task w pliku /m/javascripts/raw/util/Task.js oraz z implementacjami zadań w pliku /m/javascripts/preloading/task. Oto przykładowy fragment kodu, który pokazuje, jak skonfigurowaliśmy klasę /m/javascripts/preloading/task/MainPreloadTask.js, która jest naszym ostatecznym opakowaniem do wczytywania wstępnego:

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 warto zwrócić uwagę nie tylko na to, jak komunikuje się ona z MainPreloadTask (za pomocą wspólnej implementacji Task), ale też na sposób wczytywania zasobów zależnych od platformy. Zasadniczo mamy 4 rodzaje obrazów. Standardowe (.ext, gdzie „ext” to rozszerzenie pliku, zwykle .png lub .jpg), retina (-2x.ext), standardowe (-tab.ext) i retina (-tab-2x.ext). Zamiast wykrywać w MainPreloadTask i twardo kodować 4 tablice komponentów, po prostu podajemy nazwę i rozszerzenie komponentu do wstępnego załadowania oraz informację, czy komponent jest zależny od platformy (responsive = true / false). 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;
}

Dalej w łańcuchu klas rzeczywisty kod, który wczytuje komponenty w ramach wstępnego wczytania, 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);
}

Przewodnik: HTML5 Photo Booth (iOS6/Android)

Podczas tworzenia aplikacji OZ na urządzenia mobilne okazało się, że zamiast pracować spędzamy dużo czasu na zabawie z budką fotograficzną :D Po prostu jest to świetna zabawa. Dlatego przygotowaliśmy wersję demonstracyjną, którą możesz wypróbować.

Mobilna budka fotograficzna
Mobilna fotobudka

Tutaj możesz zobaczyć wersję demonstracyjną (uruchom ją na iPhonie lub telefonie z Androidem):

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

Aby to zrobić, musisz mieć bezpłatną instancję aplikacji Google App Engine, na której możesz uruchomić backend. Kod interfejsu nie jest skomplikowany, ale może zawierać kilka pułapek. Omówmy je teraz:

  1. Dozwolony typ pliku z obrazem Chcemy, aby użytkownicy mogli przesyłać tylko obrazy (ponieważ jest to fotobudka, a nie budka wideo). Teoretycznie możesz po prostu określić filtr w HTML w ten sposób:input id="fileInput" class="fileInput" type="file" name="file" accept="image/*" Jednak wydaje się, że działa to tylko na iOS, więc musimy dodać dodatkową kontrolę za pomocą wyrażenia regularnego po wybraniu pliku:
   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 Inną niespójnością, którą zauważyliśmy podczas procesu tworzenia, jest sposób powiadamiania o anulowaniu wyboru pliku na różnych urządzeniach. Telefony i tablety z iOS nie robią nic, nie powiadamiają w ogóle. W tym przypadku nie trzeba wykonywać żadnych specjalnych działań, ale telefony z Androidem wywołują funkcję add() nawet wtedy, gdy nie wybrano żadnego pliku. Oto jak to zrobić:
    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 dość płynnie na wszystkich platformach. Baw się dobrze!

Podsumowanie

Ze względu na ogromny rozmiar projektu Find Your Way To Oz i wiele zastosowanych technologii w tym artykule opisaliśmy tylko kilka zastosowanych przez nas metod.

Jeśli chcesz poznać cały kod źródłowy, możesz go pobrać tutaj.

Środki

Tutaj znajdziesz pełną listę twórców

Pliki referencyjne