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

פרטי הסשן עם התמונה הסטטית

סיכום

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

https://g.co/VirtualArtSessions

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

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

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

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

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

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

נשמר מידע בסיסי על כל משיכה, ולכן מתבצע איסוף של סוג המברשת, גודל המברשת וה-RGB.

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

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

הפעלת שרטוטים לאחור עם WebGL

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

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

סקיצות WebGL

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

משיכות

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

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

    });

}
ארבע מרקמים באטלס של טקסטורה עבור מברשת שמן
ארבעה מרקמים באטלס של מרקם למברשת שמן
ב-הטיה Brush
ב-הטיה Brush
ב-WebGL
ב-WebGL

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

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

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

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

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

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

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

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

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

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

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

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

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

אומן הקלטות

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

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

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

המסך תושבת לראש, הבקרים והחלקיקים מסודרים
המסך המורכב על הראש, שלטים רחוקים וחלקיקים מסודרים יחד

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

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

אומנים שמקליטים

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

אומנים שמקליטים

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

סיכום

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

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