Pengantar
“Find Your Way to Oz” adalah Eksperimen Google Chrome baru yang dihadirkan ke web oleh Disney. Dengan alat ini, Anda dapat melakukan perjalanan interaktif melalui sirkus Kansas, yang akan membawa Anda ke negeri Oz setelah tersapu badai besar.
Tujuan kami adalah menggabungkan kekayaan sinema dengan kemampuan teknis browser untuk menciptakan pengalaman yang menyenangkan dan imersif yang dapat dinikmati pengguna.
Tugas ini terlalu besar untuk dibahas secara menyeluruh dalam artikel ini, jadi kami telah menyelami dan mengambil beberapa bab dari kisah teknologi yang menurut kami menarik. Selama prosesnya, kami telah membuat beberapa tutorial yang difokuskan dengan tingkat kesulitan yang meningkat.
Banyak orang yang bekerja keras untuk mewujudkan pengalaman ini: terlalu banyak untuk dicantumkan di sini. Buka situs untuk melihat halaman kredit di bagian menu guna membaca cerita selengkapnya.
Prinsip Dasar
Find Your Way to Oz di desktop adalah dunia imersif yang kaya. Kami menggunakan 3D dan beberapa lapisan efek terinspirasi pembuatan film tradisional yang digabungkan untuk menciptakan adegan yang hampir realistis. Teknologi yang paling menonjol adalah WebGL dengan Three.js, shader buatan kustom, dan elemen animasi DOM menggunakan fitur CSS3. Selain itu, getUserMedia API (WebRTC) untuk pengalaman interaktif memungkinkan pengguna menambahkan gambar mereka langsung dari Webcam dan WebAudio untuk suara 3D.
Namun, keajaiban pengalaman teknologi seperti ini adalah cara kerjanya. Ini juga merupakan salah satu tantangan utama: bagaimana cara memadukan efek visual dan elemen interaktif dalam satu adegan untuk menciptakan keseluruhan yang konsisten? Kompleksitas visual ini sulit dikelola: sehingga sulit untuk mengetahui tahap pengembangan yang sedang kami lakukan.
Untuk mengatasi masalah efek visual dan pengoptimalan yang saling terhubung, kami banyak menggunakan panel kontrol yang akan menangkap semua setelan relevan yang kami tinjau pada saat itu. Tampilan dapat disesuaikan secara langsung di browser untuk apa pun, mulai dari kecerahan, kedalaman bidang, gamma, dll. Siapa pun dapat mencoba menyesuaikan nilai parameter yang signifikan dalam pengalaman dan berpartisipasi dalam menemukan hal yang paling efektif.
Sebelum membagikan rahasia kami, kami ingin memperingatkan Anda bahwa aplikasi mungkin akan error, seperti jika Anda mengutak-atik mesin mobil. Pastikan Anda tidak memiliki hal penting yang sedang berjalan, lalu buka URL utama situs dan tambahkan ?debug=on ke alamat tersebut. Tunggu situs dimuat dan setelah Anda berada di dalam (tekan?) tombol Ctrl-I
, Anda akan melihat dropdown muncul di sisi kanan. Jika Anda menghapus centang pada opsi “Keluar dari jalur kamera”, Anda dapat menggunakan tombol A, W, S, D, dan mouse untuk bergerak bebas di sekitar ruang.

Kita tidak akan membahas semua setelan di sini, tetapi sebaiknya Anda bereksperimen: tombol akan menampilkan setelan yang berbeda di berbagai scene. Dalam urutan badai terakhir, ada tombol tambahan: Ctrl-A
yang dapat Anda gunakan untuk mengalihkan pemutaran animasi dan terbang. Dalam tampilan ini, jika Anda menekan Esc
(untuk keluar dari fungsi kunci mouse) dan menekan lagi Ctrl-I
, Anda dapat mengakses setelan yang khusus untuk tampilan badai. Lihat-lihat dan ambil beberapa gambar kartu pos yang bagus seperti di bawah ini.

