מקרה לדוגמה – איך מגיעים למדינה אוז

מבוא

'Find Your Way to Oz' הוא ניסוי חדש של Google Chrome שחברת Disney הביאה לאינטרנט. הנסיעה מאפשרת לכם לצאת למסע אינטראקטיבי בקרוסלה של קנזס, שמובילה אתכם לארץ עוץ אחרי שאתם נשטפים בסערה עזה.

המטרה שלנו הייתה לשלב את העושר של הקולנוע עם היכולות הטכניות של הדפדפן כדי ליצור חוויה מהנה ומרתקת שהמשתמשים יוכלו להתחבר אליה.

מדובר במשימה גדולה מדי כדי לתאר אותה במלואה במאמר הזה, לכן התעמקנו בנושא והוצאנו כמה פרקים מסיפור הטכנולוגיה שלדעתנו מעניינים. לאורך הדרך, הוצאנו כמה מדריכים ממוקדים ברמות קושי עולות.

הרבה אנשים עבדו קשה כדי שהחוויה הזו תהיה אפשרית: יותר מדי אנשים מכדי שנוכל לרשום את כולם כאן. כדי לקרוא את הסיפור המלא, אפשר להיכנס לאתר ולבדוק את דף הקרדיטים בקטע התפריט.

מבט מאחורי הקלעים

המשחק 'Find Your Way to Oz' במחשב הוא עולם עשיר וסוחף. אנחנו משתמשים ב-3D ובמספר שכבות של אפקטים בהשראת שיטות קולנועיות מסורתיות, שמשתלבים כדי ליצור סצנה כמעט ריאליסטית. הטכנולוגיות הבולטות ביותר הן WebGL עם Three.js, שידורים מותאמים אישית ורכיבי DOM מונפשים באמצעות תכונות CSS3. בנוסף, getUserMedia API‏ (WebRTC) לחוויות אינטראקטיביות שמאפשרות למשתמש להוסיף את התמונה שלו ישירות מהמצלמה האינטרנטית ו-WebAudio לצורך צליל 3D.

אבל הקסם של חוויה טכנולוגית כזו הוא האופן שבו כל הגורמים האלה מתחברים. זהו גם אחד מהאתגרים העיקריים: איך לשלב אפקטים חזותיים ואלמנטים אינטראקטיביים בסצנה אחת כדי ליצור שלם עקבי? היה קשה לנהל את המורכבות החזותית הזו, כי היה קשה לדעת באיזה שלב בפיתוח היינו בכל רגע נתון.

כדי לטפל בבעיה של אופטימיזציה ואפקטים חזותיים שמקושרים זה לזה, השתמשנו הרבה בלוח בקרה שצילם את כל ההגדרות הרלוונטיות שבדקנו באותו שלב. אפשר לשנות את הסצנה בשידור חי בדפדפן, החל מבהירות ועד לעומק שדה, גמא וכו'. כל אחד יכול לנסות לשנות את הערכים של הפרמטרים המשמעותיים בחוויה ולהשתתף בגילוי מה עובד הכי טוב.

לפני שנשתף את הסוד שלנו, אנחנו רוצים להזהיר שיכול להיות שהמכשיר יתרסק, בדיוק כמו אם תסתכלו בתוך מנוע של רכב. חשוב לוודא שאין לך משהו חשוב פתוח, ואז להיכנס לכתובת ה-URL הראשית של האתר ולהוסיף את הערך ‎?debug=on לכתובת. ממתינים שהאתר ייטען, ואז לוחצים על המקש Ctrl-I כדי להציג תפריט נפתח בצד שמאל. אם מבטלים את הסימון של האפשרות 'יציאה ממסלול המצלמה', אפשר להשתמש במקשות A,‏ W,‏ S,‏ D ובעכבר כדי לנוע בחופשיות ברחבי המרחב המשותף.

נתיב המצלמה.

לא נעבור כאן על כל ההגדרות, אבל מומלץ להתנסות: המקשים חושפים הגדרות שונות בסצנות שונות. ברצף הסערה האחרון יש מקש נוסף: Ctrl-A, שבעזרתו אפשר להפעיל או להשבית את ההפעלה של האנימציה ולעוף מסביב. בסצנה הזו, אם מקישים על Esc (כדי לצאת מהפונקציונליות של נעילת העכבר) ומקישים שוב על Ctrl-I, אפשר לגשת להגדרות שספציפיות לסצנת הסערה. כדאי להסתובב ולצלם כמה תמונות יפות כמו זו שבהמשך.

סצנה של סערה

כדי לעשות זאת ולהבטיח שהיא תהיה גמישה מספיק לצרכים שלנו, השתמשנו בספרייה נהדרת בשם dat.gui (כאן מופיע מדריך קודם לשימוש בה). היא אפשרה לנו לשנות במהירות את ההגדרות שמוצגות למבקרים באתר.

קצת כמו ציור מאט

