소개
'오즈로 가는 길 찾기'는 디즈니가 웹에 제공하는 새로운 Google Chrome 실험입니다. 이 앱을 사용하면 대규모 폭풍에 휩쓸려 오즈의 땅으로 이어지는 캔자스 서커스를 통한 양방향 여정을 즐길 수 있습니다.
Google의 목표는 영화의 풍부함과 브라우저의 기술적 기능을 결합하여 사용자가 강한 유대감을 형성할 수 있는 재미있고 몰입도 높은 환경을 만드는 것이었습니다.
이 작업은 한 번에 전체를 다루기에는 너무 방대하므로 흥미롭다고 생각되는 기술 스토리의 몇 가지 챕터를 살펴봤습니다. 그 과정에서 난이도가 점점 높아지는 몇 가지 집중적인 튜토리얼을 추출했습니다.
이 환경을 만들기 위해 노력한 많은 사람들이 있습니다. 여기에 모두 나열할 수 없을 정도입니다. 사이트를 방문하여 메뉴 섹션의 크레딧 페이지에서 전체 스토리를 확인하세요.
기능의 작동 원리
데스크톱용 Find Your Way to Oz는 몰입감이 뛰어난 풍부한 세계입니다. 3D와 기존 영화 제작에서 영감을 받은 여러 레이어의 효과를 결합하여 거의 실제와 같은 장면을 만듭니다. 가장 눈에 띄는 기술은 Three.js를 사용한 WebGL, 맞춤 빌드된 셰이더, CSS3 기능을 사용하는 DOM 애니메이션 요소입니다. 또한 사용자가 3D 사운드를 위해 웹캠과 WebAudio에서 직접 이미지를 추가할 수 있는 대화형 환경을 위한 getUserMedia API (WebRTC)도 있습니다.
하지만 이러한 기술적 경험의 마법은 이러한 요소가 어떻게 조합되는지에 있습니다. 이는 주요 과제 중 하나이기도 합니다. 시각 효과와 양방향 요소를 한 장면에서 조화롭게 결합하여 일관된 전체를 만드는 방법은 무엇일까요? 이러한 시각적 복잡성은 관리하기 어려웠으며, 어느 시점에서 어떤 개발 단계에 있는지 파악하기가 어려웠습니다.
상호 연결된 시각 효과와 최적화 문제를 해결하기 위해 당시 검토 중인 모든 관련 설정을 캡처하는 제어 패널을 많이 사용했습니다. 장면은 밝기, 피사계 심도, 감마 등 다양한 요소에 대해 브라우저에서 실시간으로 조정할 수 있습니다. 누구나 환경에서 중요한 매개변수의 값을 조정해 보고 가장 효과적인 방법을 찾는 데 참여할 수 있습니다.
비밀을 공유하기 전에 자동차 엔진 내부를 들여다보는 것처럼 비정상 종료가 발생할 수 있다는 점을 알려드립니다. 중요한 작업을 진행하고 있지 않은지 확인한 후 사이트의 기본 URL로 이동하여 주소에 ?debug=on을 추가합니다. 사이트가 로드될 때까지 기다린 후 Ctrl-I
키를 누르면 오른쪽에 드롭다운이 표시됩니다. '카메라 경로 종료' 옵션을 선택 해제하면 A, W, S, D 키와 마우스를 사용하여 공간을 자유롭게 이동할 수 있습니다.

여기서는 모든 설정을 다루지는 않지만 실험해 보시기 바랍니다. 키를 사용하면 장면마다 다른 설정을 확인할 수 있습니다. 마지막 폭풍 시퀀스에는 애니메이션 재생을 전환하고 주변을 날아다닐 수 있는 추가 키 Ctrl-A
가 있습니다. 이 장면에서 Esc
(마우스 잠금 기능 종료)를 누른 다음 Ctrl-I
를 다시 누르면 폭풍 장면에 관한 설정에 액세스할 수 있습니다. 주변을 둘러보고 아래와 같은 멋진 엽서 풍경을 담아보세요.

