Einführung
„Find Your Way to Oz“ ist ein neues Google Chrome-Experiment, das von Disney im Web angeboten wird. Sie können eine interaktive Reise durch einen Zirkus in Kansas unternehmen, die Sie nach einem heftigen Sturm ins Land Oz führt.
Unser Ziel war es, die Vielfalt des Kinos mit den technischen Möglichkeiten des Browsers zu kombinieren, um ein unterhaltsames, immersives Erlebnis zu schaffen, mit dem Nutzer eine starke Verbindung aufbauen können.
Die Aufgabe ist ein bisschen zu groß, um sie in diesem Artikel vollständig zu erfassen. Deshalb haben wir uns einige Kapitel der Technologiegeschichte herausgesucht, die wir für interessant halten. Dabei haben wir einige fokussierte Tutorials mit steigendem Schwierigkeitsgrad zusammengestellt.
Viele Menschen haben hart daran gearbeitet, dieses Erlebnis möglich zu machen – zu viele, um sie hier aufzuzählen. Auf der Website findest du im Menü die Seite mit den Mitwirkenden. Dort findest du die vollständige Geschichte.
Funktionsweise
„Find Your Way to Oz“ auf dem Computer ist eine reichhaltige, immersive Welt. Wir verwenden 3D und mehrere Ebenen von Effekten, die vom traditionellen Filmemachen inspiriert sind und zusammen eine nahezu realistische Szene ergeben. Die bekanntesten Technologien sind WebGL mit Three.js, benutzerdefinierte Shader und DOM-animierte Elemente mit CSS3-Funktionen. Außerdem gibt es die getUserMedia API (WebRTC) für interaktive Funktionen, mit denen Nutzer ihr Bild direkt über die Webcam hinzufügen können, und WebAudio für 3D-Sound.
Aber das Besondere an einer solchen Technologie ist, wie sie zusammenkommt. Das ist auch eine der Hauptherausforderungen: Wie lassen sich visuelle Effekte und interaktive Elemente in einer Szene kombinieren, um ein einheitliches Ganzes zu schaffen? Diese visuelle Komplexität war schwierig zu verwalten, da es schwer zu erkennen war, in welcher Entwicklungsphase wir uns gerade befanden.
Um das Problem der miteinander verbundenen visuellen Effekte und Optimierungen anzugehen, haben wir häufig ein Steuerfeld verwendet, in dem alle relevanten Einstellungen erfasst wurden, die wir zu diesem Zeitpunkt überprüft haben. Die Szene konnte live im Browser angepasst werden, z. B. Helligkeit, Tiefenschärfe oder Gamma. Jeder konnte die Werte der wichtigsten Parameter in der Umgebung anpassen und herausfinden, was am besten funktioniert.
Bevor wir unser Geheimnis lüften, möchten wir Sie warnen, dass es zu einem Absturz kommen kann, so wie wenn Sie im Motor eines Autos herumfummeln würden. Achten Sie darauf, dass nichts Wichtiges offen ist, und rufen Sie die Haupt-URL der Website auf. Fügen Sie der Adresse ?debug=on hinzu. Warten Sie, bis die Website geladen ist. Wenn Sie sich auf der Website befinden, drücken Sie die Taste Ctrl-I
. Daraufhin wird rechts ein Drop-down-Menü angezeigt. Wenn Sie das Häkchen bei „Kamerapfad beenden“ entfernen, können Sie sich mit den Tasten A, W, S, D und der Maus frei im Raum bewegen.

Wir werden hier nicht alle Einstellungen durchgehen, aber wir empfehlen Ihnen, zu experimentieren: Die Tasten zeigen unterschiedliche Einstellungen in verschiedenen Szenen. In der letzten Sturmsequenz gibt es eine zusätzliche Taste: Ctrl-A
. Mit dieser können Sie die Animation wiedergeben und sich umsehen. Wenn Sie in dieser Szene die Taste Esc
drücken, um die Maussperre zu beenden, und dann noch einmal die Taste Ctrl-I
, können Sie auf Einstellungen zugreifen, die speziell für die Sturmszene gelten. Sehen Sie sich um und machen Sie schöne Postkartenansichten wie die unten.