בסרטים ובאנימציות קלאסיים רבים של דיסני, יצירת סצנות משמעותה שילוב של שכבות שונות. היו שכבות של פעולה בשידור חי, אנימציה של תאים, אפילו סט פיזי, ובשכבות העליונות נוצרו ציורים על זכוכית: טכניקה שנקראת 'ציור שטוח'.

המבנה של החוויה שיצרנו דומה בהרבה מובנים, למרות שחלק מה'שכבות' הן הרבה יותר מאשר רכיבים חזותיים סטטיים. למעשה, הם משפיעים על המראה של הדברים לפי חישובים מורכבים יותר. עם זאת, לפחות ברמת התמונה הגדולה, אנחנו מתמודדים עם תצוגות, שממוזגות אחת מעל השנייה. בחלק העליון מוצגת שכבת ממשק משתמש, מתחתיה מוצגת סצנה תלת-ממדית: היא עצמה מורכבת מרכיבי סצנה שונים.

שכבת הממשק העליונה נוצרה באמצעות DOM ו-CSS 3, כך שאפשר לערוך את האינטראקציות בדרכים רבות, ללא קשר לחוויית התלת-ממד, תוך תקשורת בין השתיים לפי רשימה נבחרת של אירועים. התקשורת הזו מתבצעת באמצעות Backbone Router + אירוע HTML5 מסוג onHashChange, שמאפשר לקבוע איזה אזור יוצג באנימציה. (מקור הפרויקט: ‎/develop/coffee/router/Router.coffee).

מדריך: גיליונות פריימים ותמיכה ב-Retina

אחת משיטות האופטימיזציה המעניינות שהשתמשנו בהן בממשק הייתה שילוב של תמונות שכבת-העל הרבות של הממשק בקובץ PNG אחד כדי לצמצם את הבקשות לשרת. בפרויקט הזה הממשק מורכב מיותר מ-70 תמונות (לא כולל טקסטורות תלת-ממדיות) שנטענו מראש כדי לצמצם את זמן האחזור של האתר. כאן אפשר לראות את גיליון ה-sprite החי:

מסך רגיל – http://findyourwaytooz.com/img/home/interface_1x.png מסך Retina – http://findyourwaytooz.com/img/home/interface_2x.png

ריכזנו כאן כמה טיפים לגבי האופן שבו השתמשנו ב-Sprite Sheets, ואיך משתמשים בהם במכשירי Retina כדי שהממשק יהיה חד ומסודר ככל האפשר.

יצירת גיליונות פריימים

כדי ליצור גיליונות פריימים של דמויות, השתמשנו ב-TexturePacker, שמאפשר לייצא בפורמט הרצוי. במקרה הזה ייצאנו את הקובץ כ-EaselJS, שהוא פורמט פשוט מאוד שאפשר להשתמש בו גם ליצירת ספרייטים מונפשים.

שימוש בגיליון ה-Sprite שנוצר

אחרי שתיצרו את גיליון ה-Sprite, אמור להופיע קובץ 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 של גיליון ה-sprite
  • frames הן הקואורדינטות של כל אלמנט בממשק המשתמש [x, ‏ y, ‏ width, ‏ height]
  • אנימציות הן השמות של כל נכס

שימו לב שהשתמשנו בתמונות בצפיפות גבוהה כדי ליצור את גיליון ה-Sprite, ואז יצרנו את הגרסה הרגילה רק על ידי שינוי הגודל שלה לחצי.

אז מה זה אומר?

עכשיו, אחרי שהכול מוכן, אנחנו צריכים רק קטע קוד של 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 מוסבר בהרחבה על צפיפות פיקסלים משתנה.

צינור עיבוד הנתונים של תוכן תלת-ממדי

חוויית הסביבה מוגדרת בשכבת WebGL. כשמדברים על סצנה תלת-ממדית, אחת השאלות הכי קשות היא איך אפשר ליצור תוכן שמאפשר את הפוטנציאל המקסימלי של הבעה מבחינת המודלים, האנימציה והאפקטים. במובנים רבים, הליבה של הבעיה הזו היא צינור עיבוד הנתונים של התוכן: תהליך מוסכם שצריך לפעול לפיו כדי ליצור תוכן לסצנה תלת-ממדית.

רצינו ליצור עולם מרהיב, ולכן נזקקנו לתהליך מוצק שיאפשר לאמני 3D ליצור אותו. הם צריכים לקבל כמה שיותר חופש ביטוי בתוכנות האנימציה והמודל 3D שלהם, ואנחנו צריכים להציג את התוצאה במסך באמצעות קוד.

אנחנו עובדים על בעיות כאלה כבר זמן מה, כי בכל פעם שיצרנו בעבר אתר תלת-ממדי, גילינו מגבלות בכלים שאפשר להשתמש בהם. לכן יצרנו את הכלי הזה, שנקרא 3D Librarian: חלק ממחקר פנימי. והוא היה מוכן כמעט לשימוש בעבודה אמיתית.

