Case study - Find Your Way to Oz

Introduzione

"Find Your Way to Oz" è un nuovo esperimento di Google Chrome portato sul web da Disney. Ti permette di intraprendere un viaggio interattivo attraverso un circo del Kansas, che ti porterà nel mondo di Oz dopo essere stato travolto da una forte tempesta.

Il nostro obiettivo era combinare la ricchezza del cinema con le funzionalità tecniche del browser per creare un'esperienza divertente e immersiva con cui gli utenti potessero creare una forte connessione.

Il lavoro è un po' troppo grande per poter essere immortalato nella sua interezza, quindi abbiamo analizzato in dettaglio alcuni capitoli della storia tecnologica che riteniamo interessanti. Lungo il percorso abbiamo estratto alcuni tutorial mirati con difficoltà crescente.

Molte persone si sono impegnate a fondo per rendere possibile questa esperienza: troppe persone per essere elencate qui. Visita il sito per consultare la pagina dei riconoscimenti nella sezione del menu e leggere tutta la storia.

Uno sguardo ravvicinato

Trova la tua strada per Oz sul desktop è un mondo ricco e coinvolgente. Utilizziamo il 3D e diversi livelli di effetti ispirati al cinema tradizionale che si combinano per creare una scena quasi realistica. Le tecnologie più importanti sono WebGL con Three.js, mesh personalizzati ed elementi animati DOM che utilizzano le funzionalità CSS3. Oltre a questo, l'API getUserMedia (WebRTC) per esperienze interattive che consentono all'utente di aggiungere la propria immagine direttamente da webcam e WebAudio per l'audio 3D.

Ma la magia di un'esperienza tecnologica come questa è il modo in cui si combinano. Questa è anche una delle sfide principali: come combinare effetti visivi ed elementi interattivi in un'unica scena per creare un insieme coerente? Questa complessità visiva era difficile da gestire: era difficile capire in quale fase di sviluppo ci trovavamo in un dato momento.

Per risolvere il problema dell'ottimizzazione e degli effetti visivi interconnessi, abbiamo fatto ricorso a un pannello di controllo in grado di acquisire tutte le impostazioni pertinenti che stavamo esaminando in quel momento. La scena può essere regolata in tempo reale nel browser per qualsiasi cosa, dalla luminosità alla profondità di campo, gamma e così via... Chiunque potrebbe provare a modificare i valori dei parametri significativi dell'esperienza e prendere parte alla scoperta di ciò che funzionava meglio.

Prima di svelare il nostro segreto, vogliamo avvisarti che potrebbe arrestarsi in modo anomalo, proprio come se stessi rovistando all'interno del motore di un'auto. Assicurati di non avere nulla di importante e visita l'URL principale del sito e aggiungi ?debug=on all'indirizzo. Attendi che il sito venga caricato e una volta all'interno (premi?) del tasto Ctrl-I verrà visualizzato un menu a discesa sul lato destro. Se deselezioni l'opzione "Esci dal percorso della videocamera", puoi utilizzare i tasti A, W, S e D e il mouse per spostarti liberamente nello spazio.

Percorso videocamera.

Non esamineremo tutte le impostazioni qui, ma ti consigliamo di fare una prova: i tasti rivelano impostazioni diverse in scene diverse. Nella sequenza temporale finale c'è un tasto aggiuntivo: Ctrl-A con cui puoi alternare la riproduzione dell'animazione e muoverti. In questa scena, se premi Esc (per uscire dalla funzionalità di blocco del mouse) e premi di nuovo Ctrl-I puoi accedere alle impostazioni specifiche della scena della tempesta. Dai un'occhiata intorno e scatta qualche meravigliosa vista da cartoline come quella di seguito.

Scena di tempesta

A questo scopo e per assicurarci che fosse abbastanza flessibile da soddisfare le nostre esigenze, abbiamo usato una deliziosa libreria chiamata dat.gui (visita questa pagina per un tutorial precedente su come utilizzarlo). Ci ha permesso di cambiare rapidamente le impostazioni esposte ai visitatori del sito.

Quadro opaco come un po'

In molti film e animazioni classici della Disney, creare scene significava combinare diversi strati. C'erano livelli di live action, animazione di celle, persino set fisici e sugli strati superiori creati dipingendo su vetro: una tecnica chiamata "opaco-pittura".