이를 실현하고 필요에 맞게 충분히 유연하게 만들기 위해 dat.gui라는 멋진 라이브러리를 사용했습니다 (사용 방법에 관한 이전 튜토리얼은 여기를 참고하세요). 이를 통해 사이트 방문자에게 노출되는 설정을 빠르게 변경할 수 있었습니다.
마치 마테 페인팅과도 같습니다.
많은 기존 디즈니 영화와 애니메이션에서 장면을 만드는 것은 여러 레이어를 결합하는 것을 의미했습니다. 라이브 액션, 셀 애니메이션, 실제 세트, 유리 위에 페인팅하여 만든 최상위 레이어(매트 페인팅이라고 하는 기법)가 있었습니다.
일부 '레이어'는 정적 시각 자료 이상의 의미를 지니지만, YouTube에서 만든 환경의 구조는 여러 면에서 유사합니다. 실제로 더 복잡한 계산에 따라 사물이 표시되는 방식에 영향을 미칩니다. 하지만 적어도 대략적인 수준에서는 뷰를 다루며, 뷰는 서로 겹쳐서 컴포지션됩니다. 상단에는 UI 레이어가 있고 그 아래에는 3D 장면이 있습니다. 이 장면은 다양한 장면 구성요소로 구성됩니다.
최상위 인터페이스 레이어는 DOM 및 CSS 3을 사용하여 만들어졌습니다. 즉, 선택한 이벤트 목록에 따라 3D 환경과는 별개로 다양한 방법으로 상호작용을 수정할 수 있었습니다. 이 통신은 애니메이션을 적용할 영역을 제어하는 백본 라우터 + onHashChange HTML5 이벤트를 사용합니다. (프로젝트 소스: /develop/coffee/router/Router.coffee)
튜토리얼: 스프라이트 시트 및 Retina 지원
인터페이스에 사용한 재미있는 최적화 기법 중 하나는 여러 인터페이스 오버레이 이미지를 하나의 PNG로 결합하여 서버 요청을 줄이는 것이었습니다. 이 프로젝트에서는 웹사이트의 지연 시간을 줄이기 위해 3D 텍스처를 제외하고 모두 미리 로드된 70개 이상의 이미지로 인터페이스가 구성되었습니다. 여기에서 실시간 스프라이트 시트를 확인할 수 있습니다.
일반 디스플레이 - http://findyourwaytooz.com/img/home/interface_1x.png Retina 디스플레이 - http://findyourwaytooz.com/img/home/interface_2x.png
다음은 Sprite Sheet를 활용한 방법과 이를 레티나 기기에 사용하고 인터페이스를 최대한 선명하고 깔끔하게 만드는 방법에 관한 몇 가지 팁입니다.
Spritesheet 만들기
SpriteSheets를 만들기 위해 필요한 형식으로 출력하는 TexturePacker를 사용했습니다. 이 경우 EaselJS로 내보냈습니다. 이 형식은 매우 깔끔하며 애니메이션 스프라이트를 만드는 데도 사용할 수 있습니다.
생성된 스프라이트 시트 사용
스프라이트 시트를 만들면 다음과 같은 JSON 파일이 표시됩니다.
{
"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]
},
}
각 항목의 의미는 다음과 같습니다.
- 이미지는 스프라이트 시트의 URL을 나타냅니다.
- 프레임은 각 UI 요소의 좌표입니다[x, y, 너비, 높이].
- 애니메이션은 각 애셋의 이름입니다.
고밀도 이미지를 사용하여 스프라이트 시트를 만든 다음 크기를 절반으로 줄여 일반 버전을 만들었습니다.
통합 정리
이제 설정이 완료되었으므로 이를 사용할 JavaScript 스니펫만 있으면 됩니다.
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);
};
사용 방법은 다음과 같습니다.
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'));
가변 픽셀 밀도에 대해 자세히 알아보려면 Boris Smus의 도움말을 참고하세요.
3D 콘텐츠 파이프라인
환경 환경은 WebGL 레이어에 설정됩니다. 3D 장면을 생각할 때 가장 어려운 질문 중 하나는 모델링, 애니메이션, 효과 측면에서 최대의 표현력을 발휘할 수 있는 콘텐츠를 만들 수 있는지 여부입니다. 이 문제의 핵심은 3D 장면의 콘텐츠를 만들기 위해 따르는 합의된 프로세스인 콘텐츠 파이프라인입니다.
경외심을 불러일으키는 세계를 만들고자 했으므로 3D 아티스트가 이를 만들 수 있는 확실한 프로세스가 필요했습니다. 3D 모델링 및 애니메이션 소프트웨어에서 최대한의 표현의 자유를 제공해야 하며, 코드를 통해 화면에 렌더링해야 합니다.
Google은 이전에 3D 사이트를 만들 때마다 사용할 수 있는 도구에 제한사항이 있다는 것을 발견하여 한동안 이러한 문제를 해결하기 위해 노력해 왔습니다. 그래서 내부 연구용으로 3D Librarian이라는 도구를 만들었습니다. 이제 실제 작업에 적용할 준비가 되었습니다.
이 도구는 몇 가지 역사를 가지고 있습니다. 원래는 Flash용이었으며 큰 Maya 장면을 압축 해제 런타임에 최적화된 단일 압축 파일로 가져올 수 있었습니다. 이 방법이 최적의 방법인 이유는 렌더링 및 애니메이션 중에 조작되는 것과 기본적으로 동일한 데이터 구조로 장면을 효과적으로 압축했기 때문입니다. 로드할 때 파일에 대해 실행해야 하는 파싱 작업은 거의 없습니다. 파일이 Flash에서 기본적으로 압축을 풀 수 있는 AMF 형식이므로 Flash에서 압축을 푸는 속도가 매우 빨랐습니다. WebGL에서 동일한 형식을 사용하려면 CPU에서 약간 더 많은 작업이 필요합니다. 실제로 이러한 파일의 압축을 풀고 WebGL이 작동하는 데 필요한 데이터 구조를 다시 만드는 코드의 데이터 압축 해제 JavaScript 레이어를 다시 만들어야 했습니다. 전체 3D 장면을 압축 해제하는 작업은 약간 CPU 집약적입니다. Find Your Way To Oz의 장면 1을 압축 해제하는 데 중급에서 고급 머신에서는 약 2초가 소요됩니다. 따라서 사용자 환경이 중단되지 않도록 '장면 설정' 시간 (장면이 실제로 실행되기 전)에 Web Workers 기술을 사용하여 실행됩니다.
이 편리한 도구를 사용하면 모델, 텍스처, 본 애니메이션 등 대부분의 3D 장면을 가져올 수 있습니다. 3D 엔진에서 로드할 수 있는 단일 라이브러리 파일을 만듭니다. 이 라이브러리 내에 장면에 필요한 모든 모델을 채우면 장면에 모델이 생성됩니다.
하지만 이제는 신규 기술인 WebGL을 다루어야 했습니다. 브라우저 기반 3D 환경의 표준을 설정하는 매우 어려운 작업이었습니다. 따라서 3D Librarian 압축 3D 장면 파일을 가져와 WebGL이 이해할 수 있는 형식으로 적절하게 변환하는 임시 JavaScript 레이어를 만들었습니다.
튜토리얼: 바람을 일으키세요
'Find Your Way To Oz'에서 반복되는 주제는 바람입니다. 스토리라인의 한 부분은 바람의 크레센도처럼 구성되어 있습니다.
카니발의 첫 번째 장면은 비교적 차분합니다. 다양한 장면을 거치면서 사용자는 점점 강해지는 바람을 경험하게 되며, 마지막 장면인 폭풍으로 이어집니다.
따라서 몰입도 높은 바람 효과를 제공하는 것이 중요했습니다.
이를 만들기 위해 텐트, 사진 부스 표면의 깃발, 풍선 자체와 같이 부드럽고 바람의 영향을 받을 수 있는 물체로 카니발 장면 3개를 채웠습니다.

