Introduzione
"Trova la strada per Oz" è un nuovo esperimento di Google Chrome portato sul web da Disney. Ti consente di fare un viaggio interattivo in un circo del Kansas, che ti porta nella terra di Oz dopo che sei stato travolto da una tempesta.
Il nostro obiettivo era combinare la ricchezza del cinema con le funzionalità tecniche del browser per creare un'esperienza immersiva e divertente con cui gli utenti potessero creare un legame forte.
Il lavoro è un po' troppo grande per essere riassunto in questo articolo, quindi abbiamo approfondito l'argomento e abbiamo estratto alcuni capitoli della storia della tecnologia che riteniamo interessanti. Nel corso del tempo abbiamo estratto alcuni tutorial specifici di difficoltà crescente.
Molte persone hanno lavorato duramente per rendere possibile questa esperienza: troppe per essere elencate qui. Visita il sito per controllare la pagina dei riconoscimenti nella sezione del menu e scoprire la storia completa.
Uno sguardo dietro le quinte
Trova la strada per Oz su computer è un mondo immersivo ricco. 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, shader personalizzati ed elementi animati DOM che utilizzano le funzionalità CSS3. Inoltre, l'API getUserMedia (WebRTC) per le esperienze interattive consente all'utente di aggiungere la propria immagine direttamente dalla webcam e da WebAudio per l'audio 3D.
Ma la magia di un'esperienza tecnologica come questa è il modo in cui si combinano. Questa è anche una delle principali sfide: come combinare effetti visivi ed elementi interattivi in una 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 determinato momento.
Per risolvere il problema degli effetti visivi interconnessi e dell'ottimizzazione, abbiamo fatto un uso intensivo di un pannello di controllo che acquisiva tutte le impostazioni pertinenti che stavamo esaminando in quel momento. La scena poteva essere regolata in tempo reale nel browser per qualsiasi impostazione, dalla luminosità alla profondità di campo, alla gamma e così via. Chiunque poteva provare a modificare i valori dei parametri significativi dell'esperienza e contribuire a scoprire cosa funzionava meglio.
Prima di condividere il nostro segreto, vogliamo avvisarti che potrebbe verificarsi un arresto anomalo, proprio come se tu smanettassi nel motore di un'auto. Assicurati di non avere nulla di importante attivo, poi visita l'URL principale del sito e aggiungi ?debug=on all'indirizzo. Attendi il caricamento del sito e, una volta dentro, premi il tasto Ctrl-I
. Sul lato destro viene visualizzato un menu a discesa. Se deselezioni l'opzione "Esci dal percorso della videocamera", puoi utilizzare i tasti A, W, S, D e il mouse per muoverti liberamente nello spazio.

Non esamineremo tutte le impostazioni, ma ti invitiamo a fare esperimenti: le chiavi rivelano impostazioni diverse in scene diverse. Nella sequenza finale della tempesta è presente un'altra chiave: Ctrl-A
con la quale puoi attivare/disattivare la riproduzione dell'animazione e volare. 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 di tempesta. Dai un'occhiata in giro e scatta foto di cartoline come quella di seguito.