לכלי הזה יש היסטוריה: במקור הוא היה ל-Flash, והוא איפשר לייבא סצנה גדולה ב-Maya כקובץ דחוס אחד שעובר אופטימיזציה לצורך ביטול האריזה בזמן הריצה. הסיבה לכך היא שהיא דחסה את הסצנה ביעילות באותה מבנה נתונים שבו מתבצעת מניפולציה במהלך הטרנספורמציה והאנימציה. יש מעט מאוד ניתוח שצריך לבצע בקובץ בזמן הטעינה. ביטול האריזה ב-Flash היה מהיר למדי כי הקובץ היה בפורמט AMF, ש-Flash יכול לבטל את האריזה שלו באופן מקורי. שימוש באותו פורמט ב-WebGL דורש קצת יותר עבודה על המעבד. למעשה, נאלצנו ליצור מחדש שכבת קוד של JavaScript לפריסה של נתונים, שבעצם תבצע דחיסה לאחור של הקבצים האלה ותיצור מחדש את מבני הנתונים הנדרשים ל-WebGL. ביטול האריזה של כל הסצנה התלת-ממדית היא פעולה שמכבידה מעט על המעבד: ביטול האריזה של סצנה 1 ב-Find Your Way To Oz נמשך כ-2 שניות במחשבים ברמה בינונית עד גבוהה. לכן, הפעולה הזו מתבצעת באמצעות טכנולוגיית Web Workers, בזמן 'הגדרת הסצנה' (לפני שהסצנה מופעלת בפועל), כדי לא להשהות את חוויית המשתמש.

הכלי הזה שימושי מאוד ואפשר לייבא איתו את רוב התצוגה בתלת-ממד: מודלים, טקסטורות, אנימציות של עצמות. יוצרים קובץ ספרייה יחיד, שאפשר לטעון אותו לאחר מכן במנוע התלת-ממד. אתם ממלאים את הספרייה הזו בכל המודלים שדרושים לכם בסצנה, ואז יוצרים אותם בסצנה.

הבעיה הייתה שעברנו לעבוד עם WebGL: הילד החדש בשכונה. זה היה ילד קשוח למדי: הוא קבע את הסטנדרט לחוויות תלת-ממדיות מבוססות-דפדפן. לכן יצרנו שכבת JavaScript ייעודית שתקבל את קובצי הסצנות התלת-ממדיות הנדחסים של 3D Librarian ותתרגם אותם כראוי לפורמט ש-WebGL יוכל להבין.

הדרכה: תנו לרוח לנשוב

אחד מהנושאים החוזרים ב'מצא את הדרך לאוז' היה הרוח. אחד מהנושאים של העלילה מתוכנן כקרשנדו של רוח.

הסצנה הראשונה של הקרנבל שקטה יחסית. ככל שהמשתמשים עוברים בין הסצנות השונות, הם חשים ברוח חזקה יותר ויותר, שמגיעה לשיא בסצנה האחרונה – הסערה.

לכן היה חשוב לספק אפקט רוח שגורם לטלטול.

כדי ליצור את האפקט הזה, איכלסנו את 3 הסצנות של הקרנבל באובייקטים רכים, שצריכים להיות מושפעים מהרוח, כמו אוהלים, דגלים, פני השטח של תא הצילום והבלון עצמו.

מטלית רכה.

משחקים למחשב נבנים בימינו בדרך כלל סביב מנוע פיזיקה מרכזי. לכן, כשצריך לדמות אובייקט רך בעולם תלת-ממדי, מתבצעת עבורו סימולציה מלאה של פיזיקה, שמאפשרת ליצור התנהגות ריאליסטית של אובייקט רך.

ב-WebGL או ב-JavaScript אין לנו (עדיין) את היכולת להריץ סימולציית פיזיקה מלאה. לכן באוסטרליה נאלצנו למצוא דרך ליצור את האפקט של הרוח, בלי לדמות אותה בפועל.

הטמענו את המידע על 'רגישות לרוח' של כל אובייקט במודל ה-3D עצמו. לכל קודקוד במודל התלת-ממדי היה 'מאפיין רוח' שציין באיזו מידה הקודקוד הזה אמור להיות מושפע מרוח. אז, רגישות הרוח שצוינה של אובייקטים תלת-ממדיים. לאחר מכן היינו צריכים ליצור את הרוח עצמה.

כדי לעשות זאת, יצרנו תמונה שמכילה רעש Perlin. התמונה הזו נועדה לכסות 'אזור רוח' מסוים. לכן, דרך טובה לחשוב עליה היא לדמיין תמונה של רעש ענן שמוטמעת באזור מלבני מסוים בסצנה התלת-ממדית. כל פיקסל, ערך רמת אפור, בתמונה הזו מציין את עוצמת הרוח ברגע מסוים באזור התלת-ממדי "סביב לו".

כדי ליצור את אפקט הרוח, התמונה מועברת בזמן במהירות קבועה בכיוון ספציפי – כיוון הרוח. כדי לוודא ש'האזור הסוער' לא משפיע על כל מה שמוצג בסצנה, אנחנו עוטפים את התמונה של הרוח סביב הקצוות, ומגבילים אותה לאזור ההשפעה.

