مطالعه موردی - راه خود را به اوز بیابید

معرفی

"راه خود را به اوز بیابید" یک آزمایش جدید Google Chrome است که توسط دیزنی به وب ارائه شده است. این به شما اجازه می دهد تا یک سفر تعاملی را از طریق یک سیرک کانزاس انجام دهید، که شما را پس از درگیر شدن توسط یک طوفان عظیم به سرزمین اوز هدایت می کند.

هدف ما ترکیب غنای سینما با قابلیت های فنی مرورگر برای ایجاد تجربه ای سرگرم کننده و همه جانبه بود که کاربران بتوانند با آن ارتباط قوی برقرار کنند.

این کار کمی بزرگتر از آن است که بتوان آن را به طور کلی در این قطعه ثبت کرد، بنابراین ما در آن غوطه ور شده ایم و چند فصل از داستان فناوری را که فکر می کنیم جالب هستند، بیرون آورده ایم. در طول مسیر، چند آموزش متمرکز در مورد افزایش سختی استخراج کردیم.

بسیاری از مردم سخت تلاش کردند تا این تجربه را ممکن کنند: تعداد زیادی از آنها در اینجا فهرست شده اند. لطفاً از سایت دیدن کنید تا صفحه اعتبارات را در زیر بخش منو برای داستان کامل بررسی کنید.

نگاهی زیر سرپوش

راه خود را به اوز در دسکتاپ بیابید، دنیای غوطه‌وری غنی است. ما از سه بعدی و چندین لایه از جلوه های الهام گرفته شده از فیلمسازی سنتی استفاده می کنیم که با هم ترکیب می شوند تا صحنه ای تقریباً واقعی ایجاد کنند. برجسته ترین فناوری ها WebGL با Three.js، سایه بان های ساخته شده سفارشی و عناصر متحرک DOM با استفاده از ویژگی های CSS3 هستند. فراتر از این، getUserMedia API (WebRTC) برای تجربیات تعاملی که به کاربر اجازه می دهد تصویر خود را مستقیماً از وب کم و WebAudio برای صدای سه بعدی اضافه کند.

اما جادوی یک تجربه فناوری مانند این است که چگونه با هم ترکیب می شود. این نیز یکی از چالش‌های اصلی است: چگونه جلوه‌های بصری و عناصر تعاملی را در یک صحنه با هم ترکیب کنیم تا یک کل منسجم ایجاد کنیم؟ مدیریت این پیچیدگی بصری دشوار بود: تشخیص اینکه در هر زمان در چه مرحله ای از توسعه بودیم دشوار بود.

برای رسیدگی به مشکل جلوه‌های بصری بهم پیوسته و بهینه‌سازی، ما به شدت از یک کنترل پنل استفاده کردیم که تمام تنظیمات مربوطه را که در آن مقطع زمانی بررسی می‌کردیم، ثبت می‌کرد. صحنه را می توان به صورت زنده در مرورگر برای هر چیزی از روشنایی، عمق میدان، گاما و غیره تنظیم کرد.

قبل از اینکه راز خود را به اشتراک بگذاریم، می‌خواهیم به شما هشدار دهیم که ممکن است تصادف کند، دقیقاً مانند زمانی که بخواهید داخل یک موتور ماشین بگردید. مطمئن شوید که چیز مهمی ندارید و به آدرس اصلی سایت بروید و ?debug=on را به آدرس اضافه کنید. منتظر بمانید تا سایت بارگیری شود و وقتی داخل شدید (فشار دهید؟) کلید Ctrl-I مشاهده خواهید کرد و در سمت راست پنجره کشویی ظاهر می شود. اگر تیک گزینه «خروج از مسیر دوربین» را بردارید، می‌توانید از کلیدهای A، W، S، D و ماوس برای حرکت آزادانه در فضا استفاده کنید.

مسیر دوربین.

ما در اینجا همه تنظیمات را مرور نمی کنیم، اما شما را تشویق می کنیم که آزمایش کنید: کلیدها تنظیمات مختلف را در صحنه های مختلف نشان می دهند. در آخرین سکانس طوفان یک کلید اضافی وجود دارد: Ctrl-A که با آن می توانید پخش انیمیشن را تغییر دهید و به اطراف پرواز کنید. در این صحنه اگر Esc فشار دهید (برای خروج از عملکرد قفل ماوس) و دوباره Ctrl-I فشار دهید، می توانید به تنظیماتی که مخصوص صحنه طوفان هستند دسترسی پیدا کنید. به اطراف نگاهی بیندازید و چند نما کارت پستال زیبا مانند تصویر زیر بگیرید.

