우수사례 - 오즈로 가는 길 찾기

소개

'오즈로 가는 길'은 디즈니에서 선보이는 새로운 Chrome 실험실 기능입니다. 대화형 여행을 통해 캔자스 서커스를 거닐 수 있습니다. 거대한 폭풍에 휩쓸린 후에는 오즈의 땅으로 이동할 수 있습니다.

우리의 목표는 영화의 풍요로움과 브라우저의 기술적 기능을 결합하여 사용자가 유대감을 쌓을 수 있는 재미있고 몰입도 높은 경험을 제공하는 것이었습니다.

이 작업은 전체 내용을 담기에는 너무 크므로 본론으로 들어가서 흥미롭다고 생각되는 몇 개의 장을 발췌했습니다. 그 과정에서 난이도 상승에 초점을 맞춘 튜토리얼 몇 개를 추출했습니다.

이러한 경험을 제공하기 위해 많은 사람들이 열심히 노력했습니다. 여기에 나열하기에는 너무 많습니다. 사이트를 방문하여 메뉴 섹션에 있는 크레딧 페이지에서 전체 스토리를 확인하세요.

자세히 살펴보기

데스크톱에서 '오즈로 가는 길'을 찾아보세요. 3D 및 전통적인 영화 제작에서 영감을 받은 여러 층의 효과를 사용하여 현실에 가까운 장면을 연출합니다. 가장 눈에 띄는 기술은 Three.js가 포함된 WebGL, 맞춤형으로 빌드된 셰이더 및 CSS3 기능을 사용하는 DOM 애니메이션 요소입니다. 이 외에도 대화형 환경을 위한 getUserMedia API (WebRTC)를 통해 사용자가 웹캠 및 WebAudio에서 직접 이미지를 추가하여 3D 사운드를 즐길 수 있습니다.

하지만 이와 같은 기술적 경험의 마법은 그 결합이 이루어집니다. 또한 하나의 장면에 시각 효과와 상호작용 요소를 결합하여 일관된 전체를 만들려면 어떻게 해야 하는가입니다. 이러한 시각적 복잡성을 관리하기가 어려웠기 때문에 한때 개발 단계를 파악하기가 어려웠습니다.

상호 연결된 시각 효과와 최적화 문제를 해결하기 위해 우리는 당시 검토 중이던 모든 관련 설정을 캡처하는 제어판을 많이 사용했습니다. 밝기, 피사계 심도, 감마 등 원하는 대로 브라우저에서 장면을 실시간으로 조정할 수 있습니다. 누구나 경험에서 중요한 매개변수의 값을 조정해 보고 무엇이 가장 효과적인지 확인하는 데 참여할 수 있었습니다.

비밀을 공유하기 전에, 마치 자동차 엔진 내부를 돌아다니는 것처럼 충돌이 발생할 수 있다는 경고를 드리고 싶습니다. 중요한 항목이 없는지 확인하고 사이트의 기본 URL을 방문하여 주소에 ?debug=on을 추가하세요. 사이트가 로드될 때까지 기다렸다가 들어가면(?) 키를 Ctrl-I 누르면 오른쪽에 드롭다운이 표시됩니다. '카메라 경로 종료' 옵션을 선택 해제한 경우 A, W, S, D 키와 마우스를 사용하여 공간 안에서 자유롭게 이동할 수 있습니다.

카메라 경로

여기서는 모든 설정을 다루지는 않겠지만 여러 장면에서 키를 사용하여 다양한 설정을 표시하는 실험을 해보시기 바랍니다. 마지막 폭풍 시퀀스에는 추가 키 Ctrl-A가 있습니다. 이 키를 사용하여 애니메이션 재생을 전환하고 이리저리 이동할 수 있습니다. 이 장면에서 마우스 잠금 기능을 종료하기 위해 Esc를 누르고 Ctrl-I을 다시 누르면 폭풍 장면과 관련된 설정에 액세스할 수 있습니다. 주변을 둘러보면서 아래와 같은 멋진 엽서의 모습을 찍어 보세요.

폭풍 현장

이를 실현하고 필요에 따라 유연하게 조정할 수 있도록 dat.gui라는 멋진 라이브러리를 사용했습니다 (사용 방법에 관한 이전 가이드는 여기를 참고하세요). 사이트 방문자에게 노출되는 설정을 신속하게 변경할 수 있었습니다.