מדריך פשוט ליצירת רוח תלת-ממדית

עכשיו נראה איך יוצרים את אפקט הרוח בסצנה פשוטה תלת-ממדית ב-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

עכשיו אנחנו משתמשים בחומר מותאם אישית, windMaterial, במקום ב-MeshPhongMaterial שבו השתמשנו בעבר. WindMaterial עוטף את WindMeshShader שנסביר עליו עוד רגע.

לכן, הקוד ב-instanceGrass עובר על כל הנקודות של מודל הדשא, לכל נקודה הוא מוסיף מאפיין נקודה מותאם אישית שנקרא windFactor. הערך של windFactor מוגדר כ-0 בקצה התחתון של מודל הדשא (שם הוא אמור לגעת בשטח), וכ-1 בקצה העליון של מודל הדשא.

המרכיב השני שאנחנו צריכים הוא להוסיף את הרוח בפועל לסצנה. כפי שציינתי, נשתמש לצורך כך ברעשי Perlin. ניצור באופן פרוגרמטי טקסטורה של רעש Perlin.

כדי להבהיר, אנחנו נותנים את המרקם הזה לשטח עצמו, במקום המרקם הירוק הקודם שהיה לו. כך יהיה קל יותר להבין מה קורה עם הרוח.

כך, מרקם הרעש של Perlin יכסה מבחינה מרחבית את היקף השטח של השטח, וכל פיקסל במרקם יציין את עוצמת הרוח באזור השטח שבו הפיקסל נמצא. הריבוע של פני השטח יהיה 'אזור הרוח' שלנו.

רעש Perlin נוצר באופן פרוגרמטי באמצעות שידרוג (shader) שנקרא NoiseShader. ב-shader הזה נעשה שימוש באלגוריתמים של רעש פשוט תלת-ממדי מ-https://github.com/ashima/webgl-noise . גרסת WebGL של הקוד הזה נלקחה מילה במילה מאחד מהדוגמאות של MrDoob ל-Three.js, בכתובת: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.

NoiseShader מקבל זמן, קנה מידה וקבוצת פרמטרים של אופסט, כפרמטרים אחידים, ומפיק חלוקה 2D יפה של רעש Perlin.

class NoiseShader

  uniforms:     
    "fTime"  : { type: "f", value: 1 }
    "vScale"  : { type: "v2", value: new THREE.Vector2(1,1) }
    "vOffset"  : { type: "v2", value: new THREE.Vector2(1,1) }

...

אנחנו נשתמש בשדר הזה כדי ליצור רינדור של רעש Perlin למרקם. הפעולה הזו מתבצעת בפונקציה initNoiseShader.

initNoiseShader:->
  @noiseMap  = new THREE.WebGLRenderTarget( 256, 256, { minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat } );
  @noiseShader = new NoiseShader()
  @noiseShader.uniforms.vScale.value.set(0.3,0.3)
  @noiseScene = new THREE.Scene()
  @noiseCameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2,  window.innerHeight / 2, window.innerHeight / - 2, -10000, 10000 );
  @noiseCameraOrtho.position.z = 100
  @noiseScene.add( @noiseCameraOrtho )

  @noiseMaterial = new THREE.ShaderMaterial
      fragmentShader: @noiseShader.fragmentShader
      vertexShader: @noiseShader.vertexShader
      uniforms: @noiseShader.uniforms
      lights:false

  @noiseQuadTarget = new THREE.Mesh( new THREE.PlaneGeometry(window.innerWidth,window.innerHeight,100,100), @noiseMaterial )
  @noiseQuadTarget.position.z = -500
  @noiseScene.add( @noiseQuadTarget )

הקוד שלמעלה מגדיר את noiseMap כיעד עיבוד (render target) של 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, שהוא אחראי על עיוות המודלים של הדשא בהתאם לרוח.

כדי ליצור את ה-shader הזה, התחלנו מה-shader הרגיל של Three.js MeshPhongMaterial ושינינו אותו. זו דרך מהירה ויעילה להתחיל לעבוד עם שידר (shader) שעובד, בלי שתצטרכו להתחיל מאפס.

לא נעתיק כאן את כל קוד ה-shader (אפשר לעיין בו בקובץ קוד המקור), כי רוב הקוד הוא עותק של ה-shader 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;

מה ש-shader הזה עושה הוא קודם מחשב את קואורדינטת בדיקת הטקסטורה windUV על סמך המיקום הדו-מימדי, xz (אופקי) של הנקודה. קואורדינטת ה-UV הזו משמשת לחיפוש של עוצמת הרוח, vWindForce, מטקסטורת הרוח של רעש Perlin.

הערך vWindForce מורכב מהערך של המאפיין המותאם אישית windFactor, שספציפי לנקודת הצומת, כפי שצוין למעלה, כדי לחשב את מידת העיוות הנדרשת בנקודת הצומת. יש לנו גם פרמטר גלובלי, windScale, כדי לשלוט בעוצמה הכוללת של הרוח, ווקטור windDirection שמציין באיזה כיוון צריכה להתרחש עיוות הרוח.