صحنه طوفان

برای اینکه این اتفاق بیفتد و اطمینان حاصل کنیم که به اندازه کافی برای نیازهای ما انعطاف پذیر است، از یک کتابخانه دوست داشتنی به نام dat.gui استفاده کردیم (برای آموزش قبلی در مورد نحوه استفاده از آن اینجا را ببینید ). این به ما امکان داد تا به سرعت تنظیماتی را که در معرض دید بازدیدکنندگان سایت قرار می‌گرفت، تغییر دهیم.

کمی شبیه نقاشی مات

در بسیاری از فیلم‌ها و انیمیشن‌های کلاسیک دیزنی، ایجاد صحنه‌ها به معنای ترکیب لایه‌های مختلف بود. لایه‌هایی از اکشن زنده، انیمیشن سلولی، حتی مجموعه‌های فیزیکی و روی لایه‌های بالایی که با نقاشی روی شیشه ایجاد می‌شدند وجود داشت: تکنیکی به نام نقاشی مات.

از بسیاری جهات ساختار تجربه ای که ما ایجاد کردیم مشابه است. حتی اگر برخی از "لایه ها" بسیار بیشتر از تصاویر ایستا هستند. در واقع، آن‌ها بر شکل ظاهری اشیا بر اساس محاسبات پیچیده‌تر تأثیر می‌گذارند. با این وجود، حداقل در سطح تصویر بزرگ، ما با نماهایی سروکار داریم که یکی بر روی دیگری ترکیب شده اند. در بالا یک لایه رابط کاربری را می بینید که یک صحنه سه بعدی در زیر آن وجود دارد: خودش از اجزای صحنه مختلف ساخته شده است.

لایه رابط بالایی با استفاده از DOM و CSS 3 ایجاد شد که به این معنی بود که ویرایش تعاملات می‌تواند به روش‌های مختلف مستقل از تجربه سه بعدی با ارتباط بین این دو مطابق با لیست انتخابی از رویدادها انجام شود. این ارتباط از Backbone Router + رویداد onHashChange HTML5 استفاده می‌کند که کنترل می‌کند کدام ناحیه باید به داخل/خارج متحرک شود. (منبع پروژه: /develop/coffee/router/Router.coffee).

آموزش: Sprite Sheets و Retina پشتیبانی می کند

یکی از تکنیک‌های بهینه‌سازی سرگرم‌کننده که برای رابط به آن تکیه کردیم، ترکیب بسیاری از تصاویر همپوشانی رابط در یک PNG واحد برای کاهش درخواست‌های سرور بود. در این پروژه، رابط از بیش از 70 تصویر (بدون احتساب بافت‌های سه بعدی) تشکیل شده بود که برای کاهش تأخیر وب‌سایت بارگذاری شده بودند. شما می توانید برگه اسپرایت زنده را اینجا ببینید:

نمایشگر عادی - http://findyourwaytooz.com/img/home/interface_1x.png صفحه نمایش شبکیه چشم - http://findyourwaytooz.com/img/home/interface_2x.png

در اینجا نکاتی در مورد نحوه استفاده ما از Sprite Sheets و نحوه استفاده از آنها برای دستگاه های شبکیه چشم و داشتن رابط تا حد امکان واضح و مرتب آورده شده است.

ایجاد Spritesheets

برای ایجاد SpriteSheets از TexturePacker استفاده کردیم که با هر فرمتی که شما نیاز دارید خروجی می دهد. در این مورد ما به عنوان EaselJS صادر کرده ایم که واقعاً تمیز است و می توان از آن برای ایجاد اسپرایت های متحرک نیز استفاده کرد.

با استفاده از Sprite Sheet ایجاد شده

هنگامی که Sprite Sheet خود را ایجاد کردید، باید یک فایل 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 صفحه sprite اشاره دارد
  • فریم ها مختصات هر عنصر رابط کاربری هستند [x، y، عرض، ارتفاع]
  • انیمیشن ها نام هر دارایی هستند

توجه داشته باشید که ما از تصاویر با چگالی بالا برای ایجاد صفحه Sprite استفاده کرده ایم، سپس نسخه معمولی را ایجاد کرده ایم و فقط اندازه آن را به نصف اندازه آن تغییر داده ایم.

همه چیز را کنار هم گذاشتن

اکنون که همه چیز آماده است، فقط به یک قطعه جاوا اسکریپت برای استفاده از آن نیاز داریم.

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 را بخوانید.