Um dies zu ermöglichen und dafür zu sorgen, dass es flexibel genug für unsere Anforderungen ist, haben wir die praktische Bibliothek „dat.gui“ verwendet. Hier finden Sie ein früheres Tutorial zur Verwendung. So konnten wir schnell ändern, welche Einstellungen den Besuchern der Website angezeigt wurden.
Ein bisschen wie Matte Painting
In vielen klassischen Disney-Filmen und -Animationen wurden Szenen durch das Kombinieren verschiedener Ebenen erstellt. Es gab Ebenen mit Live-Action, Zellanimation, sogar physische Kulissen und darüber hinaus Ebenen, die durch Malen auf Glas erstellt wurden: ein Verfahren, das als Matte Painting bezeichnet wird.
In vielerlei Hinsicht ist die Struktur der von uns erstellten Website ähnlich, auch wenn einige der „Ebenen“ viel mehr als statische Bilder sind. Tatsächlich wirken sie sich auf die Darstellung aus, die auf komplexeren Berechnungen basiert. Trotzdem haben wir es zumindest auf der großen Ebene mit Ansichten zu tun, die übereinander gelegt werden. Oben sehen Sie eine UI-Ebene mit einer 3D-Szene darunter, die aus verschiedenen Szenenkomponenten besteht.
Die oberste Benutzeroberfläche wurde mit DOM und CSS 3 erstellt. Das bedeutete, dass die Interaktionen unabhängig von der 3D-Umgebung auf viele Arten bearbeitet werden konnten, wobei die Kommunikation zwischen den beiden gemäß einer ausgewählten Liste von Ereignissen erfolgte. Für diese Kommunikation wird der Backbone-Router und das HTML5-Ereignis „onHashChange“ verwendet, mit dem gesteuert wird, welcher Bereich ein- oder ausgeblendet werden soll. (Projektquelle: /develop/coffee/router/Router.coffee)
Anleitung: Sprite Sheets und Retina-Unterstützung
Eine interessante Optimierungstechnik, die wir für die Benutzeroberfläche verwendet haben, war die Kombination der vielen Overlay-Bilder der Benutzeroberfläche in einem einzigen PNG, um Serveranfragen zu reduzieren. In diesem Projekt bestand die Benutzeroberfläche aus über 70 Bildern (ohne 3D-Texturen), die alle vorab geladen wurden, um die Latenz der Website zu reduzieren. Hier siehst du das Live-Spritesheet:
Normales Display: http://findyourwaytooz.com/img/home/interface_1x.png Retina-Display: http://findyourwaytooz.com/img/home/interface_2x.png
Hier sind einige Tipps dazu, wie wir Sprite-Sheets eingesetzt haben und wie sie für Retina-Geräte verwendet werden können, um die Benutzeroberfläche so scharf und übersichtlich wie möglich zu gestalten.
Spritesheets erstellen
Zum Erstellen von Sprite Sheets haben wir TexturePacker verwendet, das die Ausgabe in jedem gewünschten Format ermöglicht. In diesem Fall haben wir als EaselJS exportiert, was sehr übersichtlich ist und auch zum Erstellen animierter Sprites verwendet werden könnte.
Generiertes Sprite-Sheet verwenden
Nachdem Sie Ihr Sprite-Sheet erstellt haben, sollte eine JSON-Datei wie diese angezeigt werden:
{
"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:
- „image“ bezieht sich auf die URL des Sprite Sheets.
- Frames sind die Koordinaten der einzelnen UI-Elemente [x, y, width, height]
- „animations“ sind die Namen der einzelnen Assets.
Beachten Sie, dass wir die Bilder mit hoher Dichte verwendet haben, um das Sprite Sheet zu erstellen, und dann die normale Version erstellt haben, indem wir die Größe einfach auf die Hälfte reduziert haben.
Zusammenfassung
Jetzt brauchen 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);
};
So verwenden Sie es:
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 variablen Pixeldichten finden Sie in diesem Artikel von Boris Smus.
Die 3D-Inhaltspipeline
Die Umgebung wird auf einer WebGL-Ebene eingerichtet. Wenn Sie an eine 3D-Szene denken, ist eine der schwierigsten Fragen, wie Sie sicherstellen können, dass Sie Inhalte erstellen, die das maximale Ausdruckspotenzial in Bezug auf Modellierung, Animation und Effekte ermöglichen. In vielerlei Hinsicht steht die Content-Pipeline im Mittelpunkt dieses Problems: ein vereinbarter Prozess zum Erstellen von Inhalten für die 3D-Szene.
Wir wollten eine atemberaubende Welt schaffen. Dazu brauchten wir einen soliden Prozess, mit dem 3D-Künstler sie erstellen konnten. Sie müssten in ihrer 3D-Modellierungs- und Animationssoftware so viel Ausdrucksfreiheit wie möglich haben und wir müssten sie über Code auf dem Bildschirm rendern.
Wir haben schon seit einiger Zeit an dieser Art von Problem gearbeitet, da wir bei der Erstellung von 3D-Websites immer wieder auf Einschränkungen der verfügbaren Tools gestoßen sind. Wir haben also dieses Tool namens 3D Librarian entwickelt, ein Stück interner Forschung. Und es war fast bereit, in einem echten Job eingesetzt zu werden.
Dieses Tool hat eine gewisse Geschichte: Ursprünglich war es für Flash gedacht und ermöglichte es, eine große Maya-Szene als einzelne komprimierte Datei einzufügen, die für die Laufzeitentpackung optimiert wurde. Das war optimal, weil die Szene effektiv in derselben Datenstruktur verpackt wurde, die beim Rendern und Animieren manipuliert wird. Beim Laden der Datei muss nur sehr wenig geparst werden. Das Entpacken in Flash ging ziemlich schnell, da die Datei im AMF-Format war, das Flash nativ entpacken konnte. Die Verwendung desselben Formats in WebGL erfordert etwas mehr Arbeit für die CPU. Tatsächlich mussten wir eine Javascript-Codeebene zum Entpacken von Daten neu erstellen, die diese Dateien im Wesentlichen dekomprimieren und die für die Funktion von WebGL erforderlichen Datenstrukturen neu erstellen würde. Das Entpacken der gesamten 3D-Szene ist ein mäßig CPU-intensiver Vorgang: Das Entpacken von Szene 1 in Find Your Way To Oz dauert auf einem Mittel- bis High-End-Computer etwa zwei Sekunden. Daher wird dies mithilfe der Web Workers-Technologie bei der Szeneneinrichtung (vor dem Starten der Szene) durchgeführt, damit die Nutzeroberfläche nicht hängt.
Mit diesem praktischen Tool können Sie den Großteil der 3D-Szene importieren: Modelle, Texturen, Knochenanimationen. Sie erstellen eine einzelne Bibliotheksdatei, die dann von der 3D-Engine geladen werden kann. Sie fügen alle für Ihre Szene erforderlichen Modelle in diese Bibliothek ein und voilà, sie werden in Ihrer Szene gespawnt.
Ein Problem war jedoch, dass wir es jetzt mit WebGL zu tun hatten: dem neuen Kid auf dem Block. Das war eine ziemlich harte Nuss: Wir haben damit den Standard für browserbasierte 3D-Anwendungen gesetzt. Deshalb haben wir eine Ad-hoc-JavaScript-Ebene erstellt, die die komprimierten 3D-Szenendateien von 3D Librarian in ein Format umwandelt, das von WebGL verstanden wird.
Anleitung: Es werde Wind
Ein wiederkehrendes Thema in „Find Your Way To Oz“ war der Wind. Ein Handlungsstrang ist als Crescendo des Windes strukturiert.
Die erste Szene des Karnevals ist relativ ruhig. In den verschiedenen Szenen erleben die Nutzer einen zunehmend stärkeren Wind, der in der letzten Szene, dem Sturm, gipfelt.
Daher war es wichtig, einen möglichst realistischen Windeffekt zu schaffen.
Dazu haben wir die drei Karnevalsszenen mit Objekten gefüllt, die weich sind und sich daher vom Wind beeinflussen lassen, z. B. Zelte, Fahnen, die Oberfläche des Fotoautomaten und den Ballon selbst.