Untuk mewujudkannya dan memastikannya cukup fleksibel untuk kebutuhan kita, kita menggunakan library yang bagus bernama dat.gui (lihat di sini untuk tutorial sebelumnya tentang cara menggunakannya). Hal ini memungkinkan kami mengubah setelan yang ditampilkan kepada pengunjung situs dengan cepat.
Agak Mirip dengan Lukisan Matte
Dalam banyak film dan animasi Disney klasik, membuat scene berarti menggabungkan berbagai lapisan. Ada lapisan live action, animasi sel, bahkan set fisik dan lapisan atas yang dibuat dengan melukis di atas kaca: teknik yang disebut matte-painting.
Dalam banyak hal, struktur pengalaman yang kami buat serupa; meskipun beberapa “lapisan” jauh lebih dari sekadar visual statis. Faktanya, hal ini memengaruhi tampilan sesuai dengan komputasi yang lebih kompleks. Namun, setidaknya pada tingkat gambaran besar, kita menangani tampilan, yang disusun satu di atas yang lain. Di bagian atas, Anda akan melihat lapisan UI, dengan tampilan 3D di bawahnya: yang terbuat dari berbagai komponen tampilan.
Lapisan antarmuka atas dibuat menggunakan DOM dan CSS 3 yang berarti bahwa pengeditan interaksi dapat dilakukan dengan banyak cara secara independen dari pengalaman 3D dengan komunikasi di antara keduanya sesuai dengan daftar peristiwa yang dipilih. Komunikasi ini menggunakan Backbone Router + peristiwa HTML5 onHashChange yang mengontrol area mana yang harus dianimasikan masuk/keluar. (sumber project: /develop/coffee/router/Router.coffee).
Tutorial: Dukungan Sprite Sheet dan Retina
Salah satu teknik pengoptimalan yang menyenangkan yang kami andalkan untuk antarmuka adalah menggabungkan banyak gambar overlay antarmuka dalam satu PNG untuk mengurangi permintaan server. Dalam project ini, antarmuka terdiri dari lebih dari 70 gambar (tidak termasuk tekstur 3D) yang dimuat di awal untuk mengurangi latensi situs. Anda dapat melihat sheet sprite live di sini:
Layar Normal - http://findyourwaytooz.com/img/home/interface_1x.png Layar Retina - http://findyourwaytooz.com/img/home/interface_2x.png
Berikut adalah beberapa tips tentang cara kami memanfaatkan penggunaan Sprite Sheet, dan cara menggunakannya untuk perangkat retina serta membuat antarmuka sejelas dan serap mungkin.
Membuat Spritesheet
Untuk membuat SpriteSheet, kita menggunakan TexturePacker yang menghasilkan output dalam format apa pun yang Anda butuhkan. Dalam hal ini, kita telah mengekspor sebagai EaselJS yang sangat rapi dan juga dapat digunakan untuk membuat sprite animasi.
Menggunakan Sprite Sheet yang dihasilkan
Setelah membuat Sprite Sheet, Anda akan melihat file JSON seperti ini:
{
"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]
},
}
Dengan keterangan:
- image merujuk ke URL sheet sprite
- frame adalah koordinat setiap elemen UI [x, y, lebar, tinggi]
- animasi adalah nama setiap aset
Perhatikan bahwa kita telah menggunakan gambar dengan kepadatan tinggi untuk membuat sheet Sprite, lalu kita telah membuat versi normal dengan hanya mengubah ukurannya menjadi setengah dari ukurannya.
Rangkuman
Setelah semuanya siap, kita hanya memerlukan cuplikan JavaScript untuk menggunakannya.
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);
};
Berikut cara menggunakannya:
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'));
Untuk memahami lebih lanjut Kepadatan Piksel Variabel, Anda dapat membaca artikel ini oleh Boris Smus.
Pipeline Konten 3D
Pengalaman lingkungan disiapkan di lapisan WebGL. Saat Anda memikirkan tampilan 3D, salah satu pertanyaan tersulit adalah bagaimana Anda akan memastikan bahwa Anda dapat membuat konten yang memungkinkan potensi ekspresif maksimum dari sisi pemodelan, animasi, dan efek. Dalam banyak hal, inti masalah ini adalah pipeline konten: proses yang disepakati untuk diikuti guna membuat konten untuk tampilan 3D.
Kami ingin menciptakan dunia yang menakjubkan; jadi kami memerlukan proses yang solid yang akan memungkinkan seniman 3D untuk membuatnya. Mereka harus diberi kebebasan ekspresif sebanyak mungkin dalam software animasi dan pemodelan 3D mereka; dan kita harus merendernya di layar melalui kode.
Kami telah menangani masalah semacam ini selama beberapa waktu karena setiap kali membuat situs 3D di masa lalu, kami menemukan batasan pada alat yang dapat digunakan. Jadi, kami telah membuat alat ini, yang disebut 3D Librarian: sebuah bagian dari riset internal. Dan hampir siap diterapkan ke pekerjaan yang sebenarnya.
Alat ini memiliki beberapa sejarah: awalnya alat ini ditujukan untuk Flash, dan memungkinkan Anda memasukkan tampilan Maya besar sebagai satu file terkompresi yang dioptimalkan untuk runtime unpacking. Alasannya optimal karena secara efektif memaketkan tampilan pada dasarnya dalam struktur data yang sama yang dimanipulasi selama rendering dan animasi. Hanya ada sedikit penguraian yang perlu dilakukan pada file saat dimuat. Mengekstrak di Flash cukup cepat karena file dalam format AMF, yang dapat diekstrak secara native oleh Flash. Menggunakan format yang sama di WebGL memerlukan sedikit lebih banyak pekerjaan pada CPU. Bahkan, kita harus membuat ulang kode lapisan JavaScript data-unpack, yang pada dasarnya akan mendekompresi file tersebut dan membuat ulang struktur data yang diperlukan agar WebGL berfungsi. Mengekstrak seluruh tampilan 3D adalah operasi yang sedikit berat bagi CPU: mengekstrak tampilan 1 di Find Your Way To Oz memerlukan waktu sekitar 2 detik pada komputer kelas menengah hingga kelas atas. Oleh karena itu, hal ini dilakukan menggunakan teknologi Web Workers, pada waktu ”penyiapan tampilan” (sebelum tampilan benar-benar diluncurkan), agar tidak menggantung pengalaman pengguna.
Alat praktis ini dapat mengimpor sebagian besar tampilan 3D: model, tekstur, animasi tulang. Anda membuat satu file library, yang kemudian dapat dimuat oleh mesin 3D. Anda memasukkan semua model yang diperlukan dalam tampilan dalam library ini, dan, voilà, memunculkannya ke dalam tampilan.
Namun, masalah yang kami hadapi adalah sekarang kami harus menangani WebGL: teknologi baru. Ini adalah tantangan yang cukup berat: menetapkan standar untuk pengalaman 3D berbasis browser. Jadi, kami membuat lapisan JavaScript ad hoc yang akan mengambil file tampilan 3D yang dikompresi 3D Librarian, dan menerjemahkannya dengan benar ke format yang akan dipahami WebGL.
Tutorial: Let There Be Wind
Tema yang berulang dalam “Find Your Way To Oz” adalah angin. Thread alur cerita disusun menjadi crescendo angin.
Adegan pertama karnaval relatif tenang. Dan saat melewati berbagai scene, pengguna akan merasakan angin yang semakin kencang, yang berujung pada scene terakhir, yaitu badai.
Oleh karena itu, penting untuk memberikan efek angin yang imersif.
Untuk membuatnya, kami mengisi 3 adegan karnaval dengan objek yang lembut, sehingga seharusnya terpengaruh oleh angin, seperti tenda, bendera permukaan bilik foto, dan balon itu sendiri.