무광택 회화

많은 디즈니 고전 영화와 애니메이션에서 장면을 만드는 데는 여러 레이어를 결합해야 했습니다. 실사 레이어, 셀 애니메이션, 실제 세트 및 유리 위에 페인팅하여 만들어진 상단 레이어(매트 페인팅이라고 함)가 있었습니다.

우리가 만든 경험의 구조는 여러 면에서 비슷합니다. 그 일부 '레이어'가 정적인 시각 요소에 훨씬 더 가깝지만, 실제로 이러한 객체는 더 복잡한 계산에 따라 사물이 표시되는 방식에 영향을 미칩니다. 그럼에도 불구하고, 적어도 우리가 다루는 큰 그림 수준에서는 뷰를 합성합니다. 상단에는 UI 레이어가 있고 그 아래에는 여러 장면 구성요소로 이루어진 3D 장면이 있습니다.

상단 인터페이스 레이어는 DOM 및 CSS 3을 사용하여 만들었기 때문에 선택한 이벤트 목록에 따라 두 레이어 간의 통신에 관한 3D 경험과는 별개로 다양한 방식으로 상호작용을 편집할 수 있습니다. 이 통신에서는 애니메이션 적용/종료 영역을 제어하는 Backbone Router + onHashChange HTML5 이벤트를 사용합니다. (프로젝트 소스: /develop/coffee/router/Router.coffee)

튜토리얼: 스프라이트 시트 및 레티나 지원

우리가 인터페이스에 대해 의존했던 한 가지 재미있는 최적화 기법은 많은 인터페이스 오버레이 이미지를 하나의 단일 PNG로 결합하여 서버 요청을 줄이는 것이었습니다. 이 프로젝트에서 인터페이스는 웹사이트의 지연 시간을 줄이기 위해 미리 로드된 70개 이상의 이미지 (3D 텍스처 제외)로 구성되었습니다. 여기에서 라이브 스프라이트 시트를 확인할 수 있습니다.

일반 디스플레이 - http://findyourwaytooz.com/img/home/interface_1x.png Retina 디스플레이 - http://findyourwaytooz.com/img/home/interface_2x.png

다음은 스프라이트 시트를 활용하는 방법과 이를 레티나 기기에 사용하여 최대한 선명하고 깔끔하게 인터페이스를 만드는 방법에 대한 몇 가지 팁입니다.

스프라이트 시트 만들기

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

각 항목의 의미는 다음과 같습니다.

  • image는 스프라이트 시트의 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 모델링 및 애니메이션 소프트웨어에서 최대한 자유롭게 표현할 수 있어야 하며, 우리는 코드를 통해 화면에 렌더링해야 했습니다.

한동안 이러한 문제를 해결하기 위해 노력해 왔습니다. 예전에는 3D 사이트를 만들 때마다 사용할 수 있는 도구에 한계가 있었기 때문입니다. 그래서 Google은 내부 연구 목적으로 3D Librarian이라는 도구를 만들었습니다. 그리고 이제 막 실제 작업에 활용할 준비를 마쳤습니다.

이 도구는 어느 정도의 역사가 있습니다. 원래 Flash용이었고, 이 도구를 사용하면 대규모 Maya 장면을 압축해제 런타임용으로 최적화된 하나의 단일 압축 파일로 가져올 수 있었습니다. 이 모델이 최적의 이유는 렌더링 및 애니메이션 중에 조작되는 것과 동일한 데이터 구조로 장면을 효과적으로 압축했기 때문입니다. 로드 시 파일에 수행해야 할 파싱 작업이 거의 없습니다. 파일이 AMF 형식이어서 플래시에서 기본적으로 압축해제할 수 있었기 때문에 플래시에서 압축 해제가 상당히 빨랐습니다. WebGL에서 동일한 형식을 사용하려면 CPU에서 조금 더 많은 작업이 필요합니다. 사실, 코드의 데이터 압축해제 JavaScript 레이어를 다시 만들어야 했습니다. 이 레이어는 기본적으로 이러한 파일의 압축을 풀고 WebGL이 작동하는 데 필요한 데이터 구조를 다시 만들어야 했습니다. 전체 3D 장면 압축 해제는 CPU 사용량이 약간 많은 작업입니다. 오즈로 가는 길의 장면 1을 압축해제하는 데는 중급에서 고성능 머신에서 약 2초가 소요됩니다. 따라서 사용자의 환경이 중단되지 않도록 '장면 설정' 시간 (장면이 실제로 시작되기 전)에서 Web Workers 기술을 사용합니다.

