יצירת פלאי עולם בתלת-ממד

Ilmari Heikkinen

מבוא לכדור הארץ פלאי עולם בתלת-ממד

אם עיינתם באתר Google World Wonders שהושק לאחרונה בדפדפן שתומך ב-WebGL, ייתכן שנתקלת בגלובוס מסתובב מפואר בתחתית המסך. במאמר הזה נסביר איך פועל כדור הארץ ומה השתמשנו בו כדי לבנות אותו.

כדי לתת לכם סקירה כללית מהירה, World Wonders Globe הוא גרסה משופרת של WebGL Globe מאת צוות Google Data Arts. לקחנו את כדור הארץ המקורי, הסרנו את הסיבים בגרף העמודות, שינינו את תוכנת ההצללה (shader), הוספנו סמני HTML מהודרים הניתנים ללחיצה וגאומטריה של יבשת Earth Natural מההדגמה של GlobeTweeter של Mozilla (תודה גדולה לסדריק פינסון!) כל זה כדי ליצור גלובוס מונפש ויפה שתואם לערכת הצבעים של האתר ומוסיף שכבה נוספת של תחכום לאתר.

תקציר העיצוב של העולם היה מפה מונפשת בעיצוב יפהפה עם סמנים שניתן ללחוץ עליהם, והוצבו על גבי אתרי מורשת עולמית. מתוך המחשבה הזו, התחלתי לחפש משהו מתאים. הדבר הראשון שחשבתי עליו היה WebGL Globe שפותח על ידי צוות Google Data Arts. זה גלובוס וזה נראה מגניב. מה עוד אתה צריך, אה?

הגדרת כדור הארץ של WebGL

השלב הראשון ביצירת הווידג'ט של הגלובוס היה להוריד את WebGL Globe ולהפעיל אותו. WebGL Globe זמין באינטרנט בכתובת Google Code, וקל להוריד ולהפעיל אותו. הורד את ה-ZIP וחלץ אליו את ה-cd ומפעילים שרת אינטרנט בסיסי: python -m SimpleHTTPServer. (שים לב: UTF-8 לא מופעל כברירת מחדל; אפשר להשתמש בזה.) עכשיו, אם תעברו אל http://localhost:8000/globe/globe.html, תראו את WebGL Globe.

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

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

if(!Detector.webgl){
  Detector.addGetWebGLMessage();
} else {
  var container = document.getElementById('container');
  var globe = new DAT.Globe(container);
  globe.animate();
}

הוספת הגיאומטריה של היבשת

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

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

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

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

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

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

כך נראית ההצללה באוקיינוס של כדור הארץ השחור. תוכנת הצללה (shader) בסיסית מאוד ותוכנת הצללה לא פשוטה מסוג "אוי, נראה די פשוט צחוק ותיקון".

    'ocean' : {
      uniforms: {
        'texture': { type: 't', value: 0, texture: null }
      },
      vertexShader: [
        'varying vec3 vNormal;',
        'varying vec2 vUv;',
        'void main() {',
          'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
          'vNormal = normalize( normalMatrix * normal );',
          'vUv = uv;',
        '}'
      ].join('\n'),
      fragmentShader: [
        'uniform sampler2D texture;',
        'varying vec3 vNormal;',
        'varying vec2 vUv;',
        'void main() {',
          'vec3 diffuse = texture2D( texture, vUv ).xyz;',
          'float intensity = pow(1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) ), 4.0);',
          'float i = 0.8-pow(clamp(dot( vNormal, vec3( 0, 0, 1.0 )), 0.0, 1.0), 1.5);',
          'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * intensity;',
          'float d = clamp(pow(max(0.0,(diffuse.r-0.062)*10.0), 2.0)*5.0, 0.0, 1.0);',
          'gl_FragColor = vec4( (d*vec3(i)) + ((1.0-d)*diffuse) + atmosphere, 1.0 );',
        '}'
      ].join('\n')
    }

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

יצירת הסמנים באמצעות CSS

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

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

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

var mat = new THREE.Matrix4();
var v = new THREE.Vector3();

for (var i=0; i<locations.length; i++) {
  mat.copy(scene.matrix);
  mat.multiplySelf(locations[i].point.matrix);
  v.set(0,0,0);
  mat.multiplyVector3(v);
  projector.projectVector(v, camera);
  var x = w * (v.x + 1) / 2; // Screen coords are between -1 .. 1, so we transform them to pixels.
  var y = h - h * (v.y + 1) / 2; // The y coordinate is flipped in WebGL.
  var z = v.z;
}

בסופו של דבר, הגישה המהירה ביותר הייתה להשתמש בטרנספורמציות CSS כדי להזיז את הסמנים, ולא להשתמש בשקיפות אטימוּת, מאחר שהדבר גורם לנתיב איטי ב-Firefox ושמירת כל הסמנים ב-DOM, ולא להסיר אותם כשהם מופיעים מאחורי כדור הארץ. ניסינו גם להשתמש בהמרות תלת-ממדיות במקום ב-z-index, אבל מסיבה כלשהי זה לא עבד ישירות באפליקציה (אבל זה עבד בצורה מצומצמת במקרה של בדיקה מצומצמת), ואנחנו נמצאים כמה ימים ממועד ההשקה בשלב הזה, אז נאלצנו להשאיר את החלק הזה בתחזוקה שלאחר ההשקה.

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

לחיצה משני צדי הקובץ

לאחר שההדגמה פעלה והתחברה לשאר האתר World Wonders, עדיין נותרה בעיה גדולה אחת שצריך לפתור. הרשת בפורמט JSON עבור אזורים בעולם הייתה בגודל של כ-3 מגה. לא מתאים לדף הראשון של אתר תצוגה. היתרון הגדול היה שדחיסת הרשת עם gzip הורידה אותה ל-350KB. אבל היי, 350kB זה עדיין קצת גדול. לאחר כמה הודעות אימייל הצלחנו לגייס את וון צ'ון (Won Chin) שעבד על דחיסת רשתות העצומות של Google Body – כדי לעזור לנו לדחוס את הרשת. הוא דוחף את הרשת מרשימה שטוחה גדולה של משולשים, שניתנו כקואורדינטות של JSON, לקואורדינטות של 11 ביט עם משולשים שנוספו לאינדקס, והוריד את גודל הקובץ ל- 95 kB gzip.

השימוש ברשתות דחוסות לא רק חוסך רוחב פס, אלא גם ניתוח הרשתות מהר יותר. המרה של 3 מגה-הרץ של מספרים משורשרים למספרים מקוריים דורשת עבודה טובה יותר מאשר ניתוח של 100 קילו-בייט של נתונים בינאריים. וכתוצאה מכך הפחתה של 250kB בגודל הדף היא שימושית מאוד, וגם זמן הטעינה הראשונית קטן משנייה בחיבור של 2Mbps. מהיר וקטן יותר, נהדר!

באותו זמן, שרבטתי וטענתי את קובצי הצורות המקוריים של כדור הארץ, שמהם נגזר הרשת של GlobeTweeter. הצלחתי לטעון את קובצי ה- shapefile, אבל כדי לעבד אותם כקרקע שטוחה, צריך לעבד אותם בטריאנגולציה (עם חורים לאגמים, נאצ). קיבלתי את הצורות במשולש באמצעות THREE.js utils, אבל לא עם החורים. לרשתות שנוצרו היו קצוות ארוכים מאוד, מה שחייב לפצל את הרשת כלפי מטה לקווים קטנים יותר. בקיצור, לא הצלחתי לגרום לה לעבוד טוב, אולי בפעם הבאה.

עבודה עתידית

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

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

סיכום

במאמר הזה תיארתי איך בנינו את הגלובוס בתלת-ממד לפרויקט Google World Wonders. אני מקווה שנהנית מהדוגמאות ותנסה ליצור ווידג'ט גלובוס בהתאמה אישית.

קובצי עזר