Game desktop saat ini biasanya dibuat berdasarkan game engine fisika inti. Jadi, saat objek lunak perlu disimulasikan di dunia 3D, simulasi fisika penuh akan dijalankan untuknya, sehingga menciptakan perilaku lunak yang dapat dipercaya.
Di WebGL / JavaScript, kita (belum) memiliki kemampuan untuk menjalankan simulasi fisika yang lengkap. Jadi, di Oz, kami harus menemukan cara untuk menciptakan efek angin, tanpa benar-benar menyimulasikannya.
Kami menyematkan informasi “sensitivitas angin” untuk setiap objek dalam model 3D itu sendiri. Setiap vertex model 3D memiliki ”Atribut Angin” yang menentukan seberapa besar vertex tersebut seharusnya terpengaruh oleh angin. Jadi, sensitivitas angin yang ditentukan ini dari Objek 3D. Kemudian, kita perlu membuat angin itu sendiri.
Kami melakukannya dengan membuat gambar yang berisi Perlin Noise. Gambar ini dimaksudkan untuk mencakup “area angin” tertentu. Jadi, cara yang baik untuk memikirkannya adalah dengan membayangkan gambar awan seperti derau yang diletakkan di atas area persegi panjang tertentu dari tampilan 3D. Setiap piksel, nilai tingkat abu-abu, dari gambar ini menentukan seberapa kencang angin pada momen tertentu di area 3D yang “mengelilinginya”.
Untuk menghasilkan efek angin, gambar dipindahkan, dalam waktu, dengan kecepatan konstan, ke arah tertentu; arah angin. Dan untuk memastikan “area berangin” tidak memengaruhi semua yang ada di tampilan, kita menggabungkan gambar angin di sekitar tepi, yang dibatasi pada area efek.
Tutorial Angin 3D Sederhana
Sekarang, mari kita buat efek angin dalam tampilan 3D sederhana di Three.js.
Kita akan membuat angin di ”lapangan rumput prosedural” sederhana.
Mari kita buat tampilan terlebih dahulu. Kita akan memiliki medan datar yang sederhana dan bertekstur. Kemudian, setiap bagian rumput akan direpresentasikan dengan kerucut 3D terbalik.