خط لوله محتوای سه بعدی

تجربه محیطی بر روی یک لایه WebGL تنظیم شده است. وقتی به یک صحنه سه بعدی فکر می کنید، یکی از پیچیده ترین سؤالات این است که چگونه می خواهید مطمئن شوید که می توانید محتوایی ایجاد کنید که حداکثر پتانسیل بیانی را از جنبه های مدل سازی، انیمیشن و جلوه ها ممکن می کند. از بسیاری جهات هسته اصلی این موضوع خط لوله محتوا است: یک فرآیند توافق شده برای ایجاد محتوا برای صحنه سه بعدی.

ما می خواستیم دنیایی الهام بخش خلق کنیم. بنابراین ما به یک فرآیند محکم نیاز داشتیم که هنرمندان سه بعدی را قادر به ایجاد آن کند. آنها باید تا حد امکان آزادی بیانی در نرم افزار مدل سازی سه بعدی و انیمیشن خود داشته باشند. و ما باید آن را از طریق کد روی صفحه نمایش دهیم.

ما مدتی بود که روی این نوع مشکل کار می کردیم زیرا هر بار که در گذشته یک سایت سه بعدی ایجاد می کردیم، محدودیت هایی در ابزارهایی که می توانستیم استفاده کنیم پیدا کرده بودیم. بنابراین ما این ابزار را به نام کتابدار سه بعدی ایجاد کرده بودیم: یک بخش از تحقیقات داخلی. و تقریباً آماده بود تا در یک شغل واقعی اعمال شود.

این ابزار تاریخچه ای داشت: در اصل برای Flash بود و به شما این امکان را می داد که یک صحنه بزرگ مایا را به عنوان یک فایل فشرده که برای باز کردن زمان اجرا بهینه شده بود، وارد کنید. دلیل بهینه بودن آن این بود که به طور موثر صحنه را در همان ساختار داده ای که در حین رندر و انیمیشن دستکاری می شود، بسته بندی می کرد. تجزیه بسیار کمی وجود دارد که باید در هنگام بارگذاری روی فایل انجام شود. باز کردن بسته بندی در فلش بسیار سریع بود، زیرا فایل در فرمت AMF بود، که فلش می توانست به صورت بومی بسته بندی را باز کند. استفاده از همین فرمت در WebGL نیاز به کار بیشتر روی CPU دارد. در واقع ما مجبور بودیم یک لایه کد جاوا اسکریپت را دوباره ایجاد کنیم، که اساسا آن فایل ها را از حالت فشرده خارج می کند و ساختارهای داده مورد نیاز برای کار WebGL را بازسازی می کند. بسته‌بندی کل صحنه سه‌بعدی یک عملیات نسبتاً سنگین به CPU است: باز کردن بسته‌بندی صحنه 1 در Find Your Way To Oz به حدود 2 ثانیه در یک دستگاه متوسط ​​​​تا بالا نیاز دارد. بنابراین، این کار با استفاده از فناوری Web Workers، در زمان «تنظیم صحنه» (قبل از راه‌اندازی واقعی صحنه) انجام می‌شود تا تجربه را برای کاربر معلق نکند.

این ابزار مفید می تواند بیشتر صحنه های سه بعدی را وارد کند: مدل ها، بافت ها، انیمیشن های استخوانی. شما یک فایل کتابخانه واحد ایجاد می کنید که می تواند توسط موتور سه بعدی بارگذاری شود. شما تمام مدل‌هایی را که در صحنه‌تان نیاز دارید در این کتابخانه قرار می‌دهید، و آنها را در صحنه خود پخش می‌کنید.

مشکلی که ما داشتیم این بود که اکنون با WebGL سر و کار داشتیم: بچه جدید در بلوک. این بچه خیلی سختی بود: استانداردهایی را برای تجربیات سه بعدی مبتنی بر مرورگر تعیین می کرد. بنابراین ما یک لایه جاوا اسکریپت موقت ایجاد کردیم که فایل‌های صحنه سه‌بعدی فشرده‌شده کتابدار سه‌بعدی را می‌گیرد و به‌درستی آنها را به قالبی ترجمه می‌کند که WebGL آن را درک کند.

آموزش: بگذار باد باشد

یک موضوع تکراری در "راه خود را به اوز بیابید" باد بود. یک رشته از خط داستانی به گونه ای ساختار یافته است که اوج باد باشد.

