Nghiên cứu điển hình – Tìm đường đến xứ Oz

Giới thiệu

“Tìm đường đến xứ Oz” là một thử nghiệm mới trên Google Chrome do Disney phát hành trên web. Ứng dụng này cho phép bạn tham gia một hành trình tương tác qua rạp xiếc Kansas, dẫn bạn đến xứ Oz sau khi bị một cơn bão lớn cuốn trôi.

Mục tiêu của chúng tôi là kết hợp sự phong phú của rạp chiếu phim với các tính năng kỹ thuật của trình duyệt nhằm tạo ra trải nghiệm thú vị, sống động mà người dùng có thể tạo ra một mối gắn kết bền chặt.

Công việc khá lớn nên không thể mô tả toàn bộ trong phần này, vì vậy chúng tôi đã đi sâu vào và rút ra một số chương cho câu chuyện công nghệ mà chúng tôi cho là thú vị. Đồng thời, chúng tôi đã trích xuất một số video hướng dẫn tập trung về độ khó tăng dần.

Nhiều người đã nỗ lực để biến trải nghiệm này thành có thể: có quá nhiều người để liệt kê ở đây. Vui lòng truy cập trang web để xem trang ghi công trong phần trình đơn để xem toàn bộ câu chuyện.

Tìm hiểu sâu

Tìm đường đến xứ Oz trên máy tính là một thế giới phong phú sống động. Chúng tôi sử dụng 3D và một số lớp hiệu ứng lấy cảm hứng làm phim truyền thống kết hợp với nhau để tạo ra một cảnh gần như chân thực. Các công nghệ nổi bật nhất là WebGL với Three.js, chương trình đổ bóng được xây dựng tuỳ chỉnh và các yếu tố động DOM sử dụng các tính năng CSS3. Ngoài ra, API getUserMedia (WebRTC) cho trải nghiệm tương tác cho phép người dùng thêm hình ảnh trực tiếp từ Webcam và WebAudio cho âm thanh 3D.

Nhưng sự kỳ diệu của những trải nghiệm công nghệ như thế này là cách chúng kết hợp với nhau. Đây cũng là một trong những thách thức chính: làm cách nào để kết hợp hiệu ứng hình ảnh và các yếu tố tương tác với nhau trong một cảnh để tạo ra một tổng thể nhất quán? Sự phức tạp về mặt hình ảnh này thật khó quản lý: rất khó để biết chúng tôi đang ở giai đoạn phát triển nào tại bất kỳ thời điểm nào.

Để giải quyết vấn đề tối ưu hoá và hiệu ứng hình ảnh kết nối với nhau, chúng tôi đã sử dụng rất nhiều bảng điều khiển. Bảng điều khiển này có thể chứa tất cả chế độ cài đặt có liên quan mà chúng tôi đang xem xét tại thời điểm đó. Cảnh có thể được điều chỉnh trực tiếp trong trình duyệt cho bất cứ điều gì từ độ sáng, độ sâu trường ảnh, gamma, v.v. Bất kỳ ai cũng có thể thử điều chỉnh giá trị của các thông số quan trọng trong trải nghiệm và tham gia khám phá giá trị nào hoạt động tốt nhất.

Trước khi chia sẻ bí mật của mình, chúng tôi muốn cảnh báo với bạn rằng cảm biến có thể gặp sự cố, giống như khi bạn chọc vào bên trong động cơ ô tô. Đảm bảo rằng bạn không có thông tin quan trọng nào, sau đó truy cập vào url chính của trang web và thêm ?debug=on vào địa chỉ. Chờ trang web tải và khi bạn vào trong (nhấn?) phím Ctrl-I và bạn sẽ thấy một trình đơn thả xuống xuất hiện ở bên phải. Nếu bỏ đánh dấu tuỳ chọn "Thoát đường dẫn máy ảnh", bạn có thể sử dụng các phím A, W, S, D và chuột để tự do di chuyển xung quanh không gian.

Đường dẫn camera.

Chúng tôi sẽ không tìm hiểu về tất cả chế độ cài đặt ở đây, nhưng bạn nên thử nghiệm: các phím sẽ hiển thị các chế độ cài đặt khác nhau trong các cảnh khác nhau. Trong chuỗi bão cuối cùng có thêm một phím: Ctrl-A. Bạn có thể dùng phím này để bật/tắt chế độ phát ảnh động và di chuyển xung quanh. Trong cảnh này, nếu bạn nhấn Esc (để thoát khỏi chức năng khoá chuột) rồi nhấn lại Ctrl-I, bạn có thể truy cập vào các chế độ cài đặt dành riêng cho cảnh báo bão. Hãy xem xung quanh và chụp một số bức ảnh bưu thiếp đẹp như dưới đây.

Cảnh bão

Để thực hiện điều này và đảm bảo nó đủ linh hoạt theo nhu cầu của chúng tôi, chúng tôi đã sử dụng một thư viện đáng yêu có tên là da.gui (xem tại đây để biết hướng dẫn trước đây về cách sử dụng thư viện này). Việc này cho phép chúng tôi nhanh chóng thay đổi những chế độ cài đặt mà khách truy cập trang web thấy được.

Tranh vẽ có hiệu ứng mờ

Trong nhiều bộ phim kinh điển của Disney và các bộ phim hoạt hình, việc tạo ra cảnh kết hợp nhiều lớp khác nhau. Có nhiều lớp chuyển động trực tiếp, hoạt ảnh ô, thậm chí là các hiệu ứng vật lý và trên các lớp trên cùng được tạo ra bằng cách vẽ trên kính: một kỹ thuật có tên là sơn mờ.