La struttura dell'esperienza che abbiamo creato è per molti aspetti simile, anche se alcuni "livelli" sono molto più di immagini statiche. Infatti, influenzano l'aspetto delle cose in base a calcoli più complessi. Tuttavia, almeno a livello di quadro complessivo abbiamo a che fare con le visualizzazioni, composte una sopra l'altra. In alto è presente un livello UI con una scena 3D sotto: a sua volta è composta da diversi componenti della scena.

Il livello superiore dell'interfaccia è stato creato utilizzando DOM e CSS 3, il che significava che la modifica delle interazioni poteva essere effettuata in molti modi indipendentemente dall'esperienza 3D con comunicazione tra i due secondo un elenco selezionato di eventi. Questa comunicazione utilizza il router backbone + l'evento onHashChange HTML5 che controlla l'area in entrata/uscita. (fonte progetto: /develop/Caffè/router/Router.Caffè).

Tutorial: supporto di Sprite Fogli e Retina

Una tecnica di ottimizzazione divertente su cui abbiamo fatto affidamento per l'interfaccia è stata quella di combinare le numerose immagini in overlay dell'interfaccia in un'unica immagine PNG per ridurre le richieste del server. In questo progetto l'interfaccia era composta da oltre 70 immagini (senza le texture 3D) caricate subito per ridurre la latenza del sito web. Puoi visualizzare il foglio sprite pubblicato qui:

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

Ecco alcuni suggerimenti su come abbiamo sfruttato l'uso di Sprite Fogli e come usarlo per i dispositivi retina e ottenere un'interfaccia il più nitida e chiara possibile.

Creazione di fogli sprite

Per creare SpriteFogli abbiamo usato TexturePacker che genera output in qualsiasi formato. In questo caso abbiamo esportato come EaselJS, che è molto chiaro e avrebbe potuto essere utilizzato anche per creare sprite animati.

Utilizzare lo Sprite Sheet generato

Dopo aver creato il foglio sprite, dovresti vedere un file JSON simile al seguente:

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

Dove:

  • image si riferisce all'URL dello sprite Sheet
  • I frame sono le coordinate di ogni elemento UI [x, y, width, height]
  • le animazioni sono i nomi di ogni asset

Tieni presente che abbiamo utilizzato immagini ad alta densità per creare il foglio Sprite, quindi abbiamo creato la versione normale ridimensionandola fino a metà delle sue dimensioni.

Riepilogo

Ora che è tutto pronto, basta uno snippet JavaScript per utilizzarlo.

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

Ecco come lo useresti:

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

Per saperne di più sulle densità di pixel variabili, puoi leggere questo articolo di Boris Smus.

Pipeline di contenuti 3D

L'esperienza ambientale viene configurata su un livello WebGL. Quando pensi a una scena 3D, una delle domande più difficili è come riuscire a creare contenuti che trasmettano il massimo potenziale espressivo derivanti dalla modellazione, dall'animazione e dagli effetti. Il fulcro di questo problema è sotto molti aspetti la pipeline dei contenuti: un processo concordato da seguire per creare contenuti per la scena 3D.

Volevamo creare un mondo stupendo, quindi avevamo bisogno di un processo solido che consentisse agli artisti 3D di crearlo. Il loro software di modellazione e animazione 3D dovrebbe avere la massima libertà espressiva possibile, mentre noi dovremmo eseguire il rendering su schermo tramite codice.

Stavamo lavorando a questo tipo di problema da un po' di tempo, perché in passato, ogni volta che creavamo un sito in 3D, avevamo dei limiti negli strumenti a disposizione. Perciò avevamo creato questo strumento, chiamato 3D Librarian: un progetto di ricerca interna. Era quasi pronto per essere applicato a un lavoro reale.