이 편리한 도구는 모델, 질감, 뼈 애니메이션 등 3D 장면의 대부분을 가져올 수 있습니다. 3D 엔진에서 로드할 수 있는 단일 라이브러리 파일을 만듭니다. 이 라이브러리 내에서 장면에 필요한 모든 모델을 채우면 짜잔이고 장면에 모델을 만듭니다.

하지만 문제는 WebGL, 즉 동네의 새로운 아이를 상대해야 한다는 것이었습니다. 이는 상당히 어려운 작업이었습니다. 이는 브라우저 기반 3D 환경의 표준을 설정했기 때문입니다. 그래서 3D Librarian에서 압축된 3D 장면 파일을 가져와 WebGL이 이해할 수 있는 형식으로 적절하게 변환하는 임시 JavaScript 레이어를 만들었습니다.

튜토리얼: 바람이 불길

'오즈로 가는 길'에서 반복적으로 등장하는 주제는 바람이었습니다. 줄거리가 바람이 밀려오는 듯한 구조로 되어 있습니다.

카니발의 첫 장면은 비교적 차분합니다. 다양한 장면을 거치면서 사용자는 점점 더 강한 바람을 경험하고 마지막 장면인 폭풍으로 이어집니다.

따라서 몰입도 높은 바람 효과를 제공하는 것이 중요했습니다.

이를 만들기 위해 카니발 장면 3개에 텐트와 같이 바람의 영향을 받는 부드러운 물체, 포토 부스 표면의 깃발, 풍선 자체를 채웠습니다.

부드러운 천.

요즘 데스크톱 게임은 주로 핵심 물리학 엔진을 기반으로 구축됩니다. 따라서 3D 환경에서 소프트 객체를 시뮬레이션해야 하는 경우 전체 물리 시뮬레이션이 실행되어 믿을 수 있는 소프트 동작을 구현합니다.

WebGL / JavaScript에서 우리는 (아직) 완전한 물리 시뮬레이션을 실행할 수 있는 여력이 없습니다. 그래서 오즈에서는 실제로 시뮬레이션하지 않고 바람의 효과를 적용할 방법을 찾아야 했습니다.

우리는 3D 모델 자체에 각 물체의 '바람 민감도' 정보를 삽입했습니다. 3D 모델의 각 꼭짓점에는 꼭짓점이 바람의 영향을 받는 정도를 지정하는 '바람 속성'이 있습니다. 3D 물체의 바람 민감도를 지정합니다. 그런 다음 바람 자체를 만들어야 했습니다.

이를 위해 Perlin Noise가 포함된 이미지를 생성했습니다. 이 이미지는 특정한 '바람 영역'을 덮기 위한 것입니다. 따라서 3D 장면의 특정 직사각형 영역에 노이즈가 놓여 있는 것처럼 구름 사진을 상상해 보는 것이 좋습니다. 이 이미지의 각 픽셀, 회색 수준 값은 '주변을 둘러싼' 3D 영역에서 특정 순간에 바람의 강도를 지정합니다.

바람 효과를 만들기 위해 이미지는 시간 경과에 따라 바람의 방향이라는 특정 방향으로 일정한 속도로 이동합니다. 또한 '바람이 부는 지역'이 장면의 모든 부분에 영향을 미치지 않도록 효과 영역에만 국한된 가장자리 주위에 바람 이미지를 래핑합니다.

간단한 3D Wind 튜토리얼

이제 Three.js의 간단한 3D 장면에 바람 효과를 만들어 보겠습니다.

간단한 '절차적 잔디밭'에 바람을 만들어 보겠습니다.

먼저 장면을 만들어 보겠습니다. 단순하고 질감이 있는 평평한 지형을 만듭니다. 그러면 각 풀이 거꾸로 뒤집힌 3D 원뿔 모양으로 표현됩니다.

잔디가 우거진 지형
잔디가 가득한 지형

