Fallstudie – Der Weg nach Oz

Einleitung

Der magische Weg nach Oz ist ein neues Google Chrome-Experiment von Disney im Web. Damit kannst du eine interaktive Reise durch einen Zirkus in Kansas unternehmen, der dich ins Land von Oz führt, nachdem du von einem schweren Sturm überwältigt wurdest.

Unser Ziel war es, die Vielfalt des Kinos mit den technischen Möglichkeiten des Browsers zu kombinieren und so ein unterhaltsames, immersives Erlebnis zu schaffen, zu dem die Nutzer eine starke Bindung aufbauen können.

Die Aufgabe ist ein wenig zu groß, um in diesem Teil alles in ihrer Gesamtheit festzuhalten. Daher haben wir einige Kapitel der Technologiegeschichte herausgegeben, die wir für interessant halten. Dabei haben wir einige Tutorials mit zunehmendem Schwierigkeitsgrad extrahiert.

Viele Menschen haben hart daran gearbeitet, dieses Erlebnis zu ermöglichen. Es sind zu viele, um hier aufgelistet zu werden. Auf der Website findest du im Menübereich die Seite „Guthaben“ mit dem vollständigen Artikel.

Ein Blick hinter die Kulissen

Der Desktop-Computer „Der Weg nach Oz“ ist eine abwechslungsreiche, immersive Welt. Wir verwenden 3D- und mehrere Schichten traditioneller Filmeffekte, die zu einer fast realistischen Szenerie kombinieren. Die bekanntesten Technologien sind WebGL mit Three.js, benutzerdefinierte Shader und animierte DOM-Elemente, die CSS3-Funktionen verwenden. Darüber hinaus steht die getUserMedia API (WebRTC) für interaktive Funktionen zur Verfügung, mit denen Nutzer ihr Bild direkt über die Webcam und über WebAudio für 3D-Sound hinzufügen können.

Die Magie eines solchen technologischen Erlebnisses ist jedoch, wie es zusammenkommt. Dies ist auch eine der größten Herausforderungen: Wie kann man visuelle Effekte und interaktive Elemente in einer Szene kombinieren, um ein einheitliches Ganzes zu schaffen? Diese visuelle Komplexität war schwer zu bewältigen: Es war schwierig, zu erkennen, in welcher Entwicklungsphase wir uns zu einem bestimmten Zeitpunkt befanden.

Um das Problem zusammenhängender visueller Effekte und der Optimierung zu lösen, haben wir intensiv ein Steuerfeld verwendet, das alle relevanten Einstellungen erfasst, die wir damals überprüften. Die Szene kann live im Browser an alles angepasst werden, von Helligkeit über Schärfentiefe bis hin zu Gamma. Jeder kann die Werte der wichtigsten Parameter in der Erfahrung anpassen und mithelfen, herauszufinden, was am besten funktioniert.

Bevor wir euch unser Geheimnis vorstellen, möchten wir euch vor einem Absturz warnen – so, als ob ihr euch in einem Motor nähertten würdet. Vergewissern Sie sich, dass keine wichtigen Elemente vorhanden sind, und rufen Sie die Haupt-URL der Website auf und hängen Sie ?debug=on an die Adresse an. Warte, bis die Website geladen ist. Wenn du im Inneren bist (drücke?), drücke die Taste Ctrl-I. Daraufhin erscheint auf der rechten Seite ein Drop-down-Menü. Wenn Sie die Option „Kamerapfad beenden“ deaktivieren, können Sie sich mit den Tasten A, W, S, D und der Maus frei im Raum bewegen.

Kamerapfad.

Wir werden hier nicht alle Einstellungen durchgehen, empfehlen Ihnen jedoch, ein wenig auszuprobieren: Die Tasten zeigen verschiedene Einstellungen in verschiedenen Szenen an. In der letzten Sturmsequenz gibt es eine zusätzliche Taste: Ctrl-A. Damit kannst du die Animationswiedergabe ein- und ausschalten und umherfliegen. Wenn Sie in dieser Szene Esc drücken, um die Maussperre zu beenden, und noch einmal Ctrl-I drücken, können Sie auf spezielle Einstellungen für die Sturmszene zugreifen. Sehen Sie sich um und nehmen Sie sich ein paar schöne Postkartenansichten wie unten zu.

Sturmszene

Um dies zu erreichen und sicherzustellen, dass sie flexibel genug für unsere Anforderungen war, haben wir eine schöne Bibliothek namens dat.gui verwendet (siehe hier, um ein früheres Tutorial zur Verwendung zu finden). So konnten wir schnell ändern, welche Einstellungen den Besuchern der Website angezeigt wurden.

Ein bisschen wie mit mattem Gemälde

In vielen klassischen Disney-Filmen und -Animationen musste man verschiedene Ebenen kombinieren, um Szenen zu erstellen. Es gab Schichten mit Live-Action, Zellanimation, sogar physischen Sets und auf den oberen Schichten, die durch Malen auf Glas erzeugt wurden – eine Technik, die als mattes Malen bezeichnet wird.