Theo nhiều cách, cấu trúc của trải nghiệm mà chúng tôi tạo ra là tương tự nhau, mặc dù một số "lớp" nhiều hơn nhiều so với hình ảnh tĩnh. Trên thực tế, chúng ảnh hưởng đến cách mọi thứ hiển thị theo các phép tính phức tạp hơn. Tuy nhiên, ít nhất ở cấp độ toàn cảnh, chúng ta đang xử lý các chế độ xem khi kết hợp chế độ xem với nhau. Ở trên cùng, bạn sẽ thấy một lớp giao diện người dùng, với cảnh 3D bên dưới: chính nó được tạo từ các thành phần cảnh khác nhau.

Lớp giao diện trên cùng được tạo bằng DOM và CSS 3, điều này có nghĩa là việc chỉnh sửa tương tác có thể được thực hiện theo nhiều cách độc lập với trải nghiệm 3D với hoạt động giao tiếp giữa hai môi trường theo danh sách sự kiện được chọn. Hoạt động giao tiếp này sử dụng Sự kiện HTML5 của Bộ định tuyến xương sống + onHashChange HTML5 để kiểm soát vùng nào sẽ tạo hiệu ứng động vào/ra. (nguồn dự án: /develop/Cà phê/router/Router.quán cà phê).

Hướng dẫn: Hỗ trợ Sprite Trang tính và Retina

Một kỹ thuật tối ưu hoá thú vị mà chúng tôi sử dụng cho giao diện là kết hợp nhiều hình ảnh lớp phủ giao diện trong một tệp PNG duy nhất để giảm yêu cầu máy chủ. Trong dự án này, giao diện được tạo thành từ hơn 70 hình ảnh (không tính hoạ tiết 3D) được tải trước để giảm độ trễ của trang web. Bạn có thể xem trang sprite trực tiếp tại đây:

Hiển thị Bình thường - http://findyourwaytooz.com/img/home/interface_1x.png Màn hình Retina – http://findyourwaytooz.com/img/home/interface_2x.png

Sau đây là một số mẹo về cách chúng tôi tận dụng việc sử dụng Sprite Trang tính và cách sử dụng chúng cho các thiết bị retina để giúp giao diện trở nên sắc nét và gọn gàng nhất có thể.

Tạo các trang tính

Để tạo SpriteTrang tính, chúng tôi đã sử dụng TexturePacker ở định dạng bất kỳ mà bạn cần. Trong trường hợp này, chúng ta đã xuất dưới dạng EaselJS, mã này thực sự sạch và có thể cũng được dùng để tạo các sprite động.

Sử dụng Sprite Sheet đã tạo

Sau khi tạo Trang tính Sprite, bạn sẽ thấy một tệp JSON như sau:

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

Trong trường hợp:

  • hình ảnh đề cập đến URL của bảng sprite
  • khung là toạ độ của từng phần tử trên giao diện người dùng [x, y, width, height]
  • ảnh động là tên của từng thành phần

Lưu ý là chúng ta đã sử dụng các hình ảnh có mật độ cao để tạo trang tính Sprite, sau đó chúng ta đã tạo phiên bản thông thường, chỉ cần đổi kích thước bằng một nửa kích thước của trang tính đó.

Mối tương quan giữa các yếu tố

Giờ đây, khi đã hoàn tất, chúng ta chỉ cần một đoạn mã JavaScript để sử dụng đoạn mã này.

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

Và đây là cách bạn sẽ sử dụng:

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

Để tìm hiểu thêm về Mật độ pixel biến đổi, bạn có thể đọc bài viết này của Boris Smus.

Quy trình nội dung 3D

Trải nghiệm môi trường được thiết lập trên lớp WebGL. Khi bạn nghĩ về cảnh 3D, một trong những câu hỏi khó nhất là làm thế nào để đảm bảo bạn có thể tạo nội dung phát huy tối đa tiềm năng biểu đạt từ các khía cạnh mô hình, ảnh động và hiệu ứng. Theo nhiều cách, điểm cốt lõi của vấn đề này chính là quy trình nội dung: một quy trình được thống nhất để tạo nội dung cho cảnh 3D.

Chúng tôi muốn tạo nên một thế giới đáng kinh ngạc, tràn đầy cảm hứng; vì vậy, chúng tôi cần một quy trình vững chắc để các nghệ sĩ 3D tạo ra thế giới này. Chúng cần được tự do thể hiện nhiều nhất có thể trong phần mềm lập mô hình và hoạt hình 3D; và chúng tôi cần phải kết xuất nó trên màn hình thông qua mã.

Chúng tôi đã nghiên cứu loại vấn đề này trong một thời gian bởi vì mỗi lần chúng tôi tạo trang web 3D trong quá khứ, chúng tôi đã nhận thấy những hạn chế trong các công cụ mà chúng tôi có thể sử dụng. Vì vậy, chúng tôi đã tạo ra công cụ này có tên là 3D Librarian: một phần nghiên cứu nội bộ. Nó gần như đã sẵn sàng để áp dụng vào công việc thực tế.