요즘 데스크톱 게임은 일반적으로 핵심 물리 엔진을 중심으로 빌드됩니다. 따라서 3D 세계에서 소프트 객체를 시뮬레이션해야 할 때 전체 물리 시뮬레이션이 실행되어 믿을 수 있는 소프트 동작이 생성됩니다.
WebGL / Javascript에서는 아직 본격적인 물리 시뮬레이션을 실행할 수 없습니다. 따라서 Oz에서는 실제로 시뮬레이션하지 않고도 바람의 효과를 만드는 방법을 찾아야 했습니다.
각 객체의 '풍력 감수성' 정보를 3D 모델 자체에 삽입했습니다. 3D 모델의 각 정점에는 해당 정점이 바람의 영향을 받는 정도를 지정하는 '바람 속성'이 있습니다. 따라서 3D 객체의 지정된 바람 민감도입니다. 그런 다음 바람 자체를 만들어야 했습니다.
이를 위해 Perlin 노이즈가 포함된 이미지를 생성했습니다. 이 이미지는 특정 '풍속 영역'을 덮기 위한 것입니다. 따라서 이를 이해하는 좋은 방법은 3D 장면의 특정 직사각형 영역 위에 노이즈와 같은 구름 사진을 겹쳐 놓는다고 생각하는 것입니다. 이 이미지의 각 픽셀(회색조 값)은 '주변의' 3D 영역에서 특정 순간에 바람이 얼마나 강한지를 나타냅니다.
바람 효과를 내기 위해 이미지가 시간에 따라 일정한 속도로 특정 방향(바람의 방향)으로 이동합니다. '바람이 부는 지역'이 장면의 모든 부분에 영향을 미치지 않도록 바람 이미지를 가장자리로 감싸고 효과가 적용되는 영역으로 제한합니다.
간단한 3D 바람 튜토리얼
이제 Three.js의 간단한 3D 장면에서 바람 효과를 만들어 보겠습니다.
간단한 '프로시저럴 잔디밭'에서 바람을 만들어 보겠습니다.
먼저 장면을 만들어 보겠습니다. 간단한 질감이 있는 평평한 지형을 만들어 보겠습니다. 잔디의 각 부분은 거꾸로 된 3D 원뿔로 표현됩니다.