In vielerlei Hinsicht ist die Struktur der von uns erstellten User Experience ähnlich, auch wenn einige der „Layers“ viel mehr als statische visuelle Elemente sind. Vielmehr beeinflussen sie aufgrund komplexerer Berechnungen das Aussehen von Dingen. Trotzdem geht es zumindest im Hinblick auf das große Ganze um Aufrufe, bei denen die Aufrufe übereinander liegen. Oben sehen Sie eine UI-Ebene mit einer 3D-Szene darunter, die selbst aus verschiedenen Szenenkomponenten besteht.

Die oberste Ebene der Benutzeroberfläche wurde mit DOM und CSS 3 erstellt. Das bedeutet, dass Interaktionen unabhängig vom 3D-Erlebnis mit der Kommunikation zwischen den beiden gemäß einer ausgewählten Ereignisliste auf viele Arten bearbeitet werden können. Für diese Kommunikation wird Backbone Router und das HTML5-Ereignis „onHashChange“ verwendet, das steuert, welcher Bereich ein- und ausgeblendet wird. (Projektquelle: /develop/coffee/router/Router.coffee).

Anleitung: Unterstützung von Sprite Sheets und Retina

Eine unterhaltsame Optimierungstechnik, die wir für die Benutzeroberfläche verwendet haben, war, die vielen Overlay-Bilder der Benutzeroberfläche in einer einzigen PNG-Datei zu kombinieren, um Serveranfragen zu reduzieren. In diesem Projekt bestand die Oberfläche aus mehr als 70 Bildern (3D-Texturen werden nicht mitgezählt), die alle im Voraus geladen wurden, um die Latenz der Website zu reduzieren. Das Live-Sprite Sheet können Sie sich hier ansehen:

Normale Anzeige: http://findyourwaytooz.com/img/home/interface_1x.png Retina-Display: http://findyourwaytooz.com/img/home/interface_2x.png

Hier sind einige Tipps, wie wir Sprite Sheets genutzt haben und wie wir sie für Retina-Geräte verwenden und die Benutzeroberfläche so scharf und übersichtlich wie möglich gestalten können.

Spritesheets erstellen

Zum Erstellen von SpriteSheets haben wir TexturePacker verwendet, um Ausgaben in jedem gewünschten Format zu generieren. In diesem Fall haben wir sie als EaselJS exportiert. Das ist wirklich sauber und hätte auch zur Erstellung animierter Sprites verwendet werden können.

Das generierte Sprite Sheet verwenden

Nachdem Sie Ihr Sprite Sheet erstellt haben, sollten Sie eine JSON-Datei wie diese sehen:

{
   "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]
   },
}

Wobei:

  • „Bild“ bezieht sich auf die URL des Sprite Sheets
  • Frames sind die Koordinaten der einzelnen UI-Elemente [x, y, width, width]
  • Animationen sind die Namen der einzelnen Assets,

Hinweis: Wir haben die hochauflösenden Bilder verwendet, um das Sprite-Blatt zu erstellen, und dann die normale Version, in der nur die Größe auf die Hälfte seiner Größe reduziert wird.

Zusammenfassung

Jetzt, wo alles bereit ist, benötigen wir nur noch ein JavaScript-Snippet, um es zu verwenden.

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

Und so würden Sie es verwenden:

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

Weitere Informationen zu variabler Pixeldichte finden Sie in diesem Artikel von Boris Smus.

Die 3D-Inhaltspipeline

Die Umgebung wird auf einer WebGL-Ebene eingerichtet. Wenn man sich eine 3D-Szene vor Augen führt, ist eine der schwierigsten Fragen, wie ihr Content erstellen könnt, der durch Modellierung, Animation und Effekte das maximale Ausdruckspotenzial bietet. Der Kern dieses Problems ist in vielerlei Hinsicht die Content-Pipeline: ein vereinbarter Prozess für die Erstellung von Inhalten für die 3D-Szene.

Wir wollten eine beeindruckende Welt schaffen, also brauchten wir einen soliden Prozess, der es 3D-Künstlern ermöglicht, sie zu erschaffen. Sie müssten in ihrer 3D-Modellierungs- und Animationssoftware so viel ausdrucksstarke Freiheit wie möglich erhalten und wir müssten sie über Code auf dem Bildschirm rendern.

Wir hatten schon eine Zeit lang an dieser Art von Problem gearbeitet, da wir in der Vergangenheit bei jeder Erstellung einer 3D-Website immer wieder Einschränkungen bei den verfügbaren Tools festgestellt hatten. Daher hatten wir ein Tool namens 3D Librarian entwickelt, ein Teil der internen Forschung. Und es war fast bereit, sich auf eine echte Stelle zu bewerben.