Berikut adalah cara membuat tampilan sederhana ini di Three.js menggunakan CoffeeScript.
Pertama-tama, kita akan menyiapkan Three.js, dan menghubungkannya dengan Kamera, Pengontrol mouse, dan Beberapa Lampu:
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()
Panggilan fungsi initGrass dan initTerrain masing-masing mengisi tampilan dengan rumput dan medan:
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
Di sini kita membuat petak rumput 15x15. Kita menambahkan sedikit pengacakan ke setiap posisi rumput, sehingga rumput tidak berbaris seperti tentara, yang akan terlihat aneh.
Medan ini hanyalah bidang horizontal, yang ditempatkan di dasar potongan rumput (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 )
Jadi, yang telah kita lakukan sejauh ini adalah membuat tampilan Three.js, dan menambahkan beberapa rumput, yang terbuat dari kerucut terbalik yang dihasilkan secara terprogram, dan medan sederhana.
Sejauh ini tidak ada yang istimewa.
Sekarang, saatnya mulai menambahkan angin. Pertama-tama, kita ingin menyematkan informasi sensitivitas angin ke dalam model 3D rumput.
Kita akan menyematkan informasi ini sebagai atribut kustom, untuk setiap vertex model 3D rumput. Dan kita akan menggunakan aturan bahwa: ujung bawah model rumput (ujung kerucut) memiliki sensitivitas nol, karena menempel ke tanah. Bagian atas model rumput (dasar kerucut) memiliki sensitivitas angin maksimum, karena merupakan bagian yang lebih jauh dari tanah.
Berikut cara fungsi instanceGrass dienkode ulang, untuk menambahkan sensitivitas angin sebagai atribut kustom untuk model 3D rumput.
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
Sekarang kita menggunakan material kustom, windMaterial, bukan MeshPhongMaterial yang kita gunakan sebelumnya. WindMaterial menggabungkan WindMeshShader yang akan kita lihat sebentar lagi.
Jadi, kode di instanceGrass melakukan loop melalui semua vertex model rumput, dan untuk setiap vertex, kode ini menambahkan atribut vertex kustom, yang disebut windFactor. windFactor ini ditetapkan ke 0, untuk ujung bawah model rumput (tempat model rumput seharusnya menyentuh medan), dan nilainya 1 untuk ujung atas model rumput.
Bahan lain yang kita perlukan adalah menambahkan angin yang sebenarnya ke tampilan. Seperti yang telah dibahas, kita akan menggunakan derau Perlin untuk ini. Kita akan membuat tekstur derau Perlin secara terstruktur.
Untuk kejelasan, kita akan menetapkan tekstur ini ke medan itu sendiri, sebagai pengganti tekstur hijau sebelumnya. Hal ini akan memudahkan Anda merasakan apa yang terjadi dengan angin.
Jadi, tekstur derau Perlin ini akan secara spasial mencakup ekstensi medan kita, dan setiap piksel tekstur akan menentukan intensitas angin area medan tempat piksel tersebut berada. Persegi panjang medan akan menjadi "area angin" kita.
Derau Perlin dihasilkan secara terprogram melalui shader, yang disebut NoiseShader. Shader ini menggunakan algoritma derau simplex 3D dari: https://github.com/ashima/webgl-noise . Versi WebGL ini diambil secara verbatim dari salah satu contoh Three.js MrDoob, di: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.
NoiseShader menggunakan waktu, skala, dan kumpulan parameter offset, sebagai seragam, dan menghasilkan distribusi 2D yang bagus dari derau 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) }
...
Kita akan menggunakan Shader ini untuk merender Derau Perlin ke tekstur. Hal ini dilakukan dalam fungsi 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 )
Yang dilakukan kode di atas adalah menyiapkan noiseMap sebagai target render Three.js, melengkapinya dengan NoiseShader, lalu merendernya dengan kamera ortografis, sehingga dapat menghindari distorsi perspektif.
Seperti yang telah dibahas, sekarang kita juga akan menggunakan tekstur ini sebagai tekstur rendering utama untuk medan. Hal ini tidak terlalu diperlukan agar efek angin itu sendiri berfungsi. Namun, sebaiknya kita memilikinya agar dapat lebih memahami secara visual apa yang terjadi dengan pembangkitan angin.
Berikut adalah fungsi initTerrain yang telah diubah, menggunakan noiseMap sebagai tekstur:
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 )
Setelah tekstur angin diterapkan, mari kita lihat WindMeshShader, yang bertanggung jawab untuk mendeformasi model rumput sesuai dengan angin.
Untuk membuat shader ini, kita memulai dari shader MeshPhongMaterial Three.js standar, dan memodifikasinya. Ini adalah cara cepat dan praktis untuk memulai shader yang berfungsi, tanpa harus memulai dari awal.
Kita tidak akan menyalin seluruh kode shader di sini (lihat di file kode sumber), karena sebagian besar kode tersebut akan menjadi replika shader MeshPhongMaterial. Namun, mari kita lihat bagian yang diubah dan terkait angin di 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;
Jadi, yang dilakukan shader ini adalah menghitung koordinat pencarian tekstur windUV terlebih dahulu, berdasarkan posisi 2D, xz (horizontal) dari vertex. Koordinat UV ini digunakan untuk mencari gaya angin, vWindForce, dari tekstur angin derau Perlin.
Nilai vWindForce ini, digabungkan dengan windFactor khusus vertex, atribut kustom yang dibahas di atas, untuk menghitung jumlah deformasi yang diperlukan vertex. Kita juga memiliki parameter windScale global untuk mengontrol kekuatan angin secara keseluruhan, dan vektor windDirection yang menentukan arah deformasi angin yang perlu terjadi.
Jadi, hal ini akan membuat deformasi potongan rumput berdasarkan angin. Namun, kita belum selesai. Seperti saat ini, deformasi ini bersifat statis, dan tidak akan menyampaikan efek area berangin.
Seperti yang telah disebutkan, kita perlu menggeser tekstur derau dari waktu ke waktu, di seluruh area angin, sehingga kaca kita dapat bergerak.
Hal ini dilakukan dengan menggeser dari waktu ke waktu, seragam vOffset yang diteruskan ke NoiseShader. Ini adalah parameter vec2, yang akan memungkinkan kita menentukan offset derau, di sepanjang arah tertentu (arah angin kita).
Kita melakukannya di fungsi render, yang dipanggil di setiap 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
...
Selesai. Kita baru saja membuat tampilan dengan "rumput prosedural" yang terpengaruh oleh angin.
Menambahkan debu ke campuran
Sekarang, mari kita tambahkan sedikit bumbu ke tampilan. Mari kita tambahkan sedikit debu yang beterbangan, agar suasananya lebih menarik.

