מבוא לכדור הארץ התלת-ממדי של World Wonders
אם צפיתם באתר Google World Wonders (פלאי העולם של Google) שהושקה לאחרונה בדפדפן שתומך ב-WebGL, יכול להיות שראיתם כדור ארץ מסתובב מעוצב בתחתית המסך. במאמר הזה נסביר איך פועלת כדור הארץ ואילו כלים השתמשנו בהם כדי ליצור אותו.
סקירה כללית קצרה: כדור הארץ של 'פלאי העולם' הוא גרסה שעבר שינוי משמעותי של כדור הארץ ב-WebGL שנוצר על ידי צוות Google Data Arts. לקחנו את הגלובוס המקורי, הסרנו את החלקים של תרשים העמודות, שינינו את השכבות לשיפור התאורה, הוספנו סמנים מתוחכמים של HTML שניתן ללחוץ עליהם וגיאומטריה של יבשות ב-Natural Earth מהדגמה של GlobeTweeter ב-Mozilla (תודה רבה ל-Cedric Pinson!) כל זה כדי ליצור גלובוס יפהפה וממונע שמתאים לערכת הצבעים של האתר ומוסיף שכבת עיצוב נוספת לאתר.
הבקשה ליצירת הגלובוס הייתה ליצור מפה אנימציה יפה עם סמנים שניתן ללחוץ עליהם, שממוקמים מעל אתרי מורשת עולמית. בהתאם לכך, התחלתי לחפש משהו מתאים. הדבר הראשון שעלה בדעתי היה גלובוס WebGL שנוצר על ידי צוות Google Data Arts. זהו כדור הארץ והוא נראה מגניב. מה עוד דרושה לך?
הגדרת הגלובוס ב-WebGL
השלב הראשון ביצירת הווידג'ט של כדור הארץ היה להוריד את WebGL Globe ולהפעיל אותו. גלובוס WebGL זמין באינטרנט ב-Google Code, וקל להוריד ולהפעיל אותו. מורידים את קובץ ה-zip, מנתקים אותו לתיקייה לא מכווצת, עוברים אליה ומפעילים שרת אינטרנט בסיסי: python -m SimpleHTTPServer
. (שימו לב: האפשרות הזו לא מופעלת כברירת מחדל ב-UTF-8. אפשר להשתמש בה). עכשיו, אם עוברים אל http://localhost:8000/globe/globe.html
, אמור להופיע כדור הארץ של WebGL.
אחרי שהגלובוס ב-WebGL התחיל לפעול, הגיע הזמן לחתוך את כל החלקים הלא נחוצים. ערכתי את ה-HTML כדי לחתוך את החלקים של ממשק המשתמש והסרתי את החלק של הגדרת תרשים העמודות של הגלובוס מפונקציית האתחול של הגלובוס. בסוף התהליך הזה, הופיע במסך גלובוס WebGL בסיסי מאוד. אפשר לסובב אותו והוא נראה מגניב, אבל זה בערך הכול.
כדי לחתוך את הדברים הלא נחוצים, מחקתי את כל רכיבי ממשק המשתמש מ-index.html של הגלובוס וערךתי את סקריפט האינטליגנציה כך שייראה כך:
if(!Detector.webgl){
Detector.addGetWebGLMessage();
} else {
var container = document.getElementById('container');
var globe = new DAT.Globe(container);
globe.animate();
}
הוספת הגיאומטריה של היבשת
רצינו שהמצלמה תהיה קרובה לפני הגלובוס, אבל כשבדקנו את הגלובוס בהגדלה, היה ברור שהרזולוציה של המרקם לא מספיקה. כשמתקרבים, הטקסטורה של גלובוס WebGL נעשית מטושטשת ומרובת ריבועים. יכולנו להשתמש בתמונה גדולה יותר, אבל זה היה מאט את ההורדה וההפעלה של הגלובוס, לכן בחרנו לייצג את היבשות והגבולות באמצעות וקטור.
כדי ליצור את הגיאומטריה של היבשות, השתמשתי בדמו של GlobeTweeter בקוד פתוח והטענתי את המודל התלת-ממדי שלו ל-Three.js. אחרי שהמודל נטען והושלם הרינדור שלו, הגיע הזמן להתחיל לשפר את המראה של כדור הארץ. הבעיה הראשונה הייתה שצורת המודל של היבשות בגלובוס לא הייתה עגולה מספיק כדי להתאים לגלובוס WebGL, ולכן בסופו של דבר כתבתי אלגוריתם מהיר לפיצול רשתות (mesh) שגרם למודל היבשות להיות עגול יותר.
באמצעות מודל גלובוס מוצק, הצלחתי למקם אותו מעט מחוץ לפני השטח של הגלובוס, וכך יצרתי יבשות צפות עם קו שחור של 2 פיקסלים מתחתיהן, כדי ליצור מעין צל. בנוסף, ניסותי להשתמש בקווי מתאר בצבע ניאון כדי ליצור מראה שדומה ל-Tron.
אחרי שהשלמתי את היצירה של כדור הארץ והיבשות, התחלתי להתנסות באפשרויות שונות לעיצוב של כדור הארץ. רצינו לבחור במראה מונוכרומטי צנוע, ולכן התמקדתי בגלובוס וביבשות בגווני אפור. בנוסף לקווי המתאר של הניאון שציינתי קודם, ניסיתי גלובוס כהה עם יבשות כהות על רקע בהיר, שנראה ממש מגניב. אבל הוא היה עם ניגודיות נמוכה מדי ולא ניתן לקריאה בקלות, והוא לא התאים לאווירה של הפרויקט, אז ביטלתי אותו.
רעיון נוסף שחשבתי עליו לגבי המראה של כדור הארץ היה להפוך אותו למראה של פורצלן מזוגג. לא הצלחתי לנסות את זה כי לא הצלחתי לכתוב שידר (shader) שיצור את המראה של החרסינה (עורך חומרים חזותיים יהיה נחמד). הדבר הקרוב ביותר שניסיתי היה גלובוס לבן זוהר עם יבשות שחורות. הוא קצת מסודר אבל הניגודיות גבוהה מדי. והוא לא נראה כל כך טוב. עוד אחת לאשפה.
בשימוש בשיידרים בגלובוס השחור והלבן יש סוג של תאורה אחורית מפוזרת מזויפת. הבהירות של הגלובוס תלויה במרחק של ניצב המשטח למישור המסך. לכן, פיקסלים במרכז הגלובוס שמכוונים למסך כהים, ופיקסלים בקצוות הגלובוס בהירים. בשילוב עם רקע בהיר, כדור הארץ משקף את הרקע הבהיר והמפוזר, ויוצר מראה יוקרתי של מרכז תצוגה. בנוסף, בגלובוס השחור נעשה שימוש במרקם של גלובוס WebGL כמפת ברק, כך שהמדפים היבשתיים (אזורים של מים רדודים) נראים מבריקים בהשוואה לחלקים האחרים של הגלובוס.
כך נראה שידור האוקיינוס בגלובוס השחור. שדה פונקציות (shader) בסיסי מאוד של קודקודים ושדה פונקציות (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 מורכבים מכמה divs שממוקמים באופן מוחלט באמצעות מאפיין הטרנספורמציה של CSS. הרקע של הסמנים הוא שיפוע CSS, והחלק המשולש של הסמן הוא div מסובב. לסמנים יש צללית קטנה כדי להבליט אותם מהרקע. הבעיה הגדולה ביותר במציינים הייתה לגרום להם לפעול בצורה טובה מספיק. עצוב לשמוע, אבל ציור של כמה עשרות divs שזזים ומחליפים את הערך של z-index בכל פריים הוא דרך טובה לגרום לכל מיני בעיות ברינדור בדפדפן.
האופן שבו הסמנים מסונכרנים עם הסצנה התלת-ממדית לא מורכב מדי. לכל סמן יש אובייקט 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, אבל מסיבה כלשהי זה לא עבד כמו שצריך באפליקציה (אבל זה עבד בתרחיש בדיקה מופחת, לא יודעים למה). באותו שלב היו רק כמה ימים לפני ההשקה, ולכן נאלצנו להשאיר את החלק הזה לתחזוקה לאחר ההשקה.
כשלוחצים על סמנים, הם מתרחבים לרשימת שמות מקומות שאפשר ללחוץ עליהם. כל זה הוא קוד HTML DOM רגיל, כך שקל מאוד לכתוב אותו. כל הקישורים והטקסטים מוצגים בצורה תקינה ללא צורך בעבודה נוספת מצידנו.
צמצום גודל הקובץ
הדמו עבד והיה מחובר לשאר האתר של 'פלאי העולם', אבל עדיין הייתה בעיה גדולה אחת שצריך לפתור. הרשת בפורמט JSON של היבשות בעולם הייתה בגודל של כ-3 מגה-בייט. לא מתאים לדף הבית של אתר תצוגה. הצד החיובי הוא שדחיסת הרשת באמצעות gzip צמצמה את הגודל שלה ל-350KB. אבל 350KB עדיין גדול מדי. אחרי כמה אימיילים, הצלחנו לצרף את Won Chun – שעבד על דחיסת המרקמים הענקיים של Google Body – כדי לעזור לנו לדחוס את המרקם. הוא צמצם את הרשת מרשימת משושים גדולה ושטוחית שניתנת כקואורדינטות JSON לקווי קואורדינטה דחוסים של 11 ביט עם משושים שממוינים, והקטין את גודל הקובץ ל-95KB בפורמט GZIP.
שימוש במערכי רשתות דחוסים חוסך לא רק ברוחב פס, אלא גם מאפשר לנתח את המערכי רשתות מהר יותר. המרת 3 מגה-בייט של מספרים שהועברו למחרוזת למספרים מקומיים דורשת הרבה יותר עבודה מאשר ניתוח של מאה קילובייט של נתונים בינאריים. בנוסף, הפחתת הגודל של הדף ב-250KB היא שימושית מאוד, וזמן הטעינה הראשוני יורד מתחת לשנייה בחיבור של 2Mbps. מהיר יותר וקטן יותר, וואו!
במקביל, התעסקתי קצת בחיבור של קובצי ה-Shapefile המקוריים של Natural Earth, שמהם נגזרת רשת GlobeTweeter. הצלחתי לטעון את קובצי ה-Shapefile, אבל כדי להציג אותם כיבשות שטוחות צריך לבצע טריאנגולציה שלהם (עם חורים לאגמים, כמובן). הצלחתי ליצור משולש של הצורות באמצעות utils של THREE.js, אבל לא את החורים. בנוסף, למשטחים שהתקבלו היו קצוות ארוכים מאוד, ולכן נאלצנו לפצל את המשטח לטריאנים קטנים יותר. בקיצור, לא הצלחתי להפעיל את זה בזמן, אבל הדבר הנחמד הוא שפורמט Shapefile עם דחיסת נתונים נוספת היה מניב מודל של יבשת בגודל 8KB. אוי, אולי בפעם הבאה.
עבודות עתידיות
דבר אחד שאפשר לשפר הוא אנימציות הסימון. עכשיו, כשהם עוברים מעבר לאופק, האפקט נראה קצת מטופש. בנוסף, יהיה נחמד אם תהיה אנימציה מגניבת לפתיחת הסמן.
מבחינת הביצועים, שני הדברים שחסרים הם אופטימיזציה של האלגוריתם לפיצול הרשת והאצת הסמנים. חוץ מזה, הכל בסדר. נהדר!
סיכום
במאמר הזה תיארתי איך יצרנו את הגלובוס התלת-ממדי לפרויקט 'פלאי העולם' של Google. אני מקווה שהדוגמאות עניינו אותך, ואשמח לשמוע אם ניסית ליצור ווידג'ט מותאם אישית של גלובוס.