Dieses Tool hatte eine gewisse Geschichte: Ursprünglich war es für Flash gedacht und ermöglichte es Ihnen, eine große Maya-Szene als einzelne komprimierte Datei einzufügen, die für das Entpacken der Laufzeit optimiert war. Der Grund für die optimale Leistung lag darin, dass die Szene im Grunde in der gleichen Datenstruktur zusammengefasst war, die während des Renderings und der Animation verändert wird. Nach dem Laden muss die Datei nur wenig geparst werden. Das Entpacken in Flash war ziemlich schnell, da die Datei im AMF-Format war, das Flash nativ entpacken konnte. Die Verwendung desselben Formats in WebGL erfordert etwas mehr CPU-Leistung. Tatsächlich mussten wir eine JavaScript-Codeebene zum Datenentpacken neu erstellen, mit der diese Dateien im Wesentlichen dekomprimiert und die für WebGL benötigten Datenstrukturen neu erstellt werden mussten. Das Entpacken der gesamten 3D-Szene ist ein etwas CPU-intensiver Vorgang: Das Entpacken von Szene 1 in Finde deinen Weg nach Oz dauert auf einem Mittel- bis High-End-Computer etwa zwei Sekunden. Daher wird dies mithilfe der Web Worker-Technologie zum Zeitpunkt der Einrichtung der Szene (vor dem eigentlichen Start der Szene) erledigt, damit der Nutzer nicht aufhängt.

Mit diesem praktischen Tool können Sie den größten Teil der 3D-Szene importieren: Modelle, Texturen und Knochenanimationen. Sie erstellen eine einzelne Bibliotheksdatei, die dann in der 3D-Engine geladen werden kann. Sie laden alle Modelle, die Sie in Ihrer Szene benötigen, in diese Bibliothek ein und, voilà, bringen sie in Ihre Szene.

Ein Problem war jedoch, dass wir es jetzt mit WebGL zu tun hatten: dem neuen Kind am Block. Das war ein ziemlich schwieriges Kind: Das war der Standard für browserbasierte 3D-Erlebnisse. Also erstellten wir eine Ad-hoc-JavaScript-Ebene, die die komprimierten 3D-Szenendateien der 3D-Bibliothek nutzte und sie ordnungsgemäß in ein Format übersetzte, das WebGL verstehen kann.

Anleitung: Lass es Wind sein

Ein wiederkehrendes Thema in „Der Weg nach Oz“ war Wind. Der Thread der Storyline ist wie ein Crescendo des Windes aufgebaut.

Die erste Karnevalsszene ist relativ ruhig. Beim Durchgehen der verschiedenen Szenen erlebt der Nutzer einen immer stärkeren Wind, der in der letzten Szene, dem Sturm, endet.

Daher war es wichtig, einen immersiven Windeffekt zu erzielen.

Dazu haben wir die drei Karnevalsszenen mit Objekten befüllt, die weich waren und daher vom Wind beeinflusst werden sollten (z. B. Zelte), die Oberfläche der Fotobox und den Ballon selbst.

Weiches Tuch.

Desktop-Spiele sind heutzutage in der Regel um eine zentrale Physik-Engine aufgebaut. Wenn also ein weiches Objekt in der 3D-Welt simuliert werden muss, wird eine vollständige physikalische Simulation dafür ausgeführt, die ein glaubwürdiges weiches Verhalten erzeugt.

Mit WebGL / JavaScript haben wir (noch) nicht die Möglichkeit, eine vollständige Physiksimulation durchzuführen. In Oz mussten wir also einen Weg finden, den Effekt von Wind zu erzeugen, ohne ihn tatsächlich zu simulieren.

Wir haben die Informationen zur Windempfindlichkeit für jedes Objekt im 3D-Modell selbst eingebettet. Jeder Scheitelpunkt des 3D-Modells hatte ein „Windattribut“, das angibt, wie stark der Wind auf diesen Scheitelpunkt wirken sollte. Das ergibt die Windempfindlichkeit von 3D-Objekten. Dann mussten wir den Wind selbst erzeugen.

Dazu haben wir ein Bild mit Perlin-Rauschen generiert. Dieses Bild soll einen bestimmten Windbereich abdecken. Stellen Sie sich das Bild einer Wolke wie Rauschen vor, die über einen bestimmten rechteckigen Bereich der 3D-Szene gelegt wird. Jedes Pixel, Graustufenwert, dieses Bilds gibt an, wie stark der Wind in einem bestimmten Moment im 3D-Bereich "um ihn herum" ist.

Um den Windeffekt zu erzeugen, wird das Bild zeitlich, mit konstanter Geschwindigkeit, in eine bestimmte Richtung, also in die Richtung des Windes, bewegt. Und um sicherzustellen, dass der „windige Bereich“ sich nicht auf alles in der Szene auswirkt, wickeln wir das Windbild an den Rändern und beschränken sich auf den Wirkungsbereich.

Einfache 3D-Windanleitung

Lassen Sie uns nun den Effekt von Wind in einer einfachen 3D-Szene in Three.js erzeugen.

Wir erzeugen Wind auf einer einfachen „prozeduralen Grasfläche“.