כך נוצרת עיוות של פתיתי הדשא על ידי הרוח. אבל אנחנו עדיין לא סיימנו. בשלב זה, העיוות הזה סטטי ולא ישקף את ההשפעה של אזור סוער.

כפי שציינו, נצטרך להזיז את טקסטורת הרעש לאורך זמן, מעל אזור הרוח, כדי שהזכוכית שלנו תוכל לזוז.

כדי לעשות זאת, מעבירים לאורך זמן את המאפיין המאוחד vOffset שמוענק ל-NoiseShader. זהו פרמטר 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. באופן ספציפי, ב-vertex shader.

הקוד של ה-shader הזה הוא גרסה שונה של ParticleMaterial ב-Three.js, וכך נראה הליבה שלו:

vec4 mvPosition;
vec4 wpos = modelMatrix * vec4( position, 1.0 );
wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
float vWindForce = texture2D(tWindForce,windUV).x;
float windMod = (1.0 - vWindForce) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;

mvPosition = modelViewMatrix *  pos;

fSpeed = speed;
float fSize = size * (1.0 + sin(time * speed));

#ifdef USE_SIZEATTENUATION
    gl_PointSize = fSize * ( scale / length( mvPosition.xyz ) );
#else,
    gl_PointSize = fSize;
#endif

gl_Position = projectionMatrix * mvPosition;

ה-vertex shader הזה לא שונה בהרבה מזה שהיה לנו לצורך העיוות של הדשא על סמך הרוח. הפונקציה מקבלת את טקסטורת הרעש של Perlin כקלט, ובהתאם למיקום האבק בעולם, היא מחפשת ערך vWindForce בטקסטורת הרעש. לאחר מכן, המערכת משתמשת בערך הזה כדי לשנות את המיקום של גרגר האבק.

Riders On The Storm

הסצנה הכי הרפתקנית בסצנות ה-WebGL שלנו היא כנראה הסצנה האחרונה. כדי לראות אותה, צריך ללחוץ על הבלון ולעבור דרך העין של הסופה כדי להגיע לסוף המסע באתר, ולצפות בסרטון בלעדי של הגרסה הבאה.

סצנה של טיסה בכדור פורח

כשיצאנו לדרך עם הסצנה הזו, ידענו שאנחנו צריכים תכונה מרכזית בחוויה שתהיה משמעותית. הסופה המסתחררת תשמש כמרכז התמונה, ושכבות של תוכן אחר יעזרו ליצור את התכונה הזו כדי ליצור אפקט דרמטי. כדי לעשות זאת, יצרנו מעין אולפן קולנוע סביב ה-shader המוזר הזה.

כדי ליצור את התמונה המשולבת הריאליסטית, השתמשנו בגישה משולבת. חלק מהטריקים היו טריקים חזותיים כמו צורות אור כדי ליצור אפקט של התלקחות בעדשה, או טיפות גשם שמופיע בהן אנימציה כשהן שכבות מעל הסצנה שצופים בה. במקרים אחרים, ציירנו משטחים שטוחים שנראו כאילו הם זזים, כמו שכבות של עננים נמוכים שנעו בהתאם לקוד של מערכת חלקיקים. בעוד שחלקי הפסולת שהקיפו את הסופה היו שכבות בסצנה תלת-ממדית, שסודרו כך שיזוזו מלפנים ומאחורי הסופה.

הסיבה העיקרית לכך שנאלצנו ליצור את הסצנה בצורה הזו היא כדי להבטיח שיהיה לנו מספיק מעבדי GPU כדי לטפל בשיידר של הסופה ביחס לאפקטים האחרים שהחלטנו להחיל. בהתחלה היו לנו בעיות גדולות באיזון ה-GPU, אבל בהמשך ביצענו אופטימיזציה של הסצנה הזו והיא הפכה לקלילה יותר מהסצנות הראשיות.

מדריך: שימוש ב-Shader של Storm

כדי ליצור את רצף הסערה הסופי, שילבנו בין שיטות רבות, אבל החלק המרכזי בעבודה היה שידר GLSL מותאם אישית שנראה כמו טורנדו. ניסינו הרבה שיטות שונות, החל מ-vertex shaders ליצירת מערבולות גיאומטריות מעניינות ועד אנימציות מבוססות-חלקיקים ואפילו אנימציות תלת-ממדיות של צורות גיאומטריות מעוותות. אף אחד מהאפקטים לא נראה לי כזה שיכול ליצור את תחושת הסופה או כזה שדורש יותר מדי עיבוד.

בסופו של דבר, התשובה הגיעה מפרויקט אחר לגמרי. פרויקט מקביל של מכון מקס פלנק (brainflight.org), שעסק במשחקים למדע למיפוי המוח של העכבר, הניב אפקטים חזותיים מעניינים. הצלחנו ליצור סרטים של החלק הפנימי של נוירון של עכבר באמצעות שובר תאורה (shader) מותאם אישית.