다음은 CoffeeScript를 사용하여 Three.js에서 이 간단한 장면을 만드는 방법입니다.
먼저 Three.js를 설정하고 카메라, 마우스 컨트롤러, 조명과 연결합니다.
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()
initGrass 및 initTerrain 함수 호출은 각각 장면을 잔디와 지형으로 채웁니다.
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
여기서는 15x15비트의 잔디 그리드를 만듭니다. 잔디가 군인처럼 정렬되어 이상해 보이지 않도록 각 잔디 위치에 약간의 무작위성을 추가합니다.
이 지형은 잔디의 밑부분 (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 )
지금까지는 Three.js 장면을 만들고 절차적으로 생성된 역원뿔로 만든 잔디와 간단한 지형을 추가했습니다.
아직 특별한 점은 없습니다.
이제 바람을 추가할 차례입니다. 먼저 바람 민감도 정보를 잔디 3D 모델에 삽입합니다.
이 정보를 잔디 3D 모델의 각 꼭짓점에 맞춤 속성으로 삽입합니다. 잔디 모델의 하단 (원뿔의 끝)은 땅에 부착되어 있으므로 민감도가 0이라는 규칙을 사용합니다. 잔디 모델의 상단 부분 (원뿔의 밑면)은 지면에서 더 멀리 떨어져 있으므로 최대 풍속 민감도가 있습니다.
다음은 instanceGrass 함수를 재코딩하여 바람 민감도를 잔디 3D 모델의 맞춤 속성으로 추가하는 방법입니다.
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
이제 이전에 사용한 MeshPhongMaterial 대신 맞춤 재료 windMaterial을 사용합니다. WindMaterial는 잠시 후에 살펴볼 WindMeshShader를 래핑합니다.
따라서 instanceGrass의 코드는 잔디 모델의 모든 꼭짓점을 반복하고 각 꼭짓점에 windFactor라는 맞춤 꼭짓점 속성을 추가합니다. 이 windFactor는 잔디 모델의 하단 (지형에 닿는 지점)에 대해 0으로 설정되고 잔디 모델의 상단에 대해 1로 설정됩니다.
필요한 또 다른 요소는 장면에 실제 바람을 추가하는 것입니다. 앞서 설명한 대로 Perlin 노이즈를 사용합니다. 절차적으로 Perlin 노이즈 텍스처를 생성합니다.
명확성을 위해 이전의 녹색 텍스처 대신 이 텍스처를 지형 자체에 할당합니다. 이렇게 하면 바람의 움직임을 더 쉽게 파악할 수 있습니다.
따라서 이 Perlin 노이즈 텍스처는 지형의 확장 영역을 공간적으로 덮고 텍스처의 각 픽셀은 해당 픽셀이 위치한 지형 영역의 풍속을 지정합니다. 지형 직사각형이 '풍속 영역'이 됩니다.
Perlin 노이즈는 NoiseShader라는 셰이더를 통해 절차적으로 생성됩니다. 이 셰이더는 https://github.com/ashima/webgl-noise의 3D 단순 다면체 노이즈 알고리즘을 사용합니다 . 이의 WebGL 버전은 MrDoob의 Three.js 샘플 중 하나(http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html)에서 그대로 가져왔습니다.
NoiseShader는 시간, 크기, 오프셋 매개변수 집합을 유니폼으로 사용하고 멋진 2D 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) }
...
이 셰이더를 사용하여 Perlin 노이즈를 텍스처에 렌더링합니다. 이는 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 )
위의 코드는 noiseMap을 Three.js 렌더링 타겟으로 설정하고, NoiseShader를 장착한 다음 투시 왜곡을 방지하기 위해 직사각형 카메라로 렌더링합니다.
앞서 설명한 대로 이제 이 텍스처를 지형의 기본 렌더링 텍스처로도 사용합니다. 이는 바람 효과 자체가 작동하는 데는 필요하지 않습니다. 하지만 풍력 발전의 상황을 시각적으로 더 잘 이해할 수 있으므로 있으면 좋습니다.
다음은 noiseMap을 텍스처로 사용하는 수정된 initTerrain 함수입니다.
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 )
이제 바람 텍스처가 있으므로 바람에 따라 잔디 모델을 변형하는 WindMeshShader를 살펴보겠습니다.
이 셰이더를 만들기 위해 표준 Three.js MeshPhongMaterial 셰이더에서 시작하여 수정했습니다. 이 방법은 처음부터 시작할 필요 없이 작동하는 셰이더를 빠르게 시작하는 데 유용합니다.
여기서는 전체 셰이더 코드를 복사하지 않습니다 (소스 코드 파일에서 자유롭게 살펴보세요). 대부분 MeshPhongMaterial 셰이더의 복제이기 때문입니다. 하지만 먼저 Vertex 셰이더에서 수정된 바람 관련 부분을 살펴보겠습니다.
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;
따라서 이 셰이더는 먼저 버텍스의 2D xz (가로) 위치를 기반으로 windUV 텍스처 조회 좌표를 계산합니다. 이 UV 좌표는 Perlin 노이즈 풍력 텍스처에서 풍력 vWindForce를 조회하는 데 사용됩니다.
이 vWindForce 값은 꼭짓점에 필요한 변형 정도를 계산하기 위해 꼭짓점별 windFactor(위에서 설명한 맞춤 속성)와 합성됩니다. 또한 전반적인 바람의 세기를 제어하는 전역 windScale 매개변수와 바람 변형이 발생해야 하는 방향을 지정하는 windDirection 벡터도 있습니다.
이렇게 하면 풀잎이 바람에 의해 변형됩니다. 하지만 아직 완료되지 않았습니다. 현재 이 변형은 정적이며 바람이 부는 지역의 효과를 전달하지 않습니다.
앞서 언급했듯이 유리가 흔들리도록 시간 경과에 따라 바람의 영역에서 노이즈 텍스처를 슬라이드해야 합니다.
이는 NoiseShader에 전달되는 vOffset 유니폼을 시간 경과에 따라 이동하여 실행됩니다. 이는 특정 방향 (바람 방향)을 따라 노이즈 오프셋을 지정할 수 있는 vec2 매개변수입니다.
이는 모든 프레임에서 호출되는 render 함수에서 실행됩니다.
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
...
그러면 끝입니다. 방금 바람의 영향을 받는 '프로시저럴 그래스'가 있는 장면을 만들었습니다.
믹스에 먼지 추가
이제 장면을 조금 더 멋지게 만들어 보겠습니다. 장면을 더 흥미롭게 만들기 위해 날리는 먼지를 조금 추가해 보겠습니다.