Công cụ này có một số lịch sử: ban đầu là dành cho Flash và cho phép bạn đưa vào một cảnh lớn Maya dưới dạng một tệp nén duy nhất được tối ưu hóa cho việc giải nén thời gian chạy. Lý do tối ưu là vì tính năng này đã đóng gói cảnh một cách hiệu quả về cơ bản trong cùng một cấu trúc dữ liệu được điều khiển trong quá trình kết xuất và ảnh động. Bạn không cần phải thực hiện phân tích cú pháp đối với tệp khi tải. Việc giải nén trong Flash khá nhanh vì tệp ở định dạng AMF, Flash có thể giải nén nguyên gốc. Việc sử dụng cùng một định dạng trong WebGL sẽ yêu cầu nhiều công sức hơn trên CPU. Trên thực tế, chúng tôi phải tạo lại lớp mã JavaScript giải nén dữ liệu, về cơ bản sẽ giải nén các tệp đó và tạo lại cấu trúc dữ liệu cần thiết để WebGL hoạt động. Giải nén toàn bộ cảnh 3D là một thao tác tốn ít CPU: việc giải nén cảnh 1 trong Find Your Way To Oz sẽ mất khoảng 2 giây trên máy có cấu hình trung bình đến cao cấp. Do đó, điều này được thực hiện bằng công nghệ Web Workers, tại thời điểm "thiết lập cảnh" (trước khi cảnh thực sự xuất hiện), để không làm treo trải nghiệm cho người dùng.

Công cụ tiện lợi này có thể nhập hầu hết cảnh 3D: mô hình, hoạ tiết, hoạt ảnh xương. Bạn tạo một tệp thư viện duy nhất, sau đó có thể được tải bằng công cụ 3D. Bạn có thể đưa tất cả mô hình cần thiết vào cảnh của mình trong thư viện này, sau đó, hãy đưa chúng vào cảnh của bạn.

Tuy nhiên, một vấn đề chúng tôi gặp phải đó là chúng tôi đang xử lý WebGL: giải pháp dành cho trẻ em mới ra mắt. Đây là một đứa trẻ khá khó khăn: nó đã thiết lập tiêu chuẩn cho trải nghiệm 3D dựa trên trình duyệt. Vì vậy, chúng tôi đã tạo một lớp JavaScript đặc biệt để lấy các tệp cảnh 3D được nén của Thư viện 3D và dịch đúng cách chúng sang định dạng mà WebGL sẽ hiểu được.

Hướng dẫn: Có gió

Một chủ đề lặp lại trong “Tìm đường đến xứ Oz” là một chủ đề nổi bật. Một chuỗi trong cốt truyện có cấu trúc như một cơn gió nhẹ.

Cảnh đầu tiên của lễ hội diễn ra tương đối yên tĩnh. Sau khi trải qua nhiều cảnh, người dùng được trải nghiệm một luồng gió mạnh dần, đỉnh điểm là cảnh cuối cùng là một cơn bão.

Do đó, điều quan trọng là phải tạo được hiệu ứng gió sống động.

Để tạo ra điều này, chúng tôi đã đưa vào 3 cảnh lễ hội với các đối tượng mềm và do đó bị ảnh hưởng bởi gió, chẳng hạn như lều, gắn cờ bề mặt của gian chụp ảnh và chính quả bóng bay.

Vải mềm.

Ngày nay, trò chơi trên máy tính thường xoay quanh một công cụ vật lý cốt lõi. Vì vậy, khi cần mô phỏng một đối tượng mềm trong thế giới 3D, hoạt động mô phỏng vật lý đầy đủ sẽ được chạy cho đối tượng đó để tạo ra một hành vi mềm đáng tin cậy.

Trong WebGL / JavaScript, chúng tôi (vẫn chưa) có đủ năng lực để chạy mô phỏng thực tế đầy đủ. Vì vậy, ở Oz, chúng tôi phải tìm cách tạo ra hiệu ứng của gió mà không thực sự mô phỏng nó.

Chúng tôi đã nhúng thông tin về "độ nhạy của gió" cho từng đối tượng trong chính mô hình 3D. Mỗi đỉnh của mô hình 3D có một "Thuộc tính gió" chỉ định mức độ mà gió cho là chịu ảnh hưởng của gió. Vì vậy, độ nhạy gió được chỉ định này cho các Đối tượng 3D. Sau đó, chúng ta cần tự tạo gió.

Chúng ta thực hiện việc này bằng cách tạo một hình ảnh có chứa Tiếng ồn Perlin. Hình ảnh này nhằm bao quát một "vùng gió" nhất định. Vì vậy, bạn nên tưởng tượng một bức ảnh đám mây, giống như tiếng ồn được đặt trên một khu vực hình chữ nhật nhất định của cảnh 3D. Mỗi điểm ảnh, giá trị cấp độ xám, của hình ảnh này chỉ định mức độ mạnh của gió tại một thời điểm nhất định trong khu vực 3D "xung quanh nó".

Để tạo ra hiệu ứng gió, hình ảnh sẽ di chuyển, kịp thời, với tốc độ không đổi, theo một hướng cụ thể; hướng của gió. Và để đảm bảo "vùng gió" không ảnh hưởng đến mọi thứ trong cảnh, chúng ta sẽ quấn hình ảnh gió quanh các cạnh, giới hạn trong vùng hiệu ứng.

Hướng dẫn đơn giản về gió 3D

Bây giờ, hãy tạo hiệu ứng của gió trong một cảnh 3D đơn giản trong Three.js.

Chúng ta sẽ tạo gió trong một "cánh cỏ" đơn giản.

Trước tiên, hãy tạo cảnh. Chúng ta sẽ có một địa hình bằng phẳng đơn giản, có hoạ tiết. Sau đó, mỗi bông cỏ sẽ được thể hiện bằng một hình nón 3D lộn ngược.