Lagi pula, debu seharusnya terpengaruh oleh angin, jadi wajar jika debu beterbangan di sekitar di tampilan angin.
Debu disiapkan dalam fungsi initDust sebagai sistem partikel.
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
Di sini, 130 partikel debu dibuat. Perhatikan juga bahwa setiap objek dilengkapi dengan WindParticleShader khusus.
Sekarang, pada setiap frame, kita akan sedikit menggerakkan partikel, menggunakan CoffeeScript, secara independen dari angin. Berikut kodenya.
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 )
Selain itu, kita akan mengimbangi setiap posisi partikel sesuai dengan angin. Hal ini dilakukan di WindParticleShader. Khususnya di shader vertex.
Kode untuk shader ini adalah versi modifikasi dari ParticleMaterial Three.js, dan ini adalah tampilan intinya:
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;
Vertex shader ini tidak jauh berbeda dengan yang kita miliki untuk deformasi rumput berbasis angin. Fungsi ini menggunakan tekstur derau Perlin sebagai input, dan bergantung pada posisi dunia debu, fungsi ini akan mencari nilai vWindForce dalam tekstur derau. Kemudian, nilai ini digunakan untuk mengubah posisi partikel debu.
Riders On The Storm
Adegan WebGL kami yang paling menantang mungkin adalah adegan terakhir, yang dapat Anda lihat jika mengklik balon ke dalam mata tornado untuk mencapai akhir perjalanan Anda di situs, dan video eksklusif tentang rilis mendatang.