صحنه اول کارناوال نسبتا آرام است. و با گذر از صحنه های مختلف، کاربر باد شدیدتری را تجربه می کند که به صحنه نهایی یعنی طوفان ختم می شود.

بنابراین ارائه یک اثر باد همهجانبه مهم بود.

برای ایجاد این، ما 3 صحنه کارناوال را با اشیایی که نرم بودند و در نتیجه قرار بود تحت تأثیر باد قرار گیرند، مانند چادر، پرچم سطح غرفه عکس و خود بالون پر کردیم.

پارچه نرم.

بازی‌های رومیزی این روزها معمولاً بر اساس یک موتور فیزیک اصلی ساخته می‌شوند. بنابراین هنگامی که یک شیء نرم نیاز به شبیه سازی در دنیای سه بعدی دارد، یک شبیه سازی فیزیک کامل برای آن اجرا می شود و یک رفتار نرم باورپذیر ایجاد می کند.

در WebGL/Javascript ما (هنوز) این تجمل را نداریم که یک شبیه سازی فیزیک کامل را اجرا کنیم. بنابراین در اوز باید راهی برای ایجاد اثر باد، بدون شبیه‌سازی واقعی آن پیدا می‌کردیم.

ما اطلاعات "حساسیت باد" را برای هر شی در خود مدل سه بعدی جاسازی کردیم. هر رأس مدل سه بعدی دارای یک «ویژگی باد» بود که مشخص می‌کرد آن راس چقدر تحت تأثیر باد قرار می‌گیرد. بنابراین، این حساسیت اشیاء سه بعدی به باد مشخص شده است. سپس ما نیاز داشتیم که خود باد را ایجاد کنیم.

ما این کار را با ایجاد یک تصویر حاوی Perlin Noise انجام دادیم. این تصویر برای پوشش یک "منطقه باد" خاص در نظر گرفته شده است. بنابراین، یک راه خوب برای فکر کردن در مورد آن، تصور تصویری از ابر مانند نویز است که بر روی یک منطقه مستطیلی مشخص از صحنه سه بعدی قرار گرفته است. هر پیکسل، مقدار سطح خاکستری، از این تصویر مشخص می کند که باد در یک لحظه خاص در منطقه 3 بعدی "در اطراف آن" چقدر قوی است.

به منظور ایجاد اثر باد، تصویر در زمان، با سرعت ثابت، در یک جهت خاص حرکت می کند. جهت باد و برای اطمینان از اینکه "منطقه بادخیز" روی همه چیز در صحنه تأثیر نمی گذارد، تصویر باد را در اطراف لبه ها می پیچیم، محدود به ناحیه اثر.

آموزش ساده سه بعدی باد

حال بیایید جلوه باد را در یک صحنه سه بعدی ساده در Three.js ایجاد کنیم.

ما قصد داریم در یک "زمین چمن رویه ای" ساده باد ایجاد کنیم.

بیایید ابتدا صحنه را ایجاد کنیم. ما یک زمین صاف ساده و بافت دار خواهیم داشت. و سپس هر تکه چمن به سادگی با یک مخروط سه بعدی وارونه نشان داده می شود.

زمین پر از چمن
زمین پر از چمن

در اینجا نحوه ایجاد این صحنه ساده در Three.js با استفاده از CoffeeScript آورده شده است.

اول از همه، ما 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

در اینجا ما یک شبکه 15 در 15 بیتی از چمن ایجاد می کنیم. ما کمی تصادفی سازی را به هر موقعیت چمن اضافه می کنیم، به طوری که آنها مانند سربازان در یک ردیف قرار نگیرند، که عجیب به نظر می رسد.