Computerspiele basieren heutzutage in der Regel auf einer zentralen Physik-Engine. Wenn also ein weiches Objekt in der 3D-Welt simuliert werden muss, wird eine vollständige Physiksimulation dafür ausgeführt, um ein glaubwürdiges weiches Verhalten zu erzeugen.
In WebGL / JavaScript können wir (noch) keine vollständige Physiksimulation ausführen. In Oz mussten wir also einen Weg finden, den Windeffekt zu erzeugen, ohne ihn zu simulieren.
Wir haben die Informationen zur „Windempfindlichkeit“ für jedes Objekt in das 3D‑Modell selbst eingebettet. Jeder Eckpunkt des 3D‑Modells hatte ein „Windattribut“, das angab, wie stark dieser Eckpunkt vom Wind beeinflusst werden sollte. Die Windempfindlichkeit von 3D-Objekten. Dann mussten wir den Wind selbst erstellen.
Dazu haben wir ein Bild mit Perlin-Rauschen generiert. Dieses Bild soll einen bestimmten „Windbereich“ abdecken. Stellen Sie sich also ein Bild mit wolkenartigem Rauschen vor, das über einen bestimmten rechteckigen Bereich der 3D-Szene gelegt wird. Jeder Pixel, also Graustufenwert, dieses Bildes gibt an, wie stark der Wind in einem bestimmten Moment im „umliegenden“ 3D-Bereich ist.
Um den Windeffekt zu erzeugen, wird das Bild zeitlich mit konstanter Geschwindigkeit in eine bestimmte Richtung bewegt, nämlich in die Windrichtung. Damit sich der „windige Bereich“ nicht auf alles in der Szene auswirkt, wird das Windbild an den Rändern so zugeschnitten, dass es nur den betroffenen Bereich abdeckt.
Ein einfaches 3D-Wind-Tutorial
Erstellen wir nun den Windeffekt in einer einfachen 3D-Szene in Three.js.
Wir erstellen Wind in einem einfachen „prozeduralen Grasfeld“.
Erstellen wir zuerst die Szene. Wir erstellen ein einfaches, flaches Gelände mit Textur. Jedes Grashalm wird dann einfach mit einem umgedrehten 3D-Kegel dargestellt.