Erstellen wir zuerst die Szene. Wir haben ein einfaches, strukturiertes, flaches Gelände. Und dann wird jedes Grasstück einfach als 3D-Kegel dargestellt, der sich auf dem Kopf befindet.

Grasreiches Gelände
Grünes Gelände

So erstellen Sie diese einfache Szene in Three.js mithilfe von CoffeeScript.

Zuerst richten wir Three.js ein und verbinden es mit einer Kamera, einem Maus-Controller und einer Art von Beleuchtung:

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

Die Funktionsaufrufe initGrass und initTerrain füllen die Szene mit Gras bzw. Gelände:

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

Hier erstellen wir ein Raster mit 15 mal 15 Bit Gras. Wir fügen jeder Grasposition etwas Randomisierung hinzu, damit sie nicht wie Soldaten aufgereiht werden, was seltsam aussehen könnte.

Dieses Gelände ist eine horizontale Ebene, die am Fuß der Grasflächen liegt (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 )

Bisher haben wir einfach eine Three.js-Szene erstellt und ein paar Grasflächen hinzugefügt, die aus verfahrenstechnisch erzeugten umgekehrten Kegeln und einem einfachen Gelände bestehen.

Noch nichts Außergewöhnliches.

Jetzt ist es an der Zeit, Wind hinzuzufügen. Zuerst betten wir die Informationen zur Windempfindlichkeit in das 3D-Modell von Gras ein.

Wir betten diese Informationen für jeden Scheitelpunkt des 3D-Gras-Modells als benutzerdefiniertes Attribut ein. Und wir wenden die Regel an, dass das untere Ende des Grasmodells (die Spitze des Kegels) keine Empfindlichkeit hat, da es am Boden befestigt ist. Der obere Teil des Grasmodells (Basis des Kegels) weist die höchste Windempfindlichkeit auf, da dieser Teil weiter vom Boden entfernt ist.

So wird die Funktion instanceGrass umcodiert, um die Windempfindlichkeit als benutzerdefiniertes Attribut für das 3D-Modell „Gras“ hinzuzufügen.

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

Anstelle des bisherigen MeshPhongMaterial verwenden wir nun ein benutzerdefiniertes Material, windMaterial. WindMaterial umschließt den WindMeshShader, den wir gleich sehen werden.

Daher durchläuft der Code in instanceGrass alle Eckpunkte des Grasmodells und fügt für jeden Scheitelpunkt ein benutzerdefiniertes Scheitelpunktattribut mit dem Namen windFactor hinzu. Dieser Windfaktor ist für das untere Ende des Grasmodells (an der das Gelände liegen soll) auf 0 gesetzt und für das obere Ende des Grasmodells auf 1 festgelegt.

Die andere Zutat, die wir brauchen, ist es, unserer Szene den richtigen Wind hinzuzufügen. Wie besprochen, nutzen wir dazu das Perlin-Rauschen. Wir generieren verfahrenstechnisch eine Perlin-Rauschtextur.

Aus Gründen der Klarheit weisen wir diese Textur dem Gelände selbst zu, anstelle der vorherigen grünen Textur. So bekommen Sie einen besseren Eindruck davon, was bei Wind passiert.

Diese Perlin-Rauschtextur überdeckt also die Erweiterung unseres Geländes räumlich und jedes Pixel der Textur gibt die Windintensität des Geländebereichs an, in den dieses Pixel fällt. Das Geländerechteck ist unser „Windbereich“.

Perlin-Rauschen wird prozedural über einen Shader namens NoiseShader erzeugt. Dieser Shader verwendet 3D-Simplex-Rauschalgorithmen von: https://github.com/ashima/webgl-noise . Die WebGL-Version wurde wortgetreu aus einem der Three.js-Beispiele von MrDoob unter http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html entnommen.

NoiseShader nimmt eine Zeit, eine Skala und einen Offset-Satz von Parametern als Uniformen an und gibt eine schöne 2D-Verteilung des Perlin-Rauschens aus.

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) }

...

Wir werden diesen Shader verwenden, um unser Perlin-Rauschen in eine Textur umzuwandeln. Dies geschieht in der Funktion 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 )

Mit dem Code oben wird noiseMap als Three.js-Renderingziel eingerichtet, mit NoiseShader ausgestattet und anschließend mit einer orthografischen Kamera gerendert, um perspektivische Verzerrungen zu vermeiden.

Wie bereits erwähnt, werden wir diese Textur jetzt auch als Haupt-Rendering-Textur für das Gelände verwenden. Das ist nicht notwendig, damit der Windeffekt selbst funktioniert. Aber es ist schön, diese Funktion zu haben, damit wir besser verstehen können, was bei der Winderzeugung passiert.

Hier sehen Sie die überarbeitete Funktion initTerrain mit „NoiseMap“ als Textur:

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 )

Jetzt, wo die Windtextur vorhanden ist, schauen wir uns WindMeshShader an, der für die Verformung der Grasmodelle entsprechend dem Wind verantwortlich ist.

