התרגשתי כשצוות Google Data Arts פונה ל-Moniker ולעצמי, בנושא עבודה משותפת כדי לבדוק את האפשרויות שעומדות לרשותי ב-WebVR. צפיתי בעבודה של הצוות שלהם לאורך השנים, והפרויקטים שלהם תמיד הצליחו לפגוע בי. שיתוף הפעולה יצר את Dance Tonite, חוויית ריקוד ב-VR שמשתנה כל הזמן בעזרת LCD Soundsystem והמעריצים שלה. זה מה שעשינו.
התפיסה
התחלנו לפתח סדרה של אבות טיפוס באמצעות WebVR, תקן פתוח שמאפשר להיכנס ל-VR על ידי ביקור באתר דרך הדפדפן. המטרה היא להקל על כולם ליהנות מחוויית VR, לא משנה איזה מכשיר יש לכם.
לקחנו את העניין הזה לתשומת ליבנו. מה שנמצא נועד לפעול בכל סוגי ה-VR, החל ממשקפי VR שפועלים עם טלפונים ניידים כמו Daydream View, Cardboard ו-Gear VR של Samsung, ועד למערכות בגודל חדר כמו HTC VIVE ו-Oculus Rift, שמשקפות את התנועות הפיזיות שלכם בסביבה הווירטואלית. והכי חשוב, חשנו שזה יהיה ברוח האינטרנט ליצור משהו שמתאים גם לכל מי שאין לו מכשיר VR.
1. לכידת תנועה בעצמכם
רצינו לעודד את המשתמשים להיות יצירתיים, ולכן התחלנו לבדוק את האפשרויות ליצירת שיתוף פעולה וביטוי עצמי באמצעות VR. התרשמנו מהיכולת לזוז ולהסתכל מסביב ב-VR, ומהרזולוציה הגבוהה. זה נתן לנו רעיון. במקום לבקש מהמשתמשים להסתכל על משהו או ליצור משהו, למה לא לתעד את התנועות שלהם?
יצרנו אב-טיפוס שבו תיעדנו את המיקום של משקפי ה-VR ושל הבקרים שלנו בזמן ריקוד. החלפנו את המיקומים שנרשמו בצורות מופשטות והתרגשנו מהתוצאות. התוצאות היו אנושיות מאוד וכללו הרבה אישיות! מהר מאוד הבנו שאפשר להשתמש ב-WebVR כדי לצלם תנועה בזול בבית.
ב-WebVR, למפתח יש גישה למיקום ולכיוון של הראש של המשתמש באמצעות האובייקט VRPose. הערך הזה מתעדכן בכל פריים על ידי חומרת ה-VR, כדי שהקוד יוכל ליצור פריימים חדשים מנקודת המבט הנכונה. באמצעות GamePad API עם WebVR, אנחנו יכולים לגשת גם למיקום או לכיוון של בקרי המשתמשים באמצעות האובייקט GamepadPose. אנחנו פשוט שומרים את כל ערכי המיקום והכיוון האלה בכל פריים, וכך יוצרים 'הקלטה' של תנועות המשתמש.
2. מינימליזם ותחפושות
בעזרת ציוד ה-VR הנוכחי ברמת החדר, אנחנו יכולים לעקוב אחרי שלושה נקודות בגוף המשתמש: הראש ושתי הידיים. בסרטון Dance Tonite רצינו להתמקד באנושיות של התנועה של 3 הנקודות האלה במרחב. כדי לעשות זאת, ניסינו ליצור עיצוב מינימליסטי ככל האפשר כדי להתמקד בתנועה. אהבנו את הרעיון של ניצול המוח של אנשים.
הסרטון הזה מדגים את העבודה של הפסיכולוג השוודי גונאר ג'והנסון (Johansson) הוא אחד מהדוגמאות שהתייחסנו אליהן כששקלנו להסיר חומרים ככל האפשר. אפשר לראות איך נקודות לבנות צפות מזוהות באופן מיידי כגופים כשהן נעות.
מבחינה חזותית, קיבלנו השראה מהחדרים הצבעוניים והתלבושות הגיאומטריות בסרטון הזה, שבו מרג'ריט הסטינגס (Margarete Hastings) מביאה מחדש בשנת 1970 את ה-Triadic Ballet של אוסקר שלמר (Oskar Schlemmer).
אבל הסיבה ששלמר בחר בתחפושות גיאומטריות מופשטות הייתה להגביל את התנועה של הרקדניות שלו לתנועות של בובות ומריונטות, אבל המטרה ההפוך הייתה לרקוד טוייט.
בסופו של דבר בחרנו את הצורות על סמך כמות המידע שהן העבירו באמצעות סיבוב. כדור נראה אותו דבר בכל כיוון שבו מסובבים אותו, אבל חרוט מצביע בכיוון שאליו הוא מכוון ונראה שונה מלפנים ומאחור.
3. לחצן לולאה לתנועה
רצינו להציג קבוצות גדולות של אנשים שרוקדים ומשתתפים בתנועה יחד. לא ניתן לעשות זאת בשידור חי, מכיוון שמכשירי VR אינם מספיקים. עם זאת, עדיין רצינו ליצור קבוצות של אנשים שיגיבו זו לזו באמצעות תנועה. חשבנו על המופע הרקורסיבי של נורמן מקלארן בסרטון שלו מ-1964, 'קאנון'.
ההופעה של McClaren כוללת סדרה של פרקים עם כוריאוגרפיה גבוהה, שמתחילה באינטראקציה אחד עם השני אחרי כל לולאה. בדומה למעגל לופ במוזיקה, שבו מוזיקאים מנגנים ג'אם עם עצמם על ידי שכבות של קטעי מוזיקה חיים שונים, רצינו לבדוק אם נוכל ליצור סביבה שבה משתמשים יוכלו לאלתר גרסאות פחות מוקפדות של הופעות באופן חופשי.
4. חדרים מחוברים
כמו הרבה מוזיקה, הטראקים של LCD Soundsystem נוצרים באמצעות קצבים מתוזמנים במדויק. בטראק שלהם, Tonite, שמוצג בפרויקט שלנו, יש מדדים באורך של 8 שניות בדיוק. רצינו שהמשתמשים ייצרו ביצוע לכל לולאה של 8 שניות בטראק. למרות שהקצב של מדדים אלו לא משתנה, התוכן המוזיקלי שלהם כן משתנה. במהלך השיר יש רגעים עם כלים וזמרים שונים, והמבצעים יכולים להגיב אליהם בדרכים שונות. כל אחד מהמדדים האלה מתבטא כחדר, שבו אנשים יכולים ליצור הופעה שמתאימה לה.
אופטימיזציות לביצועים: לא להוריד פריימים
לא פשוט ליצור חוויית VR בפלטפורמות שונות, שפועלת על בסיס קוד יחיד ומשיגים ביצועים אופטימליים בכל מכשיר או פלטפורמה.
כשמשתמשים ב-VR, אחד הדברים הכי מעוררי בחילה הוא כשקצב הפריימים לא עומד בקצב התנועה שלכם. אם תסובבו את הראש אבל התמונות שרואים בעיניים לא תואמות לתנועה שהאוזן הפנימית מרגישה, תרגישו מיד בחילה. לכן, היינו צריכים להימנע מעיכוב גדול בשיעור הפריימים. הנה כמה מהאופטימיזציות שביצענו.
1. גיאומטריה של מאגרים של מופעים
מכיוון שכל הפרויקט שלנו כולל רק כמה אובייקטים תלת-ממדיים, הצלחנו לשפר את הביצועים בצורה משמעותית באמצעות שימוש בגיאומטריה של Instanced Buffer Geometry. בעיקרון, היא מאפשרת להעלות את האובייקט ל-GPU פעם אחת ולצייר כמה 'מכונות' של האובייקט הזה שרוצים בקריאה אחת ל-draw. בסרטון Dance Tonite יש רק 3 אובייקטים שונים (חרוט, גליל וחדר עם חור), אבל יכול להיות שיש מאות עותקים שלהם. גיאומטריית המכונות של מאגר הנתונים היא חלק מ-ThreeJS, אבל השתמשנו במזלג הניסיוני והפעיל של Dusan Bosnjak שמטמיע את THREE.InstanceMesh
, ולכן הרבה יותר קל לעבוד עם Instanced Buffer Geometry.
2. נמנעים מקטע האשפה
בדומה להרבה שפות סקריפטים אחרות, JavaScript משחרר את הזיכרון באופן אוטומטי על ידי לגלות האובייקטים שהוקצו לא נמצאים יותר בשימוש. התהליך הזה נקרא איסוף אשפה.
למפתחים אין שליטה על המועד שבו זה יקרה. האיסוף יכול להופיע בכל רגע ולהתחיל לרוקן את האשפה, וכתוצאה מכך יהיו פריימים חסרים כי הם ייקחו זמן רב.
הפתרון לכך הוא לייצר כמה שפחות אשפה על ידי מיחזור שלנו חפצים. במקום ליצור אובייקט וקטור חדש לכל חישוב, מסמנים אובייקטי גירוד לשימוש חוזר. אנחנו שומרים עליהם על ידי העברת ההפניה אליהם מחוץ להיקף שלנו, ולכן הם לא סומנו להסרה.
לדוגמה, זה הקוד שלנו להמרת מטריצת המיקום של הראש והידיים של המשתמש למערך של ערכי המיקום/הסיבוב שאנחנו שומרים בכל פריים. על ידי שימוש חוזר ב-SERIALIZE_POSITION
, ב-SERIALIZE_ROTATION
וב-SERIALIZE_SCALE
, אנחנו נמנעים מהקצאת הזיכרון ואיסוף האשפה שיתרחשו אם ניצור אובייקטים חדשים בכל פעם שתתבצע קריאה לפונקציה.
const SERIALIZE_POSITION = new THREE.Vector3();
const SERIALIZE_ROTATION = new THREE.Quaternion();
const SERIALIZE_SCALE = new THREE.Vector3();
export const serializeMatrix = (matrix) => {
matrix.decompose(SERIALIZE_POSITION, SERIALIZE_ROTATION, SERIALIZE_SCALE);
return SERIALIZE_POSITION.toArray()
.concat(SERIALIZE_ROTATION.toArray())
.map(compressNumber);
};
3. סריאליזציה של תנועה והפעלה פרוגרסיבית
כדי לתעד את תנועות המשתמשים ב-VR, היה עלינו לעשות סריאליזציה למיקום ולסיבוב של האוזניות ושל השלט רחוק שלהם ולהעלות את הנתונים האלה לשרתים שלנו. התחלנו לתעד את מטריצות הטרנספורמציה המלאות בכל פריים. הביצועים היו טובים, אבל עם 16 מספרים כפול 3 מיקומים לכל אחד, במהירות של 90 פריימים לשנייה, נוצרו קבצים גדולים מאוד, ולכן זמן ההמתנה היה ארוך בזמן ההעלאה וההורדה של הנתונים. על ידי חילוץ רק את נתוני המיקום והרוטציה ממטריצות הטרנספורמציה, הצלחנו להפחית את הערכים האלה מ-16 ל-7.
מאחר שבדרך כלל מבקרים באינטרנט לוחצים על קישור בלי לדעת בדיוק למה לצפות, אנחנו צריכים להציג תוכן חזותי במהירות, אחרת הם יעזבו תוך שניות.
לכן, רצינו לוודא שהפרויקט שלנו יוכל להתחיל לפעול בהקדם האפשרי. בהתחלה השתמשנו בפורמט JSON כדי לטעון את נתוני התנועה שלנו. הבעיה היא שאנחנו צריכים לטעון את קובץ ה-JSON המלא כדי שנוכל לנתח אותו. לא מאוד מתקדם.
כדי להציג פרויקט כמו Dance Tonite בשיעור הפריימים הגבוה ביותר האפשרי, לדפדפן יש רק זמן קצר בכל פריים לחישוב ב-JavaScript. אם תמשיכו יותר מדי זמן, האנימציות יתחילו להקפד. בהתחלה חווינו גמומים כי הדפדפן פיענח את קובצי ה-JSON הענקיים האלה.
נתקלנו בפורמט נוח של נתוני סטרימינג שנקרא NDJSON או JSON שמופרד בתו שורה חדשה. הטריק הוא ליצור קובץ עם סדרה של מחרוזות JSON תקינות, כל אחת בשורה משלה. כך אפשר לנתח את הקובץ בזמן שהוא נטען, וכך אנחנו יכולים להציג את הביצועים לפני שהם נטענים במלואם.
כך נראה קטע מאחת מההקלטות שלנו:
{"fps":15,"count":1,"loopIndex":"1","hideHead":false}
[-464,17111,-6568,-235,-315,-44,9992,-3509,7823,-7074, ... ]
[-583,17146,-6574,-215,-361,-38,9991,-3743,7821,-7092, ... ]
[-693,17158,-6580,-117,-341,64,9993,-3977,7874,-7171, ... ]
[-772,17134,-6591,-93,-273,205,9994,-4125,7889,-7319, ... ]
[-814,17135,-6620,-123,-248,408,9988,-4196,7882,-7376, ... ]
[-840,17125,-6644,-173,-227,530,9982,-4174,7815,-7356, ... ]
[-868,17120,-6670,-148,-183,564,9981,-4069,7732,-7366, ... ]
...
השימוש ב-NDJSON מאפשר לנו לשמור את ייצוג הנתונים של הפריימים הנפרדים של הביצועים כמחרוזות. יכולנו להמתין עד שנגיע לזמן הנדרש לפני שמפענחים אותם לנתוני מיקום, וכך לפזר את העיבוד הדרוש לאורך זמן.
4. תנועה באמצעות אינטרפולציה
ניסינו להציג בו-זמנית בין 30 ל-60 הופעות, ולכן נאלצנו להוריד את קצב הנתונים אפילו יותר מבעבר. חברי צוות Data Arts התמודדו עם אותה בעיה בפרויקט Virtual Art Sessions (סשנים וירטואליים של אומנות) שבו הם משמיעים הקלטות של אומנים שציירו ב-VR באמצעות lift Brush. כדי לפתור את הבעיה, הם יצרו גרסאות ביניים של נתוני המשתמשים עם קצב פריימים נמוך יותר ואינטרנציונליות בין הפריימים השונים תוך כדי השמעת גרסאות אחרות. להפתעתנו, בקושי הצלחנו לזהות את ההבדל בין הקלטה עם אינטרפולציה שפועלת ב-15FPS לבין ההקלטה המקורית שפועלת ב-90FPS.
כדי לראות זאת בעצמכם, תוכלו לאלץ את Dance Tonite להפעיל את הנתונים במהירויות שונות באמצעות מחרוזת השאילתה ?dataRate=
. אפשר להשתמש באפשרות הזו כדי להשוות בין התנועה שצומצמה ל90 פריימים לשנייה, ל45 פריימים לשנייה או ל15 פריימים לשנייה.
לגבי המיקום, אנחנו מבצעים אינטרפולציה לינארית בין פריים המפתח הקודם לבין הפריים הבא, על סמך מידת הקרבה בזמן בין פריטי המפתח (יחס):
const { x: x1, y: y1, z: z1 } = getPosition(previous, performanceIndex, limbIndex);
const { x: x2, y: y2, z: z2 } = getPosition(next, performanceIndex, limbIndex);
interpolatedPosition = new THREE.Vector3();
interpolatedPosition.set(
x1 + (x2 - x1) * ratio,
y1 + (y2 - y1) * ratio,
z1 + (z2 - z1) * ratio
);
כדי לקבוע את הכיוון, אנחנו מבצעים אינטרפולציה לינארית ספרית (slerp) בין נקודות מפתח. הכיוון נשמר כקוואטרניונים.
const quaternion = getQuaternion(previous, performanceIndex, limbIndex);
quaternion.slerp(
getQuaternion(next, performanceIndex, limbIndex),
ratio
);
5. סנכרון התנועות למוזיקה
כדי לדעת איזה פריים של האנימציות שתועדו להפעיל, אנחנו צריכים לדעת מה השעה הנוכחית של המוזיקה, עד אלפית השנייה. מסתבר שרכיב האודיו ב-HTML מושלם לטעינה פרוגרסיבית של אודיו ולהפעלה שלו, אבל מאפיין הזמן שהוא מספק לא משתנה בסנכרון עם לולאת המסגרות של הדפדפן. תמיד יש סטייה קלה. לפעמים היא מקדימה בחלקיק של אלפית שנייה, ולפעמים היא מאוחרת בחלקיק של אלפית שנייה.
כתוצאה מכך, יש קטעים של גמגום בסרטוני הריקוד היפים שלנו, ואנחנו רוצים להימנע מכך בכל מחיר. כדי לפתור את הבעיה, הטמענו טיימר משלה ב-JavaScript. כך אנחנו יכולים להיות בטוחים שמשך הזמן שחולף בין הפריימים הוא בדיוק משך הזמן שחלף מאז הפריים האחרון. בכל פעם שהטיימר שלנו יוצא מסנכרון עם המוזיקה ביותר מ-10 אלפיות השנייה, אנחנו מסנכרנים אותו שוב.
6. סינון וערפל
לכל סיפור צריך סוף טוב, ורצינו לעשות משהו מפתיע למשתמש שהגיע לסוף החוויה שלנו. כשמסיימים את החדר האחרון, נכנסים למרחב שמרגיש כמו נוף שקט של קונוסים וצילינדרים. "זה הסוף?", אתם תוהים. ככל שמתקדמים בשדה, לפתע הטונים של המוזיקה גורמים לקבוצות שונות של חרוטות וצילינדרים להפוך לדמויות של רקדנים. אתם מוצאים את עצמכם באמצע מסיבה ענקית! לאחר מכן, כשהמוזיקה נפסקת בבת אחת, הכל נופל ארצה.
הצופים נהנו מהתכונה הזו, אבל היא יצרה כמה בעיות בביצועים שצריך לפתור. מכשירי VR ברמת חדר והציוד המתקדם שלהם למשחקים עמדו בצורה מושלמת ב-40 ההופעות הנוספות שנדרשו לסיום החדש. עם זאת, שיעורי הפריימים במכשירים ניידים מסוימים חולקו לשניים.
כדי למנוע זאת, הוספנו ערפל. אחרי מרחק מסוים, הכל הופך לאט לאט לשחור. מכיוון שאנחנו לא צריכים לחשב או לצייר דברים שאינם גלויים, אנחנו אוספים ביצועים בחדרים שאינם גלויים, וכך אנחנו חוסכים עבודה גם למעבד (CPU) וגם ל-GPU. אבל איך מחליטים מה המרחק המתאים?
חלק מהמכשירים יכולים להתמודד עם כל דבר שמטילים אליהם, ואחרים יכולים להיות ממוקדים יותר. בחרנו להטמיע סולם מתחלק. באמצעות מדידה מתמשכת של כמות הפריימים לשנייה, אנחנו יכולים לשנות את מרחק הערפל בהתאם. כל עוד קצב הפריימים פועל בצורה חלקה, אנחנו מנסים להגדיל את עומס העבודה של העיבוד על ידי הסרת הערפל. אם קצב הפריימים לא פועל בצורה חלקה מספיק, אנחנו מקררים את הערפל כדי שנוכל לדלג על רינדור של ביצועים בחושך.
// this is called every frame
// the FPS calculation is based on stats.js by @mrdoob
tick: (interval = 3000) => {
frames++;
const time = (performance || Date).now();
if (prevTime == null) prevTime = time;
if (time > prevTime + interval) {
fps = Math.round((frames * 1000) / (time - prevTime));
frames = 0;
prevTime = time;
const lastCullDistance = settings.cullDistance;
// if the fps is lower than 52 reduce the cull distance
if (fps <= 52) {
settings.cullDistance = Math.max(
settings.minCullDistance,
settings.cullDistance - settings.roomDepth
);
}
// if the FPS is higher than 56, increase the cull distance
else if (fps > 56) {
settings.cullDistance = Math.min(
settings.maxCullDistance,
settings.cullDistance + settings.roomDepth
);
}
}
// gradually increase the cull distance to the new setting
cullDistance = cullDistance * 0.95 + settings.cullDistance * 0.05;
// mask the edge of the cull distance with fog
viewer.fog.near = cullDistance - settings.roomDepth;
viewer.fog.far = cullDistance;
}
משהו לכולם: יצירת VR לאינטרנט
כדי לעצב ולפתח חוויות אסימטריות בפלטפורמות מרובות, צריך להביא בחשבון את הצרכים של כל משתמש בהתאם למכשיר שלו. בכל החלטה עיצובית, היינו צריכים לבדוק איך היא עשויה להשפיע על משתמשים אחרים. איך מוודאים שהחוויה של הצפייה ב-VR מרגשת באותה מידה כמו הצפייה ללא VR, ולהפך?
1. הכדור הצהוב
משתמשי ה-VR בסביבה שלמה ייצרו את ההופעות, אבל איך משתמשי מכשירי VR לנייד (כמו Cardboard, Daydream View או Samsung Gear) יחוו את הפרויקט? לשם כך, הוספנו לאווירה שלנו אלמנט חדש: כדור צהוב.
כשצופים בפרויקט ב-VR, הצפייה מתבצעת מנקודת המבט של הספרה הצהובה. כשאתם עוברים מחדר לחדר, הרקדנים מגיבים לנוכחות שלכם. הם עושים תנועת צביטה לצליליכם, רוקדים סביבכם, עושים תנועות מצחיקות מאחורי הגב ומתרחקים במהירות מהדרך כדי שלא ייתקלו בכם. כדור האור הצהוב תמיד במרכז תשומת הלב.
הסיבה לכך היא שבזמן הקלטת הופעה, הכדור הצהוב נע במרכז החדר בתיאום עם המוזיקה וחוזר בלופ. המיקום של האורביט מאפשר למבצעים לדעת איפה הם נמצאים בזמן וכמה זמן נותר להם בלולאה. הוא מספק להם מוקד טבעי שבו הם יכולים לבסס את המופע.
2. נקודת מבט נוספת
לא רצינו להשאיר משתמשים בלי VR, במיוחד כי סביר להניח שהם יהיו הקהל הגדול ביותר שלנו. במקום ליצור חוויית VR מזויפת, רצינו לתת למכשירים מבוססי-מסך חוויה משלהם. היה לנו רעיון להציג את הביצועים מלמעלה, מנקודת מבט איזומטרית. זוהי נקודת מבט עם היסטוריה עשירה במשחקי מחשב. המשחק הזה שימש לראשונה ב-Zaxxon, משחק יריות בחלל משנת 1982. למרות שמשתמשי VR תמיד מרוכזים בסרטון, אבל הפרספקטיבה האיזומטרית מספקת נקודת מבט כמו אלוהית על האקשן. בחרנו להרחיב קצת את המודלים כדי לתת נגיעות אסתטיות של בית בובות.
3. צללים: 'התחזות עד שמצליחים'
גילינו שלחלק מהמשתמשים שלנו קשה לראות עומק מנקודת המבט האיזומטרית שלנו. אני די בטוח שזו הסיבה לכך ש-Zaxxon היה גם אחד ממשחקי המחשב הראשונים בהיסטוריה שהקריאו צללית דינמית מתחת לאובייקטים המעופפים שלו.
מסתבר שיצירת צללים בתלת-ממד היא משימה קשה. במיוחד למכשירים מוגבלים, כמו טלפונים ניידים. בהתחלה נאלצנו לקבל את ההחלטה הקשה להוציא אותם מהמשוואה, אבל אחרי ששאלנו את המחבר של Three.js וההאקר המנוסה של הדגמות Mr doob מה לעשות, הוא הגה את הרעיון החדשני… לזייף אותם.
במקום לחשב איך כל אחד מהאובייקטים הצפים שלנו מסתיר את התאורה שלנו וכתוצאה מכך יוצר צללים בצורות שונות, אנחנו מציירים את אותה תמונה עגולה של מרקם מטושטש מתחת לכל אחד מהם. מכיוון שהגרפיקה שלנו לא מנסה לחקות את המציאות מלכתחילה, גילינו שאפשר לעשות זאת בקלות רבה עם כמה שינויים קטנים. ככל שהאובייקטים מתקרבים לקרקע, הטקסטורות נעשות כהות וקטנות יותר. כשהם עוברים למעלה, אנחנו הופכים את המרקמים לשקופים יותר ולגדולים יותר.
כדי ליצור אותן, השתמשנו בטקסטורה הזו עם הדרגתיות של צבע לבן עד שחור (ללא שקיפות ברמת אלפא). אנחנו מגדירים את החומר כשקוף ומשתמשים בשילוב החסר. כך הם ייראו טוב יותר כשהם חופפים:
function createShadow() {
const texture = new THREE.TextureLoader().load(shadowTextureUrl);
const material = new THREE.MeshLambertMaterial({
map: texture,
transparent: true,
side: THREE.BackSide,
depthWrite: false,
blending: THREE.SubtractiveBlending,
});
const geometry = new THREE.PlaneBufferGeometry(0.5, 0.5, 1, 1);
const plane = new THREE.Mesh(geometry, material);
return plane;
}
4. להיות שם
לחיצה על ראשו של אחד מהמבצעים מאפשרת למבקרים ללא VR לצפות בדברים מנקודת המבט של הרקדן. מזווית כזו, הרבה פרטים קטנים גלויים לעין. כדי לשמור על התיאום בין התנועות, הרקדנים מעיינים זה בזה במהירות. כשהכדור נכנס לחדר, אפשר לראות אותם מביטים בכיוונים מבולבלים. הצופים לא יכולים להשפיע על התנועה הזו, אבל היא כן מעבירה את תחושת ההתמקדות בצורה מפתיעה. שוב, העדפנו לעשות זאת במקום להציג למשתמשים גרסה חסרת עניין של VR מזויף עם שליטה בעכבר.
5. שיתוף הקלטות
אנחנו יודעים כמה גאווה אתם יכולים להרגיש כשאתם מצליחים לצלם 20 שכבות של תנועות ותגובות של שחקנים ורקדנים בתיאום מושלם. ידענו שהמשתמשים שלנו ירצו להראות את התמונות לחברים שלהם. אבל תמונה סטילס של הפעולה הזו לא מספיקה כדי להעביר את המסר. במקום זאת, רצינו לאפשר למשתמשים לשתף סרטון של הביצועים שלהם. למעשה, למה לא קובץ GIF? האנימציות שלנו מוצגות בגוונים שטוחים, והן מושלמות ללוחות הצבעים המוגבלים של הפורמט.
פנינו ל-GIF.js, ספריית JavaScript שמאפשרת לקודד קובצי GIF מונפשים מתוך הדפדפן. הוא מסיר את הקידוד של פריימים לעובדי אינטרנט שיכולים לפעול ברקע כתהליכים נפרדים, וכך לנצל את היתרונות של מספר מעבדים שעובדים זה לצד זה.
לצערנו, עם כמות הפריימים שנדרשה לנו לאנימציות, תהליך הקידוד עדיין היה איטי מדי. קובצי ה-GIF יכולים להיות קטנים כי הם משתמשים בלוח צבעים מוגבל. גילינו שרוב הזמן הלך על חיפוש הצבע הקרוב ביותר לכל פיקסל. הצלחנו לבצע אופטימיזציה של התהליך הזה פי עשרה באמצעות קצת קוד: אם הצבע של הפיקסל זהה לצבע של הפיקסל הקודם, משתמשים באותו צבע מהצבעים הקודמים.
עכשיו השתמשנו בקידודים מהירים, אבל קובצי ה-GIF שהתקבלו היו גדולים מדי. פורמט ה-GIF מאפשר לציין איך כל פריים יוצג מעל הפריים הקודם, על ידי הגדרת שיטת ה-dispose שלו. כדי ליצור קבצים קטנים יותר, במקום לעדכן כל פיקסל בכל פריים, אנחנו מעדכנים רק את הפיקסלים שהשתנו. התהליך הזה אמנם האט שוב את תהליך הקידוד, אבל הוא צמצם משמעותית את גודל הקבצים.
6. יסודות מוצקים: Google Cloud ו-Firebase
הקצה העורפי של אתר מסוג 'תוכן שנוצר על ידי משתמשים' עשוי להיות מורכב ולפעמים הוא שביר, אבל פיתחנו מערכת פשוטה וחזקה הודות ל-Google Cloud ול-Firebase. כשמבצע מעלה ריקוד חדש למערכת, הוא עובר אימות בעילום שם באמצעות אימות ב-Firebase. הם מקבלים הרשאה להעלות את ההקלטה שלהם למרחב זמני באמצעות Cloud Storage for Firebase. כשההעלאה תושלם, מכונת הלקוח תפנה לטריגר HTTP של Cloud Functions for Firebase באמצעות אסימון Firebase שלה. הפעולה הזו מפעילה תהליך בשרת שמאמת את ההגשה, יוצר רשומה במסד נתונים ומעביר את ההקלטה לספרייה ציבורית ב-Google Cloud Storage.
כל התוכן הציבורי שלנו מאוחסן בסדרה של קבצים שטוחיים בקטגוריה של Cloud Storage. כלומר, אפשר לגשת לנתונים שלנו במהירות ברחבי העולם, ואין לנו מה לדאוג לגבי עומסי תנועה גבוהים שמשפיעים על זמינות הנתונים.
השתמשנו במסד נתונים בזמן אמת ב-Firebase ובנקודות הקצה של הפונקציה של Cloud Functions כדי לפתח כלי פשוט לניהול ולאיסוף שמאפשר לנו לצפות בכל תוכן חדש שנשלח ב-VR ולפרסם פלייליסטים חדשים מכל מכשיר.
7. קובצי שירות (service worker)
שירותי עבודה הם חידוש יחסית חדש שעוזר לנהל את האחסון במטמון של נכסי האתר. במקרה שלנו, רכיבי שירות (service worker) טוענים את התוכן שלנו במהירות הבזק למבקרים חוזרים, ואפילו מאפשרים לאתר לעבוד במצב אופליין. אלה תכונות חשובות, כי רבים מהמבקרים שלנו יהיו מחוברים בניידים באיכות משתנה.
הוספת שירותי עבודה לפרויקט הייתה קלה בזכות פלאגין שימושי של webpack שמטפל ברוב העבודה הקשה. בתצורה שבהמשך, אנחנו יוצרים שירות עובד (service worker) שיאחסן אוטומטית את כל הקבצים הסטטיים שלנו. המערכת תשלוף מהערוץ את קובץ הפלייליסט העדכני ביותר, אם הוא זמין, כי הפלייליסט יתעדכן כל הזמן. כל קובצי ה-json של ההקלטה צריכים לשלוף מהמטמון אם הם זמינים כי הם אף פעם לא ישתנו.
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
config.plugins.push(
new SWPrecacheWebpackPlugin({
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'service-worker.js',
minify: true,
navigateFallback: 'index.html',
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
runtimeCaching: [{
urlPattern: /playlist\.json$/,
handler: 'networkFirst',
}, {
urlPattern: /\/recordings\//,
handler: 'cacheFirst',
options: {
cache: {
maxEntries: 120,
name: 'recordings',
},
},
}],
})
);
נכון לעכשיו, הפלאגין לא מטפל בנכסי מדיה שנטענים בהדרגה, כמו קובצי המוזיקה שלנו, אז טיפלנו בזה על ידי הגדרת הכותרת Cache-Control
ב-Cloud Storage בקבצים האלה ל-public, max-age=31536000
, כך שהדפדפן ישמור את הקובץ במטמון למשך עד שנה.
סיכום
אנחנו מצפים בקוצר רוח לראות איך אומנים משפרים את החוויה הזו, ומשתמשים בה ככלי להבעה יצירתית באמצעות תנועה. פרסמנו את כל הקוד בקוד פתוח, והוא זמין בכתובת https://github.com/puckey/dance-tonite. בימים הראשונים של טכנולוגיית ה-VR ובמיוחד ב-WebVR, אנחנו מצפים לראות אילו כיוונים חדשים ויצירתיים צפויים ליצור במדיום החדש הזה. Dance on.