בתוך נוירון של עכבר באמצעות שובר תאורה (shader) מותאם אישית
בתוך נוירון של עכבר באמצעות שַדְרֵר (shader) נפחי מותאם אישית

גילינו שהחלק הפנימי של תא מוח נראה קצת כמו משפך של טורנדו. מכיוון שהשתמשנו בשיטה נפחית, ידענו שנוכל לראות את ה-shader הזה מכל הכיוונים במרחב. אפשר להגדיר את הרינדור של ה-shader כך שייכלל בסצנת הסערה, במיוחד אם הוא נמצא בין שכבות של עננים ומעל רקע דרמטי.

שיטת ה-shader כוללת טריק שמבוסס על שימוש ב-shader GLSL יחיד כדי ליצור רינדור של אובייקט שלם באמצעות אלגוריתם רינדור פשוט יותר שנקרא רינדור של צעידה בקרניים עם שדה מרחק. בשיטה הזו נוצר שידרוג פיקסלים שמעריך את המרחק הקרוב ביותר לפני שטח לכל נקודה במסך.

מידע נוסף על האלגוריתם זמין בסקירה הכללית של iq: ‏ Rendering Worlds With Two Triangles – Iñigo Quilez. כדאי גם לעיין בגלריה של שיבוטים בכתובת glsl.heroku.com. יש שם דוגמאות רבות לטכניקה הזו שאפשר להתנסות בהן.

הלב של ה-shader מתחיל בפונקציה הראשית, שמגדירה את הטרנספורמציות של המצלמה ונכנסת לולאה שבה מתבצעת הערכה חוזרת ונשנית של המרחק לפני השטח. הקריאה RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ) היא המקום שבו מתבצע החישוב של הליבה של 'צעדת קרן'.

for(int i=0;i < number_of_steps;i++) // run the ray marching loop
{
  old_d=d;
  float shape_value=Shape(q); // find out the approximate distance to or density of the tornado cone
  float density=-shape_value;
  d=max(shape_value*step_scaling,0.0);// The max function clamps values smaller than 0 to 0

  float step_dist=d+extra_step; // The point is advanced by larger steps outside the tornado,
  //  allowing us to skip empty space quicker.

  if (density>0.0) {  // When density is positive, we are inside the cloud
    float brightness=exp(-0.6*density);  // Brightness decays exponentially inside the cloud

    // This function combines density layers to create a translucent fog
    FogStep(step_dist*0.2,clamp(density, 0.0,1.0)*vec3(1,1,1), vec3(1)*brightness, colour, multiplier); 
  }
  if(dist>max_dist || multiplier.x < 0.01) { return;  } // if we've gone too far stop, we are done
  dist+=step_dist; // add a new step in distance
  q=org+dist*dir; // trace its direction according to the ray casted
}

הרעיון הוא שככל שאנחנו מתקדמים לתוך צורת הסופה, אנחנו מוסיפים באופן קבוע רכיבים של צבע לערך הצבע הסופי של הפיקסל, וגם רכיבים לשקיפות לאורך קרן האור. כך נוצרת איכות רכה בשכבות למרקם של הסופה.

ההיבט הנוסף של הסופה הוא הצורה עצמה, שנוצרת על ידי שילוב של מספר פונקציות. בהתחלה הוא חרוט, שמורכב מרעש כדי ליצור קצה מחוספס אורגני, ולאחר מכן הוא מסתובב לאורך הציר הראשי שלו ומסתובב בזמן.

mat2 Spin(float angle){
  return mat2(cos(angle),-sin(angle),sin(angle),cos(angle)); // a rotation matrix
}

// This takes noise function and makes ridges at the points where that function crosses zero
float ridged(float f){ 
  return 1.0-2.0*abs(f);
}

// the isosurface shape function, the surface is at o(q)=0 
float Shape(vec3 q) 
{
    float t=time;

    if(q.z < 0.0) return length(q);

    vec3 spin_pos=vec3(Spin(t-sqrt(q.z))*q.xy,q.z-t*5.0); // spin the coordinates in time

    float zcurve=pow(q.z,1.5)*0.03; // a density function dependent on z-depth

    // the basic cloud of a cone is perturbed with a distortion that is dependent on its spin 
    float v=length(q.xy)-1.5-zcurve-clamp(zcurve*0.2,0.1,1.0)*snoise(spin_pos*vec3(0.1,0.1,0.1))*5.0; 

    // create ridges on the tornado
    v=v-ridged(snoise(vec3(Spin(t*1.5+0.1*q.z)*q.xy,q.z-t*4.0)*0.3))*1.2; 

    return v;
}

העבודה שנדרשת כדי ליצור סוג כזה של שַדְר (shader) היא מורכבת. בנוסף לבעיות שקשורות להפשטה של הפעולות שאתם יוצרים, יש בעיות חמורות של אופטימיזציה ותאימות בפלטפורמות שונות, שצריך לאתר ולפתור לפני שאפשר להשתמש בעבודה בסביבת הייצור.