Saat membuat tampilan ini, kami tahu bahwa kami perlu memiliki fitur utama untuk pengalaman yang akan berdampak. Tornado yang berputar akan bertindak sebagai pusat perhatian dan lapisan konten lainnya akan membentuk fitur ini untuk menciptakan efek dramatis. Untuk mencapai hal ini, kita membuat sesuatu yang setara dengan studio film yang berada di sekitar shader aneh ini.
Kami menggunakan pendekatan campuran untuk membuat komposit yang realistis. Beberapa di antaranya adalah trik visual seperti bentuk cahaya untuk membuat efek lensa flare, atau tetesan hujan yang dianimasikan sebagai lapisan di atas tampilan yang Anda lihat. Dalam kasus lain, kita memiliki permukaan datar yang digambar agar tampak bergerak, seperti lapisan awan yang terbang rendah yang bergerak sesuai dengan kode sistem partikel. Sementara itu, potongan puing yang mengorbit di sekitar tornado adalah lapisan dalam tampilan 3D yang diurutkan untuk bergerak di depan dan di belakang tornado.
Alasan utama kami harus membuat tampilan dengan cara ini adalah untuk memastikan kami memiliki GPU yang cukup untuk menangani shader tornado yang seimbang dengan efek lain yang kami terapkan. Awalnya, kami mengalami masalah keseimbangan GPU yang besar, tetapi kemudian scene ini dioptimalkan dan menjadi lebih ringan daripada scene utama.
Tutorial: Shader Storm
Untuk membuat urutan badai akhir, banyak teknik yang berbeda digabungkan, tetapi inti dari karya ini adalah shader GLSL kustom yang terlihat seperti tornado. Kami telah mencoba berbagai teknik, mulai dari vertex shader untuk membuat pusaran geometris yang menarik hingga animasi berbasis partikel dan bahkan animasi 3D dari bentuk geometris yang bengkok. Tidak ada efek yang tampaknya dapat menciptakan kembali perasaan tornado atau memerlukan terlalu banyak pemrosesan.
Sebuah project yang sama sekali berbeda akhirnya memberi kami jawaban. Project paralel yang melibatkan game untuk sains guna memetakan otak tikus dari Max Planck Institute (brainflight.org) telah menghasilkan efek visual yang menarik. Kami berhasil membuat film tentang bagian dalam neuron tikus menggunakan shader volumetrik kustom.

Kami menemukan bahwa bagian dalam sel otak terlihat sedikit seperti corong tornado. Dan karena kita menggunakan teknik volumetrik, kita tahu bahwa kita dapat melihat shader ini dari semua arah dalam ruang. Kita dapat menyetel render shader untuk digabungkan dengan scene badai, terutama jika diapit di bawah lapisan awan dan di atas latar belakang yang dramatis.
Teknik shader melibatkan trik yang pada dasarnya menggunakan satu shader GLSL untuk merender seluruh objek dengan algoritma render yang disederhanakan yang disebut rendering ray marching dengan medan jarak. Dalam teknik ini, shader piksel dibuat yang memperkirakan jarak terdekat ke permukaan untuk setiap titik di layar.
Referensi yang baik untuk algoritma ini dapat ditemukan dalam ringkasan oleh iq: Rendering Worlds With Two Triangles - Iñigo Quilez. Selain menjelajahi galeri shader di glsl.heroku.com, ada banyak contoh teknik ini yang dapat ditemukan di sana dan dapat diuji coba.
Inti shader dimulai dengan fungsi utama, yang menyiapkan transformasi kamera dan memasuki loop yang berulang kali mengevaluasi jarak ke permukaan. Panggilan RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ) adalah tempat perhitungan ray marching inti terjadi.
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
}
Idenya adalah saat kita maju ke bentuk tornado, kita secara rutin menambahkan kontribusi warna ke nilai warna akhir piksel, serta kontribusi ke opasitas di sepanjang sinar. Hal ini menciptakan kualitas lembut berlapis pada tekstur tornado.
Aspek inti tornado berikutnya adalah bentuk sebenarnya yang dibuat dengan menyusun sejumlah fungsi. Awalnya, ini adalah kerucut yang disusun dengan derau untuk membuat tepi kasar organik, lalu diputar di sepanjang sumbu utamanya dan diputar dalam waktu.
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;
}
Pekerjaan yang terlibat dalam pembuatan shader semacam ini cukup rumit. Selain masalah yang terkait dengan abstraksi operasi yang Anda buat, ada masalah pengoptimalan dan kompatibilitas lintas platform yang serius yang perlu Anda lacak dan pecahkan sebelum dapat menggunakan pekerjaan dalam produksi.
Bagian pertama masalah: mengoptimalkan shader ini untuk tampilan kita. Untuk mengatasi hal ini, kita perlu memiliki pendekatan yang “aman” jika shader akan menjadi terlalu berat. Untuk melakukannya, kita menggabungkan shader tornado pada resolusi sampel yang berbeda dari seluruh tampilan. Ini berasal dari file stormTest.coffee (ya, ini adalah pengujian).
Kita memulai dengan renderTarget yang cocok dengan lebar dan tinggi tampilan sehingga kita dapat memiliki resolusi independen dari shader tornado ke tampilan. Kemudian, kita memutuskan downsampling resolusi shader badai secara dinamis bergantung pada kecepatan frame yang kita dapatkan.
...
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
Terakhir, kita merender tornado ke layar menggunakan algoritma sal2x yang disederhanakan, (untuk menghindari tampilan yang bergerigi) @baris 1107 di stormTest.coffee. Artinya, dalam kasus terburuk, kita akan memiliki tornado yang lebih buram, tetapi setidaknya tornado tersebut berfungsi tanpa mengambil alih kontrol dari pengguna.
Langkah pengoptimalan berikutnya memerlukan pemahaman tentang algoritma. Faktor komputasi pendorong dalam shader adalah iterasi yang dilakukan pada setiap piksel untuk mencoba memperkirakan jarak fungsi permukaan: jumlah iterasi loop raymarching. Dengan menggunakan ukuran langkah yang lebih besar, kita bisa mendapatkan estimasi permukaan tornado dengan lebih sedikit iterasi saat berada di luar permukaannya yang berawan. Saat berada di dalam, kita akan mengurangi ukuran langkah untuk presisi dan agar dapat mencampur nilai untuk menciptakan efek berkabut. Membuat silinder pembatas untuk mendapatkan estimasi kedalaman untuk sinar yang dipancarkan juga memberikan peningkatan kecepatan yang baik.
Bagian berikutnya dari masalah ini adalah memastikan shader ini akan berjalan di kartu video yang berbeda. Kami melakukan beberapa pengujian setiap kali dan mulai membangun intuisi untuk jenis masalah kompatibilitas yang mungkin kami alami. Alasan kami tidak dapat melakukan lebih baik daripada intuisi adalah karena kami tidak selalu bisa mendapatkan informasi proses debug yang baik tentang error. Skenario yang umum adalah error GPU yang tidak terlalu serius, atau bahkan error sistem.
Masalah kompatibilitas papan video silang memiliki solusi yang serupa: pastikan konstanta statis dimasukkan dari jenis data yang tepat seperti yang ditentukan, yaitu: 0,0 untuk float dan 0 untuk int. Berhati-hatilah saat menulis fungsi yang lebih panjang; sebaiknya bagi semuanya menjadi beberapa fungsi yang lebih sederhana dan variabel sementara karena compiler tampaknya tidak menangani kasus tertentu dengan benar. Pastikan tekstur semuanya merupakan bilangan pangkat 2, tidak terlalu besar, dan dalam hal apa pun, gunakan “kehati-hatian” saat mencari data tekstur dalam loop.
Masalah terbesar yang kami alami dalam kompatibilitas berasal dari efek pencahayaan untuk badai. Kita menggunakan tekstur siap pakai yang melilit tornado sehingga kita dapat mewarnai gumpalannya. Efeknya sangat bagus, dan memudahkan untuk memadukan tornado ke dalam warna tampilan, tetapi perlu waktu lama untuk mencoba menjalankannya di platform lain.