Per farlo e assicurarci che fosse abbastanza flessibile per le nostre esigenze, abbiamo utilizzato una fantastica libreria chiamata dat.gui (vedi qui per un tutorial precedente su come utilizzarla). Ci ha permesso di modificare rapidamente le impostazioni esposte ai visitatori del sito.
Un po' come la pittura digitale
In molti film e animazioni Disney classici, la creazione delle scene significava combinare diversi livelli. C'erano livelli di live action, animazione a cella, persino set fisici e livelli superiori creati dipingendo su vetro: una tecnica chiamata matte painting.
Per molti versi, la struttura dell'esperienza che abbiamo creato è simile, anche se alcuni dei "livelli" sono molto più di semplici immagini statiche. In realtà, influiscono sull'aspetto delle cose in base a calcoli più complessi. Tuttavia, almeno a livello generale, abbiamo a che fare con visualizzazioni composite una sopra l'altra. Nella parte superiore è visibile un livello dell'interfaccia utente, con una scena 3D sottostante, composta a sua volta da diversi componenti di scena.
Il livello di interfaccia superiore è stato creato utilizzando DOM e CSS 3, il che significa che le interazioni potevano essere modificate in molti modi indipendentemente dall'esperienza 3D, con la comunicazione tra i due in base a un elenco selezionato di eventi. Questa comunicazione utilizza il router Backbone + l'evento HTML5 onHashChange che controlla l'area da animare in entrata/uscita. (project source: /develop/coffee/router/Router.coffee).
Tutorial: sprite sheet e supporto Retina
Una tecnica di ottimizzazione divertente che abbiamo utilizzato per l'interfaccia è stata combinare le numerose immagini di overlay dell'interfaccia in un unico file PNG per ridurre le richieste al server. In questo progetto l'interfaccia era composta da oltre 70 immagini (senza contare le texture 3D) caricate tutte in anticipo per ridurre la latenza del sito web. Puoi vedere lo sprite sheet in tempo reale 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'utilizzo degli spritesheet e su come utilizzarli per i dispositivi Retina per ottenere un'interfaccia il più nitida e ordinata possibile.
Creazione di spritesheet
Per creare gli spritesheet abbiamo utilizzato TexturePacker, che genera output in qualsiasi formato necessario. In questo caso abbiamo eseguito l'esportazione in EaselJS, che è molto pulito e avrebbe potuto essere utilizzato anche per creare sprite animati.
Utilizzare la sprite sheet generata
Dopo aver creato lo sprite sheet, dovresti visualizzare un file JSON come questo:
{
"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 fa riferimento all'URL dello sprite sheet
- frames sono le coordinate di ogni elemento dell'interfaccia utente [x, y, width, height]
- le animazioni sono i nomi di ogni risorsa
Tieni presente che abbiamo utilizzato le immagini ad alta densità per creare lo sprite sheet, poi abbiamo creato la versione normale ridimensionandola a metà delle dimensioni.
Riepilogo
Ora che abbiamo tutto pronto, ci serve solo 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 utilizzarlo:
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.
La pipeline di contenuti 3D
L'esperienza dell'ambiente è configurata su un livello WebGL. Quando pensi a una scena 3D, una delle domande più difficili è come fare in modo da creare contenuti che offrano il massimo potenziale espressivo dal punto di vista della modellazione, dell'animazione e degli effetti. In molti modi, al centro di questo problema c'è la pipeline dei contenuti: una procedura concordata da seguire per creare contenuti per la scena 3D.
Volevamo creare un mondo mozzafiato, quindi avevamo bisogno di una procedura solida che consentisse agli artisti 3D di realizzarlo. Dovrebbero avere la massima libertà espressiva possibile nel software di modellazione e animazione 3D e dovremmo eseguire il rendering sullo schermo tramite codice.
Ci occupiamo di questo tipo di problema da un po' di tempo perché, in passato, ogni volta che creavamo un sito 3D, riscontravamo limitazioni negli strumenti che potevamo utilizzare. Abbiamo creato questo strumento, chiamato 3D Librarian, nell'ambito di una ricerca interna. Ed era quasi pronta per essere applicata a un lavoro reale.
Questo strumento ha una certa storia: originariamente era per Flash e consentiva di importare una grande scena Maya come un unico file compresso ottimizzato per lo scompattamento in fase di esecuzione. Il motivo per cui era ottimale era perché comprimeva efficacemente la scena nella stessa struttura di dati manipolata durante il rendering e l'animazione. Quando viene caricato, il file deve essere sottoposto a un'analisi molto ridotta. Lo scompattamento in Flash è stato abbastanza rapido perché il file era in formato AMF, che Flash poteva scompattare in modo nativo. L'utilizzo dello stesso formato in WebGL richiede un po' più di lavoro sulla CPU. Infatti, abbiamo dovuto ricreare un livello di codice JavaScript per l'estrazione dei dati, che essenzialmente decomprime i file e ricrea le strutture di dati necessarie per il funzionamento di WebGL. Lo scompattamento dell'intera scena 3D è un'operazione leggermente impegnativa per la CPU: lo scompattamento della scena 1 in Find Your Way To Oz richiede circa 2 secondi su una macchina di fascia media o alta. Pertanto, questa operazione viene eseguita utilizzando la tecnologia Web Workers al momento della "configurazione della scena" (prima del lancio effettivo della scena), in modo da non bloccare l'esperienza per l'utente.
Questo pratico strumento può importare la maggior parte della scena 3D: modelli, texture, animazioni degli ossa. Creerai un singolo file della libreria che potrà essere caricato dal motore 3D. Inserisci tutti i modelli di cui hai bisogno nella scena all'interno di questa libreria e, voilà, generali nella scena.
Il problema era che ora dovevamo fare i conti con WebGL, il nuovo arrivato. Era un cliente piuttosto esigente: stavamo definendo lo standard per le esperienze 3D basate su browser. Per questo motivo, abbiamo creato un livello JavaScript ad hoc che prende i file di scene 3D compressi di 3D Librarian e li traduce correttamente in un formato comprensibile da WebGL.
Tutorial: Let There Be Wind
Un tema ricorrente in "Find Your Way To Oz" era il vento. Un filo della trama è strutturato come un crescendo di vento.
La prima scena del carnevale è relativamente tranquilla. Mentre passa da una scena all'altra, l'utente percepisce un vento progressivamente più forte, che culmina nella scena finale, la tempesta.
Per questo motivo, era importante creare un effetto vento immersivo.
Per creare questa animazione, abbiamo riempito le tre scene di carnevale con oggetti morbidi, quindi presumibilmente influenzati dal vento, come tende, bandiere, la superficie della cabina fotografica e il pallone stesso.

Al giorno d'oggi, i giochi per computer sono in genere basati su un motore fisico di base. Pertanto, quando è necessario simulare un oggetto morbido 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. In Australia, quindi, abbiamo dovuto trovare un modo per creare l'effetto del vento, senza simularlo.
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 in che misura il vertice doveva essere influenzato dal vento. Quindi, questa sensibilità al vento specificata degli oggetti 3D. Poi abbiamo dovuto creare il vento stesso.
Per farlo, abbiamo generato un'immagine contenente rumore di Perlin. Questa immagine ha lo scopo di coprire una determinata "area di vento". Pertanto, un buon modo per immaginarla è pensare a un'immagine di rumore simile a una nuvola sovrapposta a una determinata area rettangolare della scena 3D. Ogni pixel, valore di livello di grigio, di questa immagine specifica l'intensità del vento in un determinato momento nell'area 3D "che lo circonda".
Per produrre l'effetto vento, l'immagine viene spostata, nel tempo, a velocità costante, in una direzione specifica, ovvero la direzione del vento. Per assicurarci che l'effetto "zona ventosa" non influisca su tutto ciò che è presente nella scena, avvolgiamo l'immagine del vento intorno ai bordi, confinandola nell'area di effetto.
Un semplice tutorial sul vento 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 piatto semplice e con texture. Ogni filo d'erba verrà rappresentato semplicemente con un cono 3D capovolto.

Ecco come creare questa semplice scena in Three.js utilizzando CoffeeScript.
Innanzitutto, configureremo Three.js e lo collegheremo a una videocamera, un controller del mouse e un po' 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 alle funzioni initGrass e initTerrain completano la scena rispettivamente con l'erba e il 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 erba. Aggiungiamo un po' di casualità a ogni posizione dell'erba, in modo che non siano allineate come soldati, il che sarebbe strano.
Questo terreno è solo un piano orizzontale, posizionato alla base dei fili 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 )
Finora abbiamo semplicemente creato una scena Three.js e aggiunto un po' d'erba, costituita da coni invertiti generati proceduralmente, e un semplice terreno.
Finora niente di speciale.
Ora è il momento di iniziare ad aggiungere il vento. Per prima cosa, vogliamo incorporare le informazioni sulla sensibilità al vento nel modello 3D dell'erba.
Integreremo queste informazioni come attributo personalizzato per ogni vertice del modello 3D dell'erba. Utilizzeremo la regola che prevede che l'estremità inferiore del modello dell'erba (la punta del cono) abbia una sensibilità pari a zero, in quanto è attaccata al suolo. La parte superiore del modello dell'erba (base del cono) ha la massima sensibilità al vento, in quanto è la parte più lontana dal suolo.
Ecco come viene ricodificata la funzione instanceGrass per aggiungere la sensibilità al vento come attributo personalizzato per il modello 3D dell'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 utilizziamo un materiale personalizzato, windMaterial, anziché MeshPhongMaterial, che usavamo in precedenza. WindMaterial avvolge WindMeshShader che vedremo tra un minuto.
Pertanto, il codice in instanceGrass esegue un ciclo per tutti i vertici del modello dell'erba e per ogni vertice aggiunge un attributo del vertice personalizzato, chiamato windFactor. Questo parametro è impostato su 0 per la parte inferiore del modello dell'erba (dove dovrebbe toccare il terreno) e su 1 per la parte superiore del modello dell'erba.
L'altro ingrediente di cui abbiamo bisogno è aggiungere il vento reale alla scena. Come discusso, utilizzeremo il rumore di Perlin per questo. Genereremo proceduralmente una texture di rumore di Perlin.
Per chiarezza, assegneremo questa texture al terreno stesso, al posto della texture verde precedente. In questo modo sarà più facile capire cosa sta succedendo con il vento.
Pertanto, questa texture di rumore di Perlin coprirà spazialmente l'estensione del nostro terreno e ogni pixel della texture specificherà l'intensità del vento nell'area del terreno in cui si trova. Il rettangolo del terreno sarà la nostra "area di vento".
Il rumore di Perlin viene generato proceduralmente tramite uno shader chiamato NoiseShader. Questo shader utilizza algoritmi di rumore semplice 3D da: https://github.com/ashima/webgl-noise . La versione WebGL è stata presa testualmente da uno dei sample di Three.js di MrDoob, all'indirizzo http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.
NoiseShader prende un insieme di parametri di tempo, scala e offset come uniformi e produce una bella 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) }
...
Utilizzeremo questo shader per eseguire il rendering del rumore di Perlin in una texture. Questo viene eseguito 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 configura noiseMap come target di rendering di Three.js, lo equipaggia con NoiseShader e poi lo esegue con una fotocamera ortografica, in modo da evitare distorsioni prospettiche.
Come discusso, ora utilizzeremo questa texture anche come texture di rendering principale per il terreno. Questo non è realmente necessario per il funzionamento dell'effetto vento stesso. Tuttavia, è utile per comprendere meglio visivamente cosa sta succedendo con la generazione eolica.
Ecco la funzione initTerrain ristrutturata, che utilizza la mappa di rumore 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 implementato la texture del vento, diamo un'occhiata a WindMeshShader, che si occupa di deformare i modelli dell'erba in base al vento.
Per creare questo shader, abbiamo iniziato dallo shader MeshPhongMaterial di Three.js standard e lo abbiamo modificato. Questo è un buon modo rapido e pratico per iniziare a utilizzare uno shader che funzioni, senza dover ricominciare da zero.
Non copieremo l'intero codice dello shader qui (non esitare a esaminarlo nel file di codice sorgente), perché la maggior parte sarebbe una replica dello shader MeshPhongMaterial. Diamo un'occhiata alle parti modificate relative al vento nel 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;
Questo shader calcola prima 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 con rumore di Perlin.
Questo valore vWindForce viene composto con l'attributo personalizzato windFactor specifico del vertice, discusso sopra, per calcolare la quantità di deformazione necessaria per il vertice. Abbiamo anche un parametro globale windScale per controllare l'intensità complessiva del vento e un vettore windDirection che specifica in quale direzione deve avvenire la deformazione del vento.
In questo modo, viene creata una deformazione basata sul vento dei nostri pezzi d'erba. Tuttavia, non abbiamo ancora finito. Al momento, questa deformazione è statica e non trasmette l'effetto di un'area ventosa.
Come abbiamo accennato, dovremo far scorrere la trama del rumore nel tempo, nell'area del vento, in modo che il vetro possa ondeggiare.
Questo viene fatto spostando nel tempo l'uniforme vOffset passata a NoiseShader. Si tratta di un parametro vec2 che ci consente di specificare l'offset del rumore in una determinata direzione (la direzione del vento).
Lo facciamo nella funzione render, che viene chiamata in 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
...
E questo è tutto. Abbiamo appena creato una scena con "erba procedurale" interessata dal vento.
Aggiunta di polvere al mix
Ora aggiungiamo un po' di pepe alla scena. Aggiungiamo un po' di polvere per rendere la scena più interessante.