So erstellen Sie diese einfache Szene in Three.js mit CoffeeScript.
Zuerst richten wir Three.js ein und verbinden es mit einer Kamera, einem Maus-Controller und einer Art Licht:
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 Funktionaufrufe initGrass und initTerrain füllen die Szene mit Gras und 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 × 15 Grasstücken. Wir sorgen für eine gewisse Zufälligkeit bei der Platzierung der Grashalme, damit sie sich nicht wie Soldaten aneinanderreihen, was seltsam aussehen würde.
Dieses Gelände ist nur eine horizontale Ebene, die sich am Fuß der Grasstücke befindet (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 also einfach eine Three.js-Szene erstellt und ein paar Grasstücke aus prozedural generierten umgekehrten Kegeln und ein einfaches Gelände hinzugefügt.
Bisher nichts Besonderes.
Jetzt ist es an der Zeit, Wind hinzuzufügen. Zuerst möchten wir die Informationen zur Windempfindlichkeit in das 3D‑Modell für das Gras einbetten.
Wir werden diese Informationen als benutzerdefiniertes Attribut für jeden Vertex des 3D‑Grasmodells einbetten. Wir verwenden die Regel, dass das untere Ende des Grasmodells (Spitze des Kegels) eine Nullempfindlichkeit hat, da es am Boden befestigt ist. Der obere Teil des Grasmodells (Basis des Kegels) ist am empfindlichsten für Wind, da er sich weiter vom Boden entfernt befindet.
Hier sehen Sie, wie die Funktion instanceGrass neu codiert wird, um die Windempfindlichkeit als benutzerdefiniertes Attribut für das 3D-Grasmodell 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
Wir verwenden jetzt ein benutzerdefiniertes Material, windMaterial, anstelle des zuvor verwendeten MeshPhongMaterial. WindMaterial umschließt den WindMeshShader, den wir gleich sehen werden.
Der Code in instanceGrass durchläuft also alle Eckpunkte des Grasmodells und fügt jedem Eckpunkt ein benutzerdefiniertes Eckpunktattribut namens windFactor hinzu. Dieser Windfaktor ist für das untere Ende des Grasmodells (wo es das Gelände berühren soll) auf 0 und für das obere Ende des Grasmodells auf 1 festgelegt.
Die andere Zutat, die wir brauchen, ist der Wind selbst. Wie bereits erwähnt, verwenden wir dazu Perlin-Rauschen. Wir generieren eine Perlin-Rauschtextur.
Zur besseren Verständlichkeit weisen wir diese Textur dem Gelände selbst zu, anstelle der vorherigen grünen Textur. So können Sie sich leichter ein Bild davon machen, wie der Wind weht.
Diese Perlin-Rauschtextur deckt also die Ausdehnung unseres Geländes ab und jedes Pixel der Textur gibt die Windintensität des Geländebereichs an, in dem sich dieses Pixel befindet. Das Rechteck im Gelände ist unser „Windfeld“.
Perlin-Rauschen wird prozedural über einen Shader namens NoiseShader generiert. Dieser Shader verwendet 3D-Simplex-Rauschalgorithmen von https://github.com/ashima/webgl-noise . Die WebGL-Version wurde wörtlich aus einem der Three.js-Beispiele von MrDoob übernommen, das unter http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html zu finden ist.
NoiseShader nimmt als Uniforms einen Zeit-, einen Skalierungs- und einen Offset-Parameter an und gibt eine schöne 2D-Perlin-Rauschverteilung 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) }
...
Mit diesem Shader rendern wir unseren Perlin-Rausch zu einer Textur. Das 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 )
Im obigen Code wird noiseMap als Three.js-Renderziel eingerichtet, mit dem NoiseShader ausgestattet und dann mit einer orthographischen Kamera gerendert, um perspektivische Verzerrungen zu vermeiden.
Wie bereits erwähnt, verwenden wir diese Textur jetzt auch als Haupt-Rendering-Textur für das Gelände. Dies ist für die Funktion des Windeffekts nicht unbedingt erforderlich. Aber es ist schön, sie zu haben, damit wir besser visuell nachvollziehen können, was bei der Windenergieerzeugung passiert.
Hier ist die überarbeitete Funktion initTerrain mit der Textur „noiseMap“:
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 )
Nachdem wir unsere Windtextur eingerichtet haben, sehen wir uns den WindMeshShader an, der dafür verantwortlich ist, die Grasmodelle je nach Wind zu verformen.
Um diesen Shader zu erstellen, haben wir den Standard-Three.js-MeshPhongMaterial-Shader als Ausgangspunkt verwendet und ihn modifiziert. Das ist eine gute, schnelle und einfache Möglichkeit, mit einem funktionierenden Shader zu beginnen, ohne von vorn beginnen zu müssen.
Wir kopieren hier nicht den gesamten Shadercode, da er größtenteils eine Kopie des MeshPhongMaterial-Shaders wäre. Sie können ihn sich in der Quellcodedatei ansehen. Sehen wir uns aber die geänderten, windbezogenen Teile im Vertex-Shader an.
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 zuerst die Textur-Suchkoordinate windUV basierend auf der 2D-, xz- (horizontalen) Position des Vertex. Anhand dieser UV-Koordinate wird die Windstärke vWindForce aus der Perlin-Rausch-Windtextur ermittelt.
Dieser Wert vWindForce wird mit dem oben beschriebenen benutzerdefinierten Attribut windFactor kombiniert, um zu berechnen, wie stark der Punkt verformt werden muss. Außerdem gibt es den globalen Parameter windScale, mit dem die Gesamtstärke des Windes gesteuert wird, und den Vektor windDirection, der angibt, in welche Richtung die Windverformung erfolgen soll.
Dadurch entsteht eine windbedingte Verformung unserer Grashalme. Wir sind aber noch nicht fertig. Derzeit ist diese Verformung statisch und vermittelt nicht die Wirkung eines windigen Gebiets.
Wie bereits erwähnt, müssen wir die Rauschtextur im Laufe der Zeit über den Windbereich gleiten lassen, damit sich unser Glas bewegen kann.
Dazu wird die Uniform vOffset, die an den NoiseShader übergeben wird, im Laufe der Zeit verschoben. Dies ist ein vec2-Parameter, mit dem wir den Rauschversatz in einer bestimmten Richtung (unsere Windrichtung) angeben können.
Das geschieht 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
...
Das war's auch schon. Wir haben gerade eine Szene mit „prozeduralem Gras“ erstellt, das vom Wind beeinflusst wird.
Staub in die Mischung geben
Jetzt wollen wir unsere Szene ein wenig aufpeppen. Fügen wir ein wenig fliegende Staubpartikel hinzu, um die Szene interessanter zu machen.