Situs Web Seluler
Pengalaman seluler tidak dapat menjadi terjemahan langsung dari versi desktop karena persyaratan teknologi dan pemrosesan terlalu berat. Kami harus membuat sesuatu yang baru, yang menargetkan pengguna seluler secara khusus.
Kami berpikir akan lebih menarik jika Photo-Booth Karnaval dari desktop menjadi aplikasi web seluler yang akan menggunakan kamera seluler pengguna. Sesuatu yang belum pernah kita lihat sejauh ini.
Untuk menambahkan nuansa, kami membuat kode transformasi 3D di CSS3. Setelah menautkan dengan giroskop dan akselerometer, kami dapat menambahkan banyak kedalaman ke pengalaman. Situs merespons cara Anda memegang, menggerakkan, dan melihat ponsel.
Saat menulis artikel ini, kami merasa perlu memberi Anda beberapa petunjuk tentang cara menjalankan proses pengembangan seluler dengan lancar. Ini dia! Lihat apa yang dapat Anda pelajari darinya.
Tips dan trik seluler
Pengisi daya awal adalah sesuatu yang diperlukan, bukan sesuatu yang harus dihindari. Kami tahu, terkadang hal ini terjadi. Hal ini terutama karena Anda perlu terus mempertahankan daftar hal yang dipramuat saat project berkembang. Lebih buruk lagi, cara menghitung progres pemuatan jika Anda mengambil resource yang berbeda, dan banyak di antaranya secara bersamaan, tidak terlalu jelas. Di sinilah class abstrak kustom dan sangat umum 'Task' berguna. Ide utamanya adalah untuk mengizinkan struktur bertingkat tanpa batas, dengan Task dapat memiliki sub-Task-nya sendiri, yang dapat memiliki sub-Task-nya sendiri, dan seterusnya… Selain itu, setiap tugas menghitung progresnya sehubungan dengan progres sub-tugasnya (tetapi tidak dengan progres induk). Dengan membuat semua MainPreloadTask, AssetPreloadTask, dan TemplatePreFetchTask berasal dari Task, kita membuat struktur yang terlihat seperti ini:

Berkat pendekatan tersebut dan class Task, kita dapat dengan mudah mengetahui progres global (MainPreloadTask), atau hanya progres aset (AssetPreloadTask), atau progres pemuatan template (TemplatePreFetchTask). Bahkan progres file tertentu. Untuk melihat cara melakukannya, lihat class Task di /m/javascripts/raw/util/Task.js dan implementasi tugas yang sebenarnya di /m/javascripts/preloading/task. Sebagai contoh, ini adalah ekstrak dari cara kita menyiapkan class /m/javascripts/preloading/task/MainPreloadTask.js yang merupakan wrapper pramuat utama kita:
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);
}
}
})
]);
Di class /m/javascripts/preloading/task/subtask/AssetPreloadTask.js, selain mencatat cara berkomunikasi dengan MainPreloadTask (melalui implementasi Task bersama), sebaiknya perhatikan juga cara memuat aset yang bergantung pada platform. Pada dasarnya, kita memiliki empat jenis gambar. Standar seluler (.ext, dengan ext adalah ekstensi file, biasanya .png atau .jpg), retina seluler (-2x.ext), standar tablet (-tab.ext), dan retina tablet (-tab-2x.ext). Daripada melakukan deteksi di MainPreloadTask dan melakukan hard code pada empat array aset, kita hanya menyebutkan nama dan ekstensi aset yang akan dimuat sebelumnya dan apakah aset tersebut bergantung pada platform (responsif = benar / salah). Kemudian, AssetPreloadTask akan membuat nama file untuk kita:
resolveAssetUrl : function(assetName, extension, responsive) {
return AssetPreloadTask.ASSETS_ROOT + assetName + (responsive === true ? ((Detection.getInstance().tablet ? '-tab' : '') + (Detection.getInstance().retina ? '-2x' : '')) : '') + '.' + extension;
}
Di bawah rantai class, kode sebenarnya yang melakukan pramuat aset terlihat seperti berikut (/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)
Saat mengembangkan OZ versi seluler, kami mendapati bahwa kami menghabiskan banyak waktu untuk bermain dengan photo booth, bukan bekerja :D Hal itu karena photo booth memang menyenangkan. Jadi, kami membuat demo untuk Anda coba.

Anda dapat melihat demo langsung di sini (jalankan di iPhone atau ponsel Android):
http://u9html5rocks.appspot.com/demos/mobile_photo_booth
Untuk menyiapkannya, Anda memerlukan instance aplikasi Google App Engine gratis tempat Anda dapat menjalankan backend. Kode frontend tidak rumit, tetapi ada beberapa kemungkinan masalah. Mari kita bahas sekarang:
- Jenis file Gambar yang diizinkan
Kami ingin pengguna hanya dapat mengupload gambar (karena ini adalah photo booth, bukan video booth). Secara teori, Anda cukup menentukan filter dalam HTML, seperti berikut:
input id="fileInput" class="fileInput" type="file" name="file" accept="image/*"
Namun, tampaknya hal ini hanya berfungsi di iOS, jadi kita perlu menambahkan pemeriksaan tambahan terhadap RegExp setelah file dipilih:
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();
}
}
});
- Membatalkan upload atau pemilihan file Inkonsistensi lain yang kami temukan selama proses pengembangan adalah cara perangkat yang berbeda-beda memberi tahu pemilihan file yang dibatalkan. Ponsel dan tablet iOS tidak melakukan apa pun, tidak memberi tahu sama sekali. Jadi, kita tidak memerlukan tindakan khusus untuk kasus ini, tetapi ponsel Android akan memicu fungsi add(), meskipun tidak ada file yang dipilih. Berikut cara mengatasinya:
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();
}
}
Sisanya berfungsi dengan lancar di seluruh platform. Bersenang-senanglah
Kesimpulan
Mengingat ukuran Find Your Way To Oz yang sangat besar, dan berbagai teknologi yang terlibat, dalam artikel ini kami hanya dapat membahas beberapa pendekatan yang digunakan.
Jika Anda ingin menjelajahi seluruh enchilada, silakan lihat kode sumber lengkap Find Your Way To Oz di link ini.
Kredit
Klik di sini untuk melihat daftar kredit lengkap
Referensi
- 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/