Dopotutto, la polvere dovrebbe essere influenzata dal vento, quindi ha perfettamente senso che sia presente nella nostra scena con vento.
La polvere viene 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. Tieni presente che ognuno di questi è dotato di uno speciale WindParticleShader.
Ora, a ogni frame, sposteremo un po' le particelle utilizzando 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, sposteremo ogni posizione della particella in base al vento. Questo viene eseguito in WindParticleShader. Nello shader vertex, in particolare.
Il codice di questo shader è una versione modificata di ParticleMaterial di Three.js e questo è il suo nucleo:
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 shader vertex non è molto diverso da quello che avevamo per la deformazione dell'erba in base al vento. Prende come input la trama del rumore di Perlin e, a seconda della posizione del mondo di polvere, cerca un valore vWindForce nella trama del rumore. Poi utilizza questo valore per modificare la posizione della particella di polvere.
Riders On The Storm
La scena più audace delle nostre scene WebGL è probabilmente l'ultima, che puoi vedere se fai clic sul pallone fino all'occhio del tornado per raggiungere la fine del tuo viaggio nel sito, oltre a un video esclusivo della prossima uscita.

Quando abbiamo creato questa scena, sapevamo che dovevamo avere una funzionalità centrale per l'esperienza che fosse efficace. Il tornado in rotazione fungerà da fulcro e gli altri contenuti da strati che modellano questa funzionalità per creare un effetto drammatico. Per farlo, abbiamo creato l'equivalente di un set cinematografico intorno a questo strano shader.
Abbiamo utilizzato un approccio misto per creare la composizione realistica. Alcuni erano trucchi visivi come forme di luce per creare un effetto bagliore dell'obiettivo o gocce di pioggia animate come livelli sopra la scena che stai guardando. In altri casi, abbiamo disegnato superfici piane in modo che sembrassero muoversi, come i livelli di nuvole a bassa quota che si muovono in base a un codice del sistema di particelle. I detriti che orbitano attorno al tornado erano invece rappresentati da livelli di una scena 3D ordinati in modo da muoversi davanti e dietro il tornado.
Il motivo principale per cui abbiamo dovuto creare la scena in questo modo era assicurarci di avere una GPU sufficiente per gestire lo shader del tornado in equilibrio con gli altri effetti che stavamo applicando. Inizialmente abbiamo riscontrato grossi problemi di bilanciamento della GPU, ma in seguito questa scena è stata ottimizzata ed è diventata più leggera delle scene principali.
Tutorial: shader Tempesta
Per creare la sequenza finale della tempesta sono state combinate molte tecniche diverse, ma il fulcro di questo lavoro è stato uno shader GLSL personalizzato che sembra un tornado. Abbiamo provato molte tecniche diverse, dagli shader vertici per creare vortici geometrici interessanti alle animazioni basate su particelle e persino animazioni 3D di forme geometriche contorte. Nessuno degli effetti sembrava ricreare la sensazione di un tornado o richiedere troppo in termini di elaborazione.
Alla fine, la risposta ci è stata fornita da un progetto completamente diverso. Un progetto parallelo che coinvolge giochi per la scienza per mappare il cervello del topo dell'Istituto Max Planck (brainflight.org) ha generato effetti visivi interessanti. Eravamo riusciti a creare filmati dell'interno di un neurone di topo utilizzando uno shader volumetrico personalizzato.