Staub sollte schließlich vom Wind beeinflusst werden. Daher ist es nur logisch, dass in unserer Windszene Staub 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. Beachten Sie, dass jeder von ihnen mit einem speziellen WindParticleShader ausgestattet ist.
Jetzt bewegen wir die Partikel in jedem Frame mit CoffeeScript ein wenig, 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 verschieben wir die Position jedes Particles entsprechend dem Wind. Dies geschieht im WindParticleShader. Insbesondere im Vertex-Shader.
Der Code für diesen Shader ist eine modifizierte Version des ParticleMaterial von Three.js. Hier ist der Kern:
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 wesentlich von dem, den wir für die windbasierte Verformung von Gras verwendet haben. Als Eingabe wird die Perlin-Rauschtextur verwendet. Je nach Position des Staubs in der Welt wird in der Rauschtextur ein Wert für vWindForce (Windgeschwindigkeit) ermittelt. Anhand dieses Werts wird dann die Position des Staubkorns geändert.
Riders On The Storm
Die abenteuerlichste unserer WebGL-Szenen war wahrscheinlich die letzte. Sie können sie sehen, wenn Sie sich durch den Ballon ins Auge des Tornados klicken, um das Ende Ihrer Reise auf der Website und ein exklusives Video zur kommenden Veröffentlichung zu erreichen.