این زمین فقط یک صفحه افقی است که در پایه تکه های چمن قرار گرفته است (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 ایجاد کرده‌ایم، و چند تکه چمن، ساخته‌شده از مخروط‌های معکوس ایجاد شده رویه‌ای، و یک زمین ساده به آن اضافه می‌کنیم.

هیچ چیز خیالی تا اینجا نیست.

اکنون زمان آن است که اضافه کردن باد را آغاز کنیم. اولین چیز، ما می خواهیم اطلاعات حساسیت باد را در مدل سه بعدی چمن جاسازی کنیم.

ما قصد داریم این اطلاعات را به عنوان یک ویژگی سفارشی، برای هر رأس مدل سه بعدی چمن جاسازی کنیم. و ما از این قانون استفاده خواهیم کرد که: انتهای پایین مدل چمن (نوک مخروط) حساسیت صفر دارد، زیرا به زمین متصل است. قسمت بالایی مدل چمن (پایه مخروط) دارای حداکثر حساسیت به باد است، زیرا قسمتی است که از زمین دورتر است.

در اینجا نحوه کدگذاری مجدد تابع 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 در تمام رئوس مدل grass حلقه می‌زند و برای هر رأس یک ویژگی راس سفارشی به نام windFactor اضافه می‌کند. این windFactor برای انتهای پایین مدل چمن (جایی که قرار است زمین را لمس کند) روی 0 تنظیم شده است، و مقدار آن برای انتهای بالای مدل چمن 1 است.

عنصر دیگری که به آن نیاز داریم این است که باد واقعی را به صحنه خود اضافه کنیم. همانطور که بحث شد، ما از نویز Perlin برای این کار استفاده خواهیم کرد. ما به صورت رویه‌ای یک بافت نویز پرلین تولید می‌کنیم.

برای شفافیت، این بافت را به جای بافت سبز قبلی که داشت، به خود زمین اختصاص می دهیم. این کار باعث می‌شود که احساس کنید با باد چه اتفاقی می‌افتد.

بنابراین، این بافت نویز Perlin به صورت فضایی گسترش زمین ما را پوشش می‌دهد و هر پیکسل بافت، شدت باد منطقه زمینی را که آن پیکسل می‌افتد را مشخص می‌کند. مستطیل زمین قرار است "منطقه باد" ما باشد.

نویز پرلین از طریق یک سایه زن به نام NoiseShader تولید می شود. این سایه زن از الگوریتم های نویز سه بعدی سیمپلکس از https://github.com/ashima/webgl-noise استفاده می کند. نسخه WebGL از یکی از نمونه های MrDoob's Three.js در آدرس زیر گرفته شده است: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html .

NoiseShader زمان، مقیاس و مجموعه‌ای از پارامترها را به‌عنوان یکنواخت زمان می‌گیرد و توزیع دوبعدی خوبی از نویز 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) }

...

ما قصد داریم از این Shader برای رندر کردن 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 تجهیز می کند و سپس آن را با یک دوربین املایی رندر می کند تا از اعوجاج پرسپکتیو جلوگیری شود.

همانطور که بحث شد، اکنون می خواهیم از این بافت به عنوان بافت رندر اصلی زمین نیز استفاده کنیم. این واقعاً برای کارکرد خود اثر باد ضروری نیست. اما داشتن آن بسیار خوب است، تا بتوانیم از نظر بصری بهتر بفهمیم که در تولید باد چه خبر است.

در اینجا تابع initTerrain بازسازی شده است که از noiseMap به عنوان یک بافت استفاده می کند:

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

بنابراین، کاری که این سایه زن انجام می دهد این است که ابتدا مختصات جستجوی بافت windUV را بر اساس موقعیت 2 بعدی، xz (افقی) راس محاسبه می کند. این مختصات UV برای جستجوی نیروی باد، vWindForce ، از بافت باد نویز Perlin استفاده می شود.

این مقدار vWindForce ، با راس windFactor خاص ترکیب شده است، ویژگی سفارشی مورد بحث در بالا، به منظور محاسبه میزان تغییر شکل راس. ما همچنین یک پارامتر جهانی windScale برای کنترل قدرت کلی باد و یک بردار جهت باد داریم که مشخص می‌کند تغییر شکل باد در کدام جهت باید انجام شود.

بنابراین، این تغییر شکل بر اساس باد در قطعات چمن ما ایجاد می کند. با این حال ما هنوز تمام نشده است. همانطور که در حال حاضر است، این تغییر شکل ایستا است و اثر یک منطقه بادخیز را منتقل نمی کند.

همانطور که گفته بودیم، باید بافت نویز را در طول زمان در ناحیه باد بچرخانیم تا شیشه ما بتواند موج بزند.

این کار با جابجایی در طول زمان، یکنواخت vOffset که به NoiseShader منتقل می شود، انجام می شود. این یک پارامتر vec2 است که به ما اجازه می‌دهد تا نویز را در جهت خاصی (جهت باد خود) تعیین کنیم.

ما این کار را در تابع رندر که در هر فریم فراخوانی می شود انجام می دهیم:

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 ما احتمالاً آخرین صحنه بود، که اگر راه خود را از طریق بالون به چشم گردباد بزنید تا به انتهای سفر خود در سایت برسید، می توانید آن را ببینید و ویدیوی اختصاصی از نسخه آینده.

صحنه بالون سواری

