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

מבוא

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

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

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

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

הצצה מאחורי הקלעים

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

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

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

לפני שנשתף את הסוד שלנו, רצינו להזהיר אותך שהוא עלול לקרוס, בדיוק כמו אם תציץ לתוך מנוע של מכונית. ודאו שאין לכם שום דבר חשוב ועברו אל כתובת ה-URL הראשית של האתר והוסיפו את הפרמטר ?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 תמונות (לא כולל מרקמים תלת-ממדיים) שנטענות מראש כדי לקצר את זמן האחזור של האתר. אפשר לראות את גיליון ה-Sprite הפעיל כאן:

תצוגה רגילה - http://findyourwaytooz.com/img/home/interface_1x.png תצוגת רטינה - http://findyourwaytooz.com/img/home/interface_2x.png

הנה כמה טיפים לאופן שבו ניצלנו את השימוש ב-Sprite Sheets, וכיצד להשתמש בהם במכשירים עם צג retina כדי ליצור ממשק חד ורציף ככל האפשר.

יצירת Spritesheets

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

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

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

אז מה זה אומר?

עכשיו, כשהכול מוכן, אנחנו צריכים רק קטע קוד 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'));

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

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

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

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

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

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

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

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

מדריך: צלילי רוח

אחד מהנושאים שהצגנו בסרטון "Find Your Way to Oz" היה רוח. חוט העלילה בנוי קרשנדו של רוח.

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

לכן היה חשוב ליצור אפקט רוח עשיר.

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

מטלית רכה.

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

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

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

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

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

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

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

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

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

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

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

רעש הפרלין נוצר באופן פרוציונלי באמצעות תוכנת הצללה (shader) בשם NoiseShader. תוכנת ההצללה הזו משתמשת באלגוריתמים של רעש חד-ממדי בתלת-ממד מ: https://github.com/ashima/webgl-noise . גרסת WebGL של זה נלקחה מילה במילה מאחת מדגימות Three.js של MrDoob, בכתובת: 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) }

...

אנחנו עומדים להשתמש בגוון הזה כדי לעבד את המרקם של 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 שעברה עיבוד מחדש, תוך שימוש ב-רעש Map כמרקם:

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 )

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

כדי ליצור את תוכנת ההצללה הזאת, התחלנו מהצללה הרגילה של Three.js MeshPhongMaterial ושינינו אותו. זוהי דרך טובה ומהירה להתחיל לעבוד עם תוכנת הצללה (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;

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

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

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

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

לשם כך, צריך להזיז את המדיח של 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 מיוחד.

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

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 )

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

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

נוסעים בסופה

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

נסיעה בכדור פורח

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

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

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

מדריך: גוון הסער

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

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

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

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

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

בסקירה הכללית מופיעות דוגמה טובה לאלגוריתם: Rendering Worlds WithTwo Triangles - Iñigo Quilez. בנוסף, אנחנו בוחנים את גלריית התוכנות הצללה באתר glsl.heroku.com, ומוצאים דוגמאות רבות לטכניקה הזו שניתן להתנסות בה.

החלק המרכזי של תוכנת ההצללה מתחיל בפונקציה הראשית. המצלמה מבצעת שינוי וכניסה ללולאה שבודקת שוב ושוב את המרחק לפני השטח. הקריאה RaytraceFoggy( directions_ecter, 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) הטורנדו ברזולוציה שונה משאר הסצנה. מקור התוצאה הוא מהקובץ storyTest.coffee (כן, זה היה ניסיון!).

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

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

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

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

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

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

טורנדו

אתר האינטרנט לנייד

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

חשבנו שיהיה מגניב להשתמש ב-Carnival Photo Booth מהמחשב כאפליקציית אינטרנט לנייד שתשתמש במצלמה של המשתמש בנייד. משהו שלא ראינו עד עכשיו.

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

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

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

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

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

הודות לגישה הזו וסיווג המשימה, אנחנו יכולים לדעת בקלות את ההתקדמות הגלובלית (MainPreloadTask), או רק את התקדמות הנכסים (AssetPreloadTask) או את התקדמות הטעינה של תבניות (TemplatePreFetchTask). התקדמות אפילו בקובץ מסוים. כדי לראות איך זה עובד, עיין במחלקה 'משימה' בכתובת /m/javascripts/raw/util/Task.js וביישום המשימות בפועל בכתובת /m/javascripts/preloading/task. כדוגמה, זהו תמצית של האופן שבו אנחנו מגדירים את המחלקה /m/javascripts/preloading/task/MainPreloadTask.js, שהיא ה-wrapper האולטימטיבי לטעינה מראש:

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

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

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

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

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

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

מדריך: תא צילום ב-HTML5 (iOS6/Android)

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

זיכויים

כאן אפשר למצוא את רשימת הזיכויים המלאה

קובצי עזר