Questo strumento aveva un po' di storia: in origine era per Flash, e permetteva di inserire una grande scena Maya come un unico file compresso ottimizzato per la decompressione del runtime. Il motivo per cui questa scelta era ottimale è che la scena includeva sostanzialmente la stessa struttura di dati che viene manipolata durante il rendering e l'animazione. Le analisi da eseguire per il file una volta caricato sono pochissime. La decompressione in Flash è stata piuttosto rapida perché il file era in formato AMF che Flash poteva decomprimere in modo nativo. L'utilizzo dello stesso formato in WebGL richiede un po' più di lavoro sulla CPU. Abbiamo dovuto ricreare un livello di codice JavaScript di decompressione dei dati, che avrebbe sostanzialmente decompresso i file e ricreato le strutture dei dati necessarie al funzionamento di WebGL. Il decompressione dell'intera scena 3D è un'operazione che consuma leggermente la CPU: per decomprimere la scena 1 di Find Your Way To Oz sono necessari circa 2 secondi su un computer di fascia media e di fascia alta. Pertanto, questo viene fatto utilizzando la tecnologia Web Workers, al momento della "configurazione della scena" (prima che la scena venga effettivamente lanciata), in modo da non bloccare l'esperienza per l'utente.

Questo pratico strumento consente di importare la maggior parte della scena 3D: modelli, texture, animazioni di ossa. Crea un unico file della libreria, che può essere caricato dal motore 3D. Puoi inserire tutti i modelli che ti servono nella tua scena all'interno di questa raccolta, per poi farli apparire nella tua scena.

Un problema però era che avevamo a che fare con WebGL: il nuovo arrivato. Era un bambino piuttosto duro: stava stabilendo lo standard delle esperienze 3D basate su browser. Abbiamo quindi creato un livello JavaScript ad hoc che prendeva i file di scena 3D compressi di 3D Librarian e li tradusse correttamente in un formato comprensibile da WebGL.

Tutorial: Sia vento

Un tema ricorrente in "Find Your Way To Oz" era il vento. Un filo conduttore della storia è strutturato come un crescendo di vento.

La prima scena del carnevale è relativamente calma. Durante le varie scene, l'utente sente un vento sempre più forte, che culmina nella scena finale, la tempesta.

Pertanto era importante fornire un effetto del vento immersivo.

Per realizzare questa immagine, abbiamo popolato le 3 scene di carnevale con oggetti morbidi che dovrebbero essere influenzati dal vento, come delle tende, dalla superficie della cabina fotografica e dal palloncino stesso.

Panno morbido.

Al giorno d'oggi i giochi per computer sono generalmente sviluppati intorno a un motore fisico di base. Di conseguenza, quando un oggetto morbido deve essere simulato nel mondo 3D, viene eseguita una simulazione fisica completa, creando un comportamento morbido credibile.

In WebGL / JavaScript non abbiamo (ancora) il lusso di eseguire una simulazione fisica completa. Quindi in Oz dovevamo trovare un modo per creare l'effetto del vento, senza simularlo effettivamente.

Abbiamo incorporato le informazioni sulla "sensibilità al vento" per ogni oggetto nel modello 3D stesso. Ogni vertice del modello 3D aveva un "attributo Vento" che specificava quanto il vertice avrebbe dovuto essere influenzato dal vento. Questa specifica la sensibilità al vento degli oggetti 3D. Poi dovevamo creare il vento stesso.

Per farlo, abbiamo generato un'immagine contenente Perlin Noise. Questa immagine è destinata a coprire una determinata "area di vento". Quindi, un buon modo per pensarci è immaginare l'immagine di una nuvola simile a un rumore che viene sovrapposto a una determinata area rettangolare della scena 3D. Ogni pixel, valore del livello di grigio, di questa immagine specifica la forte intensità del vento in un determinato momento nell'area 3D "circondante".

Per produrre l'effetto vento, l'immagine viene spostata, nel tempo, a velocità costante, in una direzione specifica, ossia la direzione del vento. Per assicurarci che la "zona ventosa" non influisca su tutto nella scena, avvolgiamo l'immagine del vento intorno ai bordi, confinata all'area di effetto.

Un semplice tutorial sul vento in 3D

Ora creiamo l'effetto del vento in una semplice scena 3D in Three.js.

Creeremo il vento in un semplice "campo di erba procedurale".

Per prima cosa, creiamo la scena. Avremo un terreno pianeggiante e semplice. Ogni spicchio d'erba verrà semplicemente rappresentato con un cono 3D capovolto.

Terreno erboso
Terreno ricco di erba

Ecco come creare questa semplice scena in Three.js utilizzando CoffeeScript.

Per prima cosa, configureremo Three.js e lo collegheremo a Fotocamera, controller del mouse e qualche tipo di luce:

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

Le chiamate di funzioni initGrass e initTerrain completano la scena rispettivamente con erba e terreno:

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