Địa hình nhiều cỏ
Địa hình nhiều cỏ

Dưới đây là cách tạo cảnh đơn giản này trong Three.js bằng CoffeeScript.

Trước hết, chúng ta sẽ thiết lập Three.js và kết nối nó với Camera, Bộ điều khiển chuột và Một số loại ánh sáng:

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

Các lệnh gọi initGrassinit Maps lần lượt điền sẵn cỏ và địa hình:

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

Ở đây, chúng ta sẽ tạo một lưới cỏ gồm 15 x 15 bit. Chúng tôi thêm một chút ngẫu nhiên vào mỗi vị trí cỏ, để chúng không xếp thành hàng như những quân lính, trông sẽ kỳ lạ.

Địa hình này chỉ là một mặt phẳng nằm ngang, được đặt ở gốc các mảnh cỏ (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 )

Vì vậy, những gì chúng ta đã làm cho đến thời điểm này là chỉ tạo cảnh Three.js và thêm một vài chút cỏ, được làm từ các hình nón đảo ngược được tạo theo quy trình và địa hình đơn giản.

Chưa có chủ đề nào ưa thích.

Bây giờ, đã đến lúc bắt đầu thêm gió. Trước tiên, chúng ta muốn nhúng thông tin về độ nhạy của gió vào mô hình 3D cỏ.

Chúng tôi sẽ nhúng thông tin này dưới dạng thuộc tính tùy chỉnh, cho mỗi đỉnh của mô hình 3D cỏ. Chúng ta sẽ sử dụng quy tắc: đầu dưới của mô hình cỏ (đầu hình nón) có độ nhạy bằng 0 vì mô hình được gắn với mặt đất. Phần trên cùng của mô hình cỏ (phần đế của hình nón) có độ nhạy tối đa với gió, vì phần này cách xa mặt đất hơn.

Dưới đây là cách mã hoá lại hàm instanceGrass để thêm độ nhạy của gió dưới dạng thuộc tính tùy chỉnh cho mô hình 3D của cỏ.

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

Hiện tại, chúng ta sử dụng chất liệu tuỳ chỉnh windMaterial thay vì MeshPhongMaterial chúng ta đã sử dụng trước đó. WindMaterial bao bọc WindMeshShader mà chúng ta sẽ thấy sau một phút.

Vì vậy, mã trong instanceGrass vòng lặp qua tất cả các đỉnh của mô hình cỏ, và đối với mỗi đỉnh, mã sẽ thêm một thuộc tính đỉnh tuỳ chỉnh, có tên là windFactor. Hệ số gió này được đặt thành 0 cho điểm cuối của mô hình cỏ (nơi mô hình tiếp xúc với địa hình) và có giá trị là 1 cho điểm kết thúc trên cùng của mô hình cỏ.

Nguyên liệu khác mà chúng ta cần là thêm luồng gió thực tế vào cảnh. Như đã thảo luận, chúng tôi sẽ sử dụng nhiễu Perlin cho việc này. Chúng ta sẽ tạo hoạ tiết nhiễu Perlin theo quy trình.

Để rõ ràng, chúng ta sẽ gán hoạ tiết này vào địa hình, thay cho hoạ tiết màu xanh lục trước đó. Chế độ này sẽ giúp bạn dễ dàng nắm bắt được điều gì đang xảy ra cùng gió.

Vì vậy, hoạ tiết nhiễu Perlin này sẽ che phủ phần mở rộng của địa hình của chúng ta theo không gian, và mỗi pixel của hoạ tiết sẽ chỉ rõ cường độ gió của khu vực địa hình nơi pixel đó đổ xuống. Hình chữ nhật địa hình sẽ là "khu vực gió".

Tiếng ồn Perlin được tạo theo quy trình thông qua một chương trình đổ bóng có tên là NoiseShader. Chương trình đổ bóng này sử dụng thuật toán nhiễu đơn giản 3d lấy từ: https://github.com/ashima/webgl-noise . Phiên bản WebGL của phiên bản này được lấy nguyên văn từ một trong các mẫu Three.js của MrDoob, tại: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.

NoiseShader cần thời gian, tỷ lệ và tập hợp các tham số chênh lệch dưới dạng đồng nhất và tạo ra phân phối nhiễu Perlin 2D đẹp mắt.

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

...

Chúng ta sẽ sử dụng Chương trình đổ bóng này để kết xuất Perlin Noise thành hoạ tiết. Bạn có thể thực hiện việc này trong hàm 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 )

Mã ở trên có chức năng thiết lập noiseMap làm mục tiêu kết xuất Three.js, trang bị NoiseShader, sau đó kết xuất bằng máy ảnh chỉnh hình để tránh hiện tượng méo phối cảnh.

Như đã thảo luận, giờ đây chúng ta sẽ sử dụng hoạ tiết này cũng làm hoạ tiết kết xuất chính cho địa hình. Điều này không thực sự cần thiết để hiệu ứng gió hoạt động. Nhưng thật tuyệt vì thông tin này giúp chúng ta có thể hiểu rõ hơn về việc đang diễn ra với hoạt động tạo gió.

Sau đây là hàm initTerrain được thiết kế lại, sử dụng giao diện tiếng ồn làm hoạ tiết:

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 )

Bây giờ, chúng ta đã có sẵn hoạ tiết gió, hãy tìm hiểu về WindMeshShader, chịu trách nhiệm làm biến dạng mô hình cỏ theo gió.