החלק הראשון של הבעיה: אופטימיזציה של ה-shader הזה לסצנה שלנו. כדי לטפל בבעיה הזו, נדרשה לנו גישה 'בטוחה' למקרה שהשידרוג יהיה כבד מדי. כדי לעשות זאת, שילבנו את שדה הטקסטורות של הסופה ברזולוציית דגימה שונה מזו של שאר הסצנה. הקוד הזה מגיע מהקובץ stormTest.coffee (כן, זה היה בדיקה!).

אנחנו מתחילים עם renderTarget שתואם לרוחב ולגובה של הסצנה, כדי שנוכל להפריד בין הרזולוציה של טורנדו שדר לבין הרזולוציה של הסצנה. לאחר מכן אנחנו מחליטים על דגימת-העל של רזולוציית ה-shader של הסערה באופן דינמי, בהתאם לשיעור הפריימים שאנחנו מקבלים.

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

לבסוף, אנחנו מבצעים עיבוד (render) של הסופה למסך באמצעות אלגוריתם פשוט של sal2x (כדי למנוע מראה מרובע) בשורה 1107 ב-stormTest.coffee. המשמעות היא שבמקרה הגרוע ביותר, הסופון יהיה פחות חד, אבל לפחות הוא יפעל בלי לשלול מהמשתמש את השליטה.

בשלב האופטימיזציה הבא צריך להיכנס לעומק של האלגוריתם. הגורם העיקרי לחישוב ב-shader הוא האיטרציה שמתבצעת בכל פיקסל כדי לנסות להתקרב למרחק של פונקציית פני השטח: מספר האיטרציות של לולאת ה-raymarching. באמצעות צעד גדול יותר, הצלחנו לקבל אומדן של פני השטח של הסופה בפחות חזרות, בזמן שאנחנו מחוץ לענן שלה. כשנמצאים בתוך האובייקט, צריך להקטין את גודל הצעד כדי לשפר את הדיוק ולאפשר ערבוב ערכים ליצירת האפקט של הערפל. גם יצירת גליל מקיף כדי לקבל הערכת עומק לקרן שהושלחה הובילה לשיפור משמעותי במהירות.

החלק הבא של הבעיה היה לוודא שהשידרוג יפעל בכרטיסי וידאו שונים. בכל פעם ביצענו בדיקות מסוימות והתחלנו לפתח אינטואיציה לגבי סוג בעיות התאימות שעלולות לצוץ. הסיבה לכך שלא הצלחנו לעשות הרבה יותר מאשר להסתמך על אינטואיציה היא שלא תמיד הצלחנו לקבל מידע טוב על ניפוי הבאגים של השגיאות. תרחיש טיפוסי הוא שגיאה ב-GPU עם מעט מידע נוסף, או אפילו קריסה של המערכת.

לבעיות בתאימות של לוחות וידאו שונים היו פתרונות דומים: חשוב לוודא שהקבועים הסטטיים מוגדרים לפי סוג הנתונים המדויק כפי שהוגדר, למשל: 0.0 עבור float ו-0 עבור int. חשוב להיזהר כשכותבים פונקציות ארוכות. עדיף לפצל את הדברים לכמה פונקציות פשוטות יותר ומשתני ביניים, כי נראה שהמקודדים לא מטפלים במקרים מסוימים בצורה נכונה. חשוב לוודא שכל הטקסטורות הן כפולות של 2, שהן לא גדולות מדי ובכל מקרה להפעיל "זהירות" כשמחפשים נתוני טקסטורה בלולאה.

הבעיות הגדולות ביותר בתאימות נבעו מאפקט התאורה של הסערה. השתמשנו במרקם מוכן מראש שקיפלנו סביב הסופה כדי שנוכל לצבוע את העננים שלה. זה היה אפקט יפהפה, שאפשר לשלב בו בקלות את הסופה בצבעים של הסצנה, אבל לקח הרבה זמן לנסות להפעיל אותו בפלטפורמות אחרות.

טורנדו

האתר לנייד

חוויית המשתמש בנייד לא יכלה להיות תרגום ישיר של הגרסה למחשב כי דרישות הטכנולוגיה והעיבוד היו פשוט כבדות מדי. נאלצנו ליצור משהו חדש, שמטרגט במיוחד את המשתמשים בנייד.

חשבנו שיהיה מגניב להציע את 'תחנת התמונות של הקרנבל' מהמחשב כאפליקציית אינטרנט לנייד, שתשתמש במצלמה של המכשיר הנייד של המשתמש. משהו שלא ראינו עד עכשיו.

כדי להוסיף עניין, כתבנו טרנספורמציות תלת-ממדיות ב-CSS3. קישור המכשיר לג'ירוסקופ ולמד התאוצה אפשר לנו להוסיף הרבה עומק לחוויה. האתר מגיב לאופן שבו אתם מחזיקים, מזיזים ומסתכלים בטלפון.