Qui stiamo creando una griglia di 15 x 15 pezzetti di erba. Aggiungiamo un po' di randomizzazione a ogni posizione dell'erba, in modo che non si allineino come soldati, con un aspetto strano.

Questo terreno non è altro che un piano orizzontale, posizionato alla base dei pezzi d'erba (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 )

Ciò che abbiamo fatto finora è semplicemente la creazione di una scena Three.js e l'aggiunta di qualche pezzettino di erba, fatta di coni invertiti generati proceduralmente, e di un terreno semplice.

Niente di speciale finora.

Ora è il momento di iniziare ad aggiungere il vento. Innanzitutto, vogliamo incorporare le informazioni sulla sensibilità del vento nel modello 3D dell'erba.

Incorporeremo queste informazioni come attributo personalizzato per ogni vertice del modello 3D dell'erba. Useremo la regola secondo cui l'estremità inferiore del modello di erba (la punta del cono) non ha sensibilità poiché è fissata al suolo. La parte superiore del modello di erba (la base del cono) ha la massima sensibilità al vento, poiché è la parte più lontana dal suolo.

Ecco come la funzione instanceGrass viene ricodificata, in modo da aggiungere sensibilità al vento come attributo personalizzato per il modello 3D "erba".

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

Ora usiamo un materiale personalizzato, il windMaterial, invece del MeshPhongMaterial che utilizzavamo in precedenza. WindMaterial racchiude il WindMeshShader che vedremo tra un minuto.

Di conseguenza, il codice in instanceGrass esegue loop di tutti i vertici del modello di erba e aggiunge un attributo di vertice personalizzato, chiamato windFactor per ogni vertice. Questo fattore di vento è impostato su 0 per l'estremità inferiore del modello di erba (il punto in cui deve toccare il terreno) e il valore 1 per l'estremità superiore del modello erboso.

L'altro ingrediente necessario è aggiungere il vento alla scena. Come già detto, useremo il rumore di Perlin. Genereremo proceduralmente una texture rumore Perlin.

Per maggiore chiarezza, assegneremo questa texture al terreno stesso, al posto della precedente texture verde. In questo modo sarà più facile farsi un'idea di cosa succede con il vento.

In questo modo, la texture rumore di Perlin coprirà spazialmente l'estensione del nostro terreno e ogni pixel della texture specificherà l'intensità del vento dell'area del terreno in cui cade quel pixel. Il rettangolo del terreno diventerà la nostra "area di vento".

Il rumore di Perlin viene generato proceduralmente tramite uno Shader, chiamato NoiseShader. Questo shabbyr utilizza algoritmi di rumore 3D semplici da: https://github.com/ashima/webgl-noise . La versione WebGL è stata presa testualmente da uno degli esempi di Three.js di MrDoob, all'indirizzo: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.

NoiseShader richiede un tempo, una scala e un insieme di parametri di offset come uniformi, e genera una buona distribuzione 2D del rumore di Perlin.

class NoiseShader

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

...

Useremo questo Shader per applicare il rumore Perlin alla texture. Questa operazione viene eseguita nella funzione 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 )

Il codice riportato sopra consiste nel configurare noiseMap come target di rendering di Three.js, dotato di NoiseShader e quindi con una fotocamera ortografica, in modo da evitare distorsioni prospettiche.

Come già detto, utilizzeremo questa texture anche come principale texture di rendering per il terreno. Questo non è necessario per il funzionamento dell'effetto vento. Ma è utile averne uno, in modo da poter capire visivamente meglio cosa sta succedendo con la generazione di energia eolica.

Ecco la funzione initTerrain rielaborata, che utilizza rumoreMap come texture:

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 )

Ora che abbiamo applicato la nostra texture del vento, diamo un'occhiata al WindMeshShader, che è responsabile della deformazione dei modelli di erba in base al vento.

Per creare lo Shader, abbiamo iniziato a utilizzare lo strumento standard Three.js MeshPhongMaterial e lo abbiamo modificato. Si tratta di un buon metodo rapido e non elaborato per iniziare a usare uno strumento di approfondimento che funziona, senza dover ripartire da zero.

Qui non copieremo l'intero codice dello smoothr (è possibile esaminarlo nel file del codice sorgente), poiché la maggior parte sarebbe una replica di MeshPhongMaterial. Ma diamo un'occhiata alle parti modificate, legate al vento, in 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;