Để tạo chương trình đổ bóng này, chúng tôi đã bắt đầu từ chương trình đổ bóng tiêu chuẩn Three.js MeshPhongMaterial và sửa đổi nó. Đây là một cách nhanh chóng và đơn giản để bắt đầu sử dụng chương trình đổ bóng vẫn hoạt động mà không cần phải bắt đầu từ đầu.

Chúng ta không sao chép toàn bộ đoạn mã chương trình đổ bóng ở đây (vui lòng xem trong tệp mã nguồn), vì phần lớn mã này là bản sao của chương trình đổ bóng MeshPhongMaterial. Tuy nhiên, hãy xem xét các phần đã sửa đổi, liên quan đến gió trong 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;

Vì vậy, nhiệm vụ của chương trình đổ bóng này là tính toán toạ độ tra cứu kết cấu windUV trước tiên, dựa trên vị trí 2D, xz (ngang) của đỉnh. Toạ độ UV này dùng để tra cứu lực gió, vWindForce, từ hoạ tiết gió có tiếng ồn Perlin.

Giá trị vWindForce này được kết hợp với windfactor của đỉnh, thuộc tính tùy chỉnh đã thảo luận ở trên, để tính toán độ biến dạng mà đỉnh cần. Chúng tôi cũng có tham số windScale toàn cục để kiểm soát cường độ tổng thể của gió và vectơ hướng gió, chỉ định hướng mà sự biến dạng gió cần xảy ra.

Vì vậy, điều này tạo ra sự biến dạng dựa trên gió cho các mảnh cỏ của chúng ta. Tuy nhiên, chúng tôi vẫn chưa hoàn tất. Hiện tại, biến dạng này là tĩnh và không thể hiện được hiệu ứng của khu vực có gió.

Như đã đề cập, chúng ta sẽ cần trượt hoạ tiết tiếng ồn theo thời gian, qua khu vực gió để kính có thể gợn sóng.

Điều này được thực hiện bằng cách dịch chuyển theo thời gian, đồng nhất vOffset được truyền vào NoiseShader. Đây là tham số vec2, cho phép chúng ta chỉ định mức bù tiếng ồn, dọc theo một hướng nhất định (hướng gió của chúng ta).

Chúng ta thực hiện việc này trong hàm Render (kết xuất) được gọi ở mọi khung hình:

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

Chỉ vậy thôi! Chúng tôi vừa tạo một cảnh có "cỏ quy trình" chịu ảnh hưởng của gió.

Thêm bụi vào hỗn hợp

Giờ hãy thêm một chút sinh động cho cảnh của chúng ta. Hãy thêm một chút bụi bay để làm cho cảnh quan trở nên thú vị hơn.

Đang thêm bụi
Thêm bụi

Xét cho cùng, bụi thì bị ảnh hưởng bởi gió, nên việc có bụi bay xung quanh trong cảnh gió là điều hoàn toàn hợp lý.

Bụi được thiết lập trong hàm initDust như một hệ thống hạt.

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

Ở đây 130 hạt bụi được tạo ra. Xin lưu ý rằng mỗi thành phần này đều được trang bị một WindParticleShader đặc biệt.

Bây giờ, ở mỗi khung hình, chúng ta sẽ di chuyển xung quanh các hạt một chút, sử dụng CoffeeScript, độc lập với gió. Dưới đây là mã.

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 )

Ngoài ra, chúng ta sẽ bù trừ từng vị trí hạt theo gió. Bạn có thể thực hiện việc này trong WindParticleShader. Đặc biệt là trong chương trình đổ bóng đỉnh.

Mã cho chương trình đổ bóng này là một phiên bản đã sửa đổi của ParticleMaterial Three.js và như sau:

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;

Chương trình đổ bóng đỉnh cao này không khác biệt so với những gì chúng tôi làm được đối với sự biến dạng của cỏ dựa trên gió. Phương thức này lấy kết cấu nhiễu Perlin làm đầu vào và tuỳ thuộc vào vị trí của thế giới bụi, nó sẽ tra ra một vWindForce, một giá trị trong kết cấu nhiễu. Sau đó, hệ thống sử dụng giá trị này để sửa đổi vị trí của hạt bụi.

Những người lướt sóng trên cơn bão

Cảnh phiêu lưu nhất trong WebGL của chúng tôi có lẽ là cảnh cuối cùng mà bạn có thể nhìn thấy nếu bạn nhấp vào quả bóng bay vào tâm của cơn lốc xoáy để đi đến đoạn cuối của hành trình trên trang web này, cũng như video độc quyền về bản phát hành sắp tới.

Cảnh đi khinh khí cầu

Khi tạo cảnh này, chúng tôi biết rằng cần phải tạo điểm nhấn chính cho trải nghiệm sau này. Lốc xoáy quay tròn sẽ đóng vai trò là tâm điểm và các lớp nội dung khác sẽ hình thành đặc điểm này để tạo ra hiệu ứng ấn tượng. Để đạt được điều này, chúng tôi đã xây dựng một nơi tương đương với một xưởng phim được đặt xung quanh chương trình đổ bóng lạ này.

Chúng tôi đã sử dụng phương pháp kết hợp để tạo tổ hợp thực tế. Một số ví dụ là thủ thuật hình ảnh như hình dạng ánh sáng để tạo hiệu ứng loé sáng ống kính, hoặc những giọt mưa tạo thành lớp ảnh động trên khung cảnh bạn đang xem. Trong các trường hợp khác, chúng tôi vẽ các bề mặt phẳng trông có vẻ di chuyển xung quanh, như các lớp mây bay thấp di chuyển theo mã hệ thống hạt. Trong khi các mảnh vỡ quay quanh cơn lốc xoáy là các lớp trong cảnh 3D được sắp xếp để di chuyển phía trước và phía sau cơn lốc xoáy.