وقتی این صحنه را ایجاد کردیم، می‌دانستیم که باید یک ویژگی اصلی در تجربه داشته باشیم که تأثیرگذار باشد. گردباد در حال چرخش به‌عنوان بخش مرکزی عمل می‌کند و لایه‌های محتوای دیگر این ویژگی را برای ایجاد یک اثر دراماتیک در جای خود شکل می‌دهند. برای رسیدن به این هدف، چیزی شبیه یک استودیو فیلم ساختیم که پیرامون این سایه‌زن عجیب و غریب قرار گرفته است.

ما از یک رویکرد ترکیبی برای ایجاد ترکیب واقعی استفاده کردیم. برخی از آنها ترفندهای بصری مانند اشکال نور برای ایجاد جلوه شعله ور شدن لنز یا قطرات باران بودند که به صورت لایه هایی در بالای صحنه ای که به آن نگاه می کنید متحرک می شوند. در موارد دیگر، سطوح صافی داشتیم که به نظر حرکت می‌کنند، مانند لایه‌های ابرهای کم‌پرواز که بر اساس کد سیستم ذرات حرکت می‌کنند. در حالی که تکه‌های زباله‌هایی که به دور گردباد می‌چرخند، لایه‌هایی از یک صحنه سه بعدی بودند که برای حرکت در جلو و پشت گردباد مرتب شده‌اند.

دلیل اصلی که ما مجبور بودیم صحنه را به این شکل بسازیم، اطمینان از داشتن GPU کافی برای کنترل سایه زن تورنادو در تعادل با سایر افکت هایی بود که اعمال می کردیم. در ابتدا مشکلات بزرگی در تعادل GPU داشتیم، اما بعداً این صحنه بهینه شد و از صحنه‌های اصلی سبک‌تر شد.

آموزش: The Storm Shader

برای ایجاد دنباله طوفان نهایی، بسیاری از تکنیک های مختلف با هم ترکیب شدند، اما محور این کار یک سایه زن سفارشی GLSL بود که شبیه یک گردباد است. ما تکنیک‌های مختلفی از سایه‌زن‌های راس را برای ایجاد گرداب‌های هندسی جالب گرفته تا انیمیشن‌های مبتنی بر ذرات و حتی انیمیشن‌های سه بعدی اشکال هندسی پیچ‌خورده امتحان کرده بودیم. به نظر نمی رسید که هیچ یک از این اثرات احساس یک گردباد را دوباره ایجاد کند یا از نظر پردازش نیاز به مقدار زیادی داشته باشد.

یک پروژه کاملاً متفاوت در نهایت پاسخ را به ما داد. یک پروژه موازی شامل بازی‌های علمی برای ترسیم نقشه مغز موش از موسسه ماکس پلانک (brainflight.org) جلوه‌های بصری جالبی ایجاد کرده بود. ما موفق شده بودیم با استفاده از یک سایه زن حجمی سفارشی، فیلم هایی از داخل یک نورون موش ایجاد کنیم.

داخل یک نورون موش با استفاده از یک سایه زن حجمی سفارشی
داخل یک نورون موش با استفاده از یک سایه زن حجمی سفارشی

ما متوجه شدیم که داخل یک سلول مغز کمی شبیه قیف یک گردباد است. و از آنجایی که ما از یک تکنیک حجمی استفاده می کردیم، می دانستیم که می توانیم این سایه زن را از همه جهات در فضا مشاهده کنیم. می‌توانیم رندر سایه‌زن را طوری تنظیم کنیم که با صحنه طوفان ترکیب شود، به‌ویژه اگر در زیر لایه‌های ابر و بالای پس‌زمینه‌ای دراماتیک قرار گرفته باشد.

تکنیک سایه زن شامل ترفندی است که اساساً از یک شیدر GLSL برای رندر کردن یک شیء کامل با یک الگوریتم رندر ساده به نام رندر راهپیمایی پرتو با میدان فاصله استفاده می کند. در این تکنیک یک سایه زن پیکسلی ایجاد می شود که نزدیک ترین فاصله را به یک سطح برای هر نقطه روی صفحه تخمین می زند.

یک مرجع خوب به الگوریتم را می توان در نمای کلی توسط iq پیدا کرد: Rendering Worlds With Two Triangles - Iñigo Quilez . همچنین با کاوش در گالری سایه زن ها در glsl.heroku.com، نمونه های زیادی از این تکنیک در آنجا یافت می شود که می توان با آنها آزمایش کرد.