Quindi, lo strumento di shadowing consiste nel calcolare innanzitutto la coordinata di ricerca della texture windUV, in base alla posizione 2D, xz (orizzontale) del vertice. Questa coordinata UV viene utilizzata per cercare la forza del vento, vWindForce, dalla texture del vento di Perlin.

Questo valore vWindForce è composito con il windFactor specifico del vertice, l'attributo personalizzato discusso sopra, per calcolare la deformazione necessaria al vertice. Abbiamo anche un parametro globale windScale, per controllare la forza complessiva del vento, e un vettore windDirection, che specifica in quale direzione deve avvenire la deformazione del vento.

Ciò crea una deformazione dei nostri pezzi d'erba a causa del vento. Tuttavia, non abbiamo ancora finito. Allo stato attuale, questa deformazione è statica e non trasmette l'effetto di una zona ventosa.

Come avevamo detto, dovremo far scorrere la texture del rumore nel tempo, attraverso l'area del vento, in modo che il nostro vetro possa ondeggiare.

Questo avviene spostando nel tempo l'uniforme vOffset che viene passata a NoiseShader. Si tratta di un parametro vec2 che consente di specificare l'offset del rumore in una determinata direzione (la direzione del vento).

Lo facciamo nella funzione di rendering, che viene richiamata a ogni frame:

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

Questo è tutto. Abbiamo appena creato una scena con "l'erba procedurale" influenzata dal vento.

Aggiunta di polvere alla miscela

Ora diamo una marcia in più alla nostra scena. Aggiungiamo un po' di polvere volante, in modo da rendere la scena più interessante.

Aggiunta di polvere
Aggiunta di polvere

La polvere dovrebbe essere influenzata dal vento, dopotutto, quindi ha perfettamente senso far volare la polvere nella scena del vento.

La polvere è configurata nella funzione initDust come sistema di particelle.

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

Qui vengono create 130 particelle di polvere. Inoltre, tieni presente che ognuno di questi è dotato di uno speciale WindParticleShader.

Ora, a ogni frame, ci sposteremo un po' tra le particelle, usando CoffeeScript, indipendentemente dal vento. Ecco il codice.

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 )

Inoltre, compenseremo la posizione delle particelle in base al vento. Questa operazione viene eseguita in WindParticleShader. Nello specifico per Vertex Shader.

Il codice per questoshar è una versione modificata di ParticleMaterial Three.js, con il seguente aspetto:

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;

Questo strumento per la deformazione dell'erba basata sul vento non è poi così diverso da quello che avevamo. Prende come input la texture del rumore di Perlin e, a seconda della posizione nel mondo di polvere, cerca un valore vWindForce nella texture del rumore. Dopodiché utilizza questo valore per modificare la posizione della particella di polvere.

Pilota sulla tempesta

La più avventurosa delle nostre scene WebGL è stata probabilmente l'ultima, che puoi vedere facendo clic sul palloncino per entrare nell'occhio del tornado per raggiungere la fine del tuo viaggio sul sito. Inoltre, puoi vedere un video esclusivo della prossima versione.

Scena di un giro in mongolfiera

Quando abbiamo creato questa scena, sapevamo di avere bisogno di una caratteristica centrale per creare un'esperienza di impatto. Il tornado rotante fungerà da elemento centrale e gli strati di altri contenuti avrebbero modellato questa funzionalità per creare un effetto drammatico. Per farlo, abbiamo costruito quello che sarebbe l'equivalente di uno studio cinematografico ambientato intorno a questo strano passante.

Abbiamo utilizzato un approccio misto per creare un composito realistico. Alcuni erano espedienti visivi come forme di luce per creare un effetto riflesso dell'obiettivo o gocce di pioggia che si animano come livelli nella parte superiore della scena che stai guardando. In altri casi avevamo superfici piane che sembravano muoversi, come gli strati di nubi basse che si muovevano secondo un codice di sistema particellare. Mentre i frammenti di detriti che orbitano intorno al tornado erano strati in una scena 3D ordinati per muoversi davanti e dietro il tornado.