כשכתבנו את המאמר הזה, חשבנו שזה יהיה נחמד לתת לכם כמה טיפים לניהול חלק של תהליך הפיתוח לנייד. הנה הם מופיעים! כדאי לנסות ולראות מה אפשר ללמוד ממנו.

טיפים וטריקים לנייד

חשוב להשתמש בטעינה מראש, ולא להימנע ממנה. אנחנו יודעים שלפעמים זה קורה. הסיבה העיקרית לכך היא שצריך להמשיך לתחזק את רשימת הפריטים שאתם מעלים לפני ההפעלה ככל שהפרויקט גדל. גרוע מכך, לא ברור איך צריך לחשב את התקדמות הטעינה אם מושכים משאבים שונים, ורבים מהם בו-זמנית. כאן נכנסת לתמונה הכיתה הגנרית והמותאמת אישית מאוד Task. הרעיון המרכזי הוא לאפשר מבנה עם עץ אינסופי, שבו למשימה יכולות להיות תתי-משימות משלה, שתהיה להן תתי-משימות משלה וכו'. בנוסף, כל משימה מחשבת את ההתקדמות שלה בהתאם להתקדמות של תתי-המשימות שלה (אבל לא בהתאם להתקדמות של המשימה הראשית). כשהגדרתם את כל המשימות MainPreloadTask, ‏ AssetPreloadTask ו-TemplatePreFetchTask כנגזרות מ-Task, יצרתם מבנה שנראה כך:

כלי לטעינה מראש

בזכות הגישה הזו ולקלאס 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), סטנדרטי לרזולוציית Retina לנייד (‎-2x.ext), סטנדרטי לטאבלט (‎-tab.ext) ורזולוציית Retina לטאבלט (‎-tab-2x.ext). במקום לבצע את הזיהוי ב-MainPreloadTask ולהטמיע בקוד ארבע מערכי נכסים, אנחנו פשוט מציינים את השם והסיומת של הנכס שרוצים לטעון מראש ואם הנכס תלוי בפלטפורמה (responsive = true / false). לאחר מכן, שם הקובץ ייווצר על ידי AssetPreloadTask:

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

בהמשך שרשרת הכיתות, הקוד בפועל שמבצע את טעינת הנכסים מראש נראה כך (/m/javascripts/raw/util/ImagePreloader.js):

loadUrl : function(url, type, completeHandler) {
  if(type === ImagePreloader.TYPE_BACKGROUND) {
    var $bg = $('<div>').hide().css('background-image', 'url(' + url + ')');
    this.$preloadContainer.append($bg);
  } else {
    var $img= $('<img />').attr('src', url).hide();
    this.$preloadContainer.append($img);
  }

  var image = new Image();
  this.cache[this.generateKey(url)] = image;
  image.onload = completeHandler;
  image.src = url;
}

generateKey : function(url) {
  return encodeURIComponent(url);
}

מדריך: Photo Booth ב-HTML5 (iOS6/Android)

כשפיתחנו את OZ לנייד, גילינו שאנחנו מבזבזים הרבה זמן בשימוש ב-Photo Booth במקום בעבודה :D הסיבה לכך היא פשוט כי זה כיף. לכן יצרנו דמו שתוכלו לשחק איתו.

תא צילום נייד
עמדת צילום ניידת

כאן אפשר לראות הדגמה פעילה (אפשר להריץ אותה בטלפון iPhone או Android):

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

כדי להגדיר את השירות, צריך מכונה חינמית של אפליקציה ב-Google App Engine שבה אפשר להריץ את הקצה העורפי. הקוד של הקצה הקדמי לא מורכב, אבל יש כמה מלכודות אפשריות. עכשיו נעבור עליהן:

  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 לא עושים כלום, הם לא מעדכנים בכלל. לכן אין צורך לבצע פעולה מיוחדת במקרה הזה, אבל טלפונים עם Android מפעילים את הפונקציה add() בכל מקרה, גם אם לא נבחר קובץ. כך אפשר להתאים את האתר לכך:
    add : function(e, data) {

    if(data.files.length === 0 || (data.files[0].size === 0 && data.files[0].name === "" && data.files[0].fileName === "")) {
            
    return self.onNoFileSelected();

    } else if(data.files.length > 1) {

    return self.onMultipleFilesSelected();            
    }
    }

שאר התכונות פועלות בצורה חלקה בפלטפורמות השונות. לא לשכוח ליהנות!

סיכום

בגלל הגודל העצום של Find Your Way To Oz והמגוון הרחב של הטכנולוגיות השונות שמעורבות, במאמר הזה הצלחנו להציג רק כמה מהגישות שבהן השתמשנו.

אם אתם רוצים לבדוק את כל הנתונים, אתם יכולים לעיין בקוד המקור המלא של Find Your Way To Oz בקישור הזה.

זיכויים

כאן אפשר למצוא את רשימת הקרדיטים המלאה

קובצי עזר