Um diesen Shader zu erstellen, haben wir vom Standard-Three.js-MeshPhongMaterial-Shader aus begonnen und ihn geändert. Dies ist eine gute, schnelle und schmutzige Methode, um mit einem funktionierenden Shader zu beginnen, ohne bei null anfangen zu müssen.

Wir werden hier nicht den gesamten Shader-Code kopieren (Sie können ihn sich in der Quellcodedatei ansehen), da der Großteil davon ein Replikat des MeshPhongMaterial-Shaders ist. Werfen wir jedoch einen Blick auf die modifizierten, windbezogenen Teile im 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;

Dieser Shader berechnet also zuerst die windUV-Textur-Lookup-Koordinate basierend auf der horizontalen 2D-xz-Position des Scheitelpunkts. Diese UV-Koordinate wird verwendet, um die Windkraft vWindForce aus der Perlin-Windtextur zu ermitteln.

Dieser vWindForce-Wert wird mit dem oben beschriebenen benutzerdefinierten windFactor-Attribut windFactor zusammengesetzt, um zu berechnen, wie stark der Scheitelpunkt deformatiert werden muss. Es gibt auch einen globalen windScale-Parameter, mit dem die Gesamtstärke des Windes gesteuert wird, und einen windDirection-Vektor, der angibt, in welche Richtung die Windverformung stattfinden muss.

Das erzeugt windbasierte Verformung der Grasflächen. Wir sind jedoch noch nicht fertig. Diese Verformung ist statisch und vermittelt nicht die Wirkung einer windigen Region.

Wie bereits erwähnt, muss die Rauschtextur im Laufe der Zeit über den Windbereich gleitet werden, damit unser Glas winken kann.

Dazu wird die vOffset-Einheit, die an den NoiseShader übergeben wird, mit der Zeit verschoben. Dies ist ein vec2-Parameter, mit dem Sie den Rauschversatz entlang einer bestimmten Windrichtung (unsere Windrichtung) angeben können.

Das machen wir in der Funktion render, die bei jedem Frame aufgerufen wird:

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
...

Geschafft! Wir haben gerade eine Szene mit „prozeduralem Gras“ erstellt, das vom Wind beeinflusst wird.

Staub in den Mix

Jetzt wollen wir unsere Szene ein wenig aufpeppen. Fügen wir etwas Flugstaub hinzu, um die Szene interessanter zu machen.

Staub wird hinzugefügt
Staub hinzufügen

Staub wird eigentlich durch Wind beeinflusst, daher ist es sinnvoll, wenn Staub in unserer Windszene herumfliegt.

Staub wird in der Funktion initDust als Partikelsystem eingerichtet.

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

Hier entstehen 130 Staubpartikel. Jeder von ihnen wird mit einem speziellen WindParticleShader ausgestattet.

Jetzt bewegen wir uns mit CoffeeScript in jedem Frame ein wenig zwischen den Partikeln, unabhängig vom Wind. Hier ist der Code.

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 )

Außerdem werden wir die Partikelpositionen entsprechend dem Wind verschieben. Dies geschieht in WindParticleShader. Insbesondere im Vertex-Shader.

Der Code für diesen Shader ist eine modifizierte Version von Three.js ParticleMaterial und sieht der Kern des Shaders so aus:

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;

Dieser Vertex-Shader unterscheidet sich nicht sehr von dem, was wir für die windbasierte Deformatierung von Gras hatten. Als Eingabe wird die Perlin-Rauschtextur verwendet und je nach Position der Staubwelt in der Rauschtextur ein vWindForce-Wert ermittelt. Anschließend wird anhand dieses Werts die Position des Staubpartikels geändert.

Rider im Sturm

Die abenteuerlichste unserer WebGL-Szenen war wahrscheinlich die letzte Szene, die Sie sehen können, wenn Sie sich durch das Ballon in das Auge des Tornados klicken, um das Ende Ihrer Reise durch die Website zu erreichen, sowie ein exklusives Video der bevorstehenden Veröffentlichung.

Ballonfahrtszene

Als wir diese Szene entwickelten, wussten wir, dass wir eine zentrale Funktion benötigen, die für das Erlebnis eine große Wirkung hat. Der sich drehende Tornado diente als Kernstück und Schichten anderer Inhalte fügten diese Funktion hinzu, um einen dramatischen Effekt zu erzielen. Dazu haben wir etwas erstellt, das einem Filmstudio entspricht, das um diesen seltsamen Shader herum angeordnet ist.

Wir haben einen gemischten Ansatz verwendet, um das realistische Modell zu erstellen. Bei einigen handelt es sich um visuelle Tricks wie Lichtformen, die einen Blendeneffekt erzeugen, oder Regentropfen, die sich als Schichten über der Szene bewegen, die Sie betrachten. In anderen Fällen hatten wir flache Oberflächen, die so schienen, sich zu bewegen, wie die Schichten der tief umfliegenden Wolken, die sich gemäß dem Code eines Partikelsystems bewegten. Die Trümmerteile, die den Tornado umkreisen, waren zwar Schichten einer 3D-Szene, die so sortiert wurden, dass sie sich vor und hinter dem Tornado bewegten.