Abbiamo scoperto che l'interno di una cellula cerebrale assomiglia un po' alla tromba di un tornado. Poiché utilizzavamo una tecnica volumetrica, sapevamo di poter visualizzare questo shader da tutte le direzioni nello spazio. Potremmo impostare il rendering dell'shader in modo che si combini con la scena di tempesta, in particolare se è inserito tra strati di nuvole e sopra uno sfondo drammatico.
La tecnica dello shader prevede un trucco che utilizza fondamentalmente un singolo shader GLSL per eseguire il rendering di un intero oggetto con un algoritmo di rendering semplificato chiamato ray marching con un campo di distanza. In questa tecnica viene creato un pixel shader che stima la distanza più vicina a una superficie per ogni punto dello schermo.
Un buon riferimento all'algoritmo è disponibile nella panoramica di iq: Rendering Worlds With Two Triangles - Iñigo Quilez. Inoltre, nella galleria di shader su glsl.heroku.com puoi trovare molti esempi di questa tecnica su cui puoi fare esperimenti.
Il cuore dello shader inizia con la funzione principale, imposta le trasformazioni della fotocamera ed entra in un loop che valuta ripetutamente la distanza da una superficie. La chiamata RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ) è il punto in cui viene eseguito il calcolo principale del marching ray.
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 avanziamo nella forma del tornado, aggiungiamo regolarmente contributi di colore al valore di colore finale del pixel, nonché contributi all'opacità lungo il raggio. In questo modo, la texture del tornado assume una qualità morbida a più livelli.
L'aspetto fondamentale successivo del tornado è la forma stessa, che viene creata componendo una serie di funzioni. Si tratta inizialmente di un cono, composto con rumore per creare un'irregolarità organica, che viene successivamente attorcigliato lungo il suo asse principale e ruotato 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 necessario per creare questo tipo di shader è complicato. Oltre ai problemi relativi all'astrazione delle operazioni che stai creando, esistono gravi problemi di ottimizzazione e compatibilità multipiattaforma che devi rilevare e risolvere prima di poter utilizzare il lavoro in produzione.
La prima parte del problema: ottimizzare questo shader per la nostra scena. Per risolvere il problema, abbiamo dovuto adottare un approccio "sicuro" nel caso in cui lo shader fosse troppo pesante. Per farlo, abbiamo composto lo shader del tornado a una risoluzione campionata diversa dal resto della scena. Questo è tratto dal file stormTest.coffee (sì, era un test!).
Iniziamo con un renderTarget che corrisponda alla larghezza e all'altezza della scena in modo da poter avere l'indipendenza della risoluzione dello shader tornado dalla scena. Poi decidiamo il downsampling della risoluzione dell'shader tempesta in base alla 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, rendiamo il tornado sullo schermo utilizzando un algoritmo sal2x semplificato (per evitare l'aspetto a blocchi) nella riga 1107 di stormTest.coffee. Ciò significa che, nel peggiore dei casi, avremo un tornado più sfocato, ma almeno funzionerà senza togliere il controllo all'utente.
Il passaggio di ottimizzazione successivo richiede un'analisi approfondita dell'algoritmo. Il fattore di calcolo principale nello shader è l'iterazione eseguita su ogni pixel per provare ad approssimare la distanza della funzione di superficie: il numero di iterazioni del loop di raymarching. Utilizzando un passo più grande, abbiamo potuto ottenere una stima della superficie del tornado con meno iterazioni, mentre eravamo al di fuori della sua superficie nuvolosa. All'interno, ridurremo la dimensione del passo per la precisione e per poter combinare i valori in modo da creare l'effetto nebbia. Anche la creazione di un cilindro delimitante per ottenere una stima della profondità per il raggio proiettato ha dato un buon aumento di velocità.
Il problema successivo era assicurarsi che questo shader potesse funzionare su schede video diverse. Ogni volta abbiamo eseguito alcuni test e abbiamo iniziato a capire il tipo di problemi di compatibilità che potremmo riscontrare. Il motivo per cui non siamo riusciti a fare molto meglio dell'intuizione è che non è sempre stato possibile ottenere buone informazioni di debug sugli errori. Uno scenario tipico è un semplice errore della GPU con poche altre informazioni o addirittura un arresto anomalo del sistema.
I problemi di compatibilità tra schede video avevano soluzioni simili: assicurati che le costanti statiche siano inserite con il tipo di dati preciso come definito, ad esempio 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 intermedie perché i compilatori non sembravano gestire correttamente alcuni casi. Assicurati che le texture siano tutte potenze di 2, non troppo grandi e, in ogni caso, fai attenzione quando cerchi i dati delle texture in un loop.
I problemi di compatibilità più grandi sono stati causati dall'effetto di illuminazione della tempesta. Abbiamo utilizzato una texture predefinita avvolta intorno al tornado per poter colorare le sue volute. Era un effetto stupendo e consentiva di fondere facilmente il tornado con i colori della scena, ma ci è voluto molto tempo per provare a farlo funzionare su altre piattaforme.