Lý do chính khiến chúng tôi phải xây dựng cảnh quay theo cách này là để đảm bảo chúng tôi có đủ GPU để xử lý chương trình đổ bóng lốc xoáy sao cho cân bằng với các hiệu ứng khác mà chúng tôi đang áp dụng. Ban đầu, chúng tôi gặp phải các vấn đề lớn về việc cân bằng GPU, nhưng sau đó, cảnh này đã được tối ưu hoá và sáng hơn so với các cảnh chính.

Hướng dẫn: Chương trình đổ bóng bão

Nhiều kỹ thuật kết hợp nhiều kỹ thuật để tạo chuỗi bão cuối cùng, nhưng trọng tâm của công việc này là chương trình đổ bóng GLSL tuỳ chỉnh trông giống như một cơn lốc xoáy. Chúng tôi đã thử nhiều kỹ thuật khác nhau, từ chương trình đổ bóng đỉnh để tạo các xoáy nước hình học thú vị cho đến hoạt ảnh dựa trên các hạt và thậm chí cả hoạt ảnh 3D của các hình dạng hình học xoắn. Không có hiệu ứng nào trong số này tạo lại cảm giác về một cơn lốc xoáy hoặc cần quá trình xử lý.

Một dự án hoàn toàn khác cuối cùng đã cung cấp cho chúng tôi câu trả lời. Một dự án song song liên quan đến các trò chơi dành cho khoa học nhằm lập bản đồ não của chuột do Viện Max Planck (brainflight.org) thực hiện đã tạo ra những hiệu ứng hình ảnh thú vị. Chúng tôi đã cố gắng tạo được phim về bên trong nơron chuột bằng chương trình đổ bóng thể tích tuỳ chỉnh.

Bên trong nơron chuột bằng chương trình đổ bóng thể tích tuỳ chỉnh
Bên trong nơron chuột bằng chương trình đổ bóng thể tích tuỳ chỉnh

Chúng tôi nhận thấy bên trong tế bào não trông khá giống hình phễu của một cơn lốc xoáy. Vì sử dụng kỹ thuật thể tích nên chúng tôi biết rằng mình có thể xem chương trình đổ bóng này từ mọi hướng trong không gian. Chúng ta có thể thiết lập chế độ kết xuất chương trình đổ bóng để kết hợp với cảnh bão, đặc biệt là nếu bị kẹp dưới các lớp mây và trên nền ấn tượng.

Kỹ thuật chương trình đổ bóng bao gồm một thủ thuật về cơ bản là sử dụng một chương trình đổ bóng GLSL duy nhất để kết xuất toàn bộ đối tượng bằng một thuật toán kết xuất được đơn giản hoá có tên là kết xuất tia sáng với trường khoảng cách. Trong kỹ thuật này, một chương trình đổ bóng pixel sẽ được tạo để ước tính khoảng cách gần nhất đến một bề mặt cho mỗi điểm trên màn hình.

Bạn có thể xem tài liệu tham khảo tốt về thuật toán trong phần tổng quan của iq: Kết xuất thế giới bằng hai hình tam giác – Iñigo Quilez. Ngoài ra, bạn có thể khám phá thư viện chương trình đổ bóng trên glsl.heroku.com, có rất nhiều ví dụ về kỹ thuật này có thể được thử nghiệm.

Trái tim của chương trình đổ bóng bắt đầu từ chức năng chính. Chức năng này thiết lập máy ảnh biến đổi và đi vào một vòng lặp đánh giá khoảng cách đến một khu vực. Lệnh gọi RaytraceFoggy( Direction_vectơ, max_iterations, color, color_multiplier) là nơi diễn ra phép tính toán tia cốt lõi.

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
}

Ý tưởng này là khi chuyển sang hình dạng của cơn lốc xoáy, chúng tôi thường xuyên thêm màu sắc đóng góp vào giá trị màu sắc cuối cùng của điểm ảnh, cũng như tạo nên độ mờ dọc theo tia sáng. Điều này tạo ra một lớp chất lượng mềm cho hoạ tiết của lốc xoáy.

Khía cạnh cốt lõi tiếp theo của lốc xoáy là hình dạng thực tế được tạo ra bằng cách kết hợp một số hàm. Ban đầu, nó là một hình nón, được tạo thành từ nhiễu để tạo ra cạnh thô hữu cơ, sau đó được xoắn dọc theo trục chính của nó và xoay theo thời gian.

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

Công việc để tạo ra loại chương trình đổ bóng này khá phức tạp. Ngoài các vấn đề liên quan đến việc trừu tượng hoá các thao tác bạn đang tạo, còn có các vấn đề nghiêm trọng về việc tối ưu hoá và tương thích trên nhiều nền tảng mà bạn cần theo dõi và giải quyết trước khi có thể sử dụng công việc trong thực tế.

Phần đầu tiên của vấn đề: tối ưu hoá chương trình đổ bóng này cho cảnh của chúng ta. Để giải quyết vấn đề này, chúng tôi cần có phương pháp “an toàn” trong trường hợp chương trình đổ bóng có dung lượng quá lớn. Để làm được điều này, chúng tôi đã kết hợp chương trình đổ bóng lốc xoáy ở một độ phân giải lấy mẫu khác với phần còn lại của cảnh. Nội dung này là từ tệp StormTest.Tomatoes (đúng vậy, đây là một thử nghiệm!).