Der Hauptgrund für den Aufbau der Szene war, dass wir genügend GPU hatten, um den Tornado-Shader im Gleichgewicht mit den anderen angewendeten Effekten zu verarbeiten. Anfangs hatten wir große Probleme beim GPU-Balancing, aber später wurde diese Szene optimiert und wurde heller als die Hauptszenen.

Anleitung: Der Storm-Shader

Für die endgültige Sturmsequenz wurden viele verschiedene Techniken kombiniert, aber das Herzstück dieser Arbeit war ein benutzerdefinierter GLSL-Shader, der wie ein Tornado aussieht. Wir hatten viele verschiedene Techniken ausprobiert, von Vertex-Shadern, um interessante geometrische Wirbel zu erstellen, partikelbasierte Animationen und sogar 3D-Animationen von verdrehten geometrischen Formen zu erstellen. Keiner der Effekte erweckte das Gefühl eines Wirbelsturms wieder oder erforderte zu viel Verarbeitung.

Auf ein ganz anderes Projekt kam schließlich die Antwort. Ein paralleles Projekt des Max Planck Institute (brainflight.org) mit wissenschaftlichen Spielen zur Kartierung des Gehirns der Maus führte zu interessanten visuellen Effekten. Mit einem benutzerdefinierten Volumetric-Shader konnten wir Filme vom Inneren eines Mäuseneurons erstellen.

Im Inneren eines Mausneurons mit einem benutzerdefinierten Volumetric-Shader
Im Inneren eines Mausneurons mit einem benutzerdefinierten Volumetric-Shader

Wir haben herausgefunden, dass das Innere einer Gehirnzelle ein bisschen wie der Trichter eines Tornado aussah. Und da wir eine volumetrische Technik verwendeten, wussten wir, dass wir diesen Shader aus allen Richtungen im Weltraum betrachten können. Wir könnten das Rendering des Shaders so einstellen, dass es mit der Sturmszene kombiniert wird, insbesondere, wenn es sich unter Wolkenschichten und über einem dramatischen Hintergrund befindet.

Bei der Shader-Technik handelt es sich um einen Trick, der im Grunde einen einzelnen GLSL-Shader verwendet, um ein ganzes Objekt mit einem vereinfachten Rendering-Algorithmus namens „Ray Marching Rendering“ mit einem Distanzfeld zu rendern. Bei dieser Technik wird ein Pixel-Shader erstellt, der die nächste Entfernung zu einer Oberfläche für jeden Punkt auf dem Bildschirm schätzt.

Eine gute Referenz zum Algorithmus finden Sie in der Übersicht von iq: Rendering Worlds With Two Triangles – Iñigo Quilez. In der Galerie der Shader auf glsl.heroku.com finden Sie viele Beispiele für diese Technik, die Sie ausprobieren können.

Das Herz des Shaders beginnt mit der Hauptfunktion, er richtet die Kameratransformationen ein und geht in eine Schleife über, die wiederholt die Entfernung zu einer Oberfläche bewertet. Der Aufruf RaytraceFoggy( Direction_vector, max_iterations, color, color_multiplier ) ist der Ort, an dem die Berechnung des zentralen Strahlenmarschs durchgeführt wird.

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
}

Die Idee ist, dass wir regelmäßig Farbbeiträge zum endgültigen Farbwert des Pixels und zur Deckkraft entlang des Strahls hinzufügen, wenn wir uns die Form des Tornados nähern. Dadurch entsteht eine geschichtete Weichheit der Textur des Tornado.

Der nächste Kernaspekt des Tornado ist die eigentliche Form. Sie wird durch Eingabe mehrerer Funktionen erzeugt. Zunächst ist er ein Kegel, der aus Rauschen besteht, um eine organische Schraubkante zu erzeugen. Anschließend wird er entlang seiner Hauptachse gedreht und im Zeitablauf gedreht.

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

Die Erstellung dieser Art von Shader ist kompliziert. Abgesehen von den Problemen, die mit der Abstraktion der von Ihnen erstellten Abläufe verbunden sind, gibt es ernsthafte Probleme bei der Optimierung und plattformübergreifenden Kompatibilität, die Sie verfolgen und lösen müssen, bevor Sie die Arbeit in der Produktion verwenden können.

Der erste Teil des Problems: Optimierung dieses Shaders für unsere Szene. Um dies zu bewältigen, brauchten wir einen „sicheren“ Ansatz für den Fall, dass der Shader zu schwer wäre. Dazu haben wir den Tornado-Shader mit einer anderen Abtastauflösung als der Rest der Szene zusammengesetzt. Sie stammt aus der Datei stormTest.coffee (ja, das war ein Test!).