CoffeeScript를 사용하여 Three.js에서 이 간단한 장면을 만드는 방법은 다음과 같습니다.

먼저 Three.js를 설정하고 Camera, Mouse Controller, 일부 Light로 연결합니다.

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

initGrassinitTerrain 함수 호출은 각각 잔디와 지형으로 장면을 채웁니다.

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이라는 규칙을 사용하겠습니다. 잔디 모델의 윗부분 (원뿔의 바닥)은 지면에서 더 멀리 떨어져 있는 부분이므로 바람 민감도가 최대입니다.

잔디 3D 모델의 맞춤 속성으로 바람 민감도를 추가하기 위해 instanceGrass 함수를 다시 코딩하는 방법은 다음과 같습니다.

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는 잔디 모델의 하단 (지형에 닿아야 하는 위치)의 하단, 잔디 모델의 상단 끝에 대해 1로 설정됩니다.

다른 필요한 요소는 장면에 실제 바람을 추가하는 것입니다. 앞서 설명했듯이, 이를 위해 펄린 노이즈를 사용하겠습니다. 절차적으로 펄린 노이즈 텍스처를 생성합니다.

명확성을 위해 이전의 녹색 텍스처 대신 이 텍스처를 지형 자체에 지정하겠습니다. 이렇게 하면 바람이 부는 상황을 보다 쉽게 파악할 수 있습니다.

따라서 이 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 분포를 출력합니다.

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 Noise를 텍스처에 렌더링하겠습니다. 이 작업은 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를 장착한 다음 직교 카메라로 렌더링하여 원근 왜곡을 방지하는 것입니다.

설명했듯이 이제 이 텍스처를 지형의 기본 렌더링 텍스처로 사용하겠습니다. 이는 바람 효과 자체가 작동하는 데 꼭 필요한 것은 아닙니다. 하지만 풍력 발전에 어떤 일이 일어나고 있는지 시각적으로 더 잘 이해할 수 있도록 도와주는 것이 좋습니다.

다음은 노이즈Map을 텍스처로 사용하여 재작업된 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를 조회합니다. 그런 다음 이 값을 사용하여 먼지 입자의 위치를 수정합니다.

스톰의 라이더들

WebGL 장면 중 가장 모험적인 장면은 아마도 마지막 장면이었을 것입니다. 토네이도의 눈으로 풍선을 뚫고 지나가면서 사이트 내 여정의 끝에 도달했는지 알 수 있으며 다음 릴리스의 독점 동영상도 볼 수 있습니다.

열기구 타기 장면

이 장면을 만들 때 YouTube는 영향력을 발휘할 수 있는 경험에 핵심적인 기능이 필요하다는 사실을 깨달았습니다. 회전하는 토네이도는 중심부 역할을 하며 다른 콘텐츠의 여러 층이 이 특징을 형성하여 드라마틱한 효과를 만들어 냅니다. 이를 위해 우리는 이 이상한 셰이더 주위에 영화 스튜디오와 동등한 것을 구축했습니다.

사실적인 합성물을 만들기 위해 혼합된 접근 방식을 사용했습니다. 렌즈 플레어 효과를 주는 밝은 모양이나 보고 있는 장면 위에 겹쳐진 빗방울과 같은 시각적 트릭도 있었습니다. 입자 시스템 코드에 따라 하늘을 나는 구름층을 움직이는 것처럼 움직이는 것처럼 보이게 평평한 표면을 그린 경우도 있었습니다. 토네이도 주변 궤도를 도는 이물질이 3D 장면에서 여러 겹으로 쌓여 토네이도의 앞과 뒤에서 움직일 수 있었습니다.

이런 식으로 장면을 빌드해야 했던 주된 이유는 적용한 다른 효과와 균형을 이루면서 토네이도 셰이더를 처리하기에 충분한 GPU가 있는지 확인하는 것이었습니다. 처음에는 큰 GPU 균형 문제가 발생했지만 나중에 장면이 최적화되고 기본 장면보다 가벼워졌습니다.

튜토리얼: 스톰 셰이더

최종 폭풍 시퀀스를 만들기 위해 다양한 기법을 조합했지만, 이 작업의 핵심은 토네이도처럼 보이는 커스텀 GLSL 셰이더였습니다. 우리는 꼭짓점 셰이더부터 흥미로운 기하학적 월풀, 입자 기반 애니메이션, 심지어 트위스티드 기하학적 도형의 3D 애니메이션까지 다양한 기법을 시도했습니다. 그 어떤 효과도 토네이도의 느낌을 재현하거나 처리하는 데 너무 많이 필요한 것 같지 않았습니다.