Als wir diese Szene erstellt haben, wussten wir, dass wir eine zentrale Funktion für die Erfahrung haben mussten, die wirkungsvoll sein würde. Der rotierende Tornado würde als Mittelpunkt dienen und Schichten anderer Inhalte würden diese Funktion an Ort und Stelle formen, um einen dramatischen Effekt zu erzielen. Dazu haben wir um diesen seltsamen Shader herum ein Filmstudio aufgebaut.
Wir haben einen gemischten Ansatz verwendet, um das realistische Bild zu erstellen. Dazu gehörten visuelle Tricks wie Lichtformen, um einen Lens-Flare-Effekt zu erzeugen, oder Regentropfen, die als Ebenen über der Szene animiert wurden. In anderen Fällen wurden flache Oberflächen so gezeichnet, dass sie sich zu bewegen schienen, z. B. die Schichten der tief fliegenden Wolken, die sich gemäß einem Partikelsystemcode bewegten. Die herumwirbelnden Trümmer waren Ebenen in einer 3D-Szene, die so sortiert wurden, dass sie sich vor und hinter dem Tornado bewegten.
Der Hauptgrund dafür, dass wir die Szene so aufbauen mussten, war, dass wir sicherstellen wollten, dass wir genug GPU-Leistung hatten, um den Tornado-Shader im Gleichgewicht mit den anderen angewendeten Effekten zu verarbeiten. Anfangs hatten wir große Probleme mit dem GPU-Balancing, aber später wurde diese Szene optimiert und ist jetzt weniger leistungsintensiv als die Hauptszenen.
Anleitung: Der Sturm-Shader
Für die finale Sturmsequenz wurden viele verschiedene Techniken kombiniert, aber das Herzstück dieser Arbeit war ein benutzerdefinierter GLSL-Shader, der wie ein Tornado aussieht. Wir haben viele verschiedene Techniken ausprobiert, von Vertex-Shadern, um interessante geometrische Wirbel zu erstellen, bis hin zu partikelbasierten Animationen und sogar 3D-Animationen verdrehter geometrischer Formen. Keiner der Effekte konnte das Gefühl eines Tornados erzeugen oder erforderte zu viel Rechenleistung.
Die Antwort fanden wir schließlich bei einem ganz anderen Projekt. Ein paralleles Projekt mit Spielen für die Wissenschaft, um das Gehirn der Maus des Max-Planck-Instituts (brainflight.org) zu kartieren, hatte interessante visuelle Effekte erzeugt. Mit einem benutzerdefinierten volumetrischen Shader konnten wir Filme von der Innenseite eines Mausneurons erstellen.

Wir haben festgestellt, dass das Innere einer Gehirnzelle ein wenig wie der Trichter eines Tornados aussieht. Da wir ein volumetrisches Verfahren verwendeten, wussten wir, dass wir diesen Shader aus allen Richtungen im Raum betrachten konnten. Wir könnten das Rendern des Shaders so einstellen, dass er mit der Sturmszene kombiniert wird, insbesondere wenn er zwischen Wolkenschichten und über einem dramatischen Hintergrund liegt.
Das Shader-Verfahren beinhaltet einen Trick, bei dem im Grunde ein einzelner GLSL-Shader verwendet wird, um ein ganzes Objekt mit einem vereinfachten Rendering-Algorithmus zu rendern, der als Ray-Marching-Rendering mit einem Entfernungsfeld bezeichnet wird. Bei dieser Technik wird ein Pixel-Shader erstellt, der für jeden Punkt auf dem Bildschirm die kürzeste Entfernung zu einer Oberfläche schätzt.
Eine gute Referenz zum Algorithmus finden Sie in der Übersicht von iq: Rendering Worlds With Two Triangles – Iñigo Quilez. In der Shader-Galerie auf glsl.heroku.com finden Sie viele Beispiele für diese Technik, mit denen Sie experimentieren können.
Das Herzstück des Shaders beginnt mit der Hauptfunktion. Hier werden die Kameratransformationen eingerichtet und eine Schleife gestartet, in der die Entfernung zu einer Oberfläche wiederholt ausgewertet wird. Die eigentliche Ray-Marching-Berechnung erfolgt über den Aufruf 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
}
Der Grundgedanke ist, dass wir beim Vordringen in die Form des Tornados dem endgültigen Farbwert des Pixels regelmäßig Farbbeiträge hinzufügen, sowie Beiträge zur Deckkraft entlang des Strahls. Dadurch entsteht eine weiche, mehrschichtige Textur des Tornados.
Der nächste wichtige Aspekt des Tornados ist die eigentliche Form, die durch Zusammensetzen mehrerer Funktionen erstellt wird. Zuerst ist es ein Kegel, der mit Rauschen kombiniert wird, um eine organische raue Kante zu erzeugen. Anschließend wird er entlang seiner Hauptachse verdreht und zeitlich 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;
}
Das Erstellen dieser Art von Shader ist schwierig. Neben den Problemen mit der Abstraktion der von Ihnen erstellten Vorgänge gibt es ernsthafte Optimierungs- und plattformübergreifende Kompatibilitätsprobleme, die Sie aufspüren und beheben müssen, bevor Sie die Arbeit in der Produktion verwenden können.
Der erste Teil des Problems: Optimieren dieses Shaders für unsere Szene. Um damit umzugehen, brauchten wir einen „sicheren“ Ansatz für den Fall, dass der Shader zu schwer ist. Dazu haben wir den Tornado-Shader mit einer anderen gemusterten Auflösung als der Rest der Szene zusammengesetzt. Dieser Code stammt aus der Datei „stormTest.coffee“ (ja, das war ein Test!).
Wir beginnen mit einem RenderTarget, das der Breite und Höhe der Szene entspricht, damit die Auflösung des Tornado-Shaders unabhängig von der Szene ist. Dann entscheiden wir dynamisch anhand der Framerate, wie stark der Sturm-Shader herunterskaliert wird.
...
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 auf dem Bildschirm, um den blockigen Look zu vermeiden (Zeile 1107 in stormTest.coffee). Im schlimmsten Fall ist der Tornado dann etwas verschwommener, aber zumindest funktioniert es, ohne dass die Kontrolle dem Nutzer entzogen wird.
Für den nächsten Optimierungsschritt müssen wir uns den Algorithmus genauer ansehen. Der wichtigste Berechnungsfaktor im Shader ist die Iteration, die für jedes Pixel ausgeführt wird, um die Entfernung der Oberflächenfunktion zu approximieren: die Anzahl der Iterationen des Ray-Marching-Loops. Mit einer größeren Schrittweite konnten wir eine Schätzung der Tornadofläche mit weniger Iterationen erhalten, während wir uns außerhalb der bewölkten Oberfläche befanden. Im Innenbereich verringern wir die Schrittweite, um genauer zu arbeiten und Werte mischen zu können, um den Dunsteffekt zu erzeugen. Außerdem hat es zu einer guten Beschleunigung beigetragen, einen Begrenzungszylinder zu erstellen, um eine Schätzung der Tiefe für den gesendeten Strahl zu erhalten.
Der nächste Teil des Problems bestand darin, dafür zu sorgen, dass dieser Shader auf verschiedenen Grafikkarten ausgeführt werden kann. Wir haben jedes Mal einige Tests durchgeführt und uns ein Gefühl für die Art der Kompatibilitätsprobleme verschafft, auf die wir möglicherweise stoßen würden. Der Grund, warum wir nicht viel besser als mit Intuition vorgehen konnten, war, dass wir nicht immer gute Informationen zur Fehlerbehebung erhalten konnten. Ein typisches Szenario ist ein GPU-Fehler, der nicht weiter ausgeführt werden kann, oder sogar ein Systemabsturz.
Für Probleme mit der geräteübergreifenden Videokompatibilität gab es ähnliche Lösungen: Achten Sie darauf, dass statische Konstanten den genau definierten Datentypen entsprechen, z. B. 0,0 für „float“ und 0 für „int“. Seien Sie beim Schreiben längerer Funktionen vorsichtig. Es ist besser, die Dinge in mehrere einfachere Funktionen und Zwischenvariablen aufzuteilen, da die Compiler bestimmte Fälle anscheinend nicht richtig verarbeiten. Achten Sie darauf, dass alle Texturen Potenzen von 2 sind, nicht zu groß und dass Sie in jedem Fall vorsichtig vorgehen, wenn Sie Texturdaten in einer Schleife abrufen.
Die größten Kompatibilitätsprobleme waren auf den Blitzeffekt für den Sturm zurückzuführen. Wir haben eine vorgefertigte Textur um den Tornado gewickelt, damit wir seine Fasern einfärben konnten. Es war ein wunderschöner Effekt, der es einfach machte, den Tornado in die Farben der Szene einzufügen. Es dauerte jedoch lange, bis er auf anderen Plattformen funktionierte.