Il motivo principale per cui abbiamo dovuto costruire la scena in questo modo era assicurarci di avere GPU sufficiente per gestire lo strumento di ombra dei tornado in equilibrio con gli altri effetti che stavamo applicando. Inizialmente abbiamo avuto grossi problemi di bilanciamento della GPU, ma in seguito questa scena è stata ottimizzata ed è diventata più leggera delle scene principali.

Tutorial: The Storm Shader

Per creare la sequenza finale di tempesta sono state combinate molte tecniche diverse, ma il fulcro di questo lavoro è stato un GLSL smoother personalizzato che assomiglia a un tornado. Avevamo provato molte tecniche diverse, dagli strumentatori vertice per creare vortici geometrici interessanti, fino ad animazioni basate sulle particelle e perfino animazioni 3D di forme geometriche contorte. Nessuno degli effetti sembrava ricreare la sensazione di un tornado o richiedeva troppo in termini di elaborazione.

Un progetto completamente diverso alla fine ci ha dato la risposta. Un progetto parallelo che prevedeva giochi per la scienza per mappare il cervello del topo del Max Planck Institute (brainflight.org) ha generato effetti visivi interessanti. Eravamo riusciti a creare filmati dell'interno di un neurone di topo usando uno strumento di ombra volumetrico personalizzato.

All&#39;interno di un neurone di topo con uno Shadr volumetrico personalizzato
All'interno di un neurone di topo con uno strumento volumetrico personalizzato

Abbiamo scoperto che l'interno di una cellula cerebrale somigliava all'imbuto di un tornado. E siccome stavamo usando una tecnica volumetrica, sapevamo di poter vedere questoshar da tutte le direzioni nello spazio. Abbiamo potuto impostare il rendering dello strumento in modo che si combinasse con la scena della tempesta, in particolare se circondato da strati di nuvole e su uno sfondo spettacolare.

La tecnica di Shading comporta un trucco che utilizza un singolo utente GLSL per eseguire il rendering di un intero oggetto con un algoritmo di rendering semplificato chiamato ray marching rendering con un campo di distanza. Con questa tecnica viene creato un pixel Shader che stima la distanza più vicina a una superficie per ogni punto sullo schermo.

Un buon riferimento all'algoritmo è reperibile nella panoramica di iq: Rendering Worlds With Two Triangles - Iñigo Quilez. Inoltre, esplorando la galleria degli smoother sul sito glsl.heroku.com, puoi trovare molti esempi di questa tecnica che possono essere sperimentate.

Il cuore dello strumento di messa a fuoco inizia con la funzione principale, configura la trasformazione della videocamera ed entra in un loop che valuta ripetutamente la distanza da una superficie. Nella chiamata RaytraceFoggy( Direction_vector, max_iterations, color, color_multiplier ) avviene il calcolo della marcia del raggio del raggio di base.

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
}

L'idea è che man mano che avanzamo nella forma del tornado, aggiungiamo regolarmente contributi di colore al valore finale del colore del pixel, oltre ai contributi all'opacità lungo il raggio. Ciò crea una qualità morbida a strati nella trama del tornado.

L'altro aspetto fondamentale del tornado è la forma effettiva, che viene creata componendo una serie di funzioni. Si tratta di un cono, composto da rumore per creare un bordo grezzo organico, che viene poi attorcigliato lungo il suo asse principale e fatto ruotare nel tempo.

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

Il lavoro da svolgere nella creazione di questo tipo di Shadr è complicato. Oltre ai problemi legati all'astrazione delle operazioni che stai creando, esistono seri problemi di ottimizzazione e compatibilità tra piattaforme che devi individuare e risolvere prima di poter utilizzare il lavoro in produzione.

La prima parte del problema è l'ottimizzazione dello Shadr per la scena. Per far fronte a questo problema dovevamo adottare un approccio "sicuro", nel caso in cui lo strumento di approfondimento fosse troppo pesante. Per fare ciò, abbiamo composito lo strumento di saturazione del tornado a una risoluzione campionata diversa dal resto della scena. Questo risultato è tratto dal file stormTest.caffè (sì, questo era un test!).

Iniziamo con un renderingTarget che corrisponda alla larghezza e all'altezza della scena, in modo da avere l'indipendenza dalla risoluzione del tornado alla scena. A questo punto, decidiamo che il sottocampionamento della risoluzione dello strumento di shadowing delle tempeste dipende in modo dinamico dalla frequenza fotogrammi che stiamo ottenendo.

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