먼지는 바람의 영향을 받는다고 가정할 수 있으므로 바람이 불고 있는 장면에서 먼지가 날리는 것은 당연합니다.
먼지는 initDust 함수에서 입자 시스템으로 설정됩니다.
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
여기에서 먼지 입자 130개가 생성됩니다. 각 셰이더에는 특수한 WindParticleShader가 장착되어 있습니다.
이제 각 프레임에서 CoffeeScript를 사용하여 바람과 관계없이 입자를 약간 움직입니다. 코드는 다음과 같습니다.
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 )
또한 바람에 따라 각 입자의 위치를 오프셋합니다. 이 작업은 WindParticleShader에서 실행됩니다. 특히 버텍스 셰이더에서
이 셰이더의 코드는 Three.js ParticleMaterial의 수정된 버전이며 핵심은 다음과 같습니다.
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;
이 정점 셰이더는 풀의 바람 기반 변형에 사용한 것과 크게 다르지 않습니다. Perlin 노이즈 텍스처를 입력으로 사용하고 먼지 월드 위치에 따라 노이즈 텍스처에서 vWindForce 값을 조회합니다. 그런 다음 이 값을 사용하여 먼지 입자의 위치를 수정합니다.
Riders On The Storm
가장 모험적인 WebGL 장면은 마지막 장면일 것입니다. 풍선으로 토네이도 중심부를 지나 사이트 여행의 끝에 도달하면 이 장면을 볼 수 있으며, 출시 예정인 제품의 독점 동영상도 확인할 수 있습니다.

이 장면을 만들 때는 경험의 핵심 기능이 강렬한 인상을 줄 수 있어야 한다고 생각했습니다. 회전하는 토네이도가 중심 역할을 하고 다른 콘텐츠 레이어가 이 기능을 조정하여 극적인 효과를 연출합니다. 이를 위해 이 기이한 셰이더를 중심으로 영화 스튜디오 세트와 유사한 환경을 구축했습니다.
사실적인 합성물을 만들기 위해 혼합된 접근 방식을 사용했습니다. 그중에는 렌즈 플레어 효과를 내기 위한 밝은 모양이나, 보는 장면 위에 레이어로 애니메이션이 적용되는 빗방울과 같은 시각적 트릭도 있었습니다. 다른 경우에는 입자 시스템 코드에 따라 움직이는 낮게 날아다니는 구름 레이어처럼 평평한 표면이 움직이는 것처럼 보이도록 그려졌습니다. 토네이도 주위를 공전하는 파편은 3D 장면의 레이어로, 토네이도 앞뒤로 이동하도록 정렬되었습니다.
이 방식으로 장면을 빌드해야 했던 주된 이유는 적용 중인 다른 효과와 균형을 맞춰 토네이도 셰이더를 처리할 수 있을 만큼 충분한 GPU가 필요했기 때문입니다. 처음에는 GPU 균형 문제가 심각했지만 나중에 이 장면이 최적화되어 기본 장면보다 가벼워졌습니다.
튜토리얼: 폭풍 셰이더
최종 폭풍 시퀀스를 만들기 위해 다양한 기법을 조합했지만 이 작업의 핵심은 토네이도처럼 보이는 맞춤 GLSL 셰이더였습니다. 흥미로운 기하학적 소용돌이를 만들기 위한 버텍스 셰이더부터 입자 기반 애니메이션, 심지어 뒤틀린 기하학적 도형의 3D 애니메이션에 이르기까지 다양한 기법을 시도했습니다. 효과 중 어느 것도 토네이도 느낌을 재현하지 못하거나 처리 측면에서 너무 많은 것을 요구하는 것 같았습니다.
완전히 다른 프로젝트에서 답을 찾았습니다. 막스 플랑크 연구소 (brainflight.org)의 과학 게임을 통한 마우스 뇌 지형 매핑과 관련된 프로젝트에서는 흥미로운 시각 효과를 생성했습니다. 맞춤 볼륨 셰이더를 사용하여 마우스 뉴런 내부의 영화를 만들 수 있었습니다.

