סיכום
שישה אומנים הוזמנו לצייר, לעצב ולפסל ב-VR. כאן לגבי האופן שבו תיעדנו את הסשנים שלהם, המירו את הנתונים והצגתם אותו בזמן אמת באמצעות דפדפני אינטרנט.
https://g.co/VirtualArtSessions
איזה זמן לחיות! עם מבוא למציאות מדומה כצרכן קיימות אפשרויות חדשות שלא חקרנו. lift Brush, מוצר של Google שזמין ב-HTC Vive, מאפשר לך לצייר שטח מימדי. כשניסינו את הטיה מברשת בפעם הראשונה, התחושה הזו לצייר עם בקרים למעקב אחר תנועה בשילוב עם הנוכחות של חדר עם כוחות-על" נמשך איתך, אין באמת חוויה כמו היכולת לשרטט את השטח הריק שמסביבך.
צוות Data Arts ב-Google קיבל את האתגר להציג למשתמשים שאין להם משקפי VR, באינטרנט שבו lift Brush לא פועלת שעדיין פועלים. לשם כך, הצוות הביע פסל, מאייר, מעצב קונספט, אומני אופנה, אומנית מיצג ואומני רחוב ליצור אומנות בסגנון משלהם בתוך מדיום חדש זה.
הקלטת שרטוטים במציאות מדומה
תוכנת הטיה מברשת עצמה, שמובנית ב-Unity, היא אפליקציה למחשב
נעשה שימוש ב-VR בקנה מידה נרחב כדי לעקוב אחר מיקום הראש (צג על הראש, או HMD)
לבקרים בכל אחת מהן. פריטי גרפיקה שנוצרו ב-lift Brush
ברירת המחדל מיוצאת כקובץ .tilt
. כדי לפרסם את החוויה הזו באינטרנט,
הבנו שאנחנו צריכים לא רק את הנתונים של הגרפיקה. עבדנו בשיתוף פעולה הדוק עם
הצוות של lift Brush ביצע שינויים ב-הטיה של Brush כך שייצאה גם פעולות ביטול/מחיקה
לתנוחת הראש והידיים של האומן בקצב 90 פעמים בשנייה.
בזמן השרטוט, הטיה מברשת לוקחת את המיקום ואת הזווית של השלט רחוק וממירה כמה נקודות לאורך זמן ל'קו חוצה'. אפשר לראות דוגמה כאן. כתבנו יישומי פלאגין שחילצו את התנועות האלה ויצרנו פלט שלהן כ-JSON גולמי.
{
"metadata": {
"BrushIndex": [
"d229d335-c334-495a-a801-660ac8a87360"
]
},
"actions": [
{
"type": "STROKE",
"time": 12854,
"data": {
"id": 0,
"brush": 0,
"b_size": 0.081906750798225,
"color": [
0.69848710298538,
0.39136275649071,
0.211316883564
],
"points": [
[
{
"t": 12854,
"p": 0.25791856646538,
"pos": [
[
1.9832634925842,
17.915264129639,
8.6014995574951
],
[
-0.32014992833138,
0.82291424274445,
-0.41208130121231,
-0.22473378479481
]
]
}, ...many more points
]
]
}
}, ... many more actions
]
}
קטע הקוד שלמעלה מתאר את הפורמט של פורמט ה-JSON של שרטוט.
כאן, כל קו נשמר כפעולה, מסוג: "STROKE". נוסף ל- פעולות של שבץ, רצינו להראות לאומן טעויות ומשנה את באמצע השרטוט, אז חשוב מאוד לשמור את "DELETE" פעולות שמשמשות למחוק או לבטל פעולות עבור קו שלם.
המידע הבסיסי על כל קו נשמר, לכן סוג המברשת, גודל המברשת והצבע מתבצע איסוף של כל ה-RGB.
לבסוף, כל קודקוד של הקו נשמר, שכולל את המיקום,
הזווית, הזמן, וגם עוצמת הלחץ להפעלה של השלט רחוק (מזוהה כ-p
)
בכל נקודה).
שימו לב שהסיבוב הוא קוואטרניון בן 4 רכיבים. זה חשוב בהמשך, אנחנו מסירים את הקווים כדי למנוע נעילה של גימבל.
הפעלת Sketches עם WebGL
כדי להציג את השרטוטים בדפדפן אינטרנט, השתמשנו הקוד THREE.js וכתב קוד יצירה בגיאומטריה שמחקה את מה ש-מטה מתבלבל עושה במכונה.
בעוד ש-lift Brush, מייצרת רצועות של משולשים בזמן אמת בהתבסס על יד המשתמש תנועה, כל השרטוט כבר "גמור". עד כמה זמן אנחנו מציגים אותו באינטרנט. כך אנחנו יכולים לעקוף חלק גדול מהחישוב בזמן אמת את הגיאומטריה אחרי הטעינה.
כל זוג קודקודים בקו מייצר וקטור כיוון (הקווים הכחולים
שמחברת כל נקודה כמו שמוצג למעלה, moveVector
בקטע הקוד שלמטה).
כל נקודה מכילה גם כיוון, קוואטרניון שמייצג
הזווית הנוכחית של השלט רחוק. כדי ליצור רצועה של משולש, אנחנו מבצעים איטרציה על כל אחד
שהנקודות האלה מפיקות נורמליות שצמודות לכיוון
כיוון הבקר.
התהליך לחישוב רצועת המשולש בכל קו הוא כמעט זהה לקוד שנעשה בו שימוש ב-הטיה ב- עליהם:
const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );
function computeSurfaceFrame( previousRight, moveVector, orientation ){
const pointerF = V_FORWARD.clone().applyQuaternion( orientation );
const pointerU = V_UP.clone().applyQuaternion( orientation );
const crossF = pointerF.clone().cross( moveVector );
const crossU = pointerU.clone().cross( moveVector );
const right1 = inDirectionOf( previousRight, crossF );
const right2 = inDirectionOf( previousRight, crossU );
right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );
const newRight = ( right1.clone().add( right2 ) ).normalize();
const normal = moveVector.clone().cross( newRight );
return { newRight, normal };
}
function inDirectionOf( desired, v ){
return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}
שילוב של כיוון הקו והכיוון בנפרד תוצאות מתמטיות שאינן חד-משמעיות; יכולים להיות כמה נורמליות בדרך כלל התוצאה הייתה 'טוויסט' בגיאומטריה.
כשחוזרים על הנקודות של שבץ מוחי, אנחנו שומרים על 'זכות מועדפת'
ומעבירים אותו לפונקציה computeSurfaceFrame()
. הפונקציה הזו
וזה נותן לנו נורמלית שממנו אנחנו יכולים להסיק מרובע בריבוע
כיוון הקו (מהנקודה האחרונה עד לנקודה הנוכחית),
הכיוון של הבקר (קווטרניון). וחשוב יותר, היא גם מחזירה
"זכות מועדפת" חדשה של קבוצת החישובים הבאה.
אחרי שיוצרים מרובעים שמבוססים על נקודות הבקרה של כל קו, אנחנו ממזגים את Quads על ידי אינטרפולציה של הפינות שלהם, מארבעה יחידות.
function fuseQuads( lastVerts, nextVerts) {
const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );
lastVerts[1].copy( vTopPos );
lastVerts[4].copy( vTopPos );
lastVerts[5].copy( vBottomPos );
nextVerts[0].copy( vTopPos );
nextVerts[2].copy( vBottomPos );
nextVerts[3].copy( vBottomPos );
}
כל מרובע מכיל גם קרינת UV שנוצרת בתור השלב הבא. כמה מברשות מכילים מגוון של תבניות קווים כדי ליצור את הרושם שכל קו מרגיש כמו משיחות מכחול שונות. ניתן לעשות זאת באמצעות _גיבוש של טקסטורה, _כאשר כל מרקם של מברשת מכיל את כל שונות. כדי לבחור את המרקם הנכון, משנים את ערכי קרינת ה-UV קו חוצה.
function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
let fYStart = 0.0;
let fYEnd = 1.0;
if( useAtlas ){
const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
fYStart = fYWidth * atlasIndex;
fYEnd = fYWidth * (atlasIndex + 1.0);
}
//get length of current segment
const totalLength = quadLengths.reduce( function( total, length ){
return total + length;
}, 0 );
//then, run back through the last segment and update our UVs
let currentLength = 0.0;
quadUVs.forEach( function( uvs, index ){
const segmentLength = quadLengths[ index ];
const fXStart = currentLength / totalLength;
const fXEnd = ( currentLength + segmentLength ) / totalLength;
currentLength += segmentLength;
uvs[ 0 ].set( fXStart, fYStart );
uvs[ 1 ].set( fXEnd, fYStart );
uvs[ 2 ].set( fXStart, fYEnd );
uvs[ 3 ].set( fXStart, fYEnd );
uvs[ 4 ].set( fXEnd, fYStart );
uvs[ 5 ].set( fXEnd, fYEnd );
});
}
מאחר שכל שרטוט יש מספר בלתי מוגבל של קווים, אין צורך שונה בזמן הריצה, אנחנו מחשבים מראש את הגיאומטריה של הקווים מראש וממזגים ברשת אחת. למרות שכל סוג מברשת חדש צריך להיות נפרד שעדיין מפחית את קריאות המשיכה שלנו לסרטון אחד לכל מברשת.
כדי לנצל את המערכת, יצרנו שרטוט שלקח 20 דקות כדי למלא את עם כמה קודקודים שאפשר. השרטוט שנוצר עדיין הוצג 60fps ב-WebGL.
מכיוון שכל קודקודים מקוריים של שבץ מכילים גם זמן, ניתן להשמיע את הנתונים בקלות. כדי לחשב מחדש את ההקשות לכל פריים, לאט, כך שבמקום זאת חישבנו מראש את כל השרטוט על העומס ופשוט גילינו כשהגיע הזמן לעשות את זה עם כל קבוצה.
להסתתרות מרובעת הייתה פשוט כיווץ הקודקודים לנקודה של 0,0,0. כאשר הגיע הזמן שבו הקבוצה אמורה להיחשף, ממקמים מחדש את הקודקודים במקומם.
תחום לשיפור הוא מניפולציה מלאה של הקודקודים ב-GPU באמצעות תוכנות הצללה (shader). ההטמעה הנוכחית ממקמת אותן על ידי חזרה דרך הקודקוד מחותמת הזמן הנוכחית, שבודקת אילו קודקודים צריך לחשוף ואז מעדכנים את הגיאומטריה. הפעולה הזאת גורמת לעומס רב על המעבד (CPU), וכתוצאה מכך לסובב את המאוורר וגם לבזבז את חיי הסוללה.
הקלטות של האומנים
הרגשנו שהשרטוטים עצמם לא מספיקים. רצינו להראות בתוך השרטוטים שלהם, וציירים כל מברשת מכחול.
כדי לצלם את האומנים, השתמשנו במצלמות של Microsoft Kinect של נתוני האומנים בגוף בחלל. כך אנחנו יכולים להראות צורות תלת ממדיות באותו מקום שבו מופיעים השרטוטים.
מכיוון שגופו של האומן יסתיר את עצמו ולא נוכל לראות את מה מאחוריה השתמשנו במערכת Kinect כפולה, בשני הצדדים הנגדיים של החדר שמצביעות למרכז.
בנוסף למידע על העומק, צילמנו גם את המידע על הצבעים של את הסביבה באמצעות מצלמות DSLR רגילות. השתמשנו תוכנת DepthKit לכייל ומיזוג את החומר המצולם ממצלמת העומק וממצלמות הצבע. ה-Kinect יכול של צבעי ההקלטה, אבל בחרנו להשתמש במצלמות DSLR כי יכולנו לשלוט חשיפה, להשתמש בעדשות מתקדמות יפהפיות וצלם ב-HD.
כדי להקליט את החומר המצולם, יצרנו חדר מיוחד שבו יאוחסנו HTC Vive, האומן ובין המצלמה. כל המשטחים מכוסים בחומר שנספג באינפרה-אדום אור כדי לתת לנו ענן נקודתי נקייה יותר (שמיכה על הקירות, גומי עם צלעות שטיח על הרצפה). למקרה שהחומר הופיע בענן הנקודות בחרנו חומר שחור כדי שהוא לא יסיח את הדעת כמו משהו שהיה לבן.
הקלטות הווידאו שהתקבלו נתנו לנו מספיק מידע כדי להקרין חלקיק המערכת. כתבנו כמה כלים נוספים openFrameworks כדי לנקות את החומר המצולם, במיוחד להסיר את הרצפות, הקירות והתקרות.
בנוסף להצגת האומנים, רצינו לעבד את ה-HMD ואת של בקרים בתלת-ממד. זה לא היה חשוב רק כדי להציג את מדד ה-HMD את הפלט הסופי באופן ברור (העדשות הרפלקטיביות של HTC Vive נטשו קריאות ה-IR של Kinect), הן סיפקו לנו נקודות מגע לניפוי באגים בחלקיקים ולסדר את הסרטונים באמצעות השרטוט.
כדי לעשות זאת, כתבנו פלאגין מותאם אישית ב-TIFF Brush שחילץ את של ה-HMD ושל הבקרים בכל פריים. מכיוון ש-Strong Brush פועל ב-90fps, המון נתונים זרמו החוצה ונתוני קלט של שרטוט היו של יותר מ-20MB לא דחוס. השתמשנו בשיטה הזו גם כדי לתעד אירועים שלא מתועדים בקובץ השמירה הטיפוסי של lift Brush, למשל כשהאומן בוחר אפשרות בחלונית הכלים ובמיקום של ווידג'ט המראה.
אחד האתגרים הגדולים ביותר בעיבוד הנתונים של 4TB שתועד היה להתאים בין כל המקורות החזותיים/המקורות השונים של הנתונים. כל סרטון ממצלמת DSLR צריך להיות מיושר עם ה-Kinect המתאים, כך שהפיקסלים מיושרים מרחב וגם זמן. ואז החומר המצולם משתי המצלמות האלה היה צריך להיות מתואמים זה עם זה כדי ליצור אומן אחד. אחרי זה היינו צריכים ליישר קו תלת-ממדי עם הנתונים שנלקחו מהשרטוט שלו. סוף סוף! כתבנו שמבוסס על דפדפן שיעזרו לך לבצע את רוב המשימות האלה, ואפשר לנסות אותם בעצמך כאן
אחרי יישור הנתונים, השתמשנו בכמה סקריפטים שנכתבו ב-NodeJS כדי לעבד אותם. הכול ופלט של קובץ וידאו וסדרה של קובצי JSON, שכולם חתוכים מסונכרן. כדי להקטין את הקובץ, עשינו שלושה דברים. קודם כול, צמצמנו את הדיוק של כל מספר נקודה צפה (floating-point), כך שיהיה מקסימום 3 רמת הדיוק של הנקודה העשרונית. שנית, אנחנו חותכים את מספר הנקודות בשליש 30fps, ואינטרפולציה של המיקומים בצד הלקוח. לבסוף, יצרנו סריאליזציה כך שבמקום להשתמש ב-JSON פשוט עם צמדי מפתח/ערך, סדר הערכים נוצר למיקום ולסיבוב של ה-HMD ושל הבקרים. חיתוך הקובץ גודל עד 3 מ"ב ביישנות, שהיה מקובל להעביר דרך הכבל.
מאחר שהסרטון עצמו מוצג כרכיב וידאו של HTML5 הנקרא על ידי טקסטורת WebGL להפוך לחלקיקים, הסרטון עצמו היה צריך לפעול מוסתר רקע. תוכנת הצללה ממירה את הצבעים בתמונות העומק למיקומים מרחב בתלת ממד. ג'יימס ג'ורג' שיתף דוגמה מצוינת שמסבירה איך אפשר להשתמש בחומר מצולם ישירות מתוך DepthKit.
ב-iOS יש הגבלות על הפעלת סרטונים מוטבעים, מה שאנחנו מניחים כדי למנוע מפני הטרדה של משתמשים ממודעות וידאו באינטרנט שמופעלות באופן אוטומטי. השתמשנו בשיטה בדומה לפתרונות אפשריים אחרים באינטרנט, כלומר להעתיק פריים של הסרטון באזור עריכה ועדכון ידני של זמן הדילוג לסרטון, כל 1/30 שנייה.
videoElement.addEventListener( 'timeupdate', function(){
videoCanvas.paintFrame( videoElement );
});
function loopCanvas(){
if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){
const time = Date.now();
const elapsed = ( time - lastTime ) / 1000;
if( videoState.playing && elapsed >= ( 1 / 30 ) ){
videoElement.currentTime = videoElement.currentTime + elapsed;
lastTime = time;
}
}
}
frameLoop.add( loopCanvas );
הגישה שלנו גרמה לתוצאה לא נעימה של ירידה משמעותית ב-iOS מכיוון שהעתקת מאגר הנתונים הזמני של הפיקסלים מסרטון לאזור העריכה עתיר מעבדי CPU. כדי לעקוף את הבעיה, הצגנו גרסאות קטנות יותר של אותם סרטונים שמאפשרים לפחות 30fps ב-iPhone 6.
סיכום
הקונצנזוס הכללי בכל הנוגע לפיתוח תוכנות VR נכון ל-2016, הוא לשמור גיאומטריה ורכיבי הצללה פשוטים, כך שתוכלו להריץ במהירות של 90fps ב-HMD. הזה היה יעד מצוין להדגמות של WebGL, כי הטכניקות שבהן השתמשת במפה של lift Brush יפה מאוד ל-WebGL.
למרות שדפדפני אינטרנט שמציגים רשתות תלת-ממדיות מורכבות לא מלהיבים זו הייתה הוכחה לתפיסה שהאבקנים של עבודת VR אינטרנט אפשרי לחלוטין.