Chúng ta bắt đầu bằng một RenderScriptTarget khớp với chiều rộng và chiều cao của cảnh để có thể độc lập về độ phân giải của chương trình đổ bóng lốc xoáy với cảnh. Sau đó, chúng tôi quyết định việc giảm tần số lấy mẫu độ phân giải của chương trình đổ bóng bão một cách linh hoạt phụ thuộc vào tốc độ khung hình mà chúng tôi đạt được.

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

Cuối cùng, chúng tôi kết xuất cơn lốc xoáy để màn hình bằng cách sử dụng thuật toán sal2x đơn giản, (để tránh giao diện khối) @dòng 1107 trong StormTest.Tomatoes. Điều này có nghĩa là trong trường hợp xấu hơn, chúng ta sẽ gặp phải một cơn lốc xoáy mờ hơn nhưng ít nhất nó cũng hoạt động được mà không làm người dùng mất khả năng kiểm soát.

Bước tối ưu hoá tiếp theo yêu cầu tìm hiểu kỹ thuật toán. Hệ số tính toán điều khiển trong chương trình đổ bóng là phép lặp được thực hiện trên mỗi điểm ảnh để ước tính khoảng cách của hàm surface (vùng hiển thị): số lần lặp của vòng lặp raymarching. Nếu sử dụng số bước lớn hơn, chúng ta có thể ước tính được bề mặt của lốc xoáy với ít lần lặp lại hơn trong khi ở bên ngoài khu vực nhiều mây. Khi vào bên trong, chúng ta sẽ giảm kích thước bước để đảm bảo độ chính xác và có thể kết hợp các giá trị để tạo ra hiệu ứng mờ. Ngoài ra, việc tạo một hình trụ giới hạn để ước tính độ sâu cho tia được truyền giúp tăng tốc nhanh.

Vấn đề tiếp theo là đảm bảo chương trình đổ bóng này sẽ chạy trên nhiều thẻ video. Mỗi lần, chúng tôi lại thực hiện một số thử nghiệm và bắt đầu hình thành trực giác về những loại vấn đề về khả năng tương thích mà mình có thể gặp phải. Lý do khiến chúng tôi không thể làm tốt hơn trực giác là không phải lúc nào chúng tôi cũng có được thông tin gỡ lỗi phù hợp. Một trường hợp thông thường chỉ là một lỗi GPU và thậm chí là sự cố hệ thống!

Các vấn đề về khả năng tương thích của bảng video chéo có giải pháp tương tự: đảm bảo các hằng số tĩnh được nhập của loại dữ liệu chính xác như đã định nghĩa, IE: 0.0 cho float và 0 cho int. Hãy cẩn thận khi viết các hàm dài hơn; tốt hơn là chia nhỏ mọi thứ thành nhiều hàm đơn giản hơn và biến tạm thời vì trình biên dịch dường như không xử lý chính xác các trường hợp nhất định. Đảm bảo hoạ tiết đều là luỹ thừa của 2, không quá lớn và trong mọi trường hợp cũng thực hiện "cẩn thận" khi tra cứu dữ liệu hoạ tiết trong một vòng lặp.

Vấn đề lớn nhất mà chúng tôi gặp phải về khả năng tương thích là vấn đề về hiệu ứng ánh sáng trong cơn bão. Chúng ta đã sử dụng một hoạ tiết làm sẵn bao bọc xung quanh cơn lốc xoáy để có thể tô màu cho nó. Đây là hiệu ứng tuyệt đẹp, giúp người xem dễ dàng kết hợp cơn lốc xoáy với màu sắc cảnh quan, nhưng mất nhiều thời gian mới có thể thử chạy được trên các nền tảng khác.

bão lốc

Trang web dành cho thiết bị di động

Trải nghiệm trên thiết bị di động không thể là bản dịch trực tiếp của phiên bản dành cho máy tính để bàn vì yêu cầu về công nghệ và xử lý quá nặng nề. Chúng tôi phải tạo một cái gì đó mới nhắm đến người dùng thiết bị di động.

Chúng tôi nghĩ rằng sẽ thật tuyệt khi có Carnival Photo-Booth từ máy tính để bàn dưới dạng một ứng dụng web dành cho thiết bị di động sử dụng máy ảnh trên thiết bị di động của người dùng. Điều mà chúng tôi chưa từng thấy từ trước đến nay.

Để thêm hương vị, chúng tôi mã hoá 3D biến đổi trong CSS3. Sau khi liên kết công cụ này với con quay hồi chuyển và gia tốc kế, chúng tôi đã có thể tăng thêm chiều sâu cho trải nghiệm. Trang web phản hồi cách bạn cầm, di chuyển và nhìn vào điện thoại.

Khi viết bài này, chúng tôi cho rằng nên cung cấp cho bạn một số gợi ý về cách chạy quá trình phát triển thiết bị di động suôn sẻ. Bắt đầu thôi! Hãy tiếp tục và xem bạn có thể học được những gì từ công cụ này!

Mẹo và thủ thuật dành cho thiết bị di động