قلب سایه زن با عملکرد اصلی شروع می شود، تبدیل دوربین را تنظیم می کند و وارد حلقه ای می شود که به طور مکرر فاصله تا یک سطح را ارزیابی می کند. فراخوانی RaytraceFoggy (direct_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 است (بله، این یک آزمایش بود!).

ما با یک 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

در نهایت ما گردباد را با استفاده از یک الگوریتم sal2x ساده شده (برای جلوگیری از ظاهر بلوک) @line 1107 در stormTest.coffee به نمایش می‌گذاریم. این بدان معناست که در بدتر از آن، گردباد تارتری داشته باشیم، اما حداقل بدون سلب کنترل از کاربر کار می‌کند.

مرحله بهینه سازی بعدی نیاز به فرو رفتن در الگوریتم دارد. عامل محاسباتی محرک در سایه زن، تکراری است که بر روی هر پیکسل انجام می شود تا فاصله تابع سطح را تقریبی کند: تعداد تکرارهای حلقه ریمارچ. با استفاده از اندازه پله بزرگتر می‌توانیم تخمین سطح گردباد را با تکرارهای کمتر در زمانی که خارج از سطح ابری آن بودیم، بدست آوریم. هنگامی که در داخل هستیم، اندازه گام را برای دقت کاهش می دهیم و می توانیم مقادیر را برای ایجاد جلوه مه آلود ترکیب کنیم. همچنین ایجاد یک استوانه مرزی برای بدست آوردن تخمین عمق برای پرتو ریخته‌شده سرعت خوبی را به همراه داشت.

بخش بعدی مشکل این بود که مطمئن شوید این سایه زن روی کارت های ویدیویی مختلف اجرا می شود. ما هر بار چند آزمایش انجام دادیم و شروع به ایجاد شهود برای نوع مشکلات سازگاری کردیم که ممکن است با آن مواجه شویم. دلیل اینکه ما نمی‌توانیم خیلی بهتر از شهود انجام دهیم این است که همیشه نمی‌توانیم اطلاعات اشکال‌زدایی خوبی در مورد خطاها به دست آوریم. یک سناریوی معمولی فقط یک خطای GPU است که کمی بیشتر در پیش است، یا حتی یک خرابی سیستم!

مسائل مربوط به سازگاری بردهای ویدئویی راه‌حل‌های مشابهی داشتند: مطمئن شوید که ثابت‌های استاتیک از نوع داده دقیقی که تعریف شده است، IE: 0.0 برای شناور و 0 برای int وارد شده است. هنگام نوشتن توابع طولانی تر مراقب باشید. ترجیح داده می شود که چیزها را در چندین توابع ساده تر و متغیرهای میانی تقسیم کنیم زیرا به نظر می رسد کامپایلرها موارد خاص را به درستی مدیریت نمی کنند. مطمئن شوید که تکسچرها همه توان 2 دارند، خیلی بزرگ نیستند و در هر صورت هنگام جستجوی داده های بافت در یک حلقه "احتیاط" را به خرج می دهند.

بزرگترین مشکلاتی که ما در سازگاری داشتیم، اثر نورپردازی طوفان بود. ما از یک بافت از پیش ساخته شده که دور گردباد پیچیده شده بود استفاده کردیم تا بتوانیم پوسته های آن را رنگ آمیزی کنیم. این یک افکت فوق‌العاده بود، و ترکیب گردباد را با رنگ‌های صحنه آسان می‌کرد، اما تلاش برای اجرا بر روی پلتفرم‌های دیگر زمان زیادی طول کشید.

گردباد

وب سایت موبایل

تجربه موبایل نمی تواند ترجمه مستقیم نسخه دسکتاپ باشد زیرا نیازهای فناوری و پردازش بسیار سنگین بود. ما مجبور شدیم چیزی جدید بسازیم، که به طور خاص کاربر تلفن همراه را هدف قرار دهد.

ما فکر کردیم که کارناوال Photo-Booth از دسکتاپ به عنوان یک برنامه وب تلفن همراه که از دوربین موبایل کاربر استفاده می کند، جالب است. کاری که تا کنون ندیده بودیم.

برای افزودن طعم، تبدیل های سه بعدی را در CSS3 کدگذاری کردیم. با پیوند دادن آن با ژیروسکوپ و شتاب سنج، توانستیم عمق زیادی به تجربه اضافه کنیم. این سایت به نحوه در دست گرفتن، حرکت و نگاه کردن به تلفن خود پاسخ می دهد.

هنگام نوشتن این مقاله، ما فکر کردیم که ارزش دارد نکاتی را در مورد نحوه اجرای روان فرآیند توسعه تلفن همراه به شما ارائه دهیم. اینجا رفتند! پیش بروید و ببینید چه چیزی می توانید از آن بیاموزید!

نکات و ترفندهای موبایل

Preloader چیزی است که مورد نیاز است، نه چیزی که باید از آن اجتناب کرد. ما می دانیم، که گاهی اوقات دومی اتفاق می افتد. این عمدتاً به این دلیل است که شما باید همچنان لیست چیزهایی را که از قبل بارگذاری می کنید، با رشد پروژه خود حفظ کنید. بدتر از آن، این که اگر منابع مختلف و بسیاری از آنها را همزمان می‌کشید، نحوه محاسبه پیشرفت بارگذاری خیلی واضح نیست. اینجا جایی است که کلاس انتزاعی سفارشی و بسیار عمومی ما "Task" مفید است. ایده اصلی آن اجازه دادن به ساختار تودرتوی بی‌پایان است که در آن یک Task می‌تواند وظایف فرعی خود را داشته باشد، که می‌تواند غیره خود را داشته باشد... علاوه بر این، هر کار پیشرفت خود را با توجه به پیشرفت وظایف فرعی خود محاسبه می‌کند (اما نه به پیشرفت والدین). با اینکه همه MainPreloadTask، AssetPreloadTask و TemplatePreFetchTask از Task مشتق شوند، ساختاری به شکل زیر ایجاد کردیم:

Preloader

به لطف چنین رویکردی و کلاس Task، ما به راحتی می توانیم پیشرفت جهانی (MainPreloadTask) یا فقط پیشرفت دارایی ها (AssetPreloadTask) یا پیشرفت بارگذاری قالب ها (TemplatePreFetchTask) را بدانیم. حتی پیشرفت یک فایل خاص. برای مشاهده نحوه انجام آن، نگاهی به کلاس Task در /m/javascripts/raw/util/Task.js و اجرای واقعی کار در /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 مشترک)، باید به نحوه بارگیری دارایی‌هایی که وابسته به پلتفرم هستند نیز توجه داشت. اساسا ما چهار نوع تصویر داریم. استاندارد موبایل (ext. که ext پسوند فایل است، معمولاً png. یا .jpg)، شبکیه تلفن همراه (-2x.ext)، استاندارد تبلت (-tab.ext) و شبکیه رایانه لوحی (-tab-2x.ext). به جای انجام تشخیص در MainPreloadTask و کدگذاری چهار آرایه دارایی، فقط می گوییم نام و پسوند دارایی برای پیش بارگذاری چیست و آیا دارایی وابسته به پلتفرم است (پاسخگو = درست / نادرست). سپس 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، متوجه شدیم که به جای کار کردن، زمان زیادی را صرف بازی با غرفه عکس می کنیم:D این فقط به این دلیل بود که سرگرم کننده است. بنابراین ما یک دمو برای شما ساختیم تا با آن بازی کنید.