완전히 다른 프로젝트가 마침내 해답을 제시했습니다. 맥스 플랑크 연구소 (brainflight.org)에서 만든 생쥐 뇌를 매핑하는 과학 게임을 포함하는 병행 프로젝트에서 흥미로운 시각 효과를 만들었습니다. 우리는 맞춤형 볼륨 셰이더를 사용하여 마우스 뉴런 내부의 영화를 만들 수 있었습니다.

맞춤 볼륨 셰이더를 사용하는 마우스 뉴런 내부
맞춤 볼륨 셰이더를 사용하는 마우스 뉴런 내부

그 결과, 뇌 세포 내부가 토네이도의 깔때기처럼 생겼다는 사실을 발견했습니다. 입체적 기법을 사용했기 때문에 이 셰이더를 우주의 모든 방향에서 볼 수 있다는 것을 알았습니다. 특히 구름 층 아래, 극적인 배경 위에 셰이더의 렌더링을 배치하여 폭풍 장면과 결합되도록 설정할 수 있습니다.

셰이더 기법에는 기본적으로 단일 GLSL 셰이더를 사용하여 거리 필드를 사용한 레이 마칭 렌더링이라는 간소화된 렌더링 알고리즘으로 전체 객체를 렌더링하는 트릭이 포함됩니다. 이 기법에서는 화면의 각 지점에 대해 표면에 가장 가까운 거리를 추정하는 픽셀 셰이더를 만듭니다.

이 알고리즘은 iq의 개요에서 찾을 수 있습니다. Rendering Worlds With Two Triangles - Iñigo Quilez 또한 glsl.heroku.com의 셰이더 갤러리를 살펴보면 이 기술의 다양한 예를 확인할 수 있습니다.

셰이더의 핵심은 main 함수로 시작하며, 카메라 변환을 설정하고 표면까지의 거리를 반복적으로 평가하는 루프에 진입합니다. RaytraceFoggy( Direction_vector, max_iterations, color, color_multiplier ) 호출은 코어 Ray marching 계산이 이루어지는 지점입니다.

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 파일에서 가져온 것입니다 (테스트용임).

장면에 대한 토네이도 셰이더의 해상도를 독립할 수 있도록 장면 너비와 높이와 일치하는 renderTarget으로 시작합니다. 그런 다음 얻는 프레임 속도에 따라 스톰 셰이더 해상도의 다운샘플링을 동적으로 결정합니다.

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

마지막으로, stormTest.coffee의 @line 1107 @line 1107인 단순화된 sal2x 알고리즘을 사용하여 토네이도를 화면에 렌더링합니다. 즉, 최악의 경우 토네이도가 더 흐릿해지지만 적어도 사용자의 제어권을 잃지 않고 작동합니다.

다음 최적화 단계에서는 알고리즘을 자세히 들여다봐야 합니다. 셰이더의 구동 연산 계수는 표면 함수의 거리(레이마칭 루프의 반복 횟수)를 근사치로 계산하기 위해 각 픽셀에서 수행되는 반복입니다. 보폭을 더 크게 만들면 흐린 표면 밖에 있을 때 반복 횟수를 줄이면서 토네이도 표면 추정치를 얻을 수 있었습니다. 내부에서 정밀도를 위해 보폭을 줄이고 값을 혼합하여 안개 효과를 만들 수 있습니다. 또한 경계 원통을 만들어 주조 광선의 깊이 추정값을 얻은 결과 속도가 향상되는 결과를 얻을 수 있었습니다.

다음으로 문제는 이 셰이더가 다른 비디오 카드에서 실행되도록 하는 것이었습니다. 우리는 매번 몇 가지 테스트를 실행하여 발생할 수 있는 호환성 문제의 유형에 관한 직관을 쌓기 시작했습니다. 직관보다 더 잘할 수 없는 이유는 오류에 대한 적절한 디버깅 정보를 항상 얻을 수 있는 것은 아니기 때문입니다. 일반적인 시나리오는 진행해야 할 작업이 조금 더 있는 GPU 오류 또는 시스템 비정상 종료입니다.