Infine, renderizziamo il tornado allo schermo utilizzando un algoritmo sal2x semplificato (per evitare l'aspetto a blocchi) @line 1107 in stormTest.). Ciò significa che peggio ancora finiamo per avere un tornado più sfocato, ma almeno funziona senza sottrarre il controllo all'utente.

La fase successiva dell'ottimizzazione richiede un'analisi approfondita dell'algoritmo. Il fattore computazionale determinante nello shabby è l'iterazione che viene eseguita su ogni pixel per cercare di approssimare la distanza della funzione di superficie, ovvero il numero di iterazioni del loop di raymarching. Utilizzando gradini più grandi, potremmo ottenere una stima della superficie dei tornado con meno iterazioni mentre eravamo all'esterno della sua superficie nuvolosa. All'interno, riducevamo la dimensione dei passaggi per garantire la precisione e poter combinare i valori per creare l'effetto nebbiolina. Anche la creazione di un cilindro di delimitazione per ottenere una stima della profondità del raggio gettato ha permesso di ottenere una buona velocità.

Il problema successivo consiste nell'assicurarsi che lo strumento di colore venisse eseguito su schede video diverse. Abbiamo eseguito dei test ogni volta e abbiamo iniziato a farci un'idea del tipo di problemi di compatibilità che potremmo incontrare. Il motivo per cui non potevamo fare meglio dell'intuizione è che non sempre siamo riusciti a ottenere informazioni di debug valide per gli errori. Uno scenario tipico è semplicemente un errore della GPU con poco altro da fare o persino un arresto anomalo del sistema.

I problemi di compatibilità cross-video avevano soluzioni simili: assicurati che vengano inserite costanti statiche del tipo di dati preciso definito, IE: 0,0 per float e 0 per int. Fai attenzione quando scrivi funzioni più lunghe; è preferibile suddividere le cose in più funzioni più semplici e variabili provvisorie perché i compilatori sembravano non gestire correttamente determinati casi. Assicurati che le texture abbiano tutte una potenza di 2, non troppo grandi e in ogni caso prestano "attenzione" quando cerchi i dati delle texture in un loop.

I problemi più grandi che abbiamo avuto in termini di compatibilità riguardavano l'effetto luminoso della tempesta. Abbiamo usato una trama preconfezionata avvolta attorno al tornado in modo da poter colorare i suoi wisp. È stato un effetto meraviglioso e ha reso facile fondere il tornado con i colori della scena, ma ci è voluto molto tempo per provare a far correre su altre piattaforme.

tromba d&#39;aria

Il sito web per dispositivi mobili

L'esperienza mobile non poteva essere una traduzione diretta della versione desktop, perché i requisiti di tecnologia ed elaborazione erano troppo gravosi. Dovevamo creare qualcosa di nuovo, mirato all'utente mobile specifico.

Abbiamo pensato che sarebbe stato bello poter utilizzare la funzionalità Photo Booth di Carnival dal computer come applicazione web mobile che utilizzava la fotocamera del dispositivo mobile dell'utente. Qualcosa che non avevamo mai visto fare finora.

Per dare un tocco speciale, abbiamo codificato le trasformazioni 3D in CSS3. Grazie al collegamento con il giroscopio e l'accelerometro, siamo riusciti ad aggiungere molta profondità all'esperienza. Il sito risponde al modo in cui tieni premuto, ti muovi e guardi il telefono.

Al momento della stesura di questo articolo, abbiamo pensato che potrebbe interessarti darti alcuni suggerimenti su come gestire senza problemi il processo di sviluppo mobile. Eccoli. Scopri cosa puoi imparare!

Suggerimenti utili per il mobile

Il preloader è qualcosa di necessario, non da evitare. Sappiamo che a volte succede la seconda volta. Questo perché devi continuare a mantenere l'elenco dei contenuti precaricati man mano che il progetto cresce. La cosa peggiore è che non è molto chiaro come dovresti calcolare l'avanzamento del caricamento se stai estraendo risorse diverse e molte di esse contemporaneamente. È qui che diventa utile la nostra classe astratta personalizzata e molto generica "Task". La sua idea principale è consentire una struttura nidificata all'infinito in cui un'attività può avere le proprie attività secondarie, che possono avere le proprie ecc. Inoltre, ogni attività calcola il proprio avanzamento rispetto all'avanzamento delle relative attività secondarie (ma non a quello del padre). Rendendo i valori MainPreloadTask, AssetPreloadTask e TemplatePreFetchTask da Task, abbiamo creato una struttura simile alla seguente:

Precaricatore

Grazie a questo approccio e alla classe Tasks, possiamo conoscere facilmente l'avanzamento globale (MainPreloadTask) o solo l'avanzamento degli asset (AssetPreloadTask) o l'avanzamento del caricamento dei modelli (TemplatePreFetchTask). Avanzamento uniforme di un determinato file. Per vedere come si fa, dai un'occhiata alla classe Tasks in /m/JavaScripts/raw/util/Task.js e alle implementazioni effettive in /m/javascripts/preloading/task. Ad esempio, questo è un estratto del modo in cui impostiamo la classe /m/javascripts/preloading/task/MainPreloadTask.js, che è il nostro wrapper di precaricamento finale:

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

      }
    }
  })
]);