뇌세포 내부가 토네이도 깔때기와 약간 유사한 모양을 하고 있는 것으로 확인되었습니다. 볼륨 기법을 사용하고 있었기 때문에 공간의 모든 방향에서 이 셰이더를 볼 수 있었습니다. 특히 구름 층 아래에 있고 극적인 배경 위에 있는 경우 셰이더의 렌더링을 폭풍 장면과 결합하도록 설정할 수 있습니다.
셰이더 기법에는 기본적으로 단일 GLSL 셰이더를 사용하여 거리 필드가 있는 레이 마칭 렌더링이라는 단순화된 렌더링 알고리즘으로 전체 객체를 렌더링하는 트릭이 포함됩니다. 이 기법에서는 화면의 각 지점에서 노출 영역까지의 가장 가까운 거리를 추정하는 픽셀 셰이더가 생성됩니다.
알고리즘에 관한 좋은 참조는 iq의 개요인 두 삼각형으로 렌더링하는 세계 - 이니고 퀼레스에서 확인할 수 있습니다. glsl.heroku.com의 셰이더 갤러리도 살펴보세요. 이 기술을 실험할 수 있는 많은 예가 있습니다.
셰이더의 핵심은 기본 함수로 시작하여 카메라 변환을 설정하고 표면까지의 거리를 반복적으로 평가하는 루프를 시작합니다. RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ) 호출이 핵심 광선 행진 계산이 실행되는 위치입니다.
for(int i=0;i < number_of_steps;i++) // run the ray marching loop
{
old_d=d;
float shape_value=Shape(q); // find out the approximate distance to or density of the tornado cone
float density=-shape_value;
d=max(shape_value*step_scaling,0.0);// The max function clamps values smaller than 0 to 0
float step_dist=d+extra_step; // The point is advanced by larger steps outside the tornado,
// allowing us to skip empty space quicker.
if (density>0.0) { // When density is positive, we are inside the cloud
float brightness=exp(-0.6*density); // Brightness decays exponentially inside the cloud
// This function combines density layers to create a translucent fog
FogStep(step_dist*0.2,clamp(density, 0.0,1.0)*vec3(1,1,1), vec3(1)*brightness, colour, multiplier);
}
if(dist>max_dist || multiplier.x < 0.01) { return; } // if we've gone too far stop, we are done
dist+=step_dist; // add a new step in distance
q=org+dist*dir; // trace its direction according to the ray casted
}
생각해 볼 수 있는 점은 토네이도 모양으로 진행할 때 픽셀의 최종 색상 값에 색상 기여도를 정기적으로 추가하고 광선의 불투명도에 기여도를 추가한다는 것입니다. 이렇게 하면 토네이도 질감에 레이어드된 부드러운 느낌이 생성됩니다.
토네이도의 다음 핵심 요소는 여러 함수를 컴포지션하여 생성되는 실제 도형 자체입니다. 처음에는 원뿔 모양으로 시작하여 노이즈로 구성하여 유기적이고 거친 가장자리를 만들고, 그 후에는 기본 축을 따라 비틀고 시간에 따라 회전합니다.
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;
}
이러한 종류의 셰이더를 만드는 작업은 까다롭습니다. 생성 중인 작업의 추상화와 관련된 문제 외에도 프로덕션에서 작업을 사용하기 전에 추적하고 해결해야 하는 심각한 최적화 및 교차 플랫폼 호환성 문제가 있습니다.
문제의 첫 번째 부분은 장면에 맞게 이 셰이더를 최적화하는 것입니다. 이를 해결하기 위해 셰이더가 너무 무거워질 경우를 대비한 '안전한' 접근 방식이 필요했습니다. 이를 위해 나머지 장면과는 다른 샘플링 해상도로 토네이도 셰이더를 합성했습니다. stormTest.coffee 파일에서 가져온 코드입니다. 테스트용입니다.
장면 너비와 높이에 맞는 렌더 타겟으로 시작하여 토네이도 셰이더의 해상도를 장면과 독립적으로 유지할 수 있습니다. 그런 다음 수신하는 프레임 속도에 따라 폭풍 셰이더의 해상도 다운샘플링을 동적으로 결정합니다.
...
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
마지막으로 블록이 표시되지 않도록 단순화된 sal2x 알고리즘을 사용하여 토네이도를 화면에 렌더링합니다(stormTest.coffee의 1107번 줄). 즉, 최악의 경우 더 흐릿한 토네이도가 표시될 수 있지만, 적어도 사용자의 제어권을 박탈하지 않고 작동합니다.
다음 최적화 단계에서는 알고리즘을 자세히 살펴봐야 합니다. 셰이더의 핵심 계산 요소는 표면 함수의 거리를 근사하기 위해 각 픽셀에서 실행되는 반복, 즉 레이마르칭 루프의 반복 횟수입니다. 더 큰 단계 크기를 사용하면 구름이 있는 표면 밖에 있을 때 더 적은 반복으로 토네이도 표면 추정치를 얻을 수 있습니다. 안쪽에서는 정확성을 위해 단계 크기를 줄이고 값을 혼합하여 안개 효과를 만들 수 있습니다. 또한 투사된 광선의 깊이 추정치를 얻기 위해 경계 원통을 만들어 속도를 높였습니다.
다음 문제는 이 셰이더가 다양한 비디오 카드에서 실행되는지 확인하는 것이었습니다. 매번 몇 가지 테스트를 진행하고 발생할 수 있는 호환성 문제 유형에 대한 직관을 쌓기 시작했습니다. 직관보다 나은 결과를 얻지 못한 이유는 오류에 관한 유용한 디버깅 정보를 항상 얻을 수 있는 것은 아니기 때문입니다. 일반적인 시나리오는 GPU 오류가 발생하거나 시스템이 비정상 종료되는 경우입니다.
교차 동영상 보드 호환성 문제에도 유사한 해결 방법이 있습니다. 정적 상수가 정의된 대로 정확한 데이터 유형으로 입력되어 있는지 확인합니다(예: 부동 소수점의 경우 0.0, 정수의 경우 0). 더 긴 함수를 작성할 때는 주의하세요. 컴파일러가 특정 사례를 올바르게 처리하지 않는 것 같으므로 여러 개의 간단한 함수와 임시 변수로 나누는 것이 좋습니다. 텍스처가 모두 2의 거듭제곱이고 너무 크지 않아야 하며, 루프에서 텍스처 데이터를 조회할 때는 항상 '주의'를 기울여야 합니다.
호환성에서 가장 큰 문제는 폭풍의 조명 효과에서 발생했습니다. 소용돌이의 마디에 색상을 지정할 수 있도록 미리 만들어진 텍스처를 소용돌이 주위에 래핑했습니다. 멋진 효과였으며 토네이도를 장면 색상과 쉽게 혼합할 수 있었지만 다른 플랫폼에서 실행하려면 시간이 오래 걸렸습니다.