Trình tải trước là thứ cần thiết, không phải là thứ nên tránh. Chúng tôi biết rằng đôi khi điều này cũng xảy ra. Điều này chủ yếu là vì bạn cần duy trì danh sách những nội dung bạn tải trước khi dự án phát triển. Tệ hơn nữa, bạn không rõ mình nên tính tiến trình tải như thế nào nếu đang lấy nhiều tài nguyên và nhiều tài nguyên trong số đó cùng một lúc. Đây là lúc lớp trừu tượng tuỳ chỉnh và rất chung chung "Task" của chúng ta trở nên hữu ích. Ý tưởng chính của quy trình này là cho phép cấu trúc lồng ghép vô tận, trong đó một Nhiệm vụ có thể có các Nhiệm vụ phụ riêng, có thể có các Nhiệm vụ phụ, v.v. Hơn nữa, mỗi nhiệm vụ đều tính toán tiến trình của mình theo tiến trình của các nhiệm vụ phụ (chứ không phải tiến trình của tác vụ mẹ). Chúng tôi tạo ra một cấu trúc như sau: Chúng tôi tạo ra một cấu trúc như sau:

Trình tải trước

Nhờ cách tiếp cận như vậy và lớp Tác vụ, chúng ta có thể dễ dàng biết được tiến trình chung (MainLoadingTask) hoặc chỉ tiến độ của nội dung (AssetUploadsTask) hoặc tiến trình tải mẫu (TemplatePreFetchTask). Thậm chí là tiến trình của một tệp cụ thể. Để biết cách thực hiện, hãy xem Lớp Tác vụ tại /m/javascripts/raw/util/Task.js và triển khai tác vụ thực tế tại /m/javascripts/preloading/task. Ví dụ: đây là phần trích xuất từ cách chúng tôi thiết lập lớp /m/javascripts/preloading/task/MainPreloadTask.js, đây là trình bao bọc tải trước tối ưu của chúng tôi:

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

      }
    }
  })
]);

Trong lớp /m/javascripts/previousing/task/subtask/Asset interactionsTask.js, ngoài việc lưu ý đến cách lớp này giao tiếp với MainLoadingTask (thông qua triển khai Tác vụ được chia sẻ), cũng cần lưu ý cách chúng ta tải các nội dung phụ thuộc vào nền tảng. Cơ bản, chúng ta có 4 loại hình ảnh. Chuẩn thiết bị di động (.ext, trong đó ext là đuôi tệp, thường là .png hoặc .jpg), retina di động (-2x.ext), chuẩn dành cho máy tính bảng (-tab.ext) và retina máy tính bảng (-tab-2x.ext). Thay vì phát hiện trong Main MAXTask và mã hoá cứng 4 mảng tài sản, chúng tôi chỉ cần nói tên và phần mở rộng của tài sản cần tải trước và liệu thành phần đó có phụ thuộc vào nền tảng hay không (đáp ứng = true / false). Sau đó, Asset UploadTask sẽ tạo tên tệp cho chúng ta:

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

Ở phía dưới chuỗi lớp, mã thực tế tải trước nội dung có dạng như sau (/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);
}

Hướng dẫn: HTML5 Photo Audience (iOS6/Android)

Khi phát triển oz cho điện thoại di động, chúng tôi phát hiện ra rằng chúng tôi thực sự dành rất nhiều thời gian để chơi với quầy chụp ảnh thay vì làm việc :D Đơn giản là vì nó thú vị. Vậy nên, chúng tôi đã tạo một bản minh hoạ để bạn chơi.

Buồng chụp ảnh di động
Kho ảnh dành cho thiết bị di động

Bạn có thể xem bản minh hoạ trực tiếp tại đây (chạy trên điện thoại iPhone hoặc Android):

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

Để thiết lập, bạn cần có một phiên bản ứng dụng Google App Engine miễn phí để có thể chạy phần phụ trợ. Mã giao diện người dùng không phức tạp, nhưng có thể có một số yêu cầu. Hãy cùng tìm hiểu cụ thể:

  1. Loại tệp hình ảnh được cho phép Chúng tôi muốn mọi người chỉ có thể tải hình ảnh lên (vì đó là một khu vực chụp ảnh chứ không phải buồng video). Trên lý thuyết, bạn chỉ cần chỉ định bộ lọc trong HTML như sau: input id="fileInput" class="fileInput" type="file" name="file" accept="image/*" Tuy nhiên, bộ lọc này có vẻ chỉ hoạt động trên iOS. Vì vậy, chúng ta cần thêm quy trình kiểm tra bổ sung cho RegExp sau khi chọn tệp:
   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. Huỷ thao tác tải tệp lên hoặc chọn tệp Một điểm không thống nhất khác mà chúng tôi nhận thấy trong quá trình phát triển là cách các thiết bị thông báo cho việc lựa chọn tệp bị huỷ. Điện thoại và máy tính bảng iOS không làm gì cả, chúng hoàn toàn không thông báo. Vì vậy, chúng ta không cần bất kỳ hành động đặc biệt nào cho trường hợp này. Tuy nhiên, điện thoại Android vẫn kích hoạt hàm add(), ngay cả khi không có tệp nào được chọn. Sau đây là cách phục vụ cho vấn đề này:
    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();            
    }
    }

Số còn lại hoạt động khá mượt mà trên các nền tảng. Chúc bạn sáng tạo vui vẻ!

Kết luận

Dựa vào quy mô khổng lồ của cuốn sách Tìm đường đến xứ Oz và nhiều công nghệ liên quan, trong bài viết này, chúng tôi chỉ trình bày được một vài phương pháp mà chúng tôi sử dụng.

Nếu bạn muốn khám phá toàn bộ enchilada, hãy xem nhanh mã nguồn đầy đủ của Tìm đường đến xứ Oz tại đường liên kết này.

Ghi công

Nhấp vào đây để xem danh sách tín dụng đầy đủ

Tài liệu tham khảo