Nella classe /m/JavaScripts/preloading/task/subtask/AssetPreloadTask.js, oltre a indicare come comunica con il MainPreloadTask (tramite l'implementazione di Task condivisa), vale anche la pena notare il modo in cui carichiamo gli asset che dipendono dalla piattaforma. In sostanza, abbiamo quattro tipi di immagini. Standard per dispositivi mobili (.ext, dove ext è l'estensione dei file, tipicamente .png o .jpg), retina mobile (-2x.ext), standard per tablet (-tab.ext) e retina per tablet (-tab-2x.ext). Invece di eseguire il rilevamento nel MainPreloadTask e impostare come hardcoded quattro array di asset, diciamo semplicemente il nome e l'estensione dell'asset da precaricare e se quest'ultimo dipende dalla piattaforma (adattabile = vero / falso). Successivamente, AssetPreloadTask genererà il nome del file:

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

Più in basso nella catena delle classi, il codice effettivo che esegue il precaricamento degli asset ha il seguente aspetto (/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);
}

Tutorial: Photo Booth HTML5 (iOS6/Android)

Durante lo sviluppo di OZ Mobile, abbiamo scoperto che trascorriamo molto tempo a giocare con la cabina fotografica invece di lavorare :D Questo è stato semplicemente perché è divertente. Perciò abbiamo creato una demo con cui potrai sperimentare.

Cabina fotografica mobile
Cabina per foto mobile

Puoi vedere una demo dal vivo qui (eseguila sul tuo iPhone o smartphone Android):

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

Per la configurazione, è necessaria un'istanza dell'applicazione Google App Engine senza costi in cui poter eseguire il backend. Il codice del front-end non è complesso, ma ci sono un paio di possibili trucchi. Esaminiamoli ora:

  1. Tipo di file immagine consentito Vogliamo che gli utenti possano caricare solo immagini (dal momento che si tratta di una cabina per foto, non di una cabina video). In teoria, puoi specificare il filtro in HTML nel seguente modo: input id="fileInput" class="fileInput" type="file" name="file" accept="image/*" Tuttavia, sembra funzionare solo su iOS, quindi dobbiamo aggiungere un ulteriore controllo rispetto all'espressione regolare una volta selezionato un file:
   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. Annullamento di un caricamento o della selezione di un file Un'altra incoerenza che abbiamo notato durante il processo di sviluppo riguarda il modo in cui dispositivi diversi inviano una notifica relativa alla selezione di un file annullata. I telefoni e i tablet iOS non fanno nulla e non ricevono alcuna notifica. Non è quindi necessaria alcuna azione speciale in questo caso. Tuttavia, i telefoni Android attivano comunque la funzione add(), anche se non viene selezionato alcun file. Ecco come soddisfare questa richiesta:
    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();            
    }
    }

Il resto funziona in modo piuttosto fluido su tutte le piattaforme. Buon divertimento!

Conclusione

Date le enormi dimensioni di Find Your Way To Oz e l'ampio mix di tecnologie coinvolte, in questo articolo siamo riusciti a trattare solo alcuni degli approcci che abbiamo utilizzato.

Se vuoi esplorare l'intero enchilada, non esitare a dare un'occhiata al codice sorgente completo di Find Your Way To Oz a questo link.

Crediti

Fai clic qui per l'elenco completo dei crediti

Riferimenti