Wir beginnen mit einem renderTarget, das der Szenenbreite und -höhe entspricht, sodass wir eine unabhängige Auflösung des Tornado-Shaders zur Szene haben. Und dann entscheiden wir das Downsampling der Auflösung des Storming-Shader dynamisch abhängig von der erhaltenen Framerate.

...
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

Schließlich rendern wir den Tornado mit einem vereinfachten sal2x-Algorithmus (um den blockartigen Look zu vermeiden) @line 1107 in stormTest.coffee. Das heißt, im schlimmsten Fall ist der Tornado immer verschwommener, aber wenigstens funktioniert es, ohne dem Nutzer die Kontrolle zu verlieren.

Der nächste Optimierungsschritt erfordert eine genauere Analyse des Algorithmus. Der treibende Berechnungsfaktor im Shader ist die Iteration, die für jedes Pixel durchgeführt wird, um den Abstand der Oberflächenfunktion zu nähern: die Anzahl der Iterationen der Raymarching-Schleife. Mit einer größeren Schrittgröße konnten wir eine Schätzung der Tornadooberfläche mit weniger Iterationen erhalten, während wir uns außerhalb der bewölkten Oberfläche aufhielten. Im Inneren würden wir die Schrittgröße verringern, um die Genauigkeit zu erhöhen und Werte zu mischen, um den Nebeleffekt zu erzeugen. Das Erstellen eines Begrenzungszylinders für eine Tiefenschätzung für den geworfenen Strahl führte zu einer guten Beschleunigung.

Der nächste Teil des Problems bestand darin, sicherzustellen, dass dieser Shader auf verschiedenen Grafikkarten ausgeführt werden würde. Wir haben jedes Mal einige Tests durchgeführt und uns ein Bild von den Kompatibilitätsproblemen gemacht, auf die wir stoßen könnten. Das liegt daran, dass wir nicht immer aussagekräftige Informationen zur Fehlerbehebung für die Fehler erhalten konnten. In einem typischen Szenario handelt es sich lediglich um einen GPU-Fehler, bei dem es nur wenig zu tun gibt, oder sogar um einen Systemabsturz.

Für Probleme mit der Cross-Video-Board-Kompatibilität gab es ähnliche Lösungen: Stellen Sie sicher, dass statische Konstanten mit dem exakten Datentyp eingegeben werden, wie definiert, IE: 0.0 für Gleitkommazahl und 0 für Int. Seien Sie vorsichtig, wenn Sie längere Funktionen schreiben. Es ist besser, die Dinge in mehrere einfachere Funktionen und Zwischenvariablen aufzuteilen, da die Compiler bestimmte Fälle nicht richtig verarbeiten konnten. Achten Sie darauf, dass die Texturen alle eine Potenz von 2 haben, nicht zu groß sind und auf jeden Fall „Vorsicht“ anwenden, wenn Sie Texturdaten in einer Schleife abrufen.

Die größten Kompatibilitätsprobleme waren der Lichteffekt des Sturms. Wir haben eine vorgefertigte Textur verwendet, die um den Tornado gewickelt wurde, um seine Wisps zu färben. Das war ein umwerfender Effekt und es war leicht, den Tornado in die Farben der Szene zu integrieren, aber es dauerte lange, bis der Tornado auf anderen Plattformen funktionierte.

Tornado

Mobile Website

Die mobile Nutzung ist keine einfache Übersetzung der Desktopversion, da die Anforderungen an Technologie und Verarbeitung einfach zu groß waren. Wir mussten etwas Neues entwickeln, das speziell auf die mobilen Nutzer ausgerichtet war.

Wir dachten, es wäre toll, Carnival Photo-Booth vom Desktop als mobile Webanwendung zur Verfügung zu stellen, die die mobile Kamera des Nutzers verwenden würde. Etwas, das wir bisher noch nicht gesehen hatten.

Um dem Ganzen Geschmack hinzuzufügen, haben wir 3D-Transformationen in CSS3 codiert. Durch die Verknüpfung mit Gyroskop und Beschleunigungsmesser konnten wir das Projekt viel detaillierter gestalten. Die Website reagiert darauf, wie du dein Smartphone hältst, bewegst und schaust.

In diesem Artikel möchten wir Ihnen einige Tipps dazu geben, wie Sie die Entwicklung für Mobilgeräte reibungslos gestalten können. Bitte schön! Probieren Sie aus, was Sie daraus lernen können.

Tipps und Tricks für Mobilgeräte

Preloader sollte nicht vermieden werden. Uns ist bewusst, dass Letzteres manchmal passiert. Dies liegt vor allem daran, dass Sie die Liste der Elemente, die Sie vorab laden, beibehalten müssen, wenn Ihr Projekt wächst. Noch schlimmer ist, dass es nicht ganz klar ist, wie der Ladefortschritt berechnet werden soll, wenn Sie verschiedene Ressourcen gleichzeitig abrufen. Hier kommt unsere benutzerdefinierte und sehr allgemeine abstrakte Klasse 'Task' ins Spiel. Die Hauptidee besteht darin, eine endlos verschachtelte Struktur zu ermöglichen, in der eine Aufgabe eigene Unteraufgaben haben kann, die ihre usw. haben können. Außerdem berechnet jede Aufgabe ihren Fortschritt in Bezug auf den Fortschritt der Unteraufgaben (aber nicht auf den Fortschritt der übergeordneten Aufgabe). Nachdem alle MainPreloadTask, AssetPreloadTask und TemplatePreFetchTask von Task abgeleitet wurden, haben wir eine Struktur erstellt, die wie folgt aussieht:

Preloader

Dank eines solchen Ansatzes und der Task-Klasse können wir leicht den globalen Fortschritt (MainPreloadTask) oder nur den Fortschritt von Assets (AssetPreloadTask) oder den Fortschritt des Ladens von Vorlagen (TemplatePreFetchTask) ermitteln. Den Fortschritt einer bestimmten Datei messen. Sehen Sie sich die Task-Klasse unter /m/javascripts/raw/util/Task.js und die tatsächlichen Aufgabenimplementierungen unter /m/javascripts/preloading/task an, um zu sehen, wie das funktioniert. Dies ist als Beispiel ein Auszug aus der Einrichtung der Klasse /m/javascripts/preloading/task/MainPreloadTask.js, unserem ultimativen Wrapper für das Vorabladen:

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

      }
    }
  })
]);

In der Klasse /m/javascripts/preloading/task/subtask/AssetPreloadTask.js sollte nicht nur angegeben werden, wie es mit der MainPreloadTask kommuniziert (über die gemeinsame Task-Implementierung), sondern auch, wie wir plattformabhängige Assets laden. Grundsätzlich gibt es vier Arten von Bildern. Standard für Mobilgeräte (.ext, wobei ext für Dateiendung steht, üblicherweise .png oder .jpg), Retina für Mobilgeräte (-2x.ext), Tablet-Standard (-tab.ext) und Tablet-Retina (-tab-2x.ext). Anstatt die Erkennung in der MainPreloadTask durchzuführen und vier Asset-Arrays hartzucodieren, sagen wir einfach den Namen und die Erweiterung des vorab zu ladenden Assets und ob das Asset plattformabhängig ist (responsive = true / false). Anschließend generiert AssetPreloadTask den Dateinamen:

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

Weiter unten in der Klassenkette sieht der eigentliche Code für das Vorabladen des Assets so aus (/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);
}

Anleitung: HTML5 Photo Booth (iOS6/Android)

Bei der Entwicklung von OZ Mobile stellten wir fest, dass wir viel Zeit damit verbringen, mit der Fotobox zu spielen, anstatt zu arbeiten :D Das war einfach, weil es Spaß macht. Deshalb haben wir eine Demo erstellt, mit der Sie experimentieren können.

Mobile Fotobox
Mobile Fotobox

Hier können Sie sich eine Live-Demo ansehen (auf Ihrem iPhone oder Android-Smartphone ausführen):

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

Für die Einrichtung benötigen Sie eine kostenlose Google App Engine-Anwendungsinstanz, auf der Sie das Back-End ausführen können. Der Frontend-Code ist nicht komplex, aber es gibt ein paar Fehler. Sehen wir sie uns einmal genauer an:

  1. Zulässiger Dateityp für Bilddateien Wir möchten, dass Nutzer nur Bilder hochladen können, da es sich dabei um einen Fotoautomaten und nicht um einen Videokabine handelt. Theoretisch können Sie den Filter einfach wie folgt in HTML angeben: input id="fileInput" class="fileInput" type="file" name="file" accept="image/*" Dies scheint jedoch nur unter iOS zu funktionieren. Wir müssen also eine zusätzliche Prüfung auf RegExp hinzufügen, nachdem eine Datei ausgewählt wurde:
   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. Upload oder Dateiauswahl abbrechen Während des Entwicklungsprozesses haben wir auch festgestellt, dass unterschiedliche Geräte eine abgebrochene Dateiauswahl benachrichtigen. iOS-Smartphones und -Tablets tun nichts, sie senden überhaupt keine Benachrichtigungen. In diesem Fall sind also keine besonderen Maßnahmen erforderlich. Android-Telefone lösen jedoch die Funktion add() aus, auch wenn keine Datei ausgewählt ist. Gehen Sie dazu folgendermaßen vor:
    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();            
    }
    }

Der Rest funktioniert plattformübergreifend ziemlich reibungslos. Spaß haben

Fazit

Angesichts der enormen Größe von „Find Your Way To Oz“ und der vielfältigen Kombination verschiedener Technologien konnten wir in diesem Artikel nur einige der Ansätze behandeln, die wir angewandt haben.

Wenn Sie die ganze Enchilada kennenlernen möchten, können Sie sich unter diesem Link den vollständigen Quellcode von „Find Your Way To Oz“ ansehen.

Guthaben

Hier findest du die vollständige Liste der Mitwirkenden.

Verweise