Il sito web mobile
L'esperienza mobile non poteva essere una semplice traduzione della versione desktop perché i requisiti di tecnologia ed elaborazione erano troppo elevati. Dovevamo creare qualcosa di nuovo, che avesse come target specifico gli utenti di dispositivi mobili.
Abbiamo pensato che sarebbe stato bello avere la Cabina fotografica di Carnevale da computer come applicazione web mobile che utilizza la fotocamera del dispositivo mobile dell'utente. Qualcosa che non avevamo mai visto finora.
Per aggiungere un tocco in più, abbiamo codificato le trasformazioni 3D in CSS3. Avendolo collegato a giroscopio e accelerometro, abbiamo potuto aggiungere molta profondità all'esperienza. Il sito risponde al modo in cui tieni, muovi e guardi lo smartphone.
Durante la stesura di questo articolo, abbiamo pensato che sarebbe stato utile darti alcuni suggerimenti su come eseguire senza problemi il processo di sviluppo mobile. Eccole. Dai un'occhiata e scopri cosa puoi imparare.
Suggerimenti e trucchi per i dispositivi mobili
Il preloader è un elemento necessario, non qualcosa da evitare. Sappiamo che a volte capita. Questo è dovuto principalmente al fatto che devi mantenere aggiornato l'elenco degli elementi che precarichi man mano che il progetto cresce. Inoltre, non è molto chiaro come calcolare l'avanzamento del caricamento se stai estraendo risorse diverse e molte contemporaneamente. È qui che diventa utile la nostra classe astratta personalizzata e molto generica "Task". L'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 attività secondarie (ma non all'avanzamento dell'attività principale). Facendo derivare tutte le attività MainPreloadTask, AssetPreloadTask e TemplatePreFetchTask da Task, abbiamo creato una struttura simile alla seguente:

Grazie a questo approccio e alla classe Task, possiamo facilmente conoscere l'avanzamento globale (MainPreloadTask), solo l'avanzamento delle risorse (AssetPreloadTask) o l'avanzamento del caricamento dei modelli (TemplatePreFetchTask). Anche l'avanzamento di un determinato file. Per scoprire come funziona, dai un'occhiata alla classe Task in /m/javascripts/raw/util/Task.js e alle implementazioni effettive delle attività in /m/javascripts/preloading/task. Ad esempio, questo è un estratto della configurazione della classe /m/javascripts/preloading/task/MainPreloadTask.js, che è il nostro wrapper di precaricamento definitivo:
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 notare come comunica con MainPreloadTask (tramite l'implementazione di Task condivisa), è anche importante notare come carichiamo gli asset che dipendono dalla piattaforma. Fondamentalmente, abbiamo quattro tipi di immagini. Standard per dispositivi mobili (.ext, dove ext è l'estensione del file, in genere .png o .jpg), retina per dispositivi mobili (-2x.ext), standard per tablet (-tab.ext) e retina per tablet (-tab-2x.ext). Anziché eseguire il rilevamento in MainPreloadTask e codificare in modo rigido quattro array di risorse, dichiariamo semplicemente il nome e l'estensione della risorsa da precaricare e se la risorsa è dipendente dalla piattaforma (responsive = true / false). 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ù avanti nella catena di classi, il codice effettivo che esegue il precaricamento delle risorse è il seguente (/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 (iOS 6/Android)
Durante lo sviluppo di OZ mobile, abbiamo scoperto di passare molto tempo a giocare con la cabina fotografica invece di lavorare :D semplicemente perché è divertente. Per questo abbiamo creato una demo che puoi provare.