교차 동영상 보드 호환성 문제에도 비슷한 해결 방법이 있었습니다. 정적 상수를 정의된 대로 정밀한 데이터 유형(예: 부동의 경우 0.0, int의 경우 0)으로 입력해야 합니다. 긴 함수를 작성할 때는 주의해야 합니다. 컴파일러가 특정 사례를 올바르게 처리하지 않는 것으로 보이기 때문에 여러 간단한 함수와 임시 변수로 나누는 것이 좋습니다. 텍스처는 모두 2의 거듭제곱이 되도록 너무 크지 않도록 하세요. 루프에서 텍스처 데이터를 찾을 때는 '주의'를 기울여야 합니다.

호환성에서 가장 큰 문제는 폭풍의 조명 효과였습니다. 토네이도 주위에 래핑된 미리 만들어진 텍스처를 사용하여 빗금을 색칠할 수 있었습니다. 멋진 효과가 있었고 토네이도를 장면 색상에 쉽게 혼합할 수 있었지만 다른 플랫폼에서 실행하는 데는 오랜 시간이 걸렸습니다.

토네이도

모바일 웹사이트

기술과 처리 요구사항이 너무 어려웠기 때문에 모바일 환경은 데스크톱 버전을 그대로 번역할 수 없었습니다. 모바일 사용자를 겨냥한 새로운 앱을 구축해야 했습니다.

사용자의 모바일 카메라를 사용하는 모바일 웹 애플리케이션으로 데스크톱의 Carnival Photo-Booth가 있으면 좋겠다고 생각했습니다. 지금까지는 해내지 못한 일들이 있습니다.

기능을 추가하기 위해 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와 통신하는 방법 (공유 Task 구현을 통해) 외에도 플랫폼에 종속된 애셋을 로드하는 방법도 알아두는 것이 좋습니다. 기본적으로 4가지 유형의 이미지가 있습니다. 모바일 표준 (.ext, 여기서 ext는 파일 확장자, 일반적으로 .png 또는 .jpg), 모바일 레티나 (-2x.ext), 태블릿 표준 (-tab.ext) 및 태블릿 레티나 (-tab-2x.ext). MainPreloadTask에서 감지를 수행하고 4개의 자산 배열을 하드코딩하는 대신, 미리 로드할 자산의 이름과 확장자가 무엇인지, 자산이 플랫폼에 종속되는지 (반응형 = 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 Photo Booth (iOS6/Android)

OZ Mobile을 개발할 때 작업하지 않고 실제로 포토 부스를 가지고 노는 데 많은 시간을 보낸다는 것을 알게 되었습니다. :D 그게 재미있기 때문이었습니다. 그래서 우리는 여러분이 사용할 수 있는 데모를 만들었습니다.

이동식 포토 부스
모바일 포토 부스

여기에서 라이브 데모를 볼 수 있습니다 (iPhone 또는 Android 휴대폰에서 실행).

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

이를 설정하려면 백엔드를 실행할 수 있는 무료 Google App Engine 애플리케이션 인스턴스가 필요합니다. 프런트엔드 코드는 복잡하지 않지만 몇 가지 주의사항이 있습니다. 이제 자세히 살펴보겠습니다.

  1. 허용되는 이미지 파일 형식 Google에서는 사용자가 이미지만 업로드할 수 있기를 바랍니다 (동영상 부스가 아닌 포토 부스이므로). 이론적으로는 다음과 같이 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();
     }
   }
   });
  1. 업로드 또는 파일 선택 취소 개발 과정 중에 발견된 또 다른 불일치는 기기별로 취소된 파일 선택을 알리는 방법입니다. 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();            
    }
    }

나머지는 여러 플랫폼에서 원활하게 작동합니다. 즐기기.

결론

'오즈로 가는 길'의 거대한 크기와 다양한 기술이 관련되어 있다는 점을 감안할 때 이 도움말에서는 우리가 사용한 접근 방식 중 일부만 다룰 수 있었습니다.

전체 엔칠라다에 대해 알아보고 싶다면 이 링크에서 오즈로 가는 길의 전체 소스 코드를 살펴보세요.

크레딧

전체 크레딧 목록을 보려면 여기를 클릭하세요.

참조