모바일 웹사이트
기술 및 처리 요구사항이 너무 까다로워 모바일 환경을 데스크톱 버전과 그대로 번역할 수 없었습니다. 특히 모바일 사용자를 타겟팅하는 새로운 것을 만들어야 했습니다.
데스크톱의 카니발 사진 부스를 사용자의 휴대기기 카메라를 사용하는 모바일 웹 애플리케이션으로 제공하면 좋을 것 같았습니다. 지금까지는 보지 못했던 방식입니다.
재미를 더하기 위해 CSS3로 3D 변환을 코딩했습니다. 자이로스코프 및 가속도계와 연결하여 환경에 깊이를 더할 수 있었습니다. 사이트는 휴대전화를 들고, 움직이고, 보는 방식에 반응합니다.
이 도움말을 작성할 때는 모바일 개발 프로세스를 원활하게 실행하는 방법에 관한 몇 가지 힌트를 제공하는 것이 좋겠다고 생각했습니다. 다음과 같습니다. 이 보고서에서 어떤 정보를 얻을 수 있는지 확인해 보세요.
모바일 도움말 및 유용한 정보
프리로더는 피해야 할 것이 아니라 필요한 것입니다. 후자의 경우가 발생하는 경우도 있습니다. 이는 주로 프로젝트가 커짐에 따라 미리 로드하는 항목의 목록을 계속 유지해야 하기 때문입니다. 더 나쁜 점은 여러 리소스를 동시에 가져오는 경우 로드 진행률을 계산하는 방법이 명확하지 않습니다. 여기에서 맞춤의 매우 일반적인 추상 클래스인 'Task'가 유용합니다. 기본 아이디어는 태스크에 자체 하위 태스크가 있을 수 있고 하위 태스크에 또 하위 태스크가 있을 수 있는 등 무한히 중첩된 구조를 허용하는 것입니다. 또한 각 태스크는 하위 태스크의 진행률을 기준으로 진행률을 계산하지만 상위 태스크의 진행률은 고려하지 않습니다. 모든 MainPreloadTask, AssetPreloadTask, TemplatePreFetchTask를 Task에서 파생하여 다음과 같은 구조를 만들었습니다.