Puoi guardare una demo dal vivo qui (eseguirla sul tuo iPhone o smartphone Android):
http://u9html5rocks.appspot.com/demos/mobile_photo_booth
Per configurarlo, devi disporre di un'istanza dell'applicazione Google App Engine senza costi in cui puoi eseguire il backend. Il codice frontend non è complesso, ma ci sono un paio di possibili problemi. Vediamoli insieme:
- Tipo di file immagine consentito
Vogliamo che le persone possano caricare solo immagini (poiché si tratta di una cabina fotografica, non di una videocabina). In teoria, puoi semplicemente specificare il filtro in HTML, come segue:
input id="fileInput" class="fileInput" type="file" name="file" accept="image/*"
Tuttavia, sembra che funzioni solo su iOS, quindi dobbiamo aggiungere un controllo aggiuntivo rispetto alla RegExp dopo aver 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();
}
}
});
- Annullamento di un caricamento o della selezione di un file Un'altra incoerenza che abbiamo notato durante il processo di sviluppo è il modo in cui i diversi dispositivi notificano l'annullamento della selezione di un file. I telefoni e i tablet iOS non fanno nulla, non inviano alcuna notifica. Pertanto, non è necessaria alcuna azione speciale per questo caso, tuttavia, gli smartphone Android attivano comunque la funzione add(), anche se non è selezionato alcun file. Ecco come fare:
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 piuttosto bene su tutte le piattaforme. Divertiti!
Conclusione
Date le dimensioni enormi di Trova la strada per Oz e l'ampia combinazione di diverse tecnologie coinvolte, in questo articolo abbiamo potuto trattare solo alcuni degli approcci che abbiamo utilizzato.
Se vuoi esplorare l'intera esperienza, dai un'occhiata al codice sorgente completo di Find Your Way To Oz a questo link.
Crediti
Fai clic qui per visualizzare l'elenco completo dei riconoscimenti
Riferimenti
- CoffeeScript - http://coffeescript.org/
- Backbone.js - http://backbonejs.org/
- Three.js - http://mrdoob.github.com/three.js/
- Max Planck Institute (brainflight.org) - http://brainflight.org/