غرفه عکس موبایل
غرفه عکس موبایل

می‌توانید یک نسخه آزمایشی زنده را در اینجا ببینید (آن را روی آیفون یا تلفن اندروید خود اجرا کنید):

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

برای تنظیم آن، به یک نمونه برنامه رایگان Google App Engine نیاز دارید که در آن بتوانید backend را اجرا کنید. کد جلویی پیچیده نیست، اما چند مشکل احتمالی وجود دارد. بیایید اکنون آنها را مرور کنیم:

  1. نوع فایل تصویر مجاز ما می خواهیم که افراد فقط بتوانند تصاویر را آپلود کنند (زیرا غرفه عکس است نه غرفه فیلم). در تئوری، شما فقط می توانید فیلتر را در 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 هیچ کاری نمی کنند، اصلا اطلاع رسانی نمی کنند. بنابراین ما به هیچ اقدام خاصی برای این مورد نیاز نداریم، با این حال، گوشی های اندرویدی به هر حال تابع 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، و ترکیب گسترده ای از فناوری های مختلف درگیر، در این مقاله توانستیم تنها تعدادی از رویکردهای مورد استفاده خود را پوشش دهیم.

اگر کنجکاو هستید که کل انچیلادا را کاوش کنید، در این لینک به کد منبع کامل Find Your Way To Oz مراجعه کنید.

وام

برای لیست کامل اعتبارات اینجا را کلیک کنید

منابع