이러한 접근 방식과 Task 클래스 덕분에 전반적인 진행 상황 (MainPreloadTask)이나 애셋 진행 상황 (AssetPreloadTask) 또는 템플릿 로드 진행 상황 (TemplatePreFetchTask)을 쉽게 알 수 있습니다. 특정 파일의 진행 상황도 확인할 수 있습니다. 이를 수행하는 방법을 알아보려면 /m/javascripts/raw/util/Task.js의 Task 클래스와 /m/javascripts/preloading/task의 실제 태스크 구현을 살펴보세요. 예를 들어 다음은 최고의 미리 로드 래퍼인 /m/javascripts/preloading/task/MainPreloadTask.js 클래스를 설정하는 방법을 보여주는 발췌 부분입니다.
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);
}
}
})
]);
/m/javascripts/preloading/task/subtask/AssetPreloadTask.js 클래스에서 공유 작업 구현을 통해 MainPreloadTask와 통신하는 방법 외에도 플랫폼에 종속된 애셋을 로드하는 방법도 주목할 만합니다. 기본적으로 네 가지 유형의 이미지가 있습니다. 모바일 표준 (.ext, 여기서 ext는 파일 확장자, 일반적으로 .png 또는 .jpg), 모바일 레티나 (-2x.ext), 태블릿 표준 (-tab.ext) 및 태블릿 레티나 (-tab-2x.ext) MainPreloadTask에서 감지하고 4개의 애셋 배열을 하드코딩하는 대신 미리 로드할 애셋의 이름과 확장자가 무엇인지, 애셋이 플랫폼에 종속적인지 (responsive = true / false)만 지정합니다. 그러면 AssetPreloadTask가 파일 이름을 생성합니다.
resolveAssetUrl : function(assetName, extension, responsive) {
return AssetPreloadTask.ASSETS_ROOT + assetName + (responsive === true ? ((Detection.getInstance().tablet ? '-tab' : '') + (Detection.getInstance().retina ? '-2x' : '')) : '') + '.' + extension;
}
클래스 체인의 아래쪽에서 애셋 미리 로드를 실행하는 실제 코드는 다음과 같습니다 (/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);
}
튜토리얼: HTML5 사진 부스 (iOS6/Android)
OZ 모바일을 개발할 때는 작업하는 대신 사진 부스에서 놀면서 시간을 보냈습니다. :D 그저 재미있었기 때문입니다. 그래서 사용해 볼 수 있는 데모를 만들었습니다.

여기에서 실시간 데모를 확인할 수 있습니다 (iPhone 또는 Android 휴대전화에서 실행).
http://u9html5rocks.appspot.com/demos/mobile_photo_booth
이를 설정하려면 백엔드를 실행할 수 있는 무료 Google App Engine 애플리케이션 인스턴스가 필요합니다. 프런트엔드 코드는 복잡하지 않지만 몇 가지 주의할 점이 있습니다. 이제 자세히 살펴보겠습니다.
- 허용되는 이미지 파일 형식
동영상 부스가 아닌 사진 부스이므로 사용자가 이미지만 업로드할 수 있도록 합니다. 이론적으로는 다음과 같이 HTML에서 필터를 지정하면 됩니다.
input id="fileInput" class="fileInput" type="file" name="file" accept="image/*"
하지만 이 방법은 iOS에서만 작동하는 것 같으므로 파일이 선택되면 RegExp에 대한 추가 검사를 추가해야 합니다.
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();
}
}
});
- 업로드 또는 파일 선택 취소 개발 과정에서 발견된 또 다른 불일치는 여러 기기에서 파일 선택 취소를 알리는 방식입니다. iOS 휴대전화와 태블릿은 아무것도 하지 않으며 전혀 알림을 보내지 않습니다. 따라서 이 경우에는 특별한 작업이 필요하지 않지만 Android 휴대전화는 파일이 선택되지 않은 경우에도 add() 함수를 트리거합니다. 이 문제를 해결하는 방법은 다음과 같습니다.
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();
}
}
나머지는 여러 플랫폼에서 원활하게 작동합니다. 즐거운 시간 보내시기 바랍니다.
결론
Find Your Way To Oz의 방대한 규모와 다양한 기술을 고려하여 이 도움말에서는 사용된 접근 방식 중 일부만 다룰 수 있었습니다.
전체 소스 코드를 살펴보고 싶다면 이 링크에서 Oz로 가는 길 찾기의 전체 소스 코드를 살펴보세요.
크레딧
전체 크레딧 목록을 보려면 여기를 클릭하세요.
참조
- CoffeeScript - http://coffeescript.org/
- Backbone.js - http://backbonejs.org/
- Three.js - http://mrdoob.github.com/three.js/
- 막스 플랑크 연구소 (brainflight.org) - http://brainflight.org/