Die mobile Website
Die mobile Version konnte nicht einfach aus der Desktopversion übernommen werden, da die Technologie- und Verarbeitungsanforderungen zu hoch waren. Wir mussten etwas Neues entwickeln, das sich speziell an Nutzer von Mobilgeräten richtete.
Wir fanden es cool, das Karnevals-Fotobooth als mobile Webanwendung für Computer zu haben, die die Kamera des Nutzers nutzt. Das hatten wir bisher noch nicht gesehen.
Für den besonderen Effekt haben wir 3D-Transformationen in CSS3 programmiert. Durch die Verknüpfung mit Gyroskop und Beschleunigungsmesser konnten wir dem Spiel viel mehr Tiefe verleihen. Die Website reagiert darauf, wie Sie Ihr Smartphone halten, bewegen und ansehen.
Bei der Erstellung dieses Artikels haben wir uns überlegt, Ihnen einige Tipps für einen reibungslosen Ablauf der mobilen Entwicklung zu geben. Hier sind sie. Sehen Sie sich an, was Sie daraus lernen können.
Tipps und Tricks für Mobilgeräte
Ein Preloader ist erforderlich und sollte nicht vermieden werden. Uns ist bewusst, dass das manchmal der Fall ist. Das liegt vor allem daran, dass Sie die Liste der Elemente, die Sie vorladen, im Laufe des Projekts ständig aktualisieren müssen. Schlimmer noch: Es ist nicht klar, wie Sie den Ladevorgang berechnen sollten, wenn Sie verschiedene Ressourcen abrufen und viele davon gleichzeitig. Hier kommt unsere benutzerdefinierte und sehr generische abstrakte Klasse „Task“ ins Spiel. Der Hauptgedanke besteht darin, eine endlos verschachtelte Struktur zu ermöglichen, in der eine Aufgabe eigene untergeordnete Aufgaben haben kann, die wiederum eigene untergeordnete Aufgaben haben können usw. Außerdem wird der Fortschritt jeder Aufgabe in Bezug auf den Fortschritt ihrer untergeordneten Aufgaben berechnet (nicht in Bezug auf den Fortschritt der übergeordneten Aufgabe). Wir haben alle MainPreloadTask-, AssetPreloadTask- und TemplatePreFetchTask-Klassen von Task abgeleitet und so eine Struktur erstellt, die so aussieht:

Dank dieses Ansatzes und der Task-Klasse können wir ganz einfach den globalen Fortschritt (MainPreloadTask), den Fortschritt der Assets (AssetPreloadTask) oder den Fortschritt beim Laden von Vorlagen (TemplatePreFetchTask) ermitteln. Gleichmäßiger Fortschritt bei einer bestimmten Datei. Weitere Informationen finden Sie in der Task-Klasse unter /m/javascripts/raw/util/Task.js und in den tatsächlichen Task-Implementierungen unter /m/javascripts/preloading/task. Hier ist ein Auszug aus der Einrichtung der Klasse /m/javascripts/preloading/task/MainPreloadTask.js, die unser ultimativer Preloading-Wrapper ist:
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 ist nicht nur zu beachten, wie sie über die gemeinsame Task-Implementierung mit der MainPreloadTask kommuniziert, sondern auch, wie wir platformabhängige Assets laden. Grundsätzlich gibt es vier Arten von Bildern. „mobile standard“ (.ext, wobei „ext“ die Dateiendung ist, z. B. .png oder .jpg), „mobile retina“ (-2x.ext), „tablet standard“ (-tab.ext) und „tablet retina“ (-tab-2x.ext). Anstatt die Erkennung in der MainPreloadTask durchzuführen und vier Asset-Arrays zu codieren, geben wir einfach den Namen und die Erweiterung des Assets an, das vorab geladen werden soll, und ob das Asset plattformabhängig ist (responsive = true / false). Dann generiert die AssetPreloadTask den Dateinamen für uns:
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, der das Asset-Preload ausführt, 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-Fotoautomat (iOS 6/Android)
Bei der Entwicklung von OZ mobile haben wir festgestellt, dass wir viel Zeit damit verbringen, mit dem Fotoautomaten zu spielen, anstatt zu arbeiten :D Das liegt einfach daran, dass es Spaß macht. Deshalb haben wir eine Demo für Sie erstellt.

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
Zur Einrichtung benötigen Sie eine kostenlose Google App Engine-Anwendungs-Instanz, auf der Sie das Back-End ausführen können. Der Front-End-Code ist nicht komplex, aber es gibt einige mögliche Fallstricke. Sehen wir uns diese jetzt an:
- Zulässiger Bilddateityp
Nutzer sollen nur Bilder hochladen können, da es sich um eine Fotokabine und nicht um eine Videokabine handelt. Theoretisch kannst du den Filter einfach so in HTML angeben:
input id="fileInput" class="fileInput" type="file" name="file" accept="image/*"
Das funktioniert jedoch anscheinend nur auf iOS-Geräten. Daher müssen wir nach der Auswahl einer Datei eine zusätzliche Prüfung mit dem RegExp hinzufügen:
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();
}
}
});
- Abbruch eines Uploads oder einer Dateiauswahl Eine weitere Inkonsistenz, die wir während des Entwicklungsprozesses festgestellt haben, ist die Art und Weise, wie verschiedene Geräte über einen abgebrochenen Dateiauswahlvorgang informieren. Auf iOS-Smartphones und -Tablets passiert gar nichts, es wird keine Benachrichtigung angezeigt. In diesem Fall sind keine besonderen Maßnahmen erforderlich. Auf Android-Smartphones wird die add()-Funktion jedoch trotzdem ausgelöst, auch wenn keine Datei ausgewählt ist. So gehen Sie 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 recht reibungslos. Spaß haben
Fazit
Aufgrund der enormen Größe von „Find Your Way To Oz“ und der Vielzahl der beteiligten Technologien konnten wir in diesem Artikel nur einige der verwendeten Ansätze behandeln.
Wenn Sie sich den gesamten Quellcode von „Find Your Way To Oz“ ansehen möchten, klicken Sie hier.
Gutschriften
Hier findest du die vollständige Liste der Mitwirkenden
Verweise
- CoffeeScript – http://coffeescript.org/
- Backbone.js – http://backbonejs.org/
- Three.js – http://mrdoob.github.com/three.js/
- Max-Planck-Institut (brainflight.org) – http://brainflight.org/