סשנים של אומנות וירטואלית

פרטי סשן של תמונת רקע

סיכום

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

https://g.co/VirtualArtSessions

איזה זמן נפלא לחיות בו! עם ההשקה של המציאות הווירטואלית כמוצר לצרכנים, נחשפות אפשרויות חדשות ולא נחקרו. Tilt Brush הוא מוצר של Google שזמין ב-HTC Vive ומאפשר לצייר במרחב תלת-ממדי. כשניסינו את Tilt Brush בפעם הראשונה, התחושה של ציור באמצעות בקרי מעקב תנועה בשילוב עם התחושה של 'שהגעתם לחדר עם כוחות-על' לא עוזבת אתכם. אין באמת חוויה דומה ליכולת לצייר במרחב הריק שסביבכם.

פריט אומנות וירטואלי

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

הקלטה של ציורים במציאות מדומה

תוכנת Tilt Brush, שנוצרה ב-Unity, היא אפליקציה למחשב שמשתמשת ב-VR בקנה מידה של חדר כדי לעקוב אחרי מיקום הראש (מסך רכוב על הראש, או HMD) ואחרי בקרי היד שלכם. כברירת מחדל, גרפיקה שנוצרה ב-Tilt Brush מיוצאת כקובץ .tilt. כדי להביא את חוויית הצפייה הזו לאינטרנט, הבנו שאנחנו זקוקים ליותר מאשר נתוני הגרפיקה. עבדנו בשיתוף פעולה הדוק עם צוות Tilt Brush כדי לשנות את Tilt Brush כך שיאפשר ייצוא של פעולות ביטול או מחיקה, וגם את המיקומים של הראש והיד של האומן ב-90 פעמים בשנייה.

כשאתם מציירים, אפליקציית Tilt Brush מקבלת את המיקום והזווית של השליט וממירה כמה נקודות לאורך זמן ל'מכה'. כאן אפשר לראות דוגמה. כתבנו יישומי פלאגין שחולטים את הקווים האלה ומפיקים אותם כ-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". בנוסף לפעולות של קווים, רצינו להראות אמן שמבצע שגיאות ומשנה את דעתו באמצע הרישום, ולכן היה חשוב לשמור פעולות 'מחיקה' שמיועדות למחיקת קו או לביטול פעולה של קו שלם.

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

לבסוף, כל קודקוד של הקו נשמר, כולל המיקום, הזווית, הזמן וגם עוצמת לחיצת ההדק של הבקר (הערה: p בכל נקודה).

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

הפעלה חוזרת של סקיצות באמצעות WebGL

כדי להציג את הרישומים בדפדפן אינטרנט, השתמשנו ב-THREE.js וכתבנו קוד ליצירת גיאומטריה שדומה למה ש-Tilt Brush עושה ברקע.

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

WebGL Sketches

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

התהליך לחישוב רצועת המשולשים לכל קווים כמעט זהה לקוד שמשמש ב-Tilt Brush:

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

תנועות

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

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 );
}
קוביות משולבות
ריבועים מחוברים

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

    });

}
ארבעה מרקמים במפה של מרקמים למברשת שמן
ארבעה מרקמים במאגר מרקמים למברשת שמן
ב-Tilt Brush
ב-Tilt Brush
ב-WebGL
ב-WebGL

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

כל הסקיצה שלמעלה מתבצעת בקריאה אחת ל-draw ב-WebGL
הסקיצה כולה שלמעלה מבוצעת בקריאה אחת ל-draw ב-WebGL

כדי לבצע בדיקת עומס על המערכת, יצרנו סקיצה שלקח 20 דקות למלא את המרחב בה כמה שיותר קודקודים. הסקיצה שנוצרה עדיין הופעלה במהירות של 60fps ב-WebGL.

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

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

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

פריט אומנות וירטואלי

הקלטת האומנים

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

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

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

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

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

אומן/ית

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

כל ארבעת הערוצים של סשן וידאו שתועדו (שני ערוצי צבע למעלה ושני ערוצי עומק למטה)
כל ארבעת הערוצים של סשן וידאו שתועדו (שני ערוצי צבע למעלה ושני ערוצי עומק למטה)

בנוסף להצגת האומן, רצינו ליצור גם עיבוד (רנדור) של משק ה-HMD והשלטים בתלת-ממד. זה היה חשוב לא רק כדי להציג את ה-HMD בצורה ברורה בפלט הסופי (העדשות המשתקפות של HTC Vive גרמו לשגיאות בקריאות ה-IR של Kinect), אלא גם כדי לספק לנו נקודות מגע לניפוי באגים בפלט החלקיקים וליישור הסרטונים עם הסקיצה.

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

כדי לעשות זאת, כתבנו פלאגין מותאם אישית ל-Tilt Brush שחולץ את המיקומים של ה-HMD והבקרים בכל פריים. מכיוון ש-Tilt Brush פועלת בקצב של 90fps, הרבה נתונים מועברים בסטרימינג, ונתוני הקלט של סקיצה היו גדולים מ-20MB ללא דחיסה. השתמשנו בשיטה הזו גם כדי לתעד אירועים שלא מתועדים בקובץ השמירה הרגיל של Tilt Brush, למשל כשהאמן בוחר אפשרות בלוח הכלים ואת המיקום של ווידג'ט המראה.

אחד מהאתגרים הגדולים ביותר בעיבוד 4TB של הנתונים שצילמנו היה התאמת כל מקורות הנתונים/החזותיים השונים. כל סרטון ממצלמת DSLR צריך להיות מיושר עם ה-Kinect התואם, כדי שהפיקסלים יהיו מיושרים במרחב ובזמן. לאחר מכן, היה צריך להתאים את החומר מצמד מצלמות הווידאו הזה כדי ליצור אמן אחד. לאחר מכן, היינו צריכים להתאים את האמן שלנו ל-3D לנתונים שצולמו מהציור שלו. סוף סוף! כתבנו כלים מבוססי-דפדפן שיעזרו לכם לבצע את רוב המשימות האלה, ואפשר לנסות אותם בעצמכם כאן.

אומנים

אחרי שהנתונים הותאמו, השתמשנו בסקריפטים שנכתבו ב-NodeJS כדי לעבד את כולם ולייצר קובץ וידאו וסדרה של קובצי JSON, כולם חתוכים וסונכרנו. כדי להקטין את גודל הקובץ, ביצענו שלושה דברים. קודם כל, הפחתנו את הדיוק של כל מספר עם נקודה צפה כך שיהיה לו דיוק של עד 3 ספרות אחרי הנקודה העשרונית. שנית, הפחתנו את מספר הנקודות בשליש ל-30fps, והשלמנו את המיקומים בצד הלקוח. לבסוף, אנחנו מבצעים סריאליזציה של הנתונים, כך שבמקום להשתמש ב-JSON רגיל עם צמדי מפתח/ערך, נוצרת סדרת ערכים למיקום ולרוטציה של ה-HMD והבקרים. כך צמצמנו את גודל הקובץ לקצת פחות מ-3MB, גודל שהיה מקובל להעברה באינטרנט.

אומנים

מכיוון שהסרטון עצמו מוצג כרכיב וידאו ב-HTML5 שנקרא על ידי טקסטורה של WebGL כדי להפוך לחלקיקים, הסרטון עצמו היה צריך לפעול מוסתר ברקע. שידרוג (shader) ממיר את הצבעים בתמונות העומק למיקומים במרחב תלת-ממדי. ג'יימס ג'ורג' שיתף דוגמה מעולה למה שאפשר לעשות עם קטעי וידאו ישירות מ-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, כי העתקת מאגר הפיקסלים מהווידאו ללוח היא פעולה שמחייבת שימוש נרחב במעבד. כדי לעקוף את הבעיה הזו, פשוט הצגנו גרסאות בגודל קטן יותר של אותם סרטונים, שמאפשרות לפחות 30fps ב-iPhone 6.

סיכום

הסכמה כללית לגבי פיתוח תוכנות VR נכון לשנת 2016 היא לשמור על צורות וגרפיקה פשוטים כדי שתוכלו להריץ את התוכנה במהירות של 90fps ומעלה במכשיר HMD. התברר שזו מטרה מצוינת לדמואים של WebGL, כי השיטות שבהן משתמשים ב-Tilt Brush מתאימות